/// <summary>
 /// Called to create a new identity token. An application may use this context
 /// to replace the authentication ticket before it is serialized or to use its own token format
 /// and skip the default logic using <see cref="BaseContext{OpenIdConnectServerOptions}.HandleResponse"/>.
 /// </summary>
 /// <param name="context">The context of the event carries information in and results out.</param>
 /// <returns>Task to enable asynchronous execution</returns>
 public virtual Task CreateIdentityToken(CreateIdentityTokenContext context) => OnCreateIdentityToken(context);
Exemplo n.º 2
0
        private async Task <string> CreateIdentityTokenAsync(
            ClaimsPrincipal principal, AuthenticationProperties properties,
            OpenIdConnectMessage request, OpenIdConnectMessage response)
        {
            try {
                // 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;

                identity.AddClaim(JwtRegisteredClaimNames.Iat,
                                  EpochTime.GetIntDate(properties.IssuedUtc.Value.UtcDateTime).ToString());

                if (!string.IsNullOrEmpty(response.Code))
                {
                    using (var algorithm = SHA256.Create()) {
                        // Create the c_hash using the authorization code returned by CreateAuthorizationCodeAsync.
                        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 = SHA256.Create()) {
                        // Create the at_hash using the access token returned by CreateAccessTokenAsync.
                        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));
                    }
                }

                var nonce = request.Nonce;

                // 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.
                    nonce = properties.GetNonce();
                }

                if (!string.IsNullOrEmpty(nonce))
                {
                    identity.AddClaim(JwtRegisteredClaimNames.Nonce, nonce);
                }

                // 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
                var subject = principal.FindFirst(JwtRegisteredClaimNames.Sub);
                if (subject == null)
                {
                    var identifier = principal.FindFirst(ClaimTypes.NameIdentifier);
                    if (identifier == null)
                    {
                        throw new InvalidOperationException(
                                  "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.");
                    }

                    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);
                }

                // Create a new ticket containing the updated properties and the filtered principal.
                var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme);

                var notification = new CreateIdentityTokenContext(Context, Options, request, response, ticket)
                {
                    Issuer = Context.GetIssuer(Options),
                    SecurityTokenHandler = Options.IdentityTokenHandler,
                    SignatureProvider    = Options.SignatureProvider,
                    SigningCredentials   = Options.SigningCredentials.FirstOrDefault()
                };

                // Only add client_id in the audiences list if it is non-null.
                if (!string.IsNullOrEmpty(request.ClientId))
                {
                    notification.Audiences.Add(request.ClientId);
                }

                // Sets the default identity token serializer.
                notification.Serializer = payload => {
                    if (notification.SecurityTokenHandler == null)
                    {
                        return(Task.FromResult <string>(null));
                    }

                    // Extract the main identity from the principal.
                    identity = (ClaimsIdentity)payload.Principal.Identity;

                    // Store the "usage" property as a claim.
                    identity.AddClaim(OpenIdConnectConstants.Extra.Usage, payload.Properties.GetUsage());

                    // If the ticket is marked as confidential,
                    // add a new "conf" claim in the JWT token.
                    if (payload.Properties.IsConfidential())
                    {
                        identity.AddClaim(OpenIdConnectConstants.Extra.Confidential, "true");
                    }

                    // Store the audiences as claims.
                    foreach (var audience in notification.Audiences)
                    {
                        identity.AddClaim(JwtRegisteredClaimNames.Aud, audience);
                    }

                    var token = notification.SecurityTokenHandler.CreateToken(
                        subject: identity,
                        issuer: notification.Issuer,
                        signatureProvider: notification.SignatureProvider,
                        signingCredentials: notification.SigningCredentials,
                        notBefore: payload.Properties.IssuedUtc.Value.UtcDateTime,
                        expires: payload.Properties.ExpiresUtc.Value.UtcDateTime);

                    if (notification.SigningCredentials != null)
                    {
                        var x509SecurityKey = notification.SigningCredentials.SigningKey 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 or in the security key.
                            identifier = notification.SigningCredentials.Kid ?? notification.SigningCredentials.SigningKey.KeyId;

                            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.SigningKey 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(Task.FromResult(notification.SecurityTokenHandler.WriteToken(token)));
                };

                await Options.Provider.CreateIdentityToken(notification);

                // Treat a non-null identity token like an implicit HandleResponse call.
                if (notification.HandledResponse || !string.IsNullOrEmpty(notification.IdentityToken))
                {
                    return(notification.IdentityToken);
                }

                else if (notification.Skipped)
                {
                    return(null);
                }

                // Allow the application to change the authentication
                // ticket from the CreateIdentityTokenAsync event.
                ticket = notification.AuthenticationTicket;
                ticket.Properties.CopyTo(properties);

                return(await notification.SerializeTicketAsync());
            }

            catch (Exception exception) {
                Logger.LogWarning("An exception occured when serializing an identity token.", exception);

                return(null);
            }
        }