I created a new react web application using visual studio and the react application template: https://learn.microsoft.com/en-us/aspnet/core/client-side/spa/react?view=aspnetcore-5.0&tabs=visual-studio
When I created the app, I also chose the Individual user accounts authentication option:
I created an authorization policy like this:
services.AddAuthorization(config =>
{
config.AddPolicy("ShouldBeAdmin",
options => options.RequireClaim("Admin"));
});
My user in the aspnet identity database had the claim associated with it:
When I log in with my user, the jwt token I get does not contain the Admin claim, so endpoints protected with my authorization rule do not work. How do I get the claims into the jwt token?
I got a workaround for this problem. While this doesn't add the claims to the jwt token, I can look up the claims from the database each time a request comes in using the onTokenValidated event. Something like this:
services.Configure<JwtBearerOptions>(
IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
var onTokenValidated = options.Events.OnTokenValidated;
options.Events.OnTokenValidated = async context =>
{
await onTokenValidated(context);
var userManger = context.HttpContext.RequestServices
.GetRequiredService<UserManager<ApplicationUser>>();
var user = await userManger.FindByIdAsync(context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value);
if (user == null)
{
return;
}
var claims = await userManger.GetClaimsAsync(user);
var appIdentity = new ClaimsIdentity(claims);
context.Principal?.AddIdentity(appIdentity);
};
});
This solution is based on this Microsoft documentation: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-5.0#customize-the-api-authentication-handler
And this blob post:
https://joonasw.net/view/adding-custom-claims-aspnet-core-2
I'm still looking into implementing IProfileService based on abdusco's comment.
Related
I have an app that will operate almost entirely on Spotify OAuth, that will have features to alter the playback of your music.
I'm able to get Spotify OAuth working perfectly such that I can log into my app, but after I've logged in, I need access to the current user's spotify access_token so that I can forward it to my spotify requests.
I followed this guide from ms to try to save the tokens: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/?view=aspnetcore-6.0&tabs=visual-studio
And I have tried all these ways to then save that token into the HttpContext such that I can access it:
options.Events.OnCreatingTicket = ctx =>
{
List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();
tokens.Add(new AuthenticationToken()
{
Name = "TicketCreated",
Value = DateTime.UtcNow.ToString()
});
var spotifyAccessToken = tokens.FirstOrDefault(x => x.Name == "access_token").Value;
tokens.Add(new AuthenticationToken()
{
Name = "SpofityAccessToken",
Value = spotifyAccessToken
});
//store all the tokens as directed by MS
ctx.Properties.StoreTokens(tokens);
//store the properties into the HttpContext in 2 different ways
ctx.HttpContext.Items["Properties"] = ctx.Properties;
ctx.HttpContext.Features.Set(ctx.Properties);
//try adding a claim to the user
ctx.Identity.AddClaims(new[] { new Claim("SpotifyAccessToken", spotifyAccessToken) });
return Task.CompletedTask;
};
The problem I'm having is how do I then get this token out? all of these methods are not working:
[HttpGet]
public async Task Get()
{
await HttpContext.SignInAsync(User);
// try to re-run Authenticate, even though i'm already in an [Authorize] controller
var res = await HttpContext.AuthenticateAsync();
//props2 does not have the tokens i set
var props2 = res.Properties;
//props comes back null
var props = HttpContext.Features.Get<AuthenticationProperties>();
//claims has no SpotifyAccessToken claim
var claims = User.Claims.ToList();
var token = "hard-coded";
//here is where i need the token to then query spotify
var client = new SpotifyAPI.Web.SpotifyClient(token);
var res2 = await client.Player.GetCurrentPlayback();
}
I feel like I've tried everything, what am i doing wrong?
This is in a .NET 6 blazor wasm, .net core hosted app.
Also tried the solutions here to no avail Get AuthenticationProperties in current HttpRequest after HttpContext.SignInAsync
signInManager.UpdateExternalAuthenticationTokensAsync adds the the authentication tokens in [dbo].[AspNetUserTokens]
External login is where I call it:
// Sign in the user with this external login provider if the user already has a login.
var signInResult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: true, bypassTwoFactor: true);
if (signInResult.Succeeded)
{
await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
_logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
return LocalRedirect(returnUrl);
}
Later on you can get it by using :
var token = await userManager
.GetAuthenticationTokenAsync(user, "Spotify", "access_token");
var expiresAtStr = await userManager
.GetAuthenticationTokenAsync(user, "Spotify", "expires_at");
If the token is stored in the Cookie then you can access the various tokens using:
string accessToken = await HttpContext.GetTokenAsync("access_token");
string idToken = await HttpContext.GetTokenAsync("id_token");
string refreshToken = await HttpContext.GetTokenAsync("refresh_token");
string tokenType = await HttpContext.GetTokenAsync("token_type");
string accessTokenExpire = await HttpContext.GetTokenAsync("expires_at");
However, you can not store data in ctx.HttpContext and assume it will be persisted across requests. either you sign-in the user using the cookie middleware or you store the tokens in the UserSession object.
See this article on how to configure and store data in the session, that will be persisted across requests.
Session and state management in ASP.NET Core
If you configure it properly, then you can use it like:
HttpContext.Session.SetString("token", token.Trim());
Problem: I seem unable to fetch the User or any user-related data (e.g. UserID) in any controller after the token has been recorded to browser local storage.
I've set a breakpoint and studied HttpContext member of ControllerBase instance (the client app makes request + the auth_token is kept in local storage at this stage).
You only can extract the referrer url from Headers but there's no info about tokens.
Even if you create a cookie for the token - the Request doesn't have it (0 cookies found at all).
Perhaps I misunderstand the concept of how authorization works.
Here's the bit I misunderstand most - how does ASP.NET Core fetch the token from the request made by client app - it must be kept in headers?
Also, could anyone share a working example of JWT Authentication where Angular & ASP.NET Core are separate solutions?
I've implemented login functionality and I store the access token in browser local storage.
this._authService.authenticate(this.loginModel)
.finally(finallyCallback)
.subscribe((result: AuthenticateOutput) => {
localStorage.setItem('auth_token', result.token);
});
Must the name of the token be in accordance with any conventions? (I wonder if auth_token is appropriate in this case.)
SessionController - the method which fetches current user info:
public async Task<GetCurrentLoginDetailsOutput> GetCurrentLoginDetails()
{
var output = new GetCurrentLoginDetailsOutput();
var user = await UserManager.GetUserAsync(HttpContext.User);
if (user != null)
{
output.User = Mapper.Map<UserDto>(user);
output.Tenant = Mapper.Map<TenantDto>(user.Tenant);
}
return output;
}
In my Authenticate method of AuthContoller I create Claim which holds UserID:
var user = await _userService.Authenticate(input.UserName, input.Password);
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = _config.GetValidIssuer(),
Audience = _config.GetValidAudience(),
SigningCredentials = new SigningCredentials(_config.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256),
Subject = new ClaimsIdentity(new[]
{
new Claim("id", user.Id.ToString())
})
};
_userService.Authenticate method fetches the user and checks if the password is correct as follows:
var user = _context.Users.SingleOrDefault(x => x.UserName == username);
if (user == null) { return null; }
bool correctPassword = await UserManager.CheckPasswordAsync(user, password);
JWT config in Startup.cs
services
.AddAuthentication()
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = Configuration.GetSymmetricSecurityKey(),
ValidAudience = Configuration.GetValidAudience(),
ValidIssuer = Configuration.GetValidIssuer()
};
});
CORS is configured as follows:
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
Additional info:
The Angular app is a separate solution / project - not the "one solution" template available in VS2017.
I'm using ASP.NET Core v2.1
I'm using NSwag.AspNetCore package to auto-generate services for Angular project.
Here's the tutorial which I've been using to code my app.
I have a web api project based on .net core 2.0.
I followed pretty much the very good example on http://kevinchalet.com/2017/01/30/implementing-simple-token-authentication-in-aspnet-core-with-openiddict/.
The code that returns the SignIn() result for the auth. method looks like so:
if (request.IsPasswordGrantType())
{
// (...)
if (useraccount != null && useraccount.Failcount <= AppConstants.AuthMaxAllowedFailedLogin)
{
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme, OpenIdConnectConstants.Claims.Name, OpenIdConnectConstants.Claims.Role);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, AppConstants.AuthSubjectClaim, OpenIdConnectConstants.Destinations.AccessToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Name, useraccount.Username, OpenIdConnectConstants.Destinations.AccessToken);
return SignIn(new ClaimsPrincipal(identity), OpenIdConnectServerDefaults.AuthenticationScheme);
}
// (...)
}
My startup code looks like so:
services.AddDbContext<DbContext>(options =>
{
options.UseInMemoryDatabase(nameof(DbContext));
options.UseOpenIddict();
});
services.AddOpenIddict(options =>
{
options.AddEntityFrameworkCoreStores<DbContext>();
options.AddMvcBinders();
options.EnableTokenEndpoint(DcpConstants.ApiTokenRoute);
options.AllowPasswordFlow();
options.AllowRefreshTokenFlow();
options.SetAccessTokenLifetime(TimeSpan.FromHours(1));
options.SetRefreshTokenLifetime(TimeSpan.FromDays(1));
options.DisableHttpsRequirement();
});
services.AddAuthentication(options =>
{
options.DefaultScheme = OAuthValidationDefaults.AuthenticationScheme;
}).AddOAuthValidation();
Now, when I send the post request with the following params:
username: foo#bar.com
password: myPassword
grant_type: password
scope: openid profile offline_access
I only receive scope, token_type, access_token, expires_in and id_token and no refresh_token.
What am I missing?
Returning a refresh token with the password is definitely allowed by the OAuth2 specification and thus, fully supported by OpenIddict.
For a refresh token to be returned by OpenIddict, you have to grant the special offline_access scope when calling SignIn. E.g:
if (request.IsPasswordGrantType())
{
// (...)
if (useraccount != null && useraccount.Failcount <= AppConstants.AuthMaxAllowedFailedLogin)
{
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme, OpenIdConnectConstants.Claims.Name, OpenIdConnectConstants.Claims.Role);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, AppConstants.AuthSubjectClaim, OpenIdConnectConstants.Destinations.AccessToken);
identity.AddClaim(OpenIdConnectConstants.Claims.Name, useraccount.Username, OpenIdConnectConstants.Destinations.AccessToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
// You have to grant the 'offline_access' scope to allow
// OpenIddict to return a refresh token to the caller.
ticket.SetScopes(OpenIdConnectConstants.Scopes.OfflineAccess);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
// (...)
}
Note that you'll also have to handle the grant_type=refresh_token requests in your controller. Here's an example using Identity: https://github.com/openiddict/openiddict-samples/blob/dev/samples/RefreshFlow/AuthorizationServer/Controllers/AuthorizationController.cs#L75-L109
options.AllowPasswordFlow();
Refresh Token cannot be used with Password flow, as the user is never redirected to login at Auth Server in this flow and so can’t directly authorize the application:
If the application uses the username-password OAuth authentication flow, no refresh token is issued, as the user cannot authorize the application in this flow. If the access token expires, the application using username-password OAuth flow must re-authenticate the user.
I have a default ASP.NET Core website created within Visual Studio 2017. I have chosen to authenticate using an Azure Active Directory.
I run the site and can successfully login using an account in the Active Directory.
I can retrieve Claim information provided by Active Directory, e.g. by calling the following line I get the 'name'.
User.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
I want to add a custom claim - CompanyId = 123456 for the logged in user.
I'm able to add a custom claim however it is only available on the page where the claim is set.
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
((ClaimsIdentity)User.Identity).AddClaim(claim);
My understanding is that I somehow need to update the token that has been issued by Active Directory or set the claim before the token is issued. I'm unsure how to do this.
I suspect this needs to be done in the AccountController at SignIn()
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);
}
I've read numerous articles and samples about this scenario (including https://github.com/ahelland/AADGuide-CodeSamples/tree/master/ClaimsWebApp) however have not managed to solve how to persist the Claim across requests.
I have successfully managed to persist custom Claims using ASP.NET Identity as the Authentication Provider, but this appears to be because the custom Claim is saved to the database..
OnTokenValidated offers you the chance to modify the ClaimsIdentity obtained from the incoming token , code below is for your reference :
private Task TokenValidated(TokenValidatedContext context)
{
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
(context.Ticket.Principal.Identity as ClaimsIdentity).AddClaim(claim);
return Task.FromResult(0);
}
Setting the OpenIdConnectEvents:
Events = new OpenIdConnectEvents
{
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
OnTokenValidated = TokenValidated
}
Then in controller using :
var companyId= User.Claims.FirstOrDefault(c => c.Type == "CompanyId")?.Value;
For those who would like more detail, the code provided is placed in Startup.cs
In the Configure method add/edit:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAd:ClientId"],
Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"],
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
Events = new OpenIdConnectEvents
{
OnTokenValidated = TokenValidated
}
});
The private Task TokenValidated method is in the body of Startup.cs
The following sample is a good reference.
https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore-v2/blob/master/WebApp-OpenIDConnect-DotNet/Startup.cs
I have a WebAPI 2 project that uses a token issued by an IdentityServer3 token provider. In my Startup.cs file I have the IdentityServerBearerTokenAuthorization middleware implemented and it, along with a global AuthorizateAttribute filter, is requiring that a valid token be present in the request. However, I have also added ClaimsTransformation so I can extract "roles" from the claims in either a token issued using the implicit flow or a token issued for the client credential flow. I can't use a scope here because, I have 1 scope that gives you access to use my API, but all clients are not allowed to use all api endpoints.
Startup.cs
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions()
{
Authority = ConfigurationManager.AppSettings["IdentityServer"],
RequiredScopes = new[] { "my.api" },
});
httpConfig.MapHttpAttributeRoutes();
httpConfig.Filters.Add(new AuthorizeAttribute());
//SwaggerConfig.Register(httpConfig);
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(httpConfig);
app.UseWebApi(httpConfig);
app.UseClaimsTransformation(identity =>
{
var principal = new ClaimsPrincipal(identity);
if (!identity.HasClaim(c => c.Type == "name") && identity.HasClaim(c => c.Type == "client_name"))
{
identity.Identities.First().AddClaim(new Claim("name", identity.Claims.First(c => c.Type == "client_name").Value));
}
//we want to remove the client_ from the claims so we can evaluate clients like they are users
if (identity.Claims.Any(c => c.Type.Contains("client_")))
{
foreach (var claim in identity.Claims.Where(c => c.Type.Contains("client_")))
{
var newClaimType = claim.Type.Replace("client_", "");
identity.Identities.First().AddClaim(new Claim(newClaimType, claim.Value));
}
}
//set the scopes as roles also
if (identity.Claims.Any(c => c.Type == "scope"))
{
identity.Identities.First().AddClaims(identity.Claims.Where(c => c.Type == "scope").Select(c => new Claim("role", c.Value)));
}
return Task.FromResult(principal);
});
On my APIController operation, I have an Authorize attribute with a Roles property defined. The global Authorize attribute is working but the check for roles never happens. Am I missing something? \
API Controller
[HttpDelete]
[Authorize(Roles = "item.deleter")]
[Route("{itemId:guid}")]
public async Task<HttpResponseMessage> DeleteAsync([ValidGuid] Guid itemId)
{
_log.Audit.Info($"Received Delete request for item {itemId} from user {User.Identity?.Name}.");
if (!ModelState.IsValid)
....
Your authroize attribute is most likely firing before your claims transform fires.
In your owin pipeline, you've added webAPI before the claims transform. As your request travels along the pipeline, web api will get the request & run the authorize against it before the claims transform can do its bit.
Try moving the UseClaimsTransformation before the UseWebApi