Redirection doesn't happen in open id "OnAuthorizatioCodeReceived" - asp.net-core

Im using Azure ADB2C for authentication and authorization and using Open Id Connect flow. I need to do redirect the user to the application home page with retrieved access token attached to the home page url as query parameter from "OnAuthorizationCodeReceived". But that doesn't work. Below is my code.
By doing this I intend to catch access token and id token from redirect URL.
And this is an single page application . I tried MSAL.js but it has issues in browser compatibility and even some times in different identity providers. So I decided to user open id from c# backend
public static class AzureAdB2CAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder)
=> builder.AddAzureAdB2C(_ =>
{
});
public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder, Action<AzureAdB2COptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsSetup>();
builder.AddOpenIdConnect();
return builder;
}
public class OpenIdConnectOptionsSetup : IConfigureNamedOptions<OpenIdConnectOptions>
{
public OpenIdConnectOptionsSetup(IOptions<AzureAdB2COptions> b2cOptions)
{
AzureAdB2COptions = b2cOptions.Value;
}
public AzureAdB2COptions AzureAdB2COptions { get; set; }
public void Configure(string name, OpenIdConnectOptions options)
{
options.ClientId = AzureAdB2COptions.ClientId;
options.Authority = AzureAdB2COptions.Authority;
options.UseTokenLifetime = true;
//options.CallbackPath = new Microsoft.AspNetCore.Http.PathString("/console/home");
options.TokenValidationParameters = new TokenValidationParameters() { SaveSigninToken=true, NameClaimType = "name" };
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = OnRedirectToIdentityProvider,
OnRemoteFailure = OnRemoteFailure,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
OnTokenValidated= OnTokenValidated,
OnTokenResponseReceived= OnTokenResponseReceived
};
}
public void Configure(OpenIdConnectOptions options)
{
Configure(Options.DefaultName, options);
}
public Task OnRedirectToIdentityProvider(RedirectContext context)
{
var defaultPolicy = AzureAdB2COptions.DefaultPolicy;
if (context.Properties.Items.TryGetValue(AzureAdB2COptions.PolicyAuthenticationProperty, out var policy) &&
!policy.Equals(defaultPolicy))
{
context.ProtocolMessage.Scope = OpenIdConnectScope.OpenIdProfile;
context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress.ToLower().Replace(defaultPolicy.ToLower(), policy.ToLower());
context.Properties.Items.Remove(AzureAdB2COptions.PolicyAuthenticationProperty);
}
else if (!string.IsNullOrEmpty(AzureAdB2COptions.ApiUrl))
{
context.ProtocolMessage.Scope += $" offline_access {AzureAdB2COptions.ApiScopes}";
context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.CodeIdToken;
}
return Task.FromResult(0);
}
public Task OnRemoteFailure(RemoteFailureContext context)
{
context.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
context.Response.Redirect("/Session/ResetPassword");
}
else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied"))
{
context.Response.Redirect("/");
}
else
{
context.Response.Redirect("/Home/Error?message=" + context.Failure.Message);
}
return Task.FromResult(0);
}
public Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
// Use MSAL to swap the code for an access token
// Extract the code from the response notification
var code = context.ProtocolMessage.Code;
string signedInUserID = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, context.HttpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(AzureAdB2COptions.ClientId, AzureAdB2COptions.Authority, AzureAdB2COptions.RedirectUri, new ClientCredential(AzureAdB2COptions.ClientSecret), userTokenCache, null);
try
{
List<string> apiScopes = new List<string>();
AuthenticationResult result = cca.AcquireTokenByAuthorizationCodeAsync(code, AzureAdB2COptions.ApiScopes.Split(' ')).Result;
//context.HandleResponse();
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
context.Response.Redirect("http://localhost:8836/console/home?id_token="+result.IdToken+"&access_token="+result.AccessToken);
return Task.FromResult(0);
//context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
catch (Exception ex)
{
//TODO: Handle
throw;
}
}
public Task OnTokenValidated(TokenValidatedContext context)
{
try
{
return Task.FromResult(0);
}
catch (Exception ex)
{
throw;
}
}
public Task OnTokenResponseReceived(TokenResponseReceivedContext context)
{
try
{
var cntxt = context;
context.ProtocolMessage.RedirectUri = "/console/home";
context.Response.Redirect("/Home/Error?message=test");
return Task.FromResult(0);
}
catch (Exception ex)
{
throw;
}
}
}
}
And this is my startup class
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AzureAdB2COptions>(Configuration.GetSection("Authentication:AzureAdB2C"));
services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsSetup>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options))
.AddCookie();
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddMvc();
// Adds a default in-memory implementation of IDistributedCache.
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(1);
options.CookieHttpOnly = true;
});
}

