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

.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.

Related

How to delete cookie on browser close

I'm implementing asp.net core project and I authenticate the user via ldap by using Novell.Directory.Ldap.NETStandard. Now my problem is How can I delete cookie after the user close the browser. I want whenever the user close the browser and opens the system again, he confronts with login page. here below is what I've tried in startup for setting cookie:
services.AddScoped<Ldap.IAuthenticationService, LdapAuthenticationService>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
options.LoginPath = "/Account/Login";
options.SlidingExpiration = true;
});
And here is my AccountController:
public class AccountController : Controller {
private readonly Ldap.IAuthenticationService _authenticationService;
private readonly IHttpContextAccessor _httpContext;
public AccountController(Ldap.IAuthenticationService authenticationService,IHttpContextAccessor context)
{
_httpContext = context;
_authenticationService = authenticationService;
}
public IActionResult Login()
{
return View();
}
[HttpPost]
// [ChildActionOnly]
public async Task<IActionResult> Login(LoginModel model)
{
LdapEntry result1 = null;
var result = _authenticationService.ValidateUser1("mm.fr", model.UserName, model.Password);
if (result.Equals(true))
{
result1 = _authenticationService.GetLdapUserDetail("mm.fr", model.UserName, model.Password);
ViewBag.Username = result1.GetAttribute("CN").StringValue;
Index(result1.GetAttribute("CN").StringValue);
if (result != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, model.UserName),
new Claim(ClaimTypes.Role, "Administrator"),
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
}
else
{
this.TempData["ErrorMessage"] = "Password is incorrect";
}
return RedirectToAction(nameof(MainDashboardController.Index), "MainDashboard");
}
public IActionResult Index(string str)
{
_httpContext.HttpContext.Session.SetString("mystr", str);
return View();
}
}
Here below is what I wrote in Startup:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<CSDDashboardContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("CSDDashboardContext")));
//Notice this is NOT the same class... Assuming this is a valid DBContext. You need to add this class as well.
//services.AddDbContext<CSSDDashboardContext>(options =>
// options.UseSqlServer(Configuration.GetConnectionString("CSDDashboardContext")));
//*---------------------------New Active Directory--------------------------------
services.AddScoped<IAuthenticationService, LdapAuthenticationService>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
options.LoginPath = "/Account/Login";
options.SlidingExpiration = true;
});
services.AddSession();
services.AddSingleton<MySharedDataViewComponent>();
services.AddHttpContextAccessor();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseSession();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
function deleteCookie(name) {
setCookie(name,"",-1);
}
function setCookie(name,value,days) {
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
}
else expires = "";
document.cookie = name+"="+value+expires+"; path=/";
}
$(window).unload(function() {
deleteCookie('Your cookie to delete name');
});

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

SignalR Core Custom Authentication - Context.User.Identity is null after user is authenticated in /negotiate

I wrote a custom authentication for SignalR Core. One of the feature is anonymous login. It will create new user if it's first time user connect. The code work but the problem is the authentication done after /myhub/negotiate is cleared and all the claims in Context.User.Identity is cleared again and IsAuthenticated change to false when the client request /myhub/. Only after that the claims in Context.User.Identity is not cleared. I tried to return Fail if it's request to /myhub/negotiate but then the client won't send request to /myhub/ if I do that.
Any idea on how to fix or work around this? Is my implement of custom authentication correct?
Here is the code of all the class I'm using:
public class CustomAuthRequirementHandler : AuthorizationHandler<CustomAuthRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomAuthRequirement requirement)
{
string name = context.User.Claims.Where(p => p.Type == ClaimTypes.NameIdentifier).Select(p => p.Value).SingleOrDefault();
if (!context.User.Identity.IsAuthenticated)
context.Fail();
else
context.Succeed(requirement);
return Task.CompletedTask;
}
}
public class CustomAuthRequirement : IAuthorizationRequirement
{
}
public class MyAuthenticationHandler : AuthenticationHandler<MyOptions>
{
public MyAuthenticationHandler(IOptionsMonitor<MyOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Context.User.Identity != null && Context.User.Identity.IsAuthenticated) return await Task.FromResult(
AuthenticateResult.Success(
new AuthenticationTicket(
new ClaimsPrincipal(Options.Identity),
new AuthenticationProperties(),
this.Scheme.Name)));
//if (Request.Path != "/myhub/") return await Task.FromResult(AuthenticateResult.Fail()); // only do authentication in /myhub/
var u = CreateNewUser(); // connect to db create new user
var claims = new List<Claim>() { };
claims.Add(new Claim(ClaimTypes.Name, u.Id.ToString()));
claims.Add(new Claim(ClaimTypes.NameIdentifier, u.Id.ToString()));
Options.Identity = new ClaimsIdentity(claims, "Custom");
var user = new ClaimsPrincipal(Options.Identity);
Context.User = user;
return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(user, new AuthenticationProperties(), this.Scheme.Name)));
}
}
public class MyOptions : AuthenticationSchemeOptions
{
public ClaimsIdentity Identity { get; set; }
public MyOptions()
{
}
}
The configuration code in ConfigureServices
services.AddSingleton<IAuthorizationHandler, CustomAuthRequirementHandler>();
services.AddAuthorization(p =>
{
p.AddPolicy("MainPolicy", builder =>
{
builder.Requirements.Add(new CustomAuthRequirement());
builder.AuthenticationSchemes = new List<string> { "MyScheme" };
});
});
services.AddAuthentication(o =>
{
o.DefaultScheme = "MyScheme";
}).AddScheme<MyOptions, MyAuthenticationHandler>("MyScheme", "MyScheme", p => { });
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSignalR();
Configure code
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/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.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSignalR(routes =>
{
routes.MapHub<Hubs.MainHub>("/main");
});
app.UseMvc();
}
Edit: Added client side code
#page
#{
ViewData["Title"] = "Home page";
}
<input type="button" onclick="anonLogin()" value="AnonLogin" />
<script src="~/##aspnet/signalr/dist/browser/signalr.js"></script>
<script type="text/javascript">
var connection;
function anonLogin() {
var token = "anon";
connection = new signalR.HubConnectionBuilder().withUrl("/main?anon=" + token).build();
connection.start().then(function () {
console.log("Connection ok");
console.log("Sending message....");
connection.invoke("Test").catch(function (err) {
return console.error("Error sending message: " + err.toString());
});
}).catch(function (err) {
console.log("Connection error: " + err.toString());
return console.error(err.toString());
});
}
</script>
I ended up creating a fake claims for identity just for the call to /myhub/negotiate since this call is unimportant and it just needs the authentication to success so it can go to /myhub/.
var u = new DomainUser() { Id = -1 };
var claims = new List<Claim>() { };
claims.Add(new Claim(ClaimTypes.Name, u.Id.ToString()));
claims.Add(new Claim(ClaimTypes.NameIdentifier, u.Id.ToString()));
Options.Identity = new ClaimsIdentity(claims, "Custom");
var user = new ClaimsPrincipal(Options.Identity);
Context.User = user;
return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, new AuthenticationProperties(), this.Scheme.Name)));

Redirection doesn't happen in open id "OnAuthorizatioCodeReceived"

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.