Blazor login path for multiple authentication schemes (AAD and AB2C) - authentication

I want to use Azure AD and Azure B2C in one application. I registered both authentication schemes which works fine.
Now I want to allow the user to decide which scheme should be used by clicking a "login with AD" or "login with B2C" button. Clicking on one of the buttons should redirect the user to the correct login.
I'm able to do this for AD by using the link MicrosoftIdentity/Account/SignIn. To do this, it's necessary to use services.AddControllersWithViews().AddMicrosoftIdentityUI()
So, how do I get a link like above for B2C?
Here is my code:
public static void AddAzureADAuthenticationApp(this IServiceCollection services, IConfigurationSection configuration)
{
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
configuration.Bind(options);
options.Events.OnTokenValidated = async context =>
{
await AuthorizationHelper.ValidateAADAppToken(context);
};
})
.EnableTokenAcquisitionToCallDownstreamApi().AddInMemoryTokenCaches();
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder(OpenIdConnectDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new AzureADAuthorizationRequirement()).Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.AddMicrosoftIdentityUI();
services.AddAuthorization(config =>
{
var policy = new AuthorizationPolicyBuilder(OpenIdConnectDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new AzureADAuthorizationRequirement()).Build();
config.AddPolicy(Constants.PolicyInternalUsers, policy);
});
}
public static void AddAzureB2CAuthenticationApp(this IServiceCollection services, IConfigurationSection configuration, bool RequireAccountNum = false)
{
services.AddAuthentication()
.AddMicrosoftIdentityWebApp(options =>
{
configuration.Bind(options);
options.SignInScheme = Constants.B2CAuthenticationScheme;
options.ResponseType = "code id_token";
options.Scope.Clear();
options.Scope.Add("https://graph.microsoft.com/openid");
options.Events.OnTokenValidated = async context =>
{
await AuthorizationHelper.ValidateB2CAppToken(context, RequireAccountNum);
};
}, openIdConnectScheme: Constants.B2CAuthenticationScheme, cookieScheme: Constants.B2CCookieScheme)
.EnableTokenAcquisitionToCallDownstreamApi().AddInMemoryTokenCaches();
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder(FischerLib.Extensions.Constants.B2CAuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new AzureB2CAuthorizationRequirement(RequireAccountNum)).Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.AddMicrosoftIdentityUI();
services.AddAuthorization(config =>
{
var policy = new AuthorizationPolicyBuilder(FischerLib.Extensions.Constants.B2CAuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new AzureB2CAuthorizationRequirement(RequireAccountNum)).Build();
config.AddPolicy(Constants.PolicyExternalUsers, policy);
});
}
At the moment I'm using two controller which have authorize attributes restricted to the scheme. Accessing these controller redirects the user to the login.
But this is not the way which I want to use. After the code above is part of a library and I would like to avoid having to ad the controller in every project.
I know that there is a property LoginPath if I use a different scheme like cookies but I can't set this property in AddMicrosoftIdentityWebApp(...)
Thank you very much!
Markus

Please check this blog on Building a Web Application that Supports both Azure AD and Azure AD B2C - MikaBerglund.com
which works on how to enable switching between Azure AD and Azure AD
B2C just by changing the configuration i.e;app settings.json file
where authority , clientId changes.
The controller actions need to route to selected auth schemes when required.
The way you have added controller seems to be the way.
You can raise a support request for the same.
Reference:
github ref

Related

AzureADDefaults is obsolete

I have following code for Azure AD authentication:
services
.AddAuthorization(options =>
{
options.AddPolicy(name, builder =>
{
builder
.AddAuthenticationSchemes(AzureADDefaults.AuthenticationScheme)
.RequireAuthenticatedUser();
});
})
.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options =>
{
configuration.Bind("AzureAd", options);
});
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
...
}
AzureADDefaults.AuthenticationScheme and AzureADDefaults.OpenIdScheme are now obsolete with message "Use Microsoft.Identity.Web instead. See https://aka.ms/ms-identity-web.". However I can't find any clear documentation how to upgrade following code to use Identity.Web instead of those obsolete constants.
Does anyone have instructions how to remove this obsolete code?
This blog shows you the differences between Identity Platform and Identity.Web.
For Identity.Web, we use Microsoft.Identity.Web and Microsoft.Identity.Web.UI. Try to see this sample, and it uses AddMicrosoftIdentityWebAppAuthentication to sign in users.
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
// Handling SameSite cookie according to https://learn.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
options.HandleSameSiteCookieCompatibility();
});
// Sign-in users with the Microsoft identity platform
services.AddMicrosoftIdentityWebAppAuthentication(Configuration);
services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.AddRazorPages();
}