You have to call
context.HandleResponse();
This method handle the response from the Auth server and grab the JWT.
On Microsoft Doc you can find additional info about OpenId Connect.

Related

How to get identity values from google external provider in ASP.NET Core Web API

[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
private readonly SignInManager<IdentityUser> _signInManager;
public AuthController(SignInManager<IdentityUser> signInManager)
{
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
}
[HttpGet("token")]
public ChallengeResult Token()
{
var properties = new GoogleChallengeProperties
{
RedirectUri = "/auth/retrieve",
AllowRefresh = true,
};
return Challenge(properties, "Google");
}
[HttpGet("[action]")]
public async Task Retrieve()
{
var token = await HttpContext.GetTokenAsync("access_token");
var externalLoginInfoAsync = await _signInManager.GetExternalLoginInfoAsync();
var identityName = User?.Identity?.Name;
var authenticateResult = await HttpContext.AuthenticateAsync();
}
}
I direct the user to /auth/token, where he is redirected to the Google Oauth Page, if successful, he is redirected to /auth/retrieve, where I expect the user data, but token, externalLoginInfoAsync, identityName, authenticateResult is null
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(Configuration.GetConnectionString("Default")));
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication()
.AddCookie()
.AddGoogle(options =>
{
options.Scope.Add("https://www.googleapis.com/auth/gmail.settings.basic");
options.AccessType = "offline";
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
options.Events.OnCreatingTicket = ctx =>
{
var identityName = ctx.Identity.Name;
return Task.CompletedTask;
};
options.ClientId = "SMTH_VALUE";
options.ClientSecret = "SMTH_VALUE";
});
services.AddControllers();
}
I debug the google provider and found the user values in the Events - identityName is not null.
How i can get this value in the controller?
You could refer the following code to configure Google authentication in Startup.ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication()
.AddGoogle(opt =>
{
opt.ClientId = "620831551062-rcvu44q4rhr5d8ossu3m0163jqbjdji0.apps.googleusercontent.com";
opt.ClientSecret = "GXFN0cHBbUlZ6nYLD7a7-cT8";
opt.SignInScheme = IdentityConstants.ExternalScheme;
});
services.AddControllersWithViews();
services.AddRazorPages();
}
Then, use the following sample to login using Google and get user information:
[Authorize]
public class AccountController : Controller
{
private UserManager<ApplicationUser> userManager;
private SignInManager<ApplicationUser> signInManager;
public AccountController(UserManager<ApplicationUser> userMgr, SignInManager<ApplicationUser> signinMgr)
{
userManager = userMgr;
signInManager = signinMgr;
}
// other methods
public IActionResult AccessDenied()
{
return View();
}
[AllowAnonymous]
public IActionResult GoogleLogin()
{
string redirectUrl = Url.Action("GoogleResponse", "Account");
var properties = signInManager.ConfigureExternalAuthenticationProperties("Google", redirectUrl);
return new ChallengeResult("Google", properties);
}
public IActionResult Login()
{
return View();
}
[AllowAnonymous]
public async Task<IActionResult> GoogleResponse()
{
ExternalLoginInfo info = await signInManager.GetExternalLoginInfoAsync();
if (info == null)
return RedirectToAction(nameof(Login));
var result = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, false);
string[] userInfo = { info.Principal.FindFirst(ClaimTypes.Name).Value, info.Principal.FindFirst(ClaimTypes.Email).Value };
if (result.Succeeded)
return View(userInfo);
else
{
ApplicationUser user = new ApplicationUser
{
Email = info.Principal.FindFirst(ClaimTypes.Email).Value,
UserName = info.Principal.FindFirst(ClaimTypes.Email).Value
};
IdentityResult identResult = await userManager.CreateAsync(user);
if (identResult.Succeeded)
{
identResult = await userManager.AddLoginAsync(user, info);
if (identResult.Succeeded)
{
await signInManager.SignInAsync(user, false);
return View(userInfo);
}
}
return AccessDenied();
}
}
}
The result like this:
More detail information, see How to integrate Google login feature in ASP.NET Core Identity and Google external login setup in ASP.NET Core

