public void SetPresenters_AddsPresenters(string[] presenters, string presenter) { // Arrange var ticket = new AuthenticationTicket( new ClaimsPrincipal(), new AuthenticationProperties(), nameof(AuthenticationTicket)); // Act ticket.SetPresenters(presenters); // Assert Assert.Equal(presenter, ticket.GetProperty(OpenIdConnectConstants.Properties.Presenters)); }
public async Task HandleTokenRequest_AuthorizationCodeRevocationIsIgnoredWhenTokenRevocationIsDisabled() { // Arrange var ticket = new AuthenticationTicket( new ClaimsPrincipal(), new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); ticket.SetPresenters("Fabrikam"); ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); var format = new Mock <ISecureDataFormat <AuthenticationTicket> >(); format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) .Returns(ticket); var server = CreateAuthorizationServer(builder => { builder.Services.AddSingleton(CreateApplicationManager(instance => { var application = new OpenIddictApplication(); instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny <CancellationToken>())) .ReturnsAsync(application); instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny <CancellationToken>())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); builder.Configure(options => options.AuthorizationCodeFormat = format.Object); builder.Configure(options => options.RevocationEndpointPath = PathString.Empty); builder.DisableTokenRevocation(); }); var client = new OpenIdConnectClient(server.CreateClient()); // Act var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { ClientId = "Fabrikam", Code = "SplxlOBeZQQYbYS6WxSbIA", GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, RedirectUri = "http://www.fabrikam.com/path" }); // Assert Assert.NotNull(response.AccessToken); }
private async Task <string> SerializeRefreshTokenAsync( ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage request, OpenIdConnectMessage response) { // properties.IssuedUtc and properties.ExpiresUtc // should always be preferred when explicitly set. if (properties.IssuedUtc == null) { properties.IssuedUtc = Options.SystemClock.UtcNow; } if (properties.ExpiresUtc == null) { properties.ExpiresUtc = properties.IssuedUtc + Options.RefreshTokenLifetime; } // Claims in refresh tokens are never filtered as they are supposed to be opaque: // SerializeAccessTokenAsync and SerializeIdentityTokenAsync are responsible of ensuring // that subsequent access and identity tokens are correctly filtered. var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); // By default, add the client_id to the list of the // presenters allowed to use the refresh token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeRefreshTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.RefreshTokenFormat }; await Options.Provider.SerializeRefreshToken(notification); if (!string.IsNullOrEmpty(notification.RefreshToken)) { return(notification.RefreshToken); } // Allow the application to change the authentication // ticket from the SerializeRefreshTokenAsync event. ticket = notification.AuthenticationTicket; ticket.Properties.CopyTo(properties); return(notification.DataFormat?.Protect(ticket)); }
private async Task <string> SerializeRefreshTokenAsync( ClaimsIdentity identity, AuthenticationProperties properties, OpenIdConnectRequest request, OpenIdConnectResponse response) { // Note: claims in refresh tokens are never filtered as they are supposed to be opaque: // SerializeAccessTokenAsync and SerializeIdentityTokenAsync are responsible of ensuring // that subsequent access and identity tokens are correctly filtered. // Create a new ticket containing the updated properties. var ticket = new AuthenticationTicket(identity, properties); ticket.Properties.IssuedUtc = Options.SystemClock.UtcNow; ticket.Properties.ExpiresUtc = ticket.Properties.IssuedUtc + (ticket.GetRefreshTokenLifetime() ?? Options.RefreshTokenLifetime); ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); // Associate a random identifier with the refresh token. ticket.SetTicketId(Guid.NewGuid().ToString()); // By default, add the client_id to the list of the // presenters allowed to use the refresh token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeRefreshTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.RefreshTokenFormat }; await Options.Provider.SerializeRefreshToken(notification); if (notification.HandledResponse || !string.IsNullOrEmpty(notification.RefreshToken)) { return(notification.RefreshToken); } else if (notification.Skipped) { return(null); } return(notification.DataFormat?.Protect(ticket)); }
private async Task <string> SerializeIdentityTokenAsync( ClaimsIdentity identity, AuthenticationProperties properties, OpenIdConnectRequest request, OpenIdConnectResponse response) { // Replace the identity by a new one containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). identity = identity.Clone(claim => { // Never exclude ClaimTypes.NameIdentifier. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "id_token" are not included in the identity token. return(claim.HasDestination(OpenIdConnectConstants.Destinations.IdentityToken)); }); // Create a new ticket containing the updated properties and the filtered identity. var ticket = new AuthenticationTicket(identity, properties); ticket.Properties.IssuedUtc = Options.SystemClock.UtcNow; ticket.Properties.ExpiresUtc = ticket.Properties.IssuedUtc + (ticket.GetIdentityTokenLifetime() ?? Options.IdentityTokenLifetime); ticket.SetUsage(OpenIdConnectConstants.Usages.IdentityToken); // Associate a random identifier with the identity token. ticket.SetTicketId(Guid.NewGuid().ToString()); // By default, add the client_id to the list of the // presenters allowed to use the identity token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetAudiences(request.ClientId); ticket.SetPresenters(request.ClientId); } var notification = new SerializeIdentityTokenContext(Context, Options, request, response, ticket) { Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.IdentityTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeIdentityToken(notification); if (notification.HandledResponse || !string.IsNullOrEmpty(notification.IdentityToken)) { return(notification.IdentityToken); } else if (notification.Skipped) { return(null); } if (notification.SecurityTokenHandler == null) { return(null); } if (!identity.HasClaim(claim => claim.Type == OpenIdConnectConstants.Claims.Subject) && !identity.HasClaim(claim => claim.Type == ClaimTypes.NameIdentifier)) { throw new InvalidOperationException("A unique identifier cannot be found to generate a 'sub' claim: " + "make sure to add a 'ClaimTypes.NameIdentifier' claim."); } if (notification.SigningCredentials == null) { throw new InvalidOperationException("A signing key must be provided."); } // Store the unique subject identifier as a claim. if (!identity.HasClaim(claim => claim.Type == OpenIdConnectConstants.Claims.Subject)) { identity.AddClaim(OpenIdConnectConstants.Claims.Subject, identity.GetClaim(ClaimTypes.NameIdentifier)); } // Remove the ClaimTypes.NameIdentifier claims to avoid getting duplicate claims. // Note: the "sub" claim is automatically mapped by JwtSecurityTokenHandler // to ClaimTypes.NameIdentifier when validating a JWT token. // Note: make sure to call ToArray() to avoid an InvalidOperationException // on old versions of Mono, where FindAll() is implemented using an iterator. foreach (var claim in identity.FindAll(ClaimTypes.NameIdentifier).ToArray()) { identity.RemoveClaim(claim); } // Store the "unique_id" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.JwtId, ticket.GetTicketId()); // Store the "usage" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // Store the "confidentiality_level" property as a claim. var confidentiality = ticket.GetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel); if (!string.IsNullOrEmpty(confidentiality)) { identity.AddClaim(OpenIdConnectConstants.Claims.ConfidentialityLevel, confidentiality); } // Store the audiences as claims. foreach (var audience in notification.Audiences) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Audience, audience); } // If a nonce was present in the authorization request, it MUST // be included in the id_token generated by the token endpoint. // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation var nonce = request.Nonce; if (request.IsAuthorizationCodeGrantType()) { // Restore the nonce stored in the authentication // ticket extracted from the authorization code. nonce = ticket.GetProperty(OpenIdConnectConstants.Properties.Nonce); } if (!string.IsNullOrEmpty(nonce)) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Nonce, nonce); } using (var algorithm = HashAlgorithm.Create(notification.SigningCredentials.DigestAlgorithm)) { // Create an authorization code hash if necessary. if (!string.IsNullOrEmpty(response.Code)) { var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(response.Code)); // Note: only the left-most half of the hash of the octets is used. // See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken identity.AddClaim(OpenIdConnectConstants.Claims.CodeHash, Base64UrlEncoder.Encode(hash, 0, hash.Length / 2)); } // Create an access token hash if necessary. if (!string.IsNullOrEmpty(response.AccessToken)) { var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(response.AccessToken)); // Note: only the left-most half of the hash of the octets is used. // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken identity.AddClaim(OpenIdConnectConstants.Claims.AccessTokenHash, Base64UrlEncoder.Encode(hash, 0, hash.Length / 2)); } } // Extract the presenters from the authentication ticket. var presenters = notification.Presenters.ToArray(); switch (presenters.Length) { case 0: break; case 1: identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; default: Options.Logger.LogWarning("Multiple presenters have been associated with the identity token " + "but the JWT format only accepts single values."); // Only add the first authorized party. identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; } if (ticket.Properties.IssuedUtc != null) { ticket.Identity.AddClaim(new Claim( OpenIdConnectConstants.Claims.IssuedAt, EpochTime.GetIntDate(ticket.Properties.IssuedUtc.Value.UtcDateTime).ToString(), ClaimValueTypes.Integer64)); } var descriptor = new SecurityTokenDescriptor { Subject = ticket.Identity, TokenIssuerName = notification.Issuer, SigningCredentials = notification.SigningCredentials, Lifetime = new Lifetime( notification.Ticket.Properties.IssuedUtc?.UtcDateTime, notification.Ticket.Properties.ExpiresUtc?.UtcDateTime) }; var token = notification.SecurityTokenHandler.CreateToken(descriptor); return(notification.SecurityTokenHandler.WriteToken(token)); }
private async Task <string> SerializeAccessTokenAsync( ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage request, OpenIdConnectMessage response) { // properties.IssuedUtc and properties.ExpiresUtc // should always be preferred when explicitly set. if (properties.IssuedUtc == null) { properties.IssuedUtc = Options.SystemClock.UtcNow; } if (properties.ExpiresUtc == null) { properties.ExpiresUtc = properties.IssuedUtc + Options.AccessTokenLifetime; } // Create a new principal containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). principal = principal.Clone(claim => { // Never exclude ClaimTypes.NameIdentifier. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "access_token" are not included in the access token. return(claim.HasDestination(OpenIdConnectConstants.Destinations.AccessToken)); }); var identity = (ClaimsIdentity)principal.Identity; // Create a new ticket containing the updated properties and the filtered principal. var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); ticket.SetAudiences(ticket.GetResources()); // By default, add the client_id to the list of the // presenters allowed to use the access token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeAccessTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeAccessToken(notification); if (!string.IsNullOrEmpty(notification.AccessToken)) { return(notification.AccessToken); } if (notification.SecurityTokenHandler == null) { return(notification.DataFormat?.Protect(ticket)); } // Extract the main identity from the principal. identity = (ClaimsIdentity)ticket.Principal.Identity; // Store the "usage" property as a claim. identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // If the ticket is marked as confidential, add a new // "confidential" claim in the security token. if (ticket.IsConfidential()) { identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.Confidential, "true", ClaimValueTypes.Boolean)); } // Create a new claim per scope item, that will result // in a "scope" array being added in the access token. foreach (var scope in ticket.GetScopes()) { identity.AddClaim(OpenIdConnectConstants.Claims.Scope, scope); } var handler = notification.SecurityTokenHandler as JwtSecurityTokenHandler; if (handler != null) { // Note: when used as an access token, a JWT token doesn't have to expose a "sub" claim // but the name identifier claim is used as a substitute when it has been explicitly added. // See https://tools.ietf.org/html/rfc7519#section-4.1.2 var subject = identity.FindFirst(JwtRegisteredClaimNames.Sub); if (subject == null) { var identifier = identity.FindFirst(ClaimTypes.NameIdentifier); if (identifier != null) { identity.AddClaim(JwtRegisteredClaimNames.Sub, identifier.Value); } } // Remove the ClaimTypes.NameIdentifier claims to avoid getting duplicate claims. // Note: the "sub" claim is automatically mapped by JwtSecurityTokenHandler // to ClaimTypes.NameIdentifier when validating a JWT token. // Note: make sure to call ToArray() to avoid an InvalidOperationException // on old versions of Mono, where FindAll() is implemented using an iterator. foreach (var claim in identity.FindAll(ClaimTypes.NameIdentifier).ToArray()) { identity.RemoveClaim(claim); } // Store the audiences as claims. foreach (var audience in ticket.GetAudiences()) { identity.AddClaim(JwtRegisteredClaimNames.Aud, audience); } // Extract the presenters from the authentication ticket. var presenters = ticket.GetPresenters().ToArray(); switch (presenters.Length) { case 0: break; case 1: identity.AddClaim(JwtRegisteredClaimNames.Azp, presenters[0]); break; default: Logger.LogWarning("Multiple presenters have been associated with the access token " + "but the JWT format only accepts single values."); // Only add the first authorized party. identity.AddClaim(JwtRegisteredClaimNames.Azp, presenters[0]); break; } var token = handler.CreateJwtSecurityToken( subject: identity, issuer: notification.Issuer, signingCredentials: notification.SigningCredentials, issuedAt: ticket.Properties.IssuedUtc.Value.UtcDateTime, notBefore: ticket.Properties.IssuedUtc.Value.UtcDateTime, expires: ticket.Properties.ExpiresUtc.Value.UtcDateTime); var x509SecurityKey = notification.SigningCredentials?.Key as X509SecurityKey; if (x509SecurityKey != null) { // Note: unlike "kid", "x5t" is not automatically added by JwtHeader's constructor in IdentityModel for .NET Core. // Though not required by the specifications, this property is needed for IdentityModel for Katana to work correctly. // See https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/issues/132 // and https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/181. token.Header[JwtHeaderParameterNames.X5t] = Base64UrlEncoder.Encode(x509SecurityKey.Certificate.GetCertHash()); } return(handler.WriteToken(token)); } else { var token = notification.SecurityTokenHandler.CreateToken(new SecurityTokenDescriptor { Subject = identity, Issuer = notification.Issuer, Audience = notification.Audiences.ElementAtOrDefault(0), SigningCredentials = notification.SigningCredentials, IssuedAt = notification.Ticket.Properties.IssuedUtc.Value.UtcDateTime, NotBefore = notification.Ticket.Properties.IssuedUtc.Value.UtcDateTime, Expires = notification.Ticket.Properties.ExpiresUtc.Value.UtcDateTime }); // Note: the security token is manually serialized to prevent // an exception from being thrown if the handler doesn't implement // the SecurityTokenHandler.WriteToken overload returning a string. var builder = new StringBuilder(); using (var writer = XmlWriter.Create(builder, new XmlWriterSettings { Encoding = new UTF8Encoding(false), OmitXmlDeclaration = true })) { notification.SecurityTokenHandler.WriteToken(writer, token); } return(builder.ToString()); } }
private async Task <AuthenticationTicket> DeserializeIdentityTokenAsync(string token, OpenIdConnectMessage request) { var notification = new DeserializeIdentityTokenContext(Context, Options, request, token) { Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.IdentityTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.DeserializeIdentityToken(notification); // Directly return the authentication ticket if one // has been provided by DeserializeIdentityToken. if (notification.Ticket != null) { return(notification.Ticket); } if (notification.SecurityTokenHandler == null) { return(null); } // Create new validation parameters to validate the security token. // ValidateAudience and ValidateLifetime are always set to false: // if necessary, the audience and the expiration can be validated // in InvokeIntrospectionEndpointAsync or InvokeTokenEndpointAsync. var parameters = new TokenValidationParameters { IssuerSigningKey = notification.SigningCredentials.Key, ValidIssuer = notification.Issuer, ValidateAudience = false, ValidateLifetime = false }; SecurityToken securityToken; ClaimsPrincipal principal; try { principal = notification.SecurityTokenHandler.ValidateToken(token, parameters, out securityToken); } catch (Exception exception) { Logger.LogDebug("An exception occured when deserializing an identity token: {Message}.", exception.Message); return(null); } // Parameters stored in AuthenticationProperties are lost // when the identity token is serialized using a security token handler. // To mitigate that, they are inferred from the claims or the security token. var properties = new AuthenticationProperties { ExpiresUtc = securityToken.ValidTo, IssuedUtc = securityToken.ValidFrom }; var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); var audiences = principal.FindAll(JwtRegisteredClaimNames.Aud); if (audiences.Any()) { ticket.SetAudiences(audiences.Select(claim => claim.Value)); } var presenters = principal.FindAll(JwtRegisteredClaimNames.Azp); if (presenters.Any()) { ticket.SetPresenters(presenters.Select(claim => claim.Value)); } var usage = principal.FindFirst(OpenIdConnectConstants.Claims.Usage); if (usage != null) { ticket.SetUsage(usage.Value); } var confidential = principal.FindFirst(OpenIdConnectConstants.Claims.Confidential); if (confidential != null && string.Equals(confidential.Value, "true", StringComparison.OrdinalIgnoreCase)) { ticket.Properties.Items[OpenIdConnectConstants.Properties.Confidential] = "true"; } // Ensure the received ticket is an identity token. if (!ticket.IsIdentityToken()) { Logger.LogDebug("The received token was not an identity token: {Token}.", token); return(null); } return(ticket); }
private async Task <string> SerializeIdentityTokenAsync( ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage request, OpenIdConnectMessage response) { // properties.IssuedUtc and properties.ExpiresUtc // should always be preferred when explicitly set. if (properties.IssuedUtc == null) { properties.IssuedUtc = Options.SystemClock.UtcNow; } if (properties.ExpiresUtc == null) { properties.ExpiresUtc = properties.IssuedUtc + Options.IdentityTokenLifetime; } // Replace the principal by a new one containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). principal = principal.Clone(claim => { // Never exclude ClaimTypes.NameIdentifier. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "id_token" are not included in the identity token. return(claim.HasDestination(OpenIdConnectConstants.Destinations.IdentityToken)); }); var identity = (ClaimsIdentity)principal.Identity; // Create a new ticket containing the updated properties and the filtered principal. var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); ticket.SetUsage(OpenIdConnectConstants.Usages.IdToken); // By default, add the client_id to the list of the // presenters allowed to use the identity token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetAudiences(request.ClientId); ticket.SetPresenters(request.ClientId); } var notification = new SerializeIdentityTokenContext(Context, Options, request, response, ticket) { Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.IdentityTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeIdentityToken(notification); if (!string.IsNullOrEmpty(notification.IdentityToken)) { return(notification.IdentityToken); } if (notification.SecurityTokenHandler == null) { return(null); } if (!identity.HasClaim(claim => claim.Type == JwtRegisteredClaimNames.Sub) && !identity.HasClaim(claim => claim.Type == ClaimTypes.NameIdentifier)) { Logger.LogError("A unique identifier cannot be found to generate a 'sub' claim: " + "make sure to add a 'ClaimTypes.NameIdentifier' claim."); return(null); } // Extract the main identity from the principal. identity = (ClaimsIdentity)ticket.Principal.Identity; // Store the unique subject identifier as a claim. if (!identity.HasClaim(claim => claim.Type == JwtRegisteredClaimNames.Sub)) { identity.AddClaim(JwtRegisteredClaimNames.Sub, identity.GetClaim(ClaimTypes.NameIdentifier)); } // Remove the ClaimTypes.NameIdentifier claims to avoid getting duplicate claims. // Note: the "sub" claim is automatically mapped by JwtSecurityTokenHandler // to ClaimTypes.NameIdentifier when validating a JWT token. // Note: make sure to call ToArray() to avoid an InvalidOperationException // on old versions of Mono, where FindAll() is implemented using an iterator. foreach (var claim in identity.FindAll(ClaimTypes.NameIdentifier).ToArray()) { identity.RemoveClaim(claim); } // Store the "usage" property as a claim. identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // If the ticket is marked as confidential, add a new // "confidential" claim in the security token. if (ticket.IsConfidential()) { identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.Confidential, "true", ClaimValueTypes.Boolean)); } // Store the audiences as claims. foreach (var audience in ticket.GetAudiences()) { identity.AddClaim(JwtRegisteredClaimNames.Aud, audience); } // If a nonce was present in the authorization request, it MUST // be included in the id_token generated by the token endpoint. // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation var nonce = request.Nonce; if (request.IsAuthorizationCodeGrantType()) { // Restore the nonce stored in the authentication // ticket extracted from the authorization code. nonce = ticket.GetNonce(); } if (!string.IsNullOrEmpty(nonce)) { identity.AddClaim(JwtRegisteredClaimNames.Nonce, nonce); } if (!string.IsNullOrEmpty(response.Code)) { using (var algorithm = OpenIdConnectServerHelpers.GetHashAlgorithm(notification.SigningCredentials.Algorithm)) { // Create the c_hash using the authorization code returned by SerializeAuthorizationCodeAsync. var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(response.Code)); // Note: only the left-most half of the hash of the octets is used. // See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken identity.AddClaim(JwtRegisteredClaimNames.CHash, Base64UrlEncoder.Encode(hash, 0, hash.Length / 2)); } } if (!string.IsNullOrEmpty(response.AccessToken)) { using (var algorithm = OpenIdConnectServerHelpers.GetHashAlgorithm(notification.SigningCredentials.Algorithm)) { // Create the at_hash using the access token returned by SerializeAccessTokenAsync. var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(response.AccessToken)); // Note: only the left-most half of the hash of the octets is used. // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken identity.AddClaim(JwtRegisteredClaimNames.AtHash, Base64UrlEncoder.Encode(hash, 0, hash.Length / 2)); } } // Extract the presenters from the authentication ticket. var presenters = ticket.GetPresenters().ToArray(); switch (presenters.Length) { case 0: break; case 1: identity.AddClaim(JwtRegisteredClaimNames.Azp, presenters[0]); break; default: Logger.LogWarning("Multiple presenters have been associated with the identity token " + "but the JWT format only accepts single values."); // Only add the first authorized party. identity.AddClaim(JwtRegisteredClaimNames.Azp, presenters[0]); break; } var token = notification.SecurityTokenHandler.CreateJwtSecurityToken( subject: identity, issuer: notification.Issuer, signingCredentials: notification.SigningCredentials, issuedAt: ticket.Properties.IssuedUtc.Value.UtcDateTime, notBefore: ticket.Properties.IssuedUtc.Value.UtcDateTime, expires: ticket.Properties.ExpiresUtc.Value.UtcDateTime); var x509SecurityKey = notification.SigningCredentials?.Key as X509SecurityKey; if (x509SecurityKey != null) { // Note: unlike "kid", "x5t" is not automatically added by JwtHeader's constructor in IdentityModel for .NET Core. // Though not required by the specifications, this property is needed for IdentityModel for Katana to work correctly. // See https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/issues/132 // and https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/181. token.Header[JwtHeaderParameterNames.X5t] = Base64UrlEncoder.Encode(x509SecurityKey.Certificate.GetCertHash()); } return(notification.SecurityTokenHandler.WriteToken(token)); }
public async Task HandleTokenRequest_RequestsAreNotHandledLocally(string flow) { // Arrange var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); var ticket = new AuthenticationTicket( new ClaimsPrincipal(identity), new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); ticket.SetTicketId("60FFF7EA-F98E-437B-937E-5073CC313103"); switch (flow) { case OpenIdConnectConstants.GrantTypes.AuthorizationCode: ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); ticket.SetPresenters("Fabrikam"); break; case OpenIdConnectConstants.GrantTypes.RefreshToken: ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); break; } var format = new Mock <ISecureDataFormat <AuthenticationTicket> >(); format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) .Returns(ticket); var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny <CancellationToken>())) .ReturnsAsync(token); }); var server = CreateAuthorizationServer(builder => { builder.Services.AddSingleton(CreateApplicationManager(instance => { var application = new OpenIddictApplication(); instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny <CancellationToken>())) .ReturnsAsync(application); instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny <CancellationToken>())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny <CancellationToken>())) .ReturnsAsync(true); })); builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); builder.Services.AddSingleton(manager); builder.Configure(options => options.AuthorizationCodeFormat = format.Object); builder.Configure(options => options.RefreshTokenFormat = format.Object); }); var client = new OpenIdConnectClient(server.CreateClient()); // Act var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { ClientId = "Fabrikam", ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", Code = "8xLOxBtZp8", GrantType = flow, RefreshToken = "8xLOxBtZp8", Username = "******", Password = "******" }); // Assert Assert.NotNull(response.AccessToken); }
private async Task <bool> HandleSignInAsync(AuthenticationTicket ticket) { // Extract the OpenID Connect request from the ASP.NET context. // If it cannot be found or doesn't correspond to an authorization // or a token request, throw an InvalidOperationException. var request = Context.GetOpenIdConnectRequest(); if (request == null || (!request.IsAuthorizationRequest() && !request.IsTokenRequest())) { throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); } // Note: if an OpenID Connect response was already generated, throw an exception. var response = Context.GetOpenIdConnectResponse(); if (response != null) { throw new InvalidOperationException("An OpenID Connect response has already been sent."); } if (string.IsNullOrEmpty(ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject))) { throw new InvalidOperationException("The authentication ticket was rejected because " + "it doesn't contain the mandatory subject claim."); } // Prepare a new OpenID Connect response. response = new OpenIdConnectResponse(); if (request.IsAuthorizationRequest()) { response.RedirectUri = request.GetProperty <string>(OpenIdConnectConstants.Properties.RedirectUri); response.State = request.State; } // Copy the confidentiality level associated with the request to the authentication ticket. if (!ticket.HasProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel)) { ticket.SetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel, request.GetProperty <string>(OpenIdConnectConstants.Properties.ConfidentialityLevel)); } // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. // Note: the application is allowed to specify a different "scopes": in this case, // don't replace the "scopes" property stored in the authentication ticket. if (request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !ticket.HasScope()) { ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId); } // When a "resources" property cannot be found in the ticket, // infer it from the "audiences" property. if (ticket.HasAudience() && !ticket.HasResource()) { ticket.SetResources(ticket.GetAudiences()); } // Add the validated client_id to the list of authorized presenters, // unless the presenters were explicitly set by the developer. var presenter = request.GetProperty <string>(OpenIdConnectConstants.Properties.ClientId); if (!string.IsNullOrEmpty(presenter) && !ticket.HasPresenter()) { ticket.SetPresenters(presenter); } // Only return an authorization code if the request is an authorization request and has response_type=code. if (request.IsAuthorizationRequest() && request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Code)) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); response.Code = await SerializeAuthorizationCodeAsync(ticket.Principal, properties, request, response); } // Only return an access token if the request is a token request // or an authorization request that specifies response_type=token. if (request.IsTokenRequest() || (request.IsAuthorizationRequest() && request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token))) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); // When receiving a grant_type=refresh_token request, determine whether the client application // requests a limited set of scopes/resources and replace the corresponding properties if necessary. // Note: at this stage, request.GetResources() cannot return more items than the ones that were initially granted // by the resource owner as the "resources" parameter is always validated when receiving the token request. if (request.IsTokenRequest() && request.IsRefreshTokenGrantType()) { if (!string.IsNullOrEmpty(request.Resource)) { Logger.LogDebug("The access token resources will be limited to the resources " + "requested by the client application: {Resources}.", request.GetResources()); // Replace the resources initially granted by the resources listed by the client application in the token request. // Note: request.GetResources() automatically removes duplicate entries, so additional filtering is not necessary. properties.SetProperty(OpenIdConnectConstants.Properties.Resources, request.GetResources()); } if (!string.IsNullOrEmpty(request.Scope)) { Logger.LogDebug("The access token scopes will be limited to the scopes " + "requested by the client application: {Scopes}.", request.GetScopes()); // Replace the scopes initially granted by the scopes listed by the client application in the token request. // Note: request.GetScopes() automatically removes duplicate entries, so additional filtering is not necessary. properties.SetProperty(OpenIdConnectConstants.Properties.Scopes, request.GetScopes()); } } var resources = ticket.GetResources(); if (request.IsAuthorizationCodeGrantType() || !new HashSet <string>(resources).SetEquals(request.GetResources())) { response.Resource = string.Join(" ", resources); } var scopes = ticket.GetScopes(); if (request.IsAuthorizationCodeGrantType() || !new HashSet <string>(scopes).SetEquals(request.GetScopes())) { response.Scope = string.Join(" ", scopes); } response.TokenType = OpenIdConnectConstants.TokenTypes.Bearer; response.AccessToken = await SerializeAccessTokenAsync(ticket.Principal, properties, request, response); // properties.ExpiresUtc is automatically set by SerializeAccessTokenAsync but the end user // is free to set a null value directly in the SerializeAccessToken event. if (properties.ExpiresUtc.HasValue && properties.ExpiresUtc > Options.SystemClock.UtcNow) { var lifetime = properties.ExpiresUtc.Value - Options.SystemClock.UtcNow; response.ExpiresIn = (long)(lifetime.TotalSeconds + .5); } } // Only return a refresh token if the request is a token request that specifies scope=offline_access. if (request.IsTokenRequest() && ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess)) { // Note: when sliding expiration is disabled, don't return a new refresh token, // unless the token request is not a grant_type=refresh_token request. if (Options.UseSlidingExpiration || !request.IsRefreshTokenGrantType()) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Principal, properties, request, response); } } // Only return an identity token if the openid scope was requested and granted // to avoid generating and returning an unnecessary token to pure OAuth2 clients. if (ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId)) { // Note: don't return an identity token if the request is an // authorization request that doesn't use response_type=id_token. if (request.IsTokenRequest() || request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken)) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); response.IdToken = await SerializeIdentityTokenAsync(ticket.Principal, properties, request, response); } } if (request.IsAuthorizationRequest()) { return(await SendAuthorizationResponseAsync(response, ticket)); } return(await SendTokenResponseAsync(response, ticket)); }
private async Task <bool> HandleSignInAsync(AuthenticationTicket ticket) { // Extract the OpenID Connect request from the OWIN context. // If it cannot be found or doesn't correspond to an authorization // or a token request, throw an InvalidOperationException. var request = Context.GetOpenIdConnectRequest(); if (request == null || (!request.IsAuthorizationRequest() && !request.IsTokenRequest())) { throw new InvalidOperationException("An authorization or token response cannot be returned from this endpoint."); } // Note: if a response was already generated, throw an exception. var response = Context.GetOpenIdConnectResponse(); if (response != null) { throw new InvalidOperationException("A response has already been sent."); } if (string.IsNullOrEmpty(ticket.Identity.GetClaim(OpenIdConnectConstants.Claims.Subject))) { throw new InvalidOperationException("The authentication ticket was rejected because " + "the mandatory subject claim was missing."); } Logger.LogTrace("A sign-in operation was triggered: {Claims} ; {Properties}.", ticket.Identity.Claims, ticket.Properties.Dictionary); // Prepare a new OpenID Connect response. response = new OpenIdConnectResponse(); // Copy the confidentiality level associated with the request to the authentication ticket. if (!ticket.HasProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel)) { ticket.SetConfidentialityLevel(request.GetProperty <string>(OpenIdConnectConstants.Properties.ConfidentialityLevel)); } // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. // Note: the application is allowed to specify a different "scopes": in this case, // don't replace the "scopes" property stored in the authentication ticket. if (request.HasScope(OpenIdConnectConstants.Scopes.OpenId) && !ticket.HasScope()) { ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId); } // When a "resources" property cannot be found in the ticket, // infer it from the "audiences" property. if (ticket.HasAudience() && !ticket.HasResource()) { ticket.SetResources(ticket.GetAudiences()); } // Add the validated client_id to the list of authorized presenters, // unless the presenters were explicitly set by the developer. var presenter = request.GetProperty <string>(OpenIdConnectConstants.Properties.ValidatedClientId); if (!string.IsNullOrEmpty(presenter) && !ticket.HasPresenter()) { ticket.SetPresenters(presenter); } var notification = new ProcessSigninResponseContext(Context, Options, ticket, request, response); if (request.IsAuthorizationRequest()) { // By default, return an authorization code if a response type containing code was specified. notification.IncludeAuthorizationCode = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Code); // By default, return an access token if a response type containing token was specified. notification.IncludeAccessToken = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token); // By default, prevent a refresh token from being returned as the OAuth2 specification // explicitly disallows returning a refresh token from the authorization endpoint. // See https://tools.ietf.org/html/rfc6749#section-4.2.2 for more information. notification.IncludeRefreshToken = false; // By default, return an identity token if a response type containing code // was specified and if the openid scope was explicitly or implicitly granted. notification.IncludeIdentityToken = request.HasResponseType(OpenIdConnectConstants.ResponseTypes.IdToken) && ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId); } else { // By default, prevent an authorization code from being returned as this type of token // cannot be issued from the token endpoint in the standard OAuth2/OpenID Connect flows. notification.IncludeAuthorizationCode = false; // By default, always return an access token. notification.IncludeAccessToken = true; // By default, only return a refresh token is the offline_access scope was granted and if // sliding expiration is disabled or if the request is not a grant_type=refresh_token request. notification.IncludeRefreshToken = ticket.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && (Options.UseSlidingExpiration || !request.IsRefreshTokenGrantType()); // By default, only return an identity token if the openid scope was granted. notification.IncludeIdentityToken = ticket.HasScope(OpenIdConnectConstants.Scopes.OpenId); } await Options.Provider.ProcessSigninResponse(notification); if (notification.HandledResponse) { Logger.LogDebug("The sign-in response was handled in user code."); return(true); } else if (notification.Skipped) { Logger.LogDebug("The default sign-in handling was skipped from user code."); return(false); } else if (notification.IsRejected) { Logger.LogError("The request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); if (request.IsAuthorizationRequest()) { return(await SendAuthorizationResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } // Flow the changes made to the ticket. ticket = notification.Ticket; // Ensure an authentication ticket has been provided or return // an error code indicating that the request was rejected. if (ticket == null) { Logger.LogError("The request was rejected because no authentication ticket was provided."); if (request.IsAuthorizationRequest()) { return(await SendAuthorizationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.AccessDenied, ErrorDescription = "The authorization was denied by the resource owner." })); } return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "The token request was rejected by the authorization server." })); } if (notification.IncludeAuthorizationCode) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); response.Code = await SerializeAuthorizationCodeAsync(ticket.Identity, properties, request, response); } if (notification.IncludeAccessToken) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); // When receiving a grant_type=refresh_token request, determine whether the client application // requests a limited set of scopes and replace the corresponding properties if necessary. if (!string.IsNullOrEmpty(request.Scope) && request.IsTokenRequest() && request.IsRefreshTokenGrantType()) { Logger.LogDebug("The access token scopes will be limited to the scopes requested " + "by the client application: {Scopes}.", request.GetScopes()); // Replace the scopes initially granted by the scopes listed by the client // application in the token request. Note: request.GetScopes() automatically // removes duplicate entries, so additional filtering is not necessary. properties.SetProperty(OpenIdConnectConstants.Properties.Scopes, new JArray(request.GetScopes()).ToString(Formatting.None)); } var scopes = ticket.GetScopes(); if ((request.IsTokenRequest() && request.IsAuthorizationCodeGrantType()) || !new HashSet <string>(scopes).SetEquals(request.GetScopes())) { response.Scope = string.Join(" ", scopes); } response.TokenType = OpenIdConnectConstants.TokenTypes.Bearer; response.AccessToken = await SerializeAccessTokenAsync(ticket.Identity, properties, request, response); // properties.ExpiresUtc is automatically set by SerializeAccessTokenAsync but the end user // is free to set a null value directly in the SerializeAccessToken event. if (properties.ExpiresUtc.HasValue && properties.ExpiresUtc > Options.SystemClock.UtcNow) { var lifetime = properties.ExpiresUtc.Value - Options.SystemClock.UtcNow; response.ExpiresIn = (long)(lifetime.TotalSeconds + .5); } } if (notification.IncludeRefreshToken) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); response.RefreshToken = await SerializeRefreshTokenAsync(ticket.Identity, properties, request, response); } if (notification.IncludeIdentityToken) { // Make sure to create a copy of the authentication properties // to avoid modifying the properties set on the original ticket. var properties = ticket.Properties.Copy(); response.IdToken = await SerializeIdentityTokenAsync(ticket.Identity, properties, request, response); } if (request.IsAuthorizationRequest()) { return(await SendAuthorizationResponseAsync(response, ticket)); } return(await SendTokenResponseAsync(response, ticket)); }
private async Task <string> SerializeAccessTokenAsync( ClaimsIdentity identity, AuthenticationProperties properties, OpenIdConnectMessage request, OpenIdConnectMessage response) { // properties.IssuedUtc and properties.ExpiresUtc // should always be preferred when explicitly set. if (properties.IssuedUtc == null) { properties.IssuedUtc = Options.SystemClock.UtcNow; } if (properties.ExpiresUtc == null) { properties.ExpiresUtc = properties.IssuedUtc + Options.AccessTokenLifetime; } // Create a new identity containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). identity = identity.Clone(claim => { // Never exclude ClaimTypes.NameIdentifier. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "access_token" are not included in the access token. return(claim.HasDestination(OpenIdConnectConstants.Destinations.AccessToken)); }); // Create a new ticket containing the updated properties and the filtered identity. var ticket = new AuthenticationTicket(identity, properties); ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); ticket.SetAudiences(ticket.GetResources()); // Associate a random identifier with the access token. ticket.SetTicketId(Guid.NewGuid().ToString()); // By default, add the client_id to the list of the // presenters allowed to use the access token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeAccessTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeAccessToken(notification); if (!string.IsNullOrEmpty(notification.AccessToken)) { return(notification.AccessToken); } if (notification.SecurityTokenHandler == null) { return(notification.DataFormat?.Protect(ticket)); } // Store the "unique_id" property as a claim. ticket.Identity.AddClaim(notification.SecurityTokenHandler is JwtSecurityTokenHandler ? OpenIdConnectConstants.Claims.JwtId : OpenIdConnectConstants.Claims.TokenId, ticket.GetTicketId()); // Store the "usage" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // If the ticket is marked as confidential, add a new // "confidential" claim in the security token. if (ticket.IsConfidential()) { ticket.Identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.Confidential, "true", ClaimValueTypes.Boolean)); } // Create a new claim per scope item, that will result // in a "scope" array being added in the access token. foreach (var scope in ticket.GetScopes()) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Scope, scope); } var handler = notification.SecurityTokenHandler as JwtSecurityTokenHandler; if (handler != null) { // Note: when used as an access token, a JWT token doesn't have to expose a "sub" claim // but the name identifier claim is used as a substitute when it has been explicitly added. // See https://tools.ietf.org/html/rfc7519#section-4.1.2 var subject = identity.FindFirst(OpenIdConnectConstants.Claims.Subject); if (subject == null) { var identifier = identity.FindFirst(ClaimTypes.NameIdentifier); if (identifier != null) { identity.AddClaim(OpenIdConnectConstants.Claims.Subject, identifier.Value); } } // Remove the ClaimTypes.NameIdentifier claims to avoid getting duplicate claims. // Note: the "sub" claim is automatically mapped by JwtSecurityTokenHandler // to ClaimTypes.NameIdentifier when validating a JWT token. // Note: make sure to call ToArray() to avoid an InvalidOperationException // on old versions of Mono, where FindAll() is implemented using an iterator. foreach (var claim in ticket.Identity.FindAll(ClaimTypes.NameIdentifier).ToArray()) { ticket.Identity.RemoveClaim(claim); } // Store the audiences as claims. foreach (var audience in ticket.GetAudiences()) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Audience, audience); } // Extract the presenters from the authentication ticket. var presenters = ticket.GetPresenters().ToArray(); switch (presenters.Length) { case 0: break; case 1: identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; default: Options.Logger.LogWarning("Multiple presenters have been associated with the access token " + "but the JWT format only accepts single values."); // Only add the first authorized party. identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; } var token = handler.CreateToken( subject: ticket.Identity, issuer: notification.Issuer, signingCredentials: notification.SigningCredentials, notBefore: ticket.Properties.IssuedUtc.Value.UtcDateTime, expires: ticket.Properties.ExpiresUtc.Value.UtcDateTime); token.Payload[OpenIdConnectConstants.Claims.IssuedAt] = EpochTime.GetIntDate(ticket.Properties.IssuedUtc.Value.UtcDateTime); // Try to extract a key identifier from the signing credentials // and add the "kid" property to the JWT header if applicable. LocalIdKeyIdentifierClause clause = null; if (notification.SigningCredentials?.SigningKeyIdentifier != null && notification.SigningCredentials.SigningKeyIdentifier.TryFind(out clause)) { token.Header[JwtHeaderParameterNames.Kid] = clause.LocalId; } return(handler.WriteToken(token)); } else { var descriptor = new SecurityTokenDescriptor { Subject = ticket.Identity, AppliesToAddress = notification.Audiences.ElementAtOrDefault(0), TokenIssuerName = notification.Issuer, EncryptingCredentials = notification.EncryptingCredentials, SigningCredentials = notification.SigningCredentials, Lifetime = new Lifetime( notification.Ticket.Properties.IssuedUtc.Value.UtcDateTime, notification.Ticket.Properties.ExpiresUtc.Value.UtcDateTime) }; // When the encrypting credentials use an asymmetric key, replace them by a // EncryptedKeyEncryptingCredentials instance to generate a symmetric key. if (descriptor.EncryptingCredentials != null && descriptor.EncryptingCredentials.SecurityKey is AsymmetricSecurityKey) { // Note: EncryptedKeyEncryptingCredentials automatically generates an in-memory key // that will be encrypted using the original credentials and added to the resulting token // if the security token handler fully supports token encryption (e.g SAML or SAML2). descriptor.EncryptingCredentials = new EncryptedKeyEncryptingCredentials( wrappingCredentials: notification.EncryptingCredentials, keySizeInBits: 256, encryptionAlgorithm: SecurityAlgorithms.Aes256Encryption); } var token = notification.SecurityTokenHandler.CreateToken(descriptor); // Note: the security token is manually serialized to prevent // an exception from being thrown if the handler doesn't implement // the SecurityTokenHandler.WriteToken overload returning a string. var builder = new StringBuilder(); using (var writer = XmlWriter.Create(builder, new XmlWriterSettings { Encoding = new UTF8Encoding(false), OmitXmlDeclaration = true })) { notification.SecurityTokenHandler.WriteToken(writer, token); } return(builder.ToString()); } }
private async Task <AuthenticationTicket> DeserializeAccessTokenAsync(string token, OpenIdConnectMessage request) { var notification = new DeserializeAccessTokenContext(Context, Options, request, token) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.DeserializeAccessToken(notification); // Directly return the authentication ticket if one // has been provided by DeserializeAccessToken. if (notification.Ticket != null) { return(notification.Ticket); } var handler = notification.SecurityTokenHandler as ISecurityTokenValidator; if (handler == null) { return(notification.DataFormat?.Unprotect(token)); } // Create new validation parameters to validate the security token. // ValidateAudience and ValidateLifetime are always set to false: // if necessary, the audience and the expiration can be validated // in InvokeIntrospectionEndpointAsync or InvokeTokenEndpointAsync. var parameters = new TokenValidationParameters { IssuerSigningKey = notification.SigningCredentials.SigningKey, ValidIssuer = notification.Issuer, ValidateAudience = false, ValidateLifetime = false }; SecurityToken securityToken; ClaimsPrincipal principal; try { principal = handler.ValidateToken(token, parameters, out securityToken); } catch (Exception exception) { Options.Logger.LogInformation("An exception occured when deserializing an access token: {Message}", exception.Message); return(null); } // Parameters stored in AuthenticationProperties are lost // when the identity token is serialized using a security token handler. // To mitigate that, they are inferred from the claims or the security token. var properties = new AuthenticationProperties { ExpiresUtc = securityToken.ValidTo, IssuedUtc = securityToken.ValidFrom }; var ticket = new AuthenticationTicket((ClaimsIdentity)principal.Identity, properties); var audiences = principal.FindAll(OpenIdConnectConstants.Claims.Audience); if (audiences.Any()) { ticket.SetAudiences(audiences.Select(claim => claim.Value)); } var presenters = principal.FindAll(OpenIdConnectConstants.Claims.AuthorizedParty); if (presenters.Any()) { ticket.SetPresenters(presenters.Select(claim => claim.Value)); } var scopes = principal.FindAll(OpenIdConnectConstants.Claims.Scope); if (scopes.Any()) { ticket.SetScopes(scopes.Select(claim => claim.Value)); } // Note: the token identifier may be stored in either the token_id claim // or in the jti claim, which is the standard name used by JWT tokens. var identifier = principal.FindFirst(OpenIdConnectConstants.Claims.JwtId) ?? principal.FindFirst(OpenIdConnectConstants.Claims.TokenId); if (identifier != null) { ticket.SetTicketId(identifier.Value); } var usage = principal.FindFirst(OpenIdConnectConstants.Claims.Usage); if (usage != null) { ticket.SetUsage(usage.Value); } var confidential = principal.FindFirst(OpenIdConnectConstants.Claims.Confidential); if (confidential != null && string.Equals(confidential.Value, "true", StringComparison.OrdinalIgnoreCase)) { ticket.Properties.Dictionary[OpenIdConnectConstants.Properties.Confidential] = "true"; } // Ensure the received ticket is an access token. if (!ticket.IsAccessToken()) { Options.Logger.LogWarning("The received token was not an access token: {Token}.", token); return(null); } return(ticket); }
private async Task <string> SerializeIdentityTokenAsync( ClaimsIdentity identity, AuthenticationProperties properties, OpenIdConnectMessage request, OpenIdConnectMessage response) { // properties.IssuedUtc and properties.ExpiresUtc // should always be preferred when explicitly set. if (properties.IssuedUtc == null) { properties.IssuedUtc = Options.SystemClock.UtcNow; } if (properties.ExpiresUtc == null) { properties.ExpiresUtc = properties.IssuedUtc + Options.IdentityTokenLifetime; } // Replace the identity by a new one containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). identity = identity.Clone(claim => { // Never exclude ClaimTypes.NameIdentifier. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "id_token" are not included in the identity token. return(claim.HasDestination(OpenIdConnectConstants.Destinations.IdentityToken)); }); // Create a new ticket containing the updated properties and the filtered identity. var ticket = new AuthenticationTicket(identity, properties); ticket.SetUsage(OpenIdConnectConstants.Usages.IdToken); // Associate a random identifier with the identity token. ticket.SetTicketId(Guid.NewGuid().ToString()); // By default, add the client_id to the list of the // presenters allowed to use the identity token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetAudiences(request.ClientId); ticket.SetPresenters(request.ClientId); } var notification = new SerializeIdentityTokenContext(Context, Options, request, response, ticket) { Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.IdentityTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeIdentityToken(notification); if (!string.IsNullOrEmpty(notification.IdentityToken)) { return(notification.IdentityToken); } if (notification.SecurityTokenHandler == null) { return(null); } if (!identity.HasClaim(claim => claim.Type == OpenIdConnectConstants.Claims.Subject) && !identity.HasClaim(claim => claim.Type == ClaimTypes.NameIdentifier)) { Options.Logger.LogError("A unique identifier cannot be found to generate a 'sub' claim: " + "make sure to add a 'ClaimTypes.NameIdentifier' claim."); return(null); } // Store the unique subject identifier as a claim. if (!identity.HasClaim(claim => claim.Type == OpenIdConnectConstants.Claims.Subject)) { identity.AddClaim(OpenIdConnectConstants.Claims.Subject, identity.GetClaim(ClaimTypes.NameIdentifier)); } // Remove the ClaimTypes.NameIdentifier claims to avoid getting duplicate claims. // Note: the "sub" claim is automatically mapped by JwtSecurityTokenHandler // to ClaimTypes.NameIdentifier when validating a JWT token. // Note: make sure to call ToArray() to avoid an InvalidOperationException // on old versions of Mono, where FindAll() is implemented using an iterator. foreach (var claim in identity.FindAll(ClaimTypes.NameIdentifier).ToArray()) { identity.RemoveClaim(claim); } // Store the "unique_id" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.JwtId, ticket.GetTicketId()); // Store the "usage" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // If the ticket is marked as confidential, add a new // "confidential" claim in the security token. if (ticket.IsConfidential()) { ticket.Identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.Confidential, "true", ClaimValueTypes.Boolean)); } // Store the audiences as claims. foreach (var audience in ticket.GetAudiences()) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Audience, audience); } // If a nonce was present in the authorization request, it MUST // be included in the id_token generated by the token endpoint. // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation var nonce = request.Nonce; if (request.IsAuthorizationCodeGrantType()) { // Restore the nonce stored in the authentication // ticket extracted from the authorization code. nonce = ticket.GetNonce(); } if (!string.IsNullOrEmpty(nonce)) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Nonce, nonce); } if (!string.IsNullOrEmpty(response.Code)) { using (var algorithm = HashAlgorithm.Create(notification.SigningCredentials.DigestAlgorithm)) { // Create the c_hash using the authorization code returned by SerializeAuthorizationCodeAsync. var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(response.Code)); // Note: only the left-most half of the hash of the octets is used. // See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.CodeHash, Base64UrlEncoder.Encode(hash, 0, hash.Length / 2)); } } if (!string.IsNullOrEmpty(response.AccessToken)) { using (var algorithm = HashAlgorithm.Create(notification.SigningCredentials.DigestAlgorithm)) { // Create the at_hash using the access token returned by SerializeAccessTokenAsync. var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(response.AccessToken)); // Note: only the left-most half of the hash of the octets is used. // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.AccessTokenHash, Base64UrlEncoder.Encode(hash, 0, hash.Length / 2)); } } // Extract the presenters from the authentication ticket. var presenters = ticket.GetPresenters().ToArray(); switch (presenters.Length) { case 0: break; case 1: identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; default: Options.Logger.LogWarning("Multiple presenters have been associated with the identity token " + "but the JWT format only accepts single values."); // Only add the first authorized party. identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; } var token = notification.SecurityTokenHandler.CreateToken( subject: ticket.Identity, issuer: notification.Issuer, signingCredentials: notification.SigningCredentials, notBefore: ticket.Properties.IssuedUtc.Value.UtcDateTime, expires: ticket.Properties.ExpiresUtc.Value.UtcDateTime); token.Payload[OpenIdConnectConstants.Claims.IssuedAt] = EpochTime.GetIntDate(ticket.Properties.IssuedUtc.Value.UtcDateTime); // Try to extract a key identifier from the signing credentials // and add the "kid" property to the JWT header if applicable. LocalIdKeyIdentifierClause clause = null; if (notification.SigningCredentials?.SigningKeyIdentifier != null && notification.SigningCredentials.SigningKeyIdentifier.TryFind(out clause)) { token.Header[JwtHeaderParameterNames.Kid] = clause.LocalId; } return(notification.SecurityTokenHandler.WriteToken(token)); }
private async Task <string> SerializeAuthorizationCodeAsync( ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage request, OpenIdConnectMessage response) { // properties.IssuedUtc and properties.ExpiresUtc // should always be preferred when explicitly set. if (properties.IssuedUtc == null) { properties.IssuedUtc = Options.SystemClock.UtcNow; } if (properties.ExpiresUtc == null) { properties.ExpiresUtc = properties.IssuedUtc + Options.AuthorizationCodeLifetime; } // Claims in authorization codes are never filtered as they are supposed to be opaque: // SerializeAccessTokenAsync and SerializeIdentityTokenAsync are responsible of ensuring // that subsequent access and identity tokens are correctly filtered. var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); // Associate a random identifier with the authorization code. ticket.SetTicketId(Guid.NewGuid().ToString()); // By default, add the client_id to the list of the // presenters allowed to use the authorization code. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeAuthorizationCodeContext(Context, Options, request, response, ticket) { DataFormat = Options.AuthorizationCodeFormat }; await Options.Provider.SerializeAuthorizationCode(notification); if (notification.HandledResponse || !string.IsNullOrEmpty(notification.AuthorizationCode)) { return(notification.AuthorizationCode); } else if (notification.Skipped) { return(null); } if (!ReferenceEquals(ticket, notification.Ticket)) { throw new InvalidOperationException("The authentication ticket cannot be replaced."); } if (notification.DataFormat == null) { return(null); } return(notification.DataFormat.Protect(ticket)); }
private async Task <string> SerializeAccessTokenAsync( ClaimsIdentity identity, AuthenticationProperties properties, OpenIdConnectRequest request, OpenIdConnectResponse response) { // Create a new identity containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). identity = identity.Clone(claim => { // Never exclude ClaimTypes.NameIdentifier. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "access_token" are not included in the access token. return(claim.HasDestination(OpenIdConnectConstants.Destinations.AccessToken)); }); // Create a new ticket containing the updated properties and the filtered identity. var ticket = new AuthenticationTicket(identity, properties); ticket.Properties.IssuedUtc = Options.SystemClock.UtcNow; ticket.Properties.ExpiresUtc = ticket.Properties.IssuedUtc + (ticket.GetAccessTokenLifetime() ?? Options.AccessTokenLifetime); ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); ticket.SetAudiences(ticket.GetResources()); // Associate a random identifier with the access token. ticket.SetTicketId(Guid.NewGuid().ToString()); // By default, add the client_id to the list of the // presenters allowed to use the access token. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeAccessTokenContext(Context, Options, request, response, ticket) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.SerializeAccessToken(notification); if (notification.HandledResponse || !string.IsNullOrEmpty(notification.AccessToken)) { return(notification.AccessToken); } else if (notification.Skipped) { return(null); } if (!notification.Audiences.Any()) { Options.Logger.LogInformation("No explicit audience was associated with the access token."); } if (notification.SecurityTokenHandler == null) { return(notification.DataFormat?.Protect(ticket)); } if (notification.SigningCredentials == null) { throw new InvalidOperationException("A signing key must be provided."); } // Store the "unique_id" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.JwtId, ticket.GetTicketId()); // Store the "usage" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Usage, ticket.GetUsage()); // Store the "confidentiality_level" property as a claim. var confidentiality = ticket.GetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel); if (!string.IsNullOrEmpty(confidentiality)) { identity.AddClaim(OpenIdConnectConstants.Claims.ConfidentialityLevel, confidentiality); } // Create a new claim per scope item, that will result // in a "scope" array being added in the access token. foreach (var scope in notification.Scopes) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Scope, scope); } // Note: when used as an access token, a JWT token doesn't have to expose a "sub" claim // but the name identifier claim is used as a substitute when it has been explicitly added. // See https://tools.ietf.org/html/rfc7519#section-4.1.2 var subject = ticket.Identity.FindFirst(OpenIdConnectConstants.Claims.Subject); if (subject == null) { var identifier = ticket.Identity.FindFirst(ClaimTypes.NameIdentifier); if (identifier != null) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Subject, identifier.Value); } } // Remove the ClaimTypes.NameIdentifier claims to avoid getting duplicate claims. // Note: the "sub" claim is automatically mapped by JwtSecurityTokenHandler // to ClaimTypes.NameIdentifier when validating a JWT token. // Note: make sure to call ToArray() to avoid an InvalidOperationException // on old versions of Mono, where FindAll() is implemented using an iterator. foreach (var claim in ticket.Identity.FindAll(ClaimTypes.NameIdentifier).ToArray()) { ticket.Identity.RemoveClaim(claim); } // Store the audiences as claims. foreach (var audience in notification.Audiences) { ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.Audience, audience); } // Extract the presenters from the authentication ticket. var presenters = notification.Presenters.ToArray(); switch (presenters.Length) { case 0: break; case 1: ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; default: Options.Logger.LogWarning("Multiple presenters have been associated with the access token " + "but the JWT format only accepts single values."); // Only add the first authorized party. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.AuthorizedParty, presenters[0]); break; } if (ticket.Properties.IssuedUtc != null) { ticket.Identity.AddClaim(new Claim( OpenIdConnectConstants.Claims.IssuedAt, EpochTime.GetIntDate(ticket.Properties.IssuedUtc.Value.UtcDateTime).ToString(), ClaimValueTypes.Integer64)); } var descriptor = new SecurityTokenDescriptor { Subject = ticket.Identity, TokenIssuerName = notification.Issuer, EncryptingCredentials = notification.EncryptingCredentials, SigningCredentials = notification.SigningCredentials, Lifetime = new Lifetime( notification.Ticket.Properties.IssuedUtc?.UtcDateTime, notification.Ticket.Properties.ExpiresUtc?.UtcDateTime) }; var token = notification.SecurityTokenHandler.CreateToken(descriptor); return(notification.SecurityTokenHandler.WriteToken(token)); }
/// <summary> /// Sets the presenters list in the authentication ticket. /// Note: this method automatically excludes duplicate presenters. /// </summary> /// <param name="ticket">The authentication ticket.</param> /// <param name="presenters">The presenters to store.</param> /// <returns>The authentication ticket.</returns> public static AuthenticationTicket SetPresenters([NotNull] this AuthenticationTicket ticket, params string[] presenters) { // Note: guarding the presenters parameter against null values // is not necessary as AsEnumerable() doesn't throw on null values. return(ticket.SetPresenters(presenters.AsEnumerable())); }
public async Task HandleTokenRequest_AuthorizationCodeIsAutomaticallyRevoked() { // Arrange var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); var ticket = new AuthenticationTicket( new ClaimsPrincipal(identity), new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); ticket.SetPresenters("Fabrikam"); ticket.SetTicketId("3E228451-1555-46F7-A471-951EFBA23A56"); ticket.SetUsage(OpenIdConnectConstants.Usages.AuthorizationCode); var format = new Mock <ISecureDataFormat <AuthenticationTicket> >(); format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) .Returns(ticket); var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny <CancellationToken>())) .ReturnsAsync(token); }); var server = CreateAuthorizationServer(builder => { builder.Services.AddSingleton(CreateApplicationManager(instance => { var application = new OpenIddictApplication(); instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny <CancellationToken>())) .ReturnsAsync(application); instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny <CancellationToken>())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); builder.Services.AddSingleton(manager); builder.Configure(options => options.AuthorizationCodeFormat = format.Object); }); var client = new OpenIdConnectClient(server.CreateClient()); // Act var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { ClientId = "Fabrikam", Code = "SplxlOBeZQQYbYS6WxSbIA", GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode }); // Assert Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny <CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny <CancellationToken>()), Times.Once()); }
private async Task <string> SerializeAuthorizationCodeAsync( ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage request, OpenIdConnectMessage response) { // properties.IssuedUtc and properties.ExpiresUtc // should always be preferred when explicitly set. if (properties.IssuedUtc == null) { properties.IssuedUtc = Options.SystemClock.UtcNow; } if (properties.ExpiresUtc == null) { properties.ExpiresUtc = properties.IssuedUtc + Options.AuthorizationCodeLifetime; } // Claims in authorization codes are never filtered as they are supposed to be opaque: // SerializeAccessTokenAsync and SerializeIdentityTokenAsync are responsible of ensuring // that subsequent access and identity tokens are correctly filtered. var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); ticket.SetUsage(OpenIdConnectConstants.Usages.Code); // By default, add the client_id to the list of the // presenters allowed to use the authorization code. if (!string.IsNullOrEmpty(request.ClientId)) { ticket.SetPresenters(request.ClientId); } var notification = new SerializeAuthorizationCodeContext(Context, Options, request, response, ticket) { DataFormat = Options.AuthorizationCodeFormat }; await Options.Provider.SerializeAuthorizationCode(notification); if (!string.IsNullOrEmpty(notification.AuthorizationCode)) { return(notification.AuthorizationCode); } if (notification.DataFormat == null) { return(null); } var key = Options.RandomNumberGenerator.GenerateKey(length: 256 / 8); using (var stream = new MemoryStream()) using (var writter = new StreamWriter(stream)) { writter.Write(notification.DataFormat.Protect(ticket)); writter.Flush(); // Serialize the authorization code. var bytes = stream.ToArray(); // Store the authorization code in the distributed cache. await Options.Cache.SetAsync($"asos-authorization-code:{key}", bytes, new DistributedCacheEntryOptions { AbsoluteExpiration = ticket.Properties.ExpiresUtc }); } return(key); }
private async Task <AuthenticationTicket> DeserializeIdentityTokenAsync(string token, OpenIdConnectRequest request) { var notification = new DeserializeIdentityTokenContext(Context, Options, request, token) { SecurityTokenHandler = Options.IdentityTokenHandler }; // Note: ValidateAudience and ValidateLifetime are always set to false: // if necessary, the audience and the expiration can be validated // in InvokeIntrospectionEndpointAsync or InvokeTokenEndpointAsync. notification.TokenValidationParameters = new TokenValidationParameters { IssuerSigningKeys = Options.SigningCredentials.Select(credentials => credentials.Key), NameClaimType = OpenIdConnectConstants.Claims.Name, RoleClaimType = OpenIdConnectConstants.Claims.Role, ValidIssuer = Context.GetIssuer(Options), ValidateAudience = false, ValidateLifetime = false }; await Options.Provider.DeserializeIdentityToken(notification); if (notification.HandledResponse || notification.Ticket != null) { notification.Ticket.SetUsage(OpenIdConnectConstants.Usages.IdentityToken); return(notification.Ticket); } else if (notification.Skipped) { return(null); } if (notification.SecurityTokenHandler == null) { return(null); } SecurityToken securityToken; ClaimsPrincipal principal; try { if (!notification.SecurityTokenHandler.CanReadToken(token)) { Logger.LogDebug("The identity token handler refused to read the token: {Token}", token); return(null); } principal = notification.SecurityTokenHandler.ValidateToken(token, notification.TokenValidationParameters, out securityToken); } catch (Exception exception) { Logger.LogDebug("An exception occured when deserializing an identity token: {Message}.", exception.Message); return(null); } // Parameters stored in AuthenticationProperties are lost // when the identity token is serialized using a security token handler. // To mitigate that, they are inferred from the claims or the security token. var properties = new AuthenticationProperties { ExpiresUtc = securityToken.ValidTo, IssuedUtc = securityToken.ValidFrom }; var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); var audiences = principal.FindAll(OpenIdConnectConstants.Claims.Audience); if (audiences.Any()) { ticket.SetAudiences(audiences.Select(claim => claim.Value)); } var presenters = principal.FindAll(OpenIdConnectConstants.Claims.AuthorizedParty); if (presenters.Any()) { ticket.SetPresenters(presenters.Select(claim => claim.Value)); } var identifier = principal.FindFirst(OpenIdConnectConstants.Claims.JwtId); if (identifier != null) { ticket.SetTicketId(identifier.Value); } var usage = principal.FindFirst(OpenIdConnectConstants.Claims.Usage); if (usage != null) { ticket.SetUsage(usage.Value); } var confidentiality = principal.FindFirst(OpenIdConnectConstants.Claims.ConfidentialityLevel); if (confidentiality != null) { ticket.SetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel, confidentiality.Value); } // Ensure the received ticket is an identity token. if (!ticket.IsIdentityToken()) { Logger.LogDebug("The received token was not an identity token: {Token}.", token); return(null); } return(ticket); }
public async Task ProcessSigninResponse_ReturnsErrorResponseWhenRedeemingAuthorizationCodeFails() { // Arrange var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "Bob le Bricoleur"); var ticket = new AuthenticationTicket( new ClaimsPrincipal(identity), new AuthenticationProperties(), OpenIdConnectServerDefaults.AuthenticationScheme); ticket.SetPresenters("Fabrikam"); ticket.SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); ticket.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AuthorizationCode); var format = new Mock <ISecureDataFormat <AuthenticationTicket> >(); format.Setup(mock => mock.Unprotect("SplxlOBeZQQYbYS6WxSbIA")) .Returns(ticket); var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { instance.Setup(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny <CancellationToken>())) .ReturnsAsync(token); instance.Setup(mock => mock.IsValidAsync(token, It.IsAny <CancellationToken>())) .ReturnsAsync(true); instance.Setup(mock => mock.RedeemAsync(token, It.IsAny <CancellationToken>())) .ThrowsAsync(new Exception()); }); var server = CreateAuthorizationServer(builder => { builder.Services.AddSingleton(CreateApplicationManager(instance => { var application = new OpenIddictApplication(); instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny <CancellationToken>())) .ReturnsAsync(application); instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny <CancellationToken>())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Public); })); builder.Services.AddSingleton(manager); builder.Configure(options => options.AuthorizationCodeFormat = format.Object); }); var client = new OpenIdConnectClient(server.CreateClient()); // Act var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest { ClientId = "Fabrikam", Code = "SplxlOBeZQQYbYS6WxSbIA", GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode, RedirectUri = "http://www.fabrikam.com/path" }); // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny <CancellationToken>()), Times.Exactly(2)); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny <CancellationToken>()), Times.Once()); }