Invalid Access Token/Missing Claims when logged into IdentityServer4 - oauth-2.0

I have a standard .NET Core 2.1 (MVC and API) and Identity Server 4 project setup.
I am using reference tokens instead of jwt tokens.
The scenario is as follows:
Browse to my application
Redirected to Identity Server
Enter valid valid credentials
Redirected back to application with all claims (roles) and correct access to the application and API
Wait an undetermined amount of time (I think it's an hour, I don't have the exact timing)
Browse to my application
Redirected to Identity Server
I'm still logged into the IDP so I'm redirected immediately back to my
application
At this point the logged in .NET user is missing claims (roles) and no longer has access to the API
The same result happens if I delete all application cookies
It seems obvious to me that the access token has expired. How do I handle this scenario? I'm still logged into the IDP and the middleware automatically logged me into my application, however, with an expired (?) access token and missing claims.
Does this have anything to do with the use of reference tokens?
I'm digging through a huge mess of threads and articles, any guidance and/or solution to this scenario?
EDIT: It appears my access token is valid. I have narrowed my issue down to the missing user profile data. Specifically, the role claim.
When I clear both my application and IDP cookies, everything works fine. However, after "x" (1 hour?) time period, when I attempt to refresh or access the application I am redirected to the IDP then right back to the application.
At that point I have a valid and authenticated user, however, I am missing all my role claims.
How can I configure the AddOpenIdConnect Middleware to fetch the missing claims in this scenario?
I suppose in the OnUserInformationReceived event I can check for the missing "role" claim, if missing then call the UserInfoEndpoint...that seems like a very odd workflow. Especially since on a "fresh" login the "role" claim comes back fine. (Note: I do see the role claim missing from the context in the error scenario).
Here is my client application configuration:
services.AddAuthentication(authOpts =>
{
authOpts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
authOpts.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opts => { })
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, openIdOpts =>
{
openIdOpts.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
openIdOpts.Authority = settings.IDP.Authority;
openIdOpts.ClientId = settings.IDP.ClientId;
openIdOpts.ClientSecret = settings.IDP.ClientSecret;
openIdOpts.ResponseType = settings.IDP.ResponseType;
openIdOpts.GetClaimsFromUserInfoEndpoint = true;
openIdOpts.RequireHttpsMetadata = false;
openIdOpts.SaveTokens = true;
openIdOpts.ResponseMode = "form_post";
openIdOpts.Scope.Clear();
settings.IDP.Scope.ForEach(s => openIdOpts.Scope.Add(s));
// https://leastprivilege.com/2017/11/15/missing-claims-in-the-asp-net-core-2-openid-connect-handler/
// https://github.com/aspnet/Security/issues/1449
// https://github.com/IdentityServer/IdentityServer4/issues/1786
// Add Claim Mappings
openIdOpts.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username"); /* SID alias */
openIdOpts.ClaimActions.MapJsonKey("role", "role", "role");
openIdOpts.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = settings.IDP.ClientId,
ValidIssuer = settings.IDP.Authority,
NameClaimType = "name",
RoleClaimType = "role"
};
openIdOpts.Events = new OpenIdConnectEvents
{
OnUserInformationReceived = context =>
{
Log.Info("Recieved user info from IDP.");
// check for missing roles? they are here on a fresh login but missing
// after x amount of time (1 hour?)
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = context =>
{
Log.Info("Redirecting to identity provider.");
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
Log.Debug("OnTokenValidated");
// this addressed the scenario where the Identity Server validates a user however that user does not
// exist in the currently configured source system.
// Can happen if there is a configuration mismatch between the local SID system and the IDP Client
var validUser = false;
int uid = 0;
var identity = context.Principal?.Identity as ClaimsIdentity;
if (identity != null)
{
var sub = identity.Claims.FirstOrDefault(c => c.Type == "sub");
Log.Debug($" Validating sub '{sub.Value}'");
if (sub != null && !string.IsNullOrWhiteSpace(sub.Value))
{
if (Int32.TryParse(sub.Value, out uid))
{
using (var configSvc = ApiServiceHelper.GetAdminService(settings))
{
try
{
var usr = configSvc.EaiUser.GetByID(uid);
if (usr != null && usr.ID.GetValueOrDefault(0) > 0)
validUser = true;
}
catch { }
}
}
}
Log.Debug($" Validated sub '{sub.Value}'");
}
if (!validUser)
{
// uhhh, does this work? Logout?
// TODO: test!
Log.Warn($"Unable to validate user is SID for ({uid}). Redirecting to '/Home/Logout'");
context.Response.Redirect("/Home/Logout?msg=User not validated in source system");
context.HandleResponse();
}
return Task.CompletedTask;
},
OnTicketReceived = context =>
{
// TODO: Is this necessary?
// added the below code because I thought my application access_token was expired
// however it turns out I'm actually misisng the role claims when I come back to the
// application from the IDP after about an hour
if (context.Properties != null &&
context.Properties.Items != null)
{
DateTime expiresAt = System.DateTime.MinValue;
foreach (var p in context.Properties.Items)
{
if (p.Key == ".Token.expires_at")
{
DateTime.TryParse(p.Value, null, DateTimeStyles.AdjustToUniversal, out expiresAt);
break;
}
}
if (expiresAt != DateTime.MinValue &&
expiresAt != DateTime.MaxValue)
{
// I did this to synch the .NET cookie timeout with the IDP access token timeout?
// This somewhat concerns me becuase I thought that part should be done auto-magically already
// I mean, refresh token?
context.Properties.IsPersistent = true;
context.Properties.ExpiresUtc = expiresAt;
}
}
return Task.CompletedTask;
}
};
});

