I would like to implement multiple sign up/sign in policies in Azure AD B2C similar to this question but I don't know how to configure my solution in Visual Studio to reference the different signup policies specified in the web.config file. Can anyone help please?
You can invoke a different policy for a different type of user by passing the requested policy from a controller method to the authentication middleware:
public IActionResult LogInForIndividualCustomer()
{
return LogInFor(Constants.AuthenticationSchemes.B2COpenIdConnect, Constants.Policies.SignUpOrSignInWithPersonalAccount);
}
private IActionResult LogInFor(string authenticationScheme, string policy)
{
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(
authenticationScheme,
new AuthenticationProperties(
new Dictionary<string, string>
{
{Constants.AuthenticationProperties.Policy, policy}
})
{
RedirectUri = Url.Action("LoggedIn", "Account", values: null, protocol: Request.Scheme)
});
}
return RedirectToHome();
}
and then setting the redirection URL for the requested policy in the authentication middleware:
OnRedirectToIdentityProvider = async context =>
{
var policy = context.Properties.Items.ContainsKey(Constants.AuthenticationProperties.Policy) ? context.Properties.Items[Constants.AuthenticationProperties.Policy] : Constants.Policies.SignUpOrSignInWithPersonalAccount;
var configuration = await GetB2COpenIdConnectConfigurationAsync(context, policy);
context.ProtocolMessage.IssuerAddress = configuration.AuthorizationEndpoint;
}
Related
I have an ASP.NET Core web app that is authenticating with Azure AD in a multi-tenant configuration using Microsoft.Identity.Web. We use a tenant/company identifier as the subdomain of our apps URL. (companyA.myapp.com, companyB.myapp.com). Some users have access to more than one tenant of the application, so we cannot map a Azure AD tenant directly to a single tenant/company in our app.
With Microsoft.Identity.Web, how is the state parameter set or manipulated as described here? I would like to follow the guidance provided here, but am not sure where to start.
https://learn.microsoft.com/en-us/azure/active-directory/develop/reply-url#use-a-state-parameter
If you have several subdomains and your scenario requires that, upon successful authentication, you redirect users to the same page from which they started, using a state parameter might be helpful.
In this approach:
Create a "shared" redirect URI per application to process the security tokens you receive from the authorization endpoint.
Your application can send application-specific parameters (such as subdomain URL where the user originated or anything like branding information) in the state parameter. When using a state parameter, guard against CSRF protection as specified in section 10.12 of RFC 6749).
The application-specific parameters will include all the information needed for the application to render the correct experience for the user, that is, construct the appropriate application state. The Azure AD authorization endpoint strips HTML from the state parameter so make sure you are not passing HTML content in this parameter.
When Azure AD sends a response to the "shared" redirect URI, it will send the state parameter back to the application.
The application can then use the value in the state parameter to determine which URL to further send the user to. Make sure you validate for CSRF protection.
Here is how I eventually solved the the MS Login infinite redirects with the tenant per subdomain scheme problem. (Trying to come up with a better name for the problem. 😁)
Provide a SigninRedirect controller action that accepts a returnUrl parameter that we must validate to avoid being an Open Redirect.
Example URL with returnUrl set to companyA.example.com/foo&bar=1:
https://signin.example.com/signin-redirect?returnUrl=companyA.example.com%2Ffoo%26bar%3D1
[Route("")]
public class SigninController : Controller
{
private readonly IMediator _mediator;
private readonly IConfiguration _configuration;
public SigninController(IMediator mediator, IConfiguration configuration)
{
_mediator = mediator;
_configuration = configuration;
}
[Authorize]
[HttpGet("/signin-redirect")]
public IActionResult SigninRedirect(string returnUrl)
{
string redirect;
if (!string.IsNullOrEmpty(returnUrl) && IsValidSubdomainUrl(returnUrl))
{
redirect = returnUrl;
}
else
{
var appHost = _configuration.GetValue<string>("General:ApplicationHost");
var home = new UriBuilder("https", appHost).Uri;
return Redirect(home.AbsoluteUri);
}
return Redirect(redirect);
}
/// <summary>
/// Avoid Open Redirect Vulnerability
/// https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
/// </summary>
/// <param name="returnUrl"></param>
/// <returns></returns>
private bool IsValidSubdomainUrl(string returnUrl)
{
var appHost = _configuration.GetValue<string>("General:ApplicationHost");
var isValid = Uri.IsWellFormedUriString(returnUrl, UriKind.Absolute) &&
Uri.TryCreate(returnUrl, UriKind.Absolute, out var uri) &&
uri?.Host.EndsWith(appHost) == true;
return isValid;
}
}
In ConfigureServices(IServiceCollection services) set cookies to be shared across all the subdomains and configure an OnRedirectToIdentityProvider event to redirect to your signin URL when the user is not yet authenticated:
var cookieDomain = _configuration.GetValue<string>("General:CookieDomain");
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromDays(3);
options.Cookie.Domain = cookieDomain;
options.Cookie.Path = "/";
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
_configuration.Bind("AzureAd", options);
options.Events ??= new OpenIdConnectEvents();
options.Events.OnRedirectToIdentityProvider += context => {
var originalRequestUri = context.HttpContext.Request.GetUri();
var signInHost = _configuration.GetValue<string>("General:SignInHost");
var signInPath = _configuration.GetValue<string>("General:SignInUrl");
if (!originalRequestUri.Host.Equals(signInHost, StringComparison.InvariantCultureIgnoreCase))
{
var signInUrl = QueryHelpers.AddQueryString(signInPath, "returnUrl", originalRequestUri.AbsoluteUri);
// When on a subdomain and not authorized, then redirect to
// our signin URL.
context.Response.Redirect(signInUrl);
// Let Microsoft.Identity.Web know that we already handled
// this redirect
context.HandleResponse();
}
return Task.CompletedTask;
};
},
cookieOptions =>
{
cookieOptions.Cookie.Domain = cookieDomain;
cookieOptions.Cookie.Path = "/";
cookieOptions.Cookie.SameSite = SameSiteMode.Lax;
})
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read" })
.AddDistributedTokenCaches();
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Domain = cookieDomain;
options.Cookie.Name = ".AspNet.SharedCookie";
options.Cookie.Path = "/";
options.Cookie.SameSite = SameSiteMode.Lax;
});
Configuration defined:
"General:CookieDomain": ".example.com",
"General:ApplicationHost": "example.com",
"General:SignInHost": "signin.example.com",
"General:SignInUrl": "https://signin.example.com/signin-redirect"
In addition, you will need the usual AzureAd configuration section from the Microsoft Docs for Azure AD.
"AzureAd:Instance": "https://login.microsoftonline.com/",
"AzureAd:Domain": "...",
"AzureAd:TenantId": "common",
"AzureAd:ClientId": "...",
"AzureAd:ClientSecret": "...",
"AzureAd:CallbackPath": "/signin-oidc",
"AzureAd:SignedOutCallbackPath": "/signout-callback-oidc",
I am using a B2C login page to authenticate my users. These users have multiple IDP's of their choice as per their business and I created multiple policies with selected IDP's. In the login page based on the user email, I display his login page which has only his relevant IDP's. But in my web application, I can only add one Signup or SignIn policy in my appsettings.json to authenticate the user. Is there any option to have multiple policies in appsettings.json file or any other way to handle this requirement
My Current appsettings.json looks like below
"AzureAdB2C": {
"Instance": "https://login.microsoftonline.com/tfp/",
"ClientId": "******-***-****-****-*******",
"Domain": "mycustomdomain.onmicrosoft.com",
"SignUpSignInPolicyId": "Org-signinsignout"
},
You can invoke a different policy for a different type of user by passing the requested policy from a controller method to the authentication middleware:
public IActionResult LogInForBusinessCustomer(string uiLocale)
{
return LogInFor(Constants.AuthenticationSchemes.B2COpenIdConnect, Constants.Policies.SignUpOrSignInWithWorkAccount, uiLocale);
}
public IActionResult LogInForIndividualCustomer(string uiLocale)
{
return LogInFor(Constants.AuthenticationSchemes.B2COpenIdConnect, Constants.Policies.SignUpOrSignInWithPersonalAccount, uiLocale);
}
public IActionResult LogInForPartner(string uiLocale)
{
return LogInFor(Constants.AuthenticationSchemes.B2BOpenIdConnect, null, uiLocale);
}
private IActionResult LogInFor(string authenticationScheme, string policy)
{
if (!User.Identity.IsAuthenticated)
{
return new ChallengeResult(
authenticationScheme,
new AuthenticationProperties(
new Dictionary<string, string>
{
{Constants.AuthenticationProperties.Policy, policy}
})
{
RedirectUri = Url.Action("LoggedIn", "Account", values: null, protocol: Request.Scheme)
});
}
return RedirectToHome();
}
and then setting the redirection URL for the requested policy in the authentication middleware:
OnRedirectToIdentityProvider = async context =>
{
var policy = context.Properties.Items.ContainsKey(Constants.AuthenticationProperties.Policy) ? context.Properties.Items[Constants.AuthenticationProperties.Policy] : Constants.Policies.SignUpOrSignInWithPersonalAccount;
var configuration = await GetB2COpenIdConnectConfigurationAsync(context, policy);
context.ProtocolMessage.IssuerAddress = configuration.AuthorizationEndpoint;
if (context.Properties.Items.ContainsKey(Constants.AuthenticationProperties.UILocales))
{
context.ProtocolMessage.SetParameter("ui_locales", context.Properties.Items[Constants.AuthenticationProperties.UILocales]);
}
context.ProtocolMessage.SetParameter("dc", "cdm");
context.ProtocolMessage.SetParameter("slice", "001-000");
},
Reference: https://github.com/Azure-Samples/active-directory-external-identities-woodgrove-demo/blob/2b5110c25d1a626bf9b9ac27ecaaabad8b4bccf4/src/WoodGroveGroceriesWebApplication/Startup.cs#L283
Also please check out this thread
Azure B2C - Single App with multiple login for different user types setup in Azure
Hope it helps.
I have a web forms app which I am trying to authenticate against Azure AD using SAML 2/Kentor/Owin. I think I have things configured OK, but when my login page issues the following command I am not being redirected to a login page.
HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" });
Here is my startup.cs
private void ConfigureSAML2Authentication(IAppBuilder app) {
var authServicesOptions = new KentorAuthServicesAuthenticationOptions(false)
{
SPOptions = new SPOptions
{
EntityId = new EntityId("https://login.microsoftonline.com/<tenant guid>/saml2")
}
},
AuthenticationType = "KentorAuthServices",
Caption = "ADFS - SAML2p",
};
authServicesOptions.IdentityProviders.Add(new IdentityProvider(
new EntityId("https://sts.windows.net/<tenant guid>/"),
authServicesOptions.SPOptions)
{
MetadataLocation = "https://login.microsoftonline.com/<tenant guid>/federationmetadata/2007-06/federationmetadata.xml",
LoadMetadata = true,
});
app.UseKentorAuthServicesAuthentication(authServicesOptions);
}
As far as I can tell looking at the Network Tools in chrome, no auth request is being sent at all. Is anyone able to tell me why?
The AuthServices middleware is configured as Passive by default, so it will not automatically respond to an authentication challenge unless you specify the provider.
When you issue the challenge you should specify the same AuthenticationType that you used when the middleware was set up. By default this is "KentorAuthServices" but can be changed.
If you change your challenge to include the type, it should trigger the redirect:
HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" }, "KentorAuthServices");
I have two tenants configured in my Azure AD. My users are authentication with successful in my tenant but others users that are another tenant has access in my applications.
What's wrong with my application? I'm using OpenId Connect protocol in my code, for exemple:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
return Task.FromResult(0);
}
}
});
Am I setting something wrong on my Azure?
Someone help me?
Thanks,
Vilela
I have two tenants configured in my Azure AD.
The tenants are corresponding to the Azure Active Directory. So when there are two tenants that means you have two different Azure Active Directory.( refer here about the detail concept)
And to enable the multi-tenat app, we need to enable it from the old Azure portal and locat you app. Then you can set it by refering the figure below:
Update( limit the sepcifc tenants to access the multi-tenants app)
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = ClientId,
Authority = Authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// we use this notification for injecting our custom logic
SecurityTokenValidated = (context) =>
{
// retriever caller data from the incoming principal
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
var issuer1 = "";
var issuer2 = "";
if ((issuer!=issuer1)&& (issuer != issuer2))
// the caller was neither from a trusted issuer - throw to block the authentication flow
throw new SecurityTokenValidationException();
return Task.FromResult(0);
}
}
});
Our Mvc/WebAPI solution currently has four trusted identity providers which we have registered in ADFS3. Each of these identity providers can be used by our users by direct links, effectively working around any home-realm-cookies that ADFS may have created (eg: www.ourportal.com/accounts/facebook or www.ourportal.com/accounts/twitter). Currently we are migrating from WIF to OWIN but will keep using WS-Federation protocol for the time being by implementing wsfederation and cookie authentication middleware. When using WIF, we did the following in order to go directly to a known identity provider:
var signInRequest = new SignInRequestMessage(stsUrl, realm) { HomeRealm = homeRealm };
return new RedirectResult(signInRequest.WriteQueryString());
This seems to have two concerning behaviors, it does not pass the WsFedOwinState parameter, and on the return back to the Relying Party, the Home.cshtml is built (with a windows principal) before the the Owin authentication middleware is fired. The Home.cshtml being fired before the Owin middleware is the most concering as this view relies on Claims that would is provided in the transformation done by the authentication pipeline, which is fired afterwards and thus our view does not work. It works in the correct order when going to the portal in the normal way (eg www.ourportal.com)
I understand that in order to provide the Whr parameter, you do the following when configuring the ws-federation middleware:
RedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.Whr = "SomeUrnOfAnIdentityProvider";
return Task.FromResult(0);
}
but this sets a single identity provider for the whole solution and does not allow our users to go directly to one of a list of identity providers.
The non-working method which builds the sign-in-request is currently:
private RedirectResult FederatedSignInWithHomeRealm(string homeRealm)
{
var stsUrl = new Uri(ConfigurationManager.AppSettings["ida:Issuer"]);
string realm = ConfigurationManager.AppSettings["ida:Audience"];
var signInRequest = new SignInRequestMessage(stsUrl, realm)
{
HomeRealm = homeRealm
};
HttpContext.Request.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
return new RedirectResult(signInRequest.WriteQueryString());
}
The ws-federation and cookie middleware are configured as the first middleware in OWIN startup and the default authentication is set to
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
I think I found a solution. The new method for skipping the home realm screen would be like this :
private void FederatedSignInWithHomeRealm(string homeRealm)
{
HttpContext.Request
.GetOwinContext()
.Authentication
.SignOut(CookieAuthenticationDefaults.AuthenticationType);
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
authenticationProperties.Dictionary.Add("DirectlyToIdentityProvider", homeRealm);
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
}
And the OWIN WS-Federation middleware would be configured like this :
app.UseWsFederationAuthentication(new WsFederationAuthenticationOptions
{
Notifications = new WsFederationAuthenticationNotifications()
{
RedirectToIdentityProvider = notification =>
{
string homeRealmId = null;
var authenticationResponseChallenge = notification.OwinContext
.Authentication
.AuthenticationResponseChallenge;
var setIdentityProvider = authenticationResponseChallenge != null
&& authenticationResponseChallenge.Properties
.Dictionary
.TryGetValue("DirectlyToIdentityProvider", out homeRealmId);
if (setIdentityProvider)
{
notification.ProtocolMessage.Whr = homeRealmId;
}
return Task.FromResult(0);
}
},
MetadataAddress = wsFedMetadata,
Wtrealm = realm,
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = realm
}
});