Custom Authorization attribute doesn't allow authorize in asp.net core 3

.Net core app authenticates with IdentityServer. I'm working without https.
I created custom authorization attribute, but it doesn't allow authorize. And I can't catch a problem.
Logs:
dbug: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[8]
AuthenticationScheme: Cookies was successfully authenticated.
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler: Debug: AuthenticationScheme: Cookies was successfully authenticated.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed.
info: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[13]
AuthenticationScheme: Cookies was forbidden.
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler: Information: AuthenticationScheme: Cookies was forbidden.
info: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[13]
AuthenticationScheme: oidc was forbidden.
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler: Information: AuthenticationScheme: oidc was forbidden.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAntiforgery(options =>
{
options.Cookie.HttpOnly = false;
options.HeaderName = "XSRF-TOKEN";
});
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Name = "app";
options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
options.SlidingExpiration = true;
})
.AddOpenIdConnect("oidc", options =>
{
options.Authority = $"{OpenId["ServerUrl"]}";
options.RequireHttpsMetadata = false;
options.ClientId = OpenId["ClientId"];
options.ClientSecret = OpenId["ClientSecret"];
options.ResponseType = "code";
options.UsePkce = true;
options.SignInScheme = "Cookies";
options.CallbackPath = "/signin-callback";
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("offline_access");
options.Scope.Add("profile");
options.Scope.Add("role");
options.Scope.Add("myapp");
options.ClaimActions.MapJsonKey("website", "website");
options.ClaimActions.MapJsonKey(UserClaimTypes.Role, UserClaimTypes.Role);
options.ClaimActions.MapJsonKey(UserClaimTypes.UserId, UserClaimTypes.UserId);
options.SaveTokens = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = UserClaimTypes.Role
};
options.Events = new OpenIdConnectEvents
{
OnTicketReceived = (e) =>
{
e.Properties.IsPersistent = true;
e.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(60);
return Task.CompletedTask;
}
};
});
services.AddSingleton<IAuthorizationPolicyProvider, AuthPolicyProvider>();
services.AddSingleton<IAuthorizationHandler, ArticlePermissionHandler>();
services.AddControllersWithViews(o =>
{
o.Filters.Add(typeof(ModelStateValidator));
o.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
})
.AddNewtonsoftJson();
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IDistributedCache cache,
IHttpContextAccessor httpContextAccessor,
ILogger<Startup> logger,
IAntiforgery antiforgery)
{
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllers();
});
ArticlePermissionRequirement.cs
using System;
using Microsoft.AspNetCore.Authorization;
namespace App.Application.Services.Authorization.Policies.Requirements
{
public class ArticlePermissionRequirement : IAuthorizationRequirement
{
public string PermissionName { get; }
public ArticlePermissionRequirement(string permissionName)
{
PermissionName = permissionName ?? throw new ArgumentNullException(nameof(permissionName));
}
}
}
AuthPolicyProvider.cs
using System.Threading.Tasks;
using App.Application.Services.Authorization.Operations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace App.Application.Services.Authorization.Policies.Providers
{
public class AuthPolicyProvider : DefaultAuthorizationPolicyProvider
{
public AuthPolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
{
}
public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
var policy = await base.GetPolicyAsync(policyName);
if (policy == null)
{
policy = new AuthorizationPolicyBuilder()
.AddRequirements(RequirementOperations.Requirement(policyName))
.Build();
}
return policy;
}
}
}
ArticlePermissionHandler.cs
namespace App.Application.Services.Authorization.Policies.Handlers
{
public class ArticlePermissionHandler : AuthorizationHandler<ArticlePermissionRequirement, Owner>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ArticlePermissionRequirement requirement,
Owner resource)
{
// for test purpose (always true) I putted this
context.Succeed(requirement);
var user = context.User;
if (user == null)
{
return Task.CompletedTask;
}
var roleClaim = user.Claims.FirstOrDefault(
c => c.Type == ClaimTypes.Role
)?.Value;
if (roleClaim == null)
{
return Task.CompletedTask;
}
// Administrator and Manager can do all things
if (user.IsInRole(Roles.Administrator) || user.IsInRole(Roles.Manager))
{
context.Succeed(requirement);
}
// Moderator can read, create, update all articles
else if (Equals(requirement.PermissionName, Permissions.ReadArticle)
|| Equals(requirement.PermissionName, Permissions.CreateArticle)
|| Equals(requirement.PermissionName, Permissions.UpdateArticle))
{
if (user.IsInRole(Roles.Moderator))
{
context.Succeed(requirement);
}
}
// Writer can create new article
else if (Equals(requirement.PermissionName, Permissions.CreateArticle))
{
if (user.IsInRole(Roles.Writer))
{
context.Succeed(requirement);
}
}
// Onwer can read, update, delete self article
else if (Equals(requirement.PermissionName, Permissions.ReadArticle)
|| Equals(requirement.PermissionName, Permissions.UpdateArticle)
|| Equals(requirement.PermissionName, Permissions.DeleteArticle))
{
ClaimsPrincipal claimUser = context.User;
var userClaimExtractor = new UserClaimExtractor(claimUser);
var userId = userClaimExtractor.GetId();
if (resource.OwnerId == userId)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
}
HasPermissionAttribute.cs
using Microsoft.AspNetCore.Authorization;
namespace App.Application.Services.Authorization.Policies.Attributes
{
public class HasPermissionAttribute : AuthorizeAttribute
{
public HasPermissionAttribute(string permissionName)
{
PermissionName = permissionName;
}
public string PermissionName
{
get
{
return Policy;
}
set
{
Policy = value;
}
}
}
}
Controller
public class DraftController : ApiController
{
public DraftController(IMediator mediator,
IHttpContextAccessor context) : base(mediator, context)
{
}
[HasPermission(Permissions.CreateArticle)]
[HttpPost("drafts")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Drafts([FromBody] JsonPatchDocument<DraftDto> patchDoc)
{
...
}
But there is an interest thing - when I rewrite attribute with authorize:
[Authorize]
[HttpPost("drafts")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Drafts(
Then app is authorized, and I can work with action Drafts. But with HasPermission attribute authorization always failed.
Where I can find a problem? Why the log writes: "Cookies was successfully authenticated" and then "Cookies was forbidden"?
Yes! I got! Problem was with resource Owner.
AuthorizationHandler<ArticlePermissionRequirement, Owner>
After remove this resource ArticlePermissionHandler started to work.
AuthorizationHandler<ArticlePermissionRequirement>
This info helped understand the problem.

Blazor authentication role based

I'm working on a client-side blazor application with the last webassembly version (3.2.0).
I started the project from the visual tool with enabling local authentications and I tried to add roles.
First, I added the roles in the ApplicationDbContext :
public class ApplicationDbContext : ApiAuthorizationDbContext<ApplicationUser>
{
public ApplicationDbContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<IdentityRole>()
.HasData(new IdentityRole { Name = "User", NormalizedName = "USER", Id = Guid.NewGuid().ToString(), ConcurrencyStamp = Guid.NewGuid().ToString() });
builder.Entity<IdentityRole>()
.HasData(new IdentityRole { Name = "Admin", NormalizedName = "ADMIN", Id = Guid.NewGuid().ToString(), ConcurrencyStamp = Guid.NewGuid().ToString() });
}
}
Then I added Roles to the IdentityBuilder in the startup class :
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
}
And then in my DbInitializer I created an Admin account with both roles :
private async Task SeedASPIdentityCoreAsync()
{
if (!await context.Users.AnyAsync())
{
var admin = new ApplicationUser()
{
UserName = "admin#admin.com",
Email = "admin#admin.com",
EmailConfirmed = true,
};
var result = await userManager.CreateAsync(admin, "aA&123");
if (!result.Succeeded)
{
throw new Exception(result.Errors.First().Description);
}
result = await userManager.AddClaimsAsync(admin, new Claim[]
{
new Claim(JwtClaimTypes.Email, "admin#admin.com"),
new Claim(JwtClaimTypes.Name, "admin#admin.com")
});
ApplicationUser user = await userManager.FindByNameAsync("admin#admin.com");
try
{
result = await userManager.AddToRoleAsync(user, "User");
result = await userManager.AddToRoleAsync(user, "Admin");
}
catch
{
await userManager.DeleteAsync(user);
throw;
}
if (!result.Succeeded)
{
await userManager.DeleteAsync(user);
throw new Exception(result.Errors.First().Description);
}
}
}
But the roles doen't appear in the JWT, and the client-side has no idea about the roles.
How can I add the roles in the JWT, as with the new version of blazor, there is no need of the LoginController ? (If i well understood the changes)
Ok I found what I needed :
1) Create a CustomUserFactory in your client App
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;
public class CustomUserFactory
: AccountClaimsPrincipalFactory<RemoteUserAccount>
{
public CustomUserFactory(IAccessTokenProviderAccessor accessor)
: base(accessor)
{
}
public async override ValueTask<ClaimsPrincipal> CreateUserAsync(
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
if (user.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)user.Identity;
var roleClaims = identity.FindAll(identity.RoleClaimType);
if (roleClaims != null && roleClaims.Any())
{
foreach (var existingClaim in roleClaims)
{
identity.RemoveClaim(existingClaim);
}
var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
if (rolesElem is JsonElement roles)
{
if (roles.ValueKind == JsonValueKind.Array)
{
foreach (var role in roles.EnumerateArray())
{
identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
}
}
else
{
identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
}
}
}
}
return user;
}
}
2) Register the client factory
builder.Services.AddApiAuthorization()
.AddAccountClaimsPrincipalFactory<CustomUserFactory>();
3) In the Server App, call IdentityBuilder.AddRoles
services.AddDefaultIdentity<ApplicationUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
4) Configure Identity Server
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
options.IdentityResources["openid"].UserClaims.Add("name");
options.ApiResources.Single().UserClaims.Add("name");
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
There is an other way, by creating a ProfileService
5) User Authorization mechanisms :
<AuthorizeView Roles="admin">
Source : https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/security/blazor/webassembly/hosted-with-identity-server.md#Name-and-role-claim-with-API-authorization