I'm sorry folks, looks like I found the source of my issue.
Total fail on my side :(.
I had a bug in the ProfileService in my Identity Server implementation that was causing the roles to not be returned in all cases
humph, thanks!

Related

Best practice when using OpenIddict application properties

I'm using the Properties column in my OpenIddict applications to store some metadata about an application instead of using a custom entity - comments on this post custom properties within token imply that's what its meant for.
However, despite reading the above post I'm struggling to understand how best to implement this.
Currently my "create application" looks like this:
public interface IOpenIddictAppService
{
Task<CreateOpenIddictAppResponseDto> CreateAsync(CreateOpenIddictAppRequestDto appDto);
}
public class OpenIddictAppService : IOpenIddictAppService
{
private readonly IOpenIddictApplicationManager _appManager;
public OpenIddictAppService(IOpenIddictApplicationManager appManager)
{
_appManager = appManager;
}
public async Task<CreateOpenIddictAppResponseDto> CreateAsync(CreateOpenIddictAppRequestDto appDto)
{
var sha = SHA512.Create();
var clientId = Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())));
var clientSecret = SecureRandomStringHelper.Create(StaticData.ClientSecretSize);
var data = new OpenIddictApplicationDescriptor
{
ClientId = clientId,
ClientSecret = clientSecret,
DisplayName = appDto.Name,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.Prefixes.Scope + "api",
OpenIddictConstants.Permissions.ResponseTypes.Code
}
};
// Postman test URL"https://oauth.pstmn.io/v1/callback"
appDto.RedirectUrls.Split(" ").ToList().ForEach(x => data.RedirectUris.Add(new Uri(x)));
data.Properties.Add("IdentityConfig", JsonSerializer.SerializeToElement(new AppIdentityProperties
{
ClientSystemId = appDto.ClientSystemId,
CustomerAccountId = appDto.CustomerAccountId
}));
var app = await _appManager.CreateAsync(data);
CreateOpenIddictAppResponseDto result = new()
{
Id = new Guid(await _appManager.GetIdAsync(app) ?? throw new ArgumentNullException("OpenIddictApplication not found")),
ClientId = clientId, // Send back the client id used
ClientSecret = clientSecret, // Send back the secret used so it can be displayed one-time-only for copy/paste
RedirectUrls = string.Join(" ", await _appManager.GetRedirectUrisAsync(app))
};
return result;
}
}
And I'm loading it like this in my authorization controller:
[HttpPost("~/connect/token")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
ClaimsPrincipal claimsPrincipal;
if (request.IsClientCredentialsGrantType())
{
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
throw new InvalidOperationException("The application details cannot be found in the database.");
}
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens (use the client_id as the subject identifier).
identity.AddClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application))
.AddClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application));
var properties = await _applicationManager.GetPropertiesAsync(application);
if (properties.Any(o => o.Key == "IdentityConfig"))
{
var identityConfig = JsonSerializer.Deserialize<AppIdentityProperties>(properties.FirstOrDefault(o => o.Key == "IdentityConfig").Value);
if (identityConfig != null)
{
identity.AddClaim(StaticData.Claims.ClientSystem, identityConfig.ClientSystemId.ToString())
.AddClaim(StaticData.Claims.CustomerAccount, identityConfig.CustomerAccountId.ToString());
}
}
identity.SetDestinations(static claim => claim.Type switch
{
// Allow the "name" claim to be stored in both the access and identity tokens
// when the "profile" scope was granted (by calling principal.SetScopes(...)).
Claims.Name when claim.Subject.HasScope(Scopes.Profile)
=> new[] { Destinations.AccessToken, Destinations.IdentityToken },
// Otherwise, only store the claim in the access tokens.
_ => new[] { Destinations.AccessToken }
});
// Note: In the original OAuth 2.0 specification, the client credentials grant
// doesn't return an identity token, which is an OpenID Connect concept.
//
// As a non-standardized extension, OpenIddict allows returning an id_token
// to convey information about the client application when the "openid" scope
// is granted (i.e specified when calling principal.SetScopes()). When the "openid"
// scope is not explicitly set, no identity token is returned to the client application.
// Set the list of scopes granted to the client application in access_token.
claimsPrincipal = new ClaimsPrincipal(identity);
claimsPrincipal.SetScopes(request.GetScopes());
claimsPrincipal.SetResources(await _scopeManager.ListResourcesAsync(claimsPrincipal.GetScopes()).ToListAsync());
}
else if (request.IsAuthorizationCodeGrantType())
{
// Retrieve the claims principal stored in the authorization code
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
}
else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the refresh token.
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
}
else
{
throw new InvalidOperationException("The specified grant type is not supported.");
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
Now - it's working...but I have a sneaky feeling that I'm just using it wrong especially based on Kevin Chalet's comments on that post... I think I'm probably also setting the redirect URLs the wrong way too!
Can anyone give me any more specific guidance on how I should really be doing this.

Implicit grant SPA with identity server4 concurrent login

how to restrict x amount of login on each client app in specific the SPA client with grant type - implicit
This is out of scope within Identity server
Solutions tried -
Access tokens persisted to DB, however this approach the client kept updating the access token without coming to code because the client browser request is coming with a valid token though its expired the silent authentication is renewing the token by issues a new reference token ( that can be seen in the table persistGrants token_type 'reference_token')
Cookie event - on validateAsync - not much luck though this only works for the server web, we can't put this logic on the oidc library on the client side for SPA's.
Custom signInManager by overriding SignInAsync - but the the executing is not reaching to this point in debug mode because the IDM kept recognising the user has a valid toke ( though expired) kept re issueing the token ( please note there is no refresh token here to manage it by storing and modifying!!!)
Any clues how the IDM re issue the token without taking user to login screen, even though the access token is expired??(Silent authentication. ??
implement profile service overrride activeasync
public override async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await userManager.FindByIdAsync(sub);
//Check existing sessions
if (context.Caller.Equals("AccessTokenValidation", StringComparison.OrdinalIgnoreCase))
{
if (user != null)
context.IsActive = !appuser.VerifyRenewToken(sub, context.Client.ClientId);
else
context.IsActive = false;
}
else
context.IsActive = user != null;
}
startup
services.AddTransient<IProfileService, ProfileService>();
while adding the identity server service to collection under configure services
.AddProfileService<ProfileService>();
Update
Session.Abandon(); //is only in aspnet prior versions not in core
Session.Clear();//clears the session doesn't mean that session expired this should be controlled by addSession life time when including service.
I have happened to found a better way i.e. using aspnetuser securitystamp, every time user log-in update the security stamp so that any prior active session/cookies will get invalidated.
_userManager.UpdateSecurityStampAsync(_userManager.FindByEmailAsync(model.Email).Result).Result
Update (final):
On sign-in:-
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberLogin, false);
if (result.Succeeded)
{
//Update security stamp to invalidate existing sessions
var user = _userManager.FindByEmailAsync(model.Email).Result;
var test= _userManager.UpdateSecurityStampAsync(user).Result;
//Refresh the cookie to update securitystamp on authenticationmanager responsegrant to the current request
await _signInManager.RefreshSignInAsync(user);
}
Profile service implementation :-
public class ProfileService : ProfileService<ApplicationUser>
{
public override async Task IsActiveAsync(IsActiveContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Subject == null) throw new ArgumentNullException(nameof(context.Subject));
context.IsActive = false;
var subject = context.Subject;
var user = await userManager.FindByIdAsync(context.Subject.GetSubjectId());
if (user != null)
{
var security_stamp_changed = false;
if (userManager.SupportsUserSecurityStamp)
{
var security_stamp = (
from claim in subject.Claims
where claim.Type =="AspNet.Identity.SecurityStamp"
select claim.Value
).SingleOrDefault();
if (security_stamp != null)
{
var latest_security_stamp = await userManager.GetSecurityStampAsync(user);
security_stamp_changed = security_stamp != latest_security_stamp;
}
}
context.IsActive =
!security_stamp_changed &&
!await userManager.IsLockedOutAsync(user);
}
}
}
*
Hook in the service collection:-
*
services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<ProfileService>();
i.e. on every login, the security stamp of the user gets updated and pushed to the cookie, when the token expires, the authorize end point will verify on the security change, If there is any then redirects the user to login. This way we are ensuring there will only be one active session

