[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
Related
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');
});
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
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;
}
}
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
Good day.
The API is for a Quote sharing web-app.
I have setup role-based JWT authentication where I have "Member" and "Admin" roles with users of those roles correctly registered and able to retrieve tokens.
So far, methods (or classes) with only
[Authorize]
can be correctly accessed provided a registered token.
Now once I added roles, access to methods or classes that require a certain role
[Authorize(Role="Admin")]
is Forbidden (403), even though I do pass a correct token with the Authorization header.
Please note: I have verified that users are correctly created (dbo.AspNetUsers), roles are correctly created (dbo.AspNetRoles containing "Admin" and "Member" roles) and user-roles are correctly mapped (dbo.AspNetUserRoles).
This is the Startup class which contains a method CreateRoles() that's called by Configure():
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<QuotContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<Member, IdentityRole>()
.AddEntityFrameworkStores<QuotContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = false;
options.Password.RequiredLength = 4;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 2;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
});
services.AddLogging(builder =>
{
builder.AddConfiguration(Configuration.GetSection("Logging"))
.AddConsole()
.AddDebug();
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider, QuotContext dbContext)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
app.UseDatabaseErrorPage();
}
app.UseAuthentication();
app.UseMvc();
dbContext.Database.EnsureCreated();
CreateRoles(serviceProvider).Wait();
}
private async Task CreateRoles(IServiceProvider serviceProvider)
{
//initializing custom roles
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<Member>>();
string[] roleNames = { "Admin", "Member" };
IdentityResult roleResult;
foreach (var roleName in roleNames)
{
var roleExist = await RoleManager.RoleExistsAsync(roleName);
if (!roleExist)
roleResult = await RoleManager.CreateAsync(new IdentityRole(roleName));
}
var poweruser = new Member
{
UserName = Configuration["AppSettings:AdminEmail"],
Email = Configuration["AppSettings:AdminEmail"],
};
string password = Configuration["AppSettings:AdminPassword"];
var user = await UserManager.FindByEmailAsync(Configuration["AppSettings:AdminEmail"]);
if (user == null)
{
var createPowerUser = await UserManager.CreateAsync(poweruser, password);
if (createPowerUser.Succeeded)
await UserManager.AddToRoleAsync(poweruser, "Admin");
}
}
}
This is the MembersController class containing Register() and Login() methods:
[Authorize]
public class MembersController : Controller
{
private readonly QuotContext _context;
private readonly UserManager<Member> _userManager;
private readonly SignInManager<Member> _signInManager;
private readonly ILogger<MembersController> _logger;
private readonly IConfiguration _configuration;
public MembersController(QuotContext context, UserManager<Member> userManager,
SignInManager<Member> signInManager, ILogger<MembersController> logger,
IConfiguration configuration)
{
_context = context;
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_configuration = configuration;
}
[HttpPost("register")]
[AllowAnonymous]
public async Task<IActionResult> Register([FromBody] RegisterModel model)
{
if (ModelState.IsValid)
{
var newMember = new Member
{
UserName = model.Email,
Email = model.Email,
PostCount = 0,
Reputation = 10,
ProfilePicture = "default.png"
};
var result = await _userManager.CreateAsync(newMember, model.Password);
if (result.Succeeded)
{
_logger.LogInformation(1, "User registered.");
await _signInManager.SignInAsync(newMember, false);
return Ok(new { token = BuildToken(model.Email, newMember) });
}
_logger.LogInformation(1, "Registeration failed.");
return BadRequest();
}
return BadRequest();
}
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.Email,
model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation(1, "User logged in." + _configuration["AppSettings:AdminPassword"]);
var member = _userManager.Users.SingleOrDefault(r => r.Email == model.Email);
return Ok(new { token = BuildToken(model.Email, member) });
}
_logger.LogInformation(1, "Login failed.");
return BadRequest();
}
return BadRequest(ModelState);
}
private string BuildToken(string email, Member member)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, member.Id)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expires = DateTime.Now.AddDays(Convert.ToDouble(_configuration["JwtExpireDays"]));
var token = new JwtSecurityToken(
_configuration["JwtIssuer"],
_configuration["JwtIssuer"],
claims,
expires: expires,
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
Here's the example of 2 methods: the first requiring simple authentication which is successfully accessed provided a user token, and the second which is forbidden even given an admin token:
public class AuthorsController : Controller
{
private readonly QuotContext _context;
public AuthorsController(QuotContext context)
{
_context = context;
}
[HttpGet]
[Authorize]
public IEnumerable<Author> GetAuthors()
{
return _context.Authors;
}
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> PostAuthor([FromBody] Author author)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_context.Authors.Add(author);
await _context.SaveChangesAsync();
return StatusCode(201);
}
}
Thank you for your help.
A github repo containing the full project: https://github.com/theStrayPointer/QuotAPI
I got the same problem. I've just find a way. In fact, a JWT token embeds the roles. So you have to add role claims in your token when you generate it.
var roles = await _userManager.GetRolesAsync(user);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(dateTime).ToString(), ClaimValueTypes.Integer64)
};
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token");
// Adding roles code
// Roles property is string collection but you can modify Select code if it it's not
claimsIdentity.AddClaims(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var token = new JwtSecurityToken
(
_configuration["Auth:Token:Issuer"],
_configuration["Auth:Token:Audience"],
claimsIdentity.Claims,
expires: dateTime,
notBefore: DateTime.UtcNow,
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Auth:Token:Key"])), SecurityAlgorithms.HmacSha256)
);
Found an explanation here and here.