OAuth .NET Core - to many redirects after sign in with custom options - Multi Tenant

I'm trying to implement OAuth per tenant. Each tenant has their own OAuthOptions.
I overwritten OAuthOptions and with IOptionsMonitor i resolve the OAuthOptions every time. Based on this answer: openid connect - identifying tenant during login
But right now after log in on external, the callback to signin ends up in to many redirects.
Signin successfull -> redirect to app -> app says not authenticated -> redirect to external login -> repeat.
The code:
My Startup.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddAuthentication(options =>
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "OAuth";
})
.AddCookie("OAuth.Cookie", options =>
{
options.Cookie.Name = "OAuth-cookiename";
options.Cookie.SameSite = SameSiteMode.None;
options.LoginPath = "/account/login";
options.AccessDeniedPath = "/account/login";
})
.AddOAuth<MyOptions, OAuthHandler<MyOptions>>("OAuth", options =>
{
// All options are set at runtime by tenant settings
});
services.AddScoped<IOptionsMonitor<MyOptions>, MyOptionsMonitor>();
services.AddScoped<IConfigureOptions<MyOptions>, ConfigureMyOptions>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
Startup.Configure:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
ConfigureMyOptions.cs
public class ConfigureMyOptions : IConfigureNamedOptions<MyOptions>
{
private HttpContext _httpContext;
private IDataProtectionProvider _dataProtectionProvider;
private MyOptions myCurrentOptions;
public ConfigureMyOptions(IHttpContextAccessor contextAccessor, IDataProtectionProvider dataProtectionProvider)
{
_httpContext = contextAccessor.HttpContext;
_dataProtectionProvider = dataProtectionProvider;
}
public void Configure(string name, MyOptions options)
{
//var tenant = _httpContext.ResolveTenant();
// in my code i use tenant.Settings for these:
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
options.UserInformationEndpoint = "https://api.github.com/user";
options.ClientId = "redacted";
options.ClientSecret = "redacted";
options.Scope.Add("openid");
options.Scope.Add("write:gpg_key");
options.Scope.Add("repo");
options.Scope.Add("read:user");
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey("External", "id");
options.SignInScheme = "OAuth.Cookie";
options.CallbackPath = new PathString("/signin");
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = _onCreatingTicket,
OnTicketReceived = _onTicketReceived
};
myCurrentOptions = options;
}
public void Configure(MyOptions options) => Configure(Options.DefaultName, options);
private static async Task _onCreatingTicket(OAuthCreatingTicketContext context)
{
// Get the external user id and set it as a claim
using (var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint))
{
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
using (var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted))
{
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
}
}
private static Task _onTicketReceived(TicketReceivedContext context)
{
context.Properties.IsPersistent = true;
context.Properties.AllowRefresh = true;
context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30);
return Task.CompletedTask;
}
}
MyOptions:
// Overwritten to by pass validate
public class MyOptions : OAuthOptions
{
public override void Validate()
{
return;
}
public override void Validate(string scheme)
{
return;
}
}
MyOptionsMonitor:
// TODO caching
public class MyOptionsMonitor : IOptionsMonitor<MyOptions>
{
// private readonly TenantContext<Tenant> _tenantContext;
private readonly IOptionsFactory<MyOptions> _optionsFactory;
public MyOptionsMonitor(
// TenantContext<Tenant> tenantContext,
IOptionsFactory<MyOptions> optionsFactory)
{
// _tenantContext = tenantContext;
_optionsFactory = optionsFactory;
}
public MyOptions CurrentValue => Get(Options.DefaultName);
public MyOptions Get(string name)
{
return _optionsFactory.Create($"{name}");
}
public IDisposable OnChange(Action<MyOptions, string> listener)
{
return null;
}
}