How to handle posts with IdentityServer3 as authentication server

TL;DR
How do you POST data in an ASP.NET MVC project (form, jQuery, axios), using IdentityServer3 as the authentication server. Also, what flow to use, to make this work?
What I'm experiencing
I have a working IdentityServer3 instance. I also have an ASP.NET MVC project. Using hybrid flow, as I will have to pass the user's token to other services. The authentication itself works - when the pages are only using GET. Even if the authenticated user's tokens are expired, something in the background redirects the requests to the auth. server, and the user can continue it's work, without asking the user to log in again. (As far as I understand, the hybrid flow can use refresh tokens, so I assume that's how it can re-authenticate the user. Even if HttpContext.Current.User.Identity.IsAuthenticated=false)
For testing purposes, I set the AccessTokenLifetime, AuthorizationCodeLifetime and IdentityTokenLifetime values to 5 seconds in the auth. server. As far as I know, the refresh token's expire time measured in days, and I did not change the default value.
But when I try to use POST, things get "ugly".
Using form POST, with expired tokens, the request gets redirected to IdentityServer3. It does it's magic (the user gets authenticated) and redirects to my page - as a GET request... I see the response_mode=form_post in the URL, yet the posted payload is gone.
Using axios POST, the request gets redirected to IdentityServer3, but fails with at the pre-flight OPTIONS request.
Using the default jQuery POST, got same error. (Even though, the default jQuery POST uses application/x-www-form-urlencoded to solve the pre-flight issue.)
startup.cs
const string authType = "Cookies";
// resetting Microsoft's default mapper
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
// ensure, that the MVC anti forgery key engine will use our "custom" user id
AntiForgeryConfig.UniqueClaimTypeIdentifier = "sub";
app.UseCookieAuthentication(new Microsoft.Owin.Security.Cookies.CookieAuthenticationOptions
{
AuthenticationType = authType
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
RedirectUri = adminUri,
PostLogoutRedirectUri = adminUri,
Authority = idServerIdentityEndpoint,
SignInAsAuthenticationType = authType,
ResponseType = "code id_token",
Scope = "openid profile roles email offline_access",
Notifications = new OpenIdConnectAuthenticationNotifications
{
#region Handle automatic redirect (on logout)
RedirectToIdentityProvider = async n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType ==
OpenIdConnectRequestType.LogoutRequest)
{
var token = n.OwinContext.Authentication.User.FindFirst(idTokenName);
if (token != null)
{
var idTokenHint =
token.Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
}
},
#endregion
AuthorizationCodeReceived = async n =>
{
System.Diagnostics.Debug.Print("AuthorizationCodeReceived " + n.ProtocolMessage.ToString());
// fetch the identity from authentication response
var identity = n.AuthenticationTicket.Identity;
// exchange the "code" token for access_token, id_token, refresh_token, using the client secret
var requestResponse = await OidcClient.CallTokenEndpointAsync(
new Uri(idServerTokenEndpoint),
new Uri(adminUri),
n.Code,
clientId,
clientSecret
);
// fetch tokens from the exchange response
identity.AddClaims(new []
{
new Claim("access_token", requestResponse.AccessToken),
new Claim("id_token", requestResponse.IdentityToken),
new Claim("refresh_token", requestResponse.RefreshToken)
});
// store the refresh_token in the session, as the user might be logged out, when the authorization attribute is executed
// see OrganicaAuthorize.cs
HttpContext.Current.Session["refresh_token"] = requestResponse.RefreshToken;
// get the userinfo from the openId endpoint
// this actually retreives all the claims, but using the normal access token
var userInfo = await EndpointAndTokenHelper.CallUserInfoEndpoint(idServerUserInfoEndpoint, requestResponse.AccessToken); // todo: userinfo
if (userInfo == null) throw new Exception("Could not retreive user information from identity server.");
#region Extract individual claims
// extract claims we are interested in
var nameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Name,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Name)); // full name
var givenNameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.GivenName,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.GivenName)); // given name
var familyNameClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.FamilyName,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.FamilyName)); // family name
var emailClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Email,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Email)); // email
var subClaim = new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject,
userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject)); // userid
#endregion
#region Extract roles
List<string> roles;
try
{
roles = userInfo.Value<JArray>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role).Select(r => r.ToString()).ToList();
}
catch (InvalidCastException) // if there is only 1 item
{
roles = new List<string> { userInfo.Value<string>(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role) };
}
#endregion
// attach the claims we just extracted
identity.AddClaims(new[] { nameClaim, givenNameClaim, familyNameClaim, subClaim, emailClaim });
// attach roles
identity.AddClaims(roles.Select(r => new Claim(Thinktecture.IdentityModel.Client.JwtClaimTypes.Role, r.ToString())));
// update the return value of the SecurityTokenValidated method (this method...)
n.AuthenticationTicket = new AuthenticationTicket(
identity,
n.AuthenticationTicket.Properties);
},
AuthenticationFailed = async n =>
{
System.Diagnostics.Debug.Print("AuthenticationFailed " + n.Exception.ToString());
},
MessageReceived = async n =>
{
System.Diagnostics.Debug.Print("MessageReceived " + n.State.ToString());
},
SecurityTokenReceived = async n =>
{
System.Diagnostics.Debug.Print("SecurityTokenReceived " + n.State.ToString());
},
SecurityTokenValidated = async n =>
{
System.Diagnostics.Debug.Print("SecurityTokenValidated " + n.State.ToString());
}
}
});
Have you configured cookie authentication middleware in the MVC app? After the authentication with identity server, an authentication cookie should be set. When the authentication cookie is set and valid IdentityServer redirection will not occur until the cookie expires/deleted.
Update 1:
Ok, I misunderstood the quesion. It is logical to redirect to identity server when session times out. It won't work with post payload. You can try doing something like follows.
If the request is a normal post, redirect user again to the form
fill page.
If request is ajax post, return unauthorized result and based on
that response refresh the page from javascript.
Anyway I don't think you will be able to keep the posted data unless you are designing your own solution for that. (e.g keep data stored locally).
But you might be able to avoid this scenario altogether if you carefuly decide identity server's session timeout and your app's session timeout.
In OpenIdConnectAuthenticationOptions set UseTokenLifetime = false that will break connection between identity token's lifetime and cookie session lifetime.
In CookieAuthenticationOptions make sliding expiration
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(50),
Now you are incontrol of your apps session lifetime. Adjust it to match your needs and security conserns.

User is always null when using AspNet.Security.OpenIdConnect.Server

I'm trying to generate access tokens for my aspnet core web app. I created the following provider:
public class CustomOpenIdConnectServerProvider : OpenIdConnectServerProvider
{
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
// Reject the token requests that don't use grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() && !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only the resource owner password credentials and refresh token " +
"grants are accepted by this authorization server");
return Task.FromResult(0);
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private), call Skip()
// to inform the server the request should be accepted without
// enforcing client authentication.
context.Skip();
return Task.FromResult(0);
}
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
// Resolve ASP.NET Core Identity's user manager from the DI container.
var manager = context.HttpContext.RequestServices.GetRequiredService<UserManager<User>>();
// Only handle grant_type=password requests and let ASOS
// process grant_type=refresh_token requests automatically.
if (context.Request.IsPasswordGrantType())
{
var user = await manager.FindByNameAsync(context.Request.Username);
if (user == null)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
return;
}
// Ensure the password is valid.
if (!await manager.CheckPasswordAsync(user, context.Request.Password))
{
if (manager.SupportsUserLockout)
{
await manager.AccessFailedAsync(user);
}
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
return;
}
if (manager.SupportsUserLockout)
{
await manager.ResetAccessFailedCountAsync(user);
}
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
// Note: the name identifier is always included in both identity and
// access tokens, even if an explicit destination is not specified.
identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserIdAsync(user));
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, await manager.GetUserIdAsync(user));
// When adding custom claims, you MUST specify one or more destinations.
// Read "part 7" for more information about custom claims and scopes.
identity.AddClaim("username", await manager.GetUserNameAsync(user),
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var claims = await manager.GetClaimsAsync(user);
foreach (var claim in claims)
{
identity.AddClaim(claim.Type, claim.Value, OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Set the list of scopes granted to the client application.
ticket.SetScopes(
/* openid: */ OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.OfflineAccess,
/* email: */ OpenIdConnectConstants.Scopes.Email,
/* profile: */ OpenIdConnectConstants.Scopes.Profile);
// Set the resource servers the access token should be issued for.
ticket.SetResources("resource_server");
context.Validate(ticket);
}
}
This works just fine, I can get the access token and the users are authenticated successfully. The issue that I'm facing here is that in any authorized action method when I do this: var user = await _userManager.GetUserAsync(User); the value for user is always null! Of course, I'm passing the Authorization header with a valid access token and the request goes into actions annotated with Authorize without any problems. It's just the value of user is null. Can anybody tell me whats wrong with my code?
By default, UserManager.GetUserAsync(User) uses the ClaimTypes.NameIdentifier claim as the user identifier.
In your case, ClaimTypes.NameIdentifier - which is no longer considered by the OpenID Connect server middleware as a special claim in 1.0 - is not added to the access token because it doesn't have the appropriate destination. As a consequence, Identity is unable to extract the user identifier from the access token.
You have 3 options to fix that:
Replace the default user identifier claim used by Identity by calling services.Configure<IdentityOptions>(options => options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject); in your Startup.ConfigureServices() method.
Keep using the ClaimTypes.NameIdentifier claim but give it the right destination (OpenIdConnectConstants.Destinations.AccessToken).
Use UserManager.FindByIdAsync(User.FindFirstValue(OpenIdConnectConstants.Claims.Subject)) instead of UserManager.GetUserAsync(User).

Web API OAUTH - Distinguish between if Identify Token Expired or UnAuthorized

Am currently developing an Authorization server using Owin, Oauth, Claims.
Below is my Oauth Configuration and i have 2 questions
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(1000),
Provider = new AuthorizationServerProvider()
//RefreshTokenProvider = new SimpleRefreshTokenProvider()
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
If the token is expired and user accessing using the expired token user is getting 401(unAuthorized).Checking using Fiddler.
How can i send a customized message to an user stating your token as expired. Which function or module i need to override.
and my another quesiton is What is the use of the below line ?
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
Do i really need this to implement because when i checked it still works without the above line. Any security violation ?
You can't directly customize the behavior for expired tokens but you can do that with a custom middleware.
First override the AuthenticationTokenProvider so that you can intercept the authentication ticket before it is discarded as expired.
public class CustomAuthenticationTokenProvider : AuthenticationTokenProvider
{
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
if (context.Ticket != null &&
context.Ticket.Properties.ExpiresUtc.HasValue &&
context.Ticket.Properties.ExpiresUtc.Value.LocalDateTime < DateTime.Now)
{
//store the expiration in the owin context so that we can read it later a middleware
context.OwinContext.Set("custom.ExpriredToken", true);
}
}
}
and configure it in the Startup along with a small custom middleware
using AppFunc = System.Func<System.Collections.Generic.IDictionary<string, object>, System.Threading.Tasks.Task>;
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
AccessTokenProvider = new CustomAuthenticationTokenProvider()
});
//after the request has been authenticated or not
//check for our custom env setting and act accordingly
app.Use(new Func<AppFunc, AppFunc>(next => (env) =>
{
var ctx = new OwinContext(env);
if (ctx.Get<bool>("custom.ExpriredToken"))
{
//do wathever you want with the response
ctx.Response.StatusCode = 401;
ctx.Response.ReasonPhrase = "Token exprired";
//terminate the request with this middleware
return Task.FromResult(0);
}
else
{
//proceed with the rest of the middleware pipeline
return next(env);
}
}));
If you have noticed I've placed the custom middleware after the call to UseOAuthBearerAuthentication and this is important and stems from the answer to your second question.
The OAuthBearerAuthenticationMidlleware is responsible for the authentication but not for the authorization. So it just reads the token and fills in the information so that it can be accessed with IAuthenticationManager later in the pipeline.
So yes, with or without it all your request will come out as 401(unauthorized), even those with valid tokens.

Resources