ASP.NET Core 3.1 Use both OpenIDConnect and Custom Cookie Authentication

I have an existing application that makes use of Cookie Authentication, and would like to add the ability to authenticate users using Active Directory. The current application uses Cookie based authentication and custom authorisation - roles in a database.
I am adding bits from example located here:
Add sign-in with Microsoft to an ASP.NET Core web app
When I run the application I get an error:
System.InvalidOperationException: Scheme already exists: Cookies
What is the correct way to configure OpenIdConnect and Cookie Authentication.
// STEP 1 Basic Cookie Auth
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth";
options.AccessDeniedPath = "/Home/AccessDenied";
options.Cookie.IsEssential = true;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromSeconds(day/2.0);
options.Cookie.HttpOnly = true; // not accessible via JavaScript
options.Cookie.Name = "login_token";
options.TicketDataFormat = new CustomJwtDataFormat(
SecurityAlgorithms.HmacSha256,
tokenValidationParameters);
});
// STEP 2 OpenID Connect Auth
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), "OpenIdConnect", "Cookies", true);
I am not able to find any examples using both Cookie Authentication and OpenID Connect. Is this possible? Allowing users to login selectively using Active Directory authentication, or local authentication (details stored in local database).
After changing the "Cookie" name, get's rid of the error message,
but breaks the local authorisation, e.g.
When a valid Username and Password is given, I typically
authorise the login.
HttpContext.Response.Cookies.Append("login_token", token, GetCookieOptions());
Currently with OpenIDConnect configured User.Identity.IsAuthenticated
remains false.
According to the error messages, it tell you that you have multiple Scheme which named cookies.
According to the AddMicrosoftIdentityWebApp Method document, you could find the third parameter name is the cookieScheme.
The cookie-based scheme name to be used. By default it uses "Cookies".
But you have already set this name at above, so you should use other one. For example: "ADCookies".
Like below:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"), "OpenIdConnect", "ADCookies", true);
The mixed approach is a minefield but the below is allowing use to Authenticate Users via IdentityServer4 using OIDC while authenticating the Application into AzureAD with Identity.Web to get tokens for Api calls.
services.AddAuthentication(options =>
{
options.DefaultScheme = "IS4Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("IS4Cookies")
.AddOpenIdConnect("oidc", "OpenID Connect", options =>
{
options.SignInScheme = "IS4Cookies";
// Get IdentityServer configuration from appsettings.json.
var config = Configuration.GetSection("IdentityServerOptions").Get<IdentityServerOptions>();
options.Authority = config.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = config.ClientId;
options.ClientSecret = config.ClientSecret;
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("role", "role");
options.ClaimActions.MapJsonKey("role", System.Security.Claims.ClaimTypes.Role);
options.ClaimActions.MapJsonKey("email", "email");
options.ClaimActions.MapJsonKey("preferred_username", "preferred_username");
options.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context =>
{
context.Response.Redirect("/");
context.HandleResponse();
return Task.FromResult(0);
}
};
})
.AddMicrosoftIdentityWebApp(Configuration, "AzureOptions")
.EnableTokenAcquisitionToCallDownstreamApi(new string[]{"sms.all" })
.AddInMemoryTokenCaches();
This is what I use and it works, you just need to specify the configureCookieAuthenticationOptions and set the name inside there and you should be good to go, also I had to use lax for SameSite or it would not work for me.
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(identityOptions =>
/* {identityOptions.ClientId ="";}, // if you want to specify the options manually instead of Configuration.GetSection() call*/
Configuration.GetSection("AzureAd"),
configureCookieAuthenticationOptions: authCookie => { // Setup SSO cookie
authCookie.Cookie.Name ="Your.Cookie.Name.Here";// change name to hide .net identifiers in name
authCookie.Cookie.HttpOnly = true;// make so client cannot alter cookie
authCookie.Cookie.SecurePolicy = CookieSecurePolicy.Always;// require https
authCookie.Cookie.SameSite = SameSiteMode.Lax;// from external resource
// verify options are valid or throw exception
authCookie.Validate();
}
);
You may or may not need all of the authCookie values here, but it should get you started in the right direction!
It's possible to mix two mechanisms.
I use MicrosoftIdentity authentication for access to administration web pages and cookies authentication for my APIs and SignalR hubs.
I use this in startup ConfigureServices
services
.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie("CookiesApiScheme", options =>
{
options.SlidingExpiration = true;
// There is no redirection to a login page for APIs and SignalR Hubs, I just made a call to /Api/Login/SignIn with credential
options.AccessDeniedPath = new PathString("/Api/Login/AccessDenied"); // Action who just returns an Unauthorized
})
.AddMicrosoftIdentityWebApp(Configuration); // By default scheme is "CookieAuthenticationDefaults.AuthenticationScheme"
And in API controller you can use something like this
[Route("api/[controller]")]
[ApiController]
[Authorize(Roles = Roles.ADMIN)]
[Authorize(AuthenticationSchemes = "CookiesApiScheme")]
public class DefaultController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
Based on this post: ASP.NET Core 2.0 AzureAD Authentication not working

Authentication flows when a web app authenticate visitors and itself towards Microsoft Graph

I am trying to build a small web application that will authenticate the visiting user with Azure AD and then add the user to a specified group in Azure AD. Components used are C#/dotnet core, MSAL and Microsoft Graph library for .NET.
The steps are simple enough:
user visits website.
user gets authenticated towards Azure AD
w/OpenID Connect.
when successfully authenticated, the website
adds the user as member in a specific Azure AD group using Microsoft
Graph API.
user is presented with the status of the operation.
The application is registered in Azure AD with implicit grant (for ID tokens) and with the following Azure AD permissions:
Microsoft Graph: Group.ReadWrite.All
Microsoft Graph: User.Read.All
The controller looks like this:
public async Task<string> Test()
{
//get authenticated user
var identity = User.Identity as ClaimsIdentity;
string preferred_username = identity.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value;
//get appsettings.json
var azureAdOptions = new AzureADOptions();
_configuration.Bind("AzureAd", azureAdOptions);
//do Microsoft Graph stuff
GraphServiceClient graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(
async requestMessage =>
{
string authority = $"{azureAdOptions.Instance}{azureAdOptions.TenantId}";
ClientCredential clientCredentials = new ClientCredential(azureAdOptions.ClientSecret);
var app = new ConfidentialClientApplication(azureAdOptions.ClientId, authority, "https://daemon",
clientCredentials, null, new TokenCache());
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
// Passing tenant ID to the sample auth provider to use as a cache key
AuthenticationResult authResult = null;
authResult = await app.AcquireTokenForClientAsync(scopes);
// Append the access token to the request
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
}));
User userToAdd = await graphClient.Users[preferred_username].Request().GetAsync();
await graphClient.Groups["c388b7a4-2a22-4e3f-ac11-900cef9f74c6"].Members.References.Request().AddAsync(userToAdd);
return $"added {userToAdd.DisplayName} to group";
}
Startup.cs looks like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = true;
});
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
With this code I have two authentication flows. One for authenticating the visiting user and one for authenticating the app towards Microsoft Graph. The rationale behind this is that the user does not hold enough permissions to add a member to the group. The code works and does exactly as expected.
Is the double authentication flow against a single Azure AD app registration the best way to ensure this goal or is there a sleeker design where only one authentication flow is needed?
As far as I know you do need to support these two flows. Your user needs one token to talk to your web application and your web application needs a different token to talk to Graph.
Hopefully you won't need all that code in the DelegateAuthenticationProvider soon as we will be previewing a bunch of scenario based AuthenticationProviders shortly. The ClientCredentialProvider should do all that work for you.

IdentityServer4- Challenge ALL requests to API not just [Authorize]

I have an ASP.Net Core 2 API using IdentityServer4. I would like challenge ALL requests to the server and invoke the login redirect if the user is not authenticated, calling back to a specific URL after authentication.
The default is to invoke the login redirect only when an unauthenticated user requests a resource protected by the [Authorize] attribute. This will not work in my use case.
Basically, I want the functional equivalent of an [Authorize] attribute for the whole application not just specific controllers.
What is the easiest way to do this? Is there a setting I can use when configuring the services in Startup.cs (services.AddAuthentication)? Or through custom middleware right after app.UseAuthentication()?
I tried the following custom middleware but it says a handler is not configured.
ConfigureServices
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:4000";
options.ApiName = "myapi";
});
Configure
app.UseAuthentication();
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync(IdentityServerAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties {RedirectUri= "https://localhost:5000/" });
}
else { await next.Invoke(); }
});
app.UseMvc();
For configuring [Authorize] for the whole controllers, you could try AuthorizeFilter like below
services.AddMvc(config => {
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
For redirecting, you could try configure UserInteraction.LoginUrl
services.AddIdentityServer(opt => {
opt.UserInteraction.LoginUrl = "/Identity/Account/LogIn";
})
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddAspNetIdentity<IdentityUser>();

Multiple login pages for different claims

Here is my situation.
To do some actions on the web site user should be authenticated. Different actions require different claims. For example, to make an order user is authenticated by phone number only, to view the purchase history user should be authenticated by phone number and password, and to change the phone number user should be authenticated using two-factor authentication.
I create a login page for each of the authentication methods and when user is authenticated I give her a set of claims depending on the authentication method.
I add [Authorize(Policy="CanCreateOrder")] to the CreateOrder action method. The policy has the logic what claims required to authorize user. In case user is not authorized I want to redirect the user to the appropriate login page.
The question is how I could specify the url where user should be redirected for authentication?
Looking at CookieAuthenticationMiddleware I could not see how to to specify login page depending on what claims required. Documentation suggests to set LoginPath property at the configuration time, but in my case login url depends on what claims I need to authorize the user.
You can use different authentication scheme for each different claim:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "Phone",
LoginPath = "<phone - path>"
....
}
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "Password",
LoginPath = "<password - path>"
....
}
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "TwoFactor",
LoginPath = "<twofactor - path>",
....
}
And then usage:
[Authorize(Policy="CanCreateOrder", ActiveAuthenticationSchemes = "Phone")]
Also you can use multiple scheme:
[Authorize(Policy="CanCreateOrder", ActiveAuthenticationSchemes = "Phone,TwoFactor")]
See https://docs.asp.net/en/latest/security/authorization/limitingidentitybyscheme.html
From aspnet core 2, use can use the attribute for the controller, action o razor page you need to configure:
[Authorize(Policy="CanCreateOrder")]
Configure your policy with the authenticationscheme
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthorization(options =>
{
options.AddPolicy("CanCreateOrder",
authBuilder =>
{
authBuilder.AddAuthenticationSchemes("Management_Scheme");
authBuilder.RequireClaim("Manager");
});
});
...
}
and multiple cookie authentication configuration depending on the authentication scheme:
public void ConfigureServices(IServiceCollection services)
{
...
services
.AddAuthentication()
.AddCookie("Public_Scheme" , options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
})
.AddCookie("Management_Scheme",options =>
{
options.LoginPath = "/management/login";
options.LogoutPath = "/management/logout";
});
...
}
This extensions must be used inside ConfigureServices method and not Configure.
Take into account that configuring the authentiucation scheme inside the policy is not mandatory, it can be can addes to the authorization attribute wherever needed:
[Authorize("CanCreateOrders",AuthenticationSchemes = "ManagementScheme")]
You can see more about migrating in to 2.0 in this article.
This Is Perfect, The method I use for two different login authorizations
services.AddAuthentication(options =>
{
options.RequireAuthenticatedSignIn = false;
}).
AddCookie("CustomSchema1",
options =>
{
options.Cookie.Name = "CustomCookieName1";
options.LoginPath = "/login";
options.ExpireTimeSpan = TimeSpan.FromDays(10);
options.SlidingExpiration = true;
}).AddCookie("CustomSchema2", options =>
{
options.Cookie.Name = "CustomCookieName2";
options.LoginPath = "/writer/login";
options.ExpireTimeSpan = TimeSpan.FromDays(10);
options.SlidingExpiration = true;
});