Blazor server-side custom login using Identity lose logged state after restart

I implemented own solution to make login without page refresh. However can't figure out why I am losing logged-in state on application restart (new debug run).
Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DatabaseContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
services.AddIdentity<User, Role>(options =>
{ // options...
}).AddEntityFrameworkStores<DatabaseContext>().AddDefaultTokenProviders();
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo("/keys")).SetApplicationName("App").SetDefaultKeyLifetime(TimeSpan.FromDays(90));
services.AddRazorPages();
services.AddServerSideBlazor().AddCircuitOptions(options =>
{
options.DetailedErrors = true;
});
services.AddSession();
services.AddSignalR();
services.AddBlazoredLocalStorage();
services.AddHttpClient();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddAuthorization(options =>
{ // options...
});
// Add application services.
services.AddScoped<AuthenticationStateProvider, IdentityAuthenticationStateProvider>();
// .. services
services.AddMvc(options =>
{
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
}).AddSessionStateTempDataProvider();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseSession();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
AuthorizeController
[HttpPost("Login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (ModelState.IsValid)
{
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
Microsoft.AspNetCore.Identity.SignInResult result = await signInMgr.PasswordSignInAsync(model.LEmail, model.LPassword, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
return Ok();
return BadRequest(string.Join(", ", ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage)));
}
return BadRequest();
}
[HttpGet("UserInfo")]
public UserInfo UserInfo()
{
return new UserInfo
{
IsAuthenticated = User.Identity.IsAuthenticated,
UserName = User.Identity.Name,
ExposedClaims = User.Claims.ToDictionary(c => c.Type, c => c.Value)
};
}
I believe problem is in my AuthenticationStateProvider implementation
public class IdentityAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
readonly AuthorizeService authorizeSvc;
readonly IServiceScopeFactory scopeFactory;
readonly IdentityOptions options;
UserInfo userInfoCache;
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
public IdentityAuthenticationStateProvider(AuthorizeService authorizeService, ILoggerFactory loggerFactory, IServiceScopeFactory serviceScopeFactory, IOptions<IdentityOptions> optionsAccessor) : base(loggerFactory)
{
authorizeSvc = authorizeService;
scopeFactory = serviceScopeFactory;
options = optionsAccessor.Value;
}
public async Task LoginAsync(LoginViewModel model)
{
await authorizeSvc.LoginAsync(model);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task RegisterAsync(RegisterViewModel register)
{
await authorizeSvc.RegisterAsync(register);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task LogoutAsync()
{
await authorizeSvc.LogoutAsync();
userInfoCache = null;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
async Task<UserInfo> GetUserInfoAsync()
{
if (userInfoCache != null && userInfoCache.IsAuthenticated)
return userInfoCache;
userInfoCache = await authorizeSvc.GetUserInfo();
return userInfoCache;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
ClaimsIdentity identity = new ClaimsIdentity();
try
{
UserInfo userInfo = await GetUserInfoAsync();
if (userInfo.IsAuthenticated)
{
IEnumerable<Claim> claims = new[] { new Claim(ClaimTypes.Name, userInfoCache.UserName) }.Concat(userInfoCache.ExposedClaims.Select(c => new Claim(c.Key, c.Value)));
identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
}
}
catch (HttpRequestException ex)
{
Console.WriteLine("Request failed:" + ex.ToString());
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}
protected override async Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
IServiceScope scope = scopeFactory.CreateScope();
try
{
UserManager<User> userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync();
else
scope.Dispose();
}
}
async Task<bool> ValidateSecurityStampAsync(UserManager<User> userManager, ClaimsPrincipal principal)
{
User user = await userManager.GetUserAsync(principal);
if (user is null)
return false;
else if (!userManager.SupportsUserSecurityStamp)
return true;
string principalStamp = principal.FindFirstValue(options.ClaimsIdentity.SecurityStampClaimType);
string userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
AuthorizeService just calls httprequests like
public async Task<UserInfo> GetUserInfo()
{
HttpContext context = contextAccessor.HttpContext;
HttpClient client = clientFactory.CreateClient();
client.BaseAddress = new Uri($"{context.Request.Scheme}://{context.Request.Host}");
string json = await client.GetStringAsync("api/Authorize/UserInfo");
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(json);
}
I noticed in Chrome developer tools that cookies is unchanged after login. This is probably main issue. Any idea how to fix it?
Thanks
AuthorizeService has to pass cookies. For server-side on prerendering pass cookies from HttpContext, at runtime pass cookies from javascript via IJSRuntime injection.
Implemntation looks like for custom AuthenticationStateProvider
async Task<string> GetCookiesAsync()
{
try
{
return $".AspNetCore.Identity.Application={await jsRuntime.InvokeAsync<string>("getLoginCookies")}";
}
catch
{
return $".AspNetCore.Identity.Application={httpContextAccessor.HttpContext.Request.Cookies[".AspNetCore.Identity.Application"]}";
}
}
public async Task<UserInfo> GetUserInfoAsync()
{
if (userInfoCache != null && userInfoCache.IsAuthenticated)
return userInfoCache;
userInfoCache = await authorizeSvc.GetUserInfo(await GetCookiesAsync());
return userInfoCache;
}
AuthorizeService
public async Task<UserInfo> GetUserInfo(string cookie)
{
string json = await CreateClient(cookie).GetStringAsync("api/Authorize/UserInfo");
return Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(json);
}
HttpClient CreateClient(string cookie = null)
{
HttpContext context = contextAccessor.HttpContext;
HttpClient client = clientFactory.CreateClient();
client.BaseAddress = new Uri($"{context.Request.Scheme}://{context.Request.Host}");
if(!string.IsNullOrEmpty(cookie))
client.DefaultRequestHeaders.Add("Cookie", cookie);
return client;
}
There is also need to mention need to do following steps for following actions
Login/Register
SignIn
GetUserInfo (including cookies)
Write cookie via javascript
Logout
Remove cookie via javascript
SignOut