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)); }
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; } properties.SetUsage(OpenIdConnectConstants.Usages.IdToken); // Replace the principal by a new one containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). principal = principal.Clone(claim => { // ClaimTypes.NameIdentifier and JwtRegisteredClaimNames.Sub are never excluded. if (string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Sub, StringComparison.Ordinal)) { 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.ResponseTypes.IdToken)); }); 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); var notification = new SerializeIdentityTokenContext(Context, Options, request, response, ticket) { Confidential = ticket.IsConfidential(), Issuer = Context.GetIssuer(Options), Nonce = request.Nonce, SecurityTokenHandler = Options.IdentityTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; // 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 if (request.IsAuthorizationCodeGrantType()) { // Restore the nonce stored in the authentication // ticket extracted from the authorization code. notification.Nonce = ticket.GetNonce(); } // While the 'sub' claim is declared mandatory by the OIDC specs, // it is not always issued as-is by the authorization servers. // When missing, the name identifier claim is used as a substitute. // See http://openid.net/specs/openid-connect-core-1_0.html#IDToken notification.Subject = identity.GetClaim(JwtRegisteredClaimNames.Sub) ?? identity.GetClaim(ClaimTypes.NameIdentifier); // Only add client_id in the audiences list if it is non-null. if (!string.IsNullOrEmpty(request.ClientId)) { notification.Audiences.Add(request.ClientId); } if (!string.IsNullOrEmpty(response.Code)) { using (var algorithm = SHA256.Create()) { // 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 notification.CHash = Base64UrlEncoder.Encode(hash, 0, hash.Length / 2); } } if (!string.IsNullOrEmpty(response.AccessToken)) { using (var algorithm = SHA256.Create()) { // 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 notification.AtHash = Base64UrlEncoder.Encode(hash, 0, hash.Length / 2); } } await Options.Provider.SerializeIdentityToken(notification); if (!string.IsNullOrEmpty(notification.IdentityToken)) { return(notification.IdentityToken); } // Allow the application to change the authentication // ticket from the SerializeIdentityTokenAsync event. ticket = notification.AuthenticationTicket; ticket.Properties.CopyTo(properties); if (notification.SecurityTokenHandler == null) { return(null); } if (string.IsNullOrEmpty(notification.Subject)) { Logger.LogError("A unique identifier cannot be found to generate a 'sub' claim. " + "Make sure to either add a 'sub' or a 'ClaimTypes.NameIdentifier' claim " + "in the returned ClaimsIdentity before calling SignIn."); return(null); } // Extract the main identity from the principal. identity = (ClaimsIdentity)ticket.Principal.Identity; // 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 subject identifier as a claim. identity.AddClaim(JwtRegisteredClaimNames.Sub, notification.Subject); // Store the "usage" property as a claim. identity.AddClaim(OpenIdConnectConstants.Extra.Usage, ticket.Properties.GetUsage()); // Store the audiences as claims. foreach (var audience in notification.Audiences) { identity.AddClaim(JwtRegisteredClaimNames.Aud, audience); } if (!string.IsNullOrEmpty(notification.AtHash)) { identity.AddClaim(JwtRegisteredClaimNames.AtHash, notification.AtHash); } if (!string.IsNullOrEmpty(notification.CHash)) { identity.AddClaim(JwtRegisteredClaimNames.CHash, notification.CHash); } if (!string.IsNullOrEmpty(notification.Nonce)) { identity.AddClaim(JwtRegisteredClaimNames.Nonce, notification.Nonce); } // If the ticket is marked as confidential, add a new // "confidential" claim in the security token. if (notification.Confidential) { identity.AddClaim(new Claim(OpenIdConnectConstants.Extra.Confidential, "true", ClaimValueTypes.Boolean)); } var token = notification.SecurityTokenHandler.CreateToken( 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); if (notification.SigningCredentials != null) { 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 ASP.NET 5. // 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()); } object identifier; if (!token.Header.TryGetValue(JwtHeaderParameterNames.Kid, out identifier) || identifier == null) { // When the token doesn't contain a "kid" parameter in the header, automatically // add one using the identifier specified in the signing credentials. identifier = notification.SigningCredentials.Kid; if (identifier == null) { // When no key identifier has been explicitly added by the developer, a "kid" is automatically // inferred from the hexadecimal representation of the certificate thumbprint (SHA-1). if (x509SecurityKey != null) { identifier = x509SecurityKey.Certificate.Thumbprint; } // When no key identifier has been explicitly added by the developer, a "kid" // is automatically inferred from the modulus if the signing key is a RSA key. var rsaSecurityKey = notification.SigningCredentials.Key as RsaSecurityKey; if (rsaSecurityKey != null) { // Only use the 40 first chars to match the identifier used by the JWKS endpoint. identifier = Base64UrlEncoder.Encode(rsaSecurityKey.Parameters.Modulus) .Substring(0, 40) .ToUpperInvariant(); } } token.Header[JwtHeaderParameterNames.Kid] = identifier; } } return(notification.SecurityTokenHandler.WriteToken(token)); }
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)) { throw new InvalidOperationException("A unique identifier cannot be found to generate a 'sub' claim: " + "make sure to add a 'ClaimTypes.NameIdentifier' claim."); } // 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)); }