private async Task <bool> InvokeCryptographyEndpointAsync() { // Metadata requests must be made via GET. // See http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest if (!string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The cryptography request was rejected because an invalid " + "HTTP method was specified: {Method}.", Request.Method); return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified HTTP method is not valid." })); } var request = new OpenIdConnectRequest(Request.Query); // Note: set the message type before invoking the ExtractCryptographyRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.CryptographyRequest); // Store the cryptography request in the OWIN context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractCryptographyRequestContext(Context, Options, request); await Options.Provider.ExtractCryptographyRequest(@event); if (@event.HandledResponse) { Logger.LogDebug("The cryptography request was handled in user code."); return(true); } else if (@event.Skipped) { Logger.LogDebug("The default cryptography request handling was skipped from user code."); return(false); } else if (@event.IsRejected) { Logger.LogError("The cryptography request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } Logger.LogInformation("The cryptography request was successfully extracted " + "from the HTTP request: {Request}.", request); var context = new ValidateCryptographyRequestContext(Context, Options, request); await Options.Provider.ValidateCryptographyRequest(context); if (context.HandledResponse) { Logger.LogDebug("The cryptography request was handled in user code."); return(true); } else if (context.Skipped) { Logger.LogDebug("The default cryptography request handling was skipped from user code."); return(false); } else if (context.IsRejected) { Logger.LogError("The cryptography request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } var notification = new HandleCryptographyRequestContext(Context, Options, request); foreach (var credentials in Options.SigningCredentials) { // If the signing key is not an asymmetric key, ignore it. if (!(credentials.Key is AsymmetricSecurityKey)) { Logger.LogDebug("A non-asymmetric signing key of type '{Type}' was excluded " + "from the key set.", credentials.Key.GetType().FullName); continue; } #if SUPPORTS_ECDSA if (!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512)) { Logger.LogInformation("An unsupported signing key of type '{Type}' was ignored and excluded " + "from the key set. Only RSA and ECDSA asymmetric security keys can be " + "exposed via the JWKS endpoint.", credentials.Key.GetType().Name); continue; } #else if (!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256)) { Logger.LogInformation("An unsupported signing key of type '{Type}' was ignored and excluded " + "from the key set. Only RSA asymmetric security keys can be exposed " + "via the JWKS endpoint.", credentials.Key.GetType().Name); continue; } #endif var key = new JsonWebKey { Use = JsonWebKeyUseNames.Sig, // Resolve the JWA identifier from the algorithm specified in the credentials. Alg = OpenIdConnectServerHelpers.GetJwtAlgorithm(credentials.Algorithm), // Use the key identifier specified in the signing credentials. Kid = credentials.Kid, }; if (credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256)) { RSA algorithm = null; // Note: IdentityModel 5 doesn't expose a method allowing to retrieve the underlying algorithm // from a generic asymmetric security key. To work around this limitation, try to cast // the security key to the built-in IdentityModel types to extract the required RSA instance. // See https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/395 if (credentials.Key is X509SecurityKey x509SecurityKey) { algorithm = x509SecurityKey.PublicKey as RSA; } else if (credentials.Key is RsaSecurityKey rsaSecurityKey) { algorithm = rsaSecurityKey.Rsa; // If no RSA instance can be found, create one using // the RSA parameters attached to the security key. if (algorithm == null) { var rsa = RSA.Create(); rsa.ImportParameters(rsaSecurityKey.Parameters); algorithm = rsa; } } // Skip the key if an algorithm instance cannot be extracted. if (algorithm == null) { Logger.LogWarning("A signing key was ignored because it was unable " + "to provide the requested algorithm instance."); continue; } // Export the RSA public key to create a new JSON Web Key // exposing the exponent and the modulus parameters. var parameters = algorithm.ExportParameters(includePrivateParameters: false); Debug.Assert(parameters.Exponent != null && parameters.Modulus != null, "RSA.ExportParameters() shouldn't return null parameters."); key.Kty = JsonWebAlgorithmsKeyTypes.RSA; // Note: both E and N must be base64url-encoded. // See https://tools.ietf.org/html/rfc7518#section-6.3.1.1 key.E = Base64UrlEncoder.Encode(parameters.Exponent); key.N = Base64UrlEncoder.Encode(parameters.Modulus); } #if SUPPORTS_ECDSA else if (credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256) || credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384) || credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512)) { ECDsa algorithm = null; if (credentials.Key is X509SecurityKey x509SecurityKey) { algorithm = x509SecurityKey.PublicKey as ECDsa; } else if (credentials.Key is ECDsaSecurityKey ecdsaSecurityKey) { algorithm = ecdsaSecurityKey.ECDsa; } // Skip the key if an algorithm instance cannot be extracted. if (algorithm == null) { Logger.LogWarning("A signing key was ignored because it was unable " + "to provide the requested algorithm instance."); continue; } // Export the ECDsa public key to create a new JSON Web Key // exposing the coordinates of the point on the curve. var parameters = algorithm.ExportParameters(includePrivateParameters: false); Debug.Assert(parameters.Q.X != null && parameters.Q.Y != null, "ECDsa.ExportParameters() shouldn't return null coordinates."); key.Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve; key.Crv = OpenIdConnectServerHelpers.GetJwtAlgorithmCurve(parameters.Curve); // Note: both X and Y must be base64url-encoded. // See https://tools.ietf.org/html/rfc7518#section-6.2.1.2 key.X = Base64UrlEncoder.Encode(parameters.Q.X); key.Y = Base64UrlEncoder.Encode(parameters.Q.Y); } #endif // If the signing key is embedded in a X.509 certificate, set // the x5t and x5c parameters using the certificate details. var certificate = (credentials.Key as X509SecurityKey)?.Certificate; if (certificate != null) { // x5t must be base64url-encoded. // See https://tools.ietf.org/html/rfc7517#section-4.8 key.X5t = Base64UrlEncoder.Encode(certificate.GetCertHash()); // Unlike E or N, the certificates contained in x5c // must be base64-encoded and not base64url-encoded. // See https://tools.ietf.org/html/rfc7517#section-4.7 key.X5c.Add(Convert.ToBase64String(certificate.RawData)); } notification.Keys.Add(key); } await Options.Provider.HandleCryptographyRequest(notification); if (notification.HandledResponse) { Logger.LogDebug("The cryptography request was handled in user code."); return(true); } else if (notification.Skipped) { Logger.LogDebug("The default cryptography request handling was skipped from user code."); return(false); } else if (notification.IsRejected) { Logger.LogError("The cryptography request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } var keys = new JArray(); foreach (var key in notification.Keys) { var item = new JObject(); // Ensure a key type has been provided. // See https://tools.ietf.org/html/rfc7517#section-4.1 if (string.IsNullOrEmpty(key.Kty)) { Logger.LogError("A JSON Web Key was excluded from the key set because " + "it didn't contain the mandatory 'kid' parameter."); continue; } // Create a dictionary associating the // JsonWebKey components with their values. var parameters = new Dictionary <string, string> { [JsonWebKeyParameterNames.Kid] = key.Kid, [JsonWebKeyParameterNames.Use] = key.Use, [JsonWebKeyParameterNames.Kty] = key.Kty, [JsonWebKeyParameterNames.Alg] = key.Alg, [JsonWebKeyParameterNames.Crv] = key.Crv, [JsonWebKeyParameterNames.E] = key.E, [JsonWebKeyParameterNames.N] = key.N, [JsonWebKeyParameterNames.X] = key.X, [JsonWebKeyParameterNames.Y] = key.Y, [JsonWebKeyParameterNames.X5t] = key.X5t, [JsonWebKeyParameterNames.X5u] = key.X5u }; foreach (var parameter in parameters) { if (!string.IsNullOrEmpty(parameter.Value)) { item.Add(parameter.Key, parameter.Value); } } if (key.KeyOps.Count != 0) { item.Add(JsonWebKeyParameterNames.KeyOps, new JArray(key.KeyOps)); } if (key.X5c.Count != 0) { item.Add(JsonWebKeyParameterNames.X5c, new JArray(key.X5c)); } keys.Add(item); } // Note: AddParameter() is used here to ensure the mandatory "keys" node // is returned to the caller, even if the key set doesn't expose any key. // See https://tools.ietf.org/html/rfc7517#section-5 for more information. var response = new OpenIdConnectResponse(); response.AddParameter(OpenIdConnectConstants.Parameters.Keys, keys); return(await SendCryptographyResponseAsync(response)); }
private async Task <bool> InvokeTokenEndpointAsync() { if (!string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogError("The token request was rejected because an invalid " + "HTTP method was received: {Method}.", Request.Method); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed token request has been received: make sure to use POST." })); } // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrEmpty(Request.ContentType)) { Options.Logger.LogError("The token request was rejected because the " + "mandatory 'Content-Type' header was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed token request has been received: " + "the mandatory 'Content-Type' header was missing from the POST request." })); } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogError("The token request was rejected because an invalid 'Content-Type' " + "header was received: {ContentType}.", Request.ContentType); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed token request has been received: " + "the 'Content-Type' header contained an unexcepted value. " + "Make sure to use 'application/x-www-form-urlencoded'." })); } var request = new OpenIdConnectRequest(await Request.ReadFormAsync()); // Note: set the message type before invoking the ExtractTokenRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.Token); // Store the token request in the OWIN context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractTokenRequestContext(Context, Options, request); await Options.Provider.ExtractTokenRequest(@event); if (@event.HandledResponse) { return(true); } else if (@event.Skipped) { return(false); } else if (@event.IsRejected) { Options.Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } // Reject token requests missing the mandatory grant_type parameter. if (string.IsNullOrEmpty(request.GrantType)) { Options.Logger.LogError("The token request was rejected because the grant type was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'grant_type' parameter was missing.", })); } // Reject grant_type=authorization_code requests if the authorization endpoint is disabled. else if (request.IsAuthorizationCodeGrantType() && !Options.AuthorizationEndpointPath.HasValue) { Options.Logger.LogError("The token request was rejected because the authorization code grant was disabled."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.UnsupportedGrantType, ErrorDescription = "The authorization code grant is not allowed by this authorization server." })); } // Reject grant_type=authorization_code requests missing the authorization code. // See https://tools.ietf.org/html/rfc6749#section-4.1.3 else if (request.IsAuthorizationCodeGrantType() && string.IsNullOrEmpty(request.Code)) { Options.Logger.LogError("The token request was rejected because the authorization code was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'code' parameter was missing." })); } // Reject grant_type=refresh_token requests missing the refresh token. // See https://tools.ietf.org/html/rfc6749#section-6 else if (request.IsRefreshTokenGrantType() && string.IsNullOrEmpty(request.RefreshToken)) { Options.Logger.LogError("The token request was rejected because the refresh token was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'refresh_token' parameter was missing." })); } // Reject grant_type=password requests missing username or password. // See https://tools.ietf.org/html/rfc6749#section-4.3.2 else if (request.IsPasswordGrantType() && (string.IsNullOrEmpty(request.Username) || string.IsNullOrEmpty(request.Password))) { Options.Logger.LogError("The token request was rejected because the resource owner credentials were missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'username' and/or 'password' parameters " + "was/were missing from the request message." })); } // When client_id and client_secret are both null, try to extract them from the Authorization header. // See http://tools.ietf.org/html/rfc6749#section-2.3.1 and // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication if (string.IsNullOrEmpty(request.ClientId) && string.IsNullOrEmpty(request.ClientSecret)) { var header = Request.Headers.Get("Authorization"); if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { try { var value = header.Substring("Basic ".Length).Trim(); var data = Encoding.UTF8.GetString(Convert.FromBase64String(value)); var index = data.IndexOf(':'); if (index >= 0) { request.ClientId = data.Substring(0, index); request.ClientSecret = data.Substring(index + 1); } } catch (FormatException) { } catch (ArgumentException) { } } } var context = new ValidateTokenRequestContext(Context, Options, request); await Options.Provider.ValidateTokenRequest(context); // If the validation context was set as fully validated, // mark the OpenID Connect request as confidential. if (context.IsValidated) { request.SetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel, OpenIdConnectConstants.ConfidentialityLevels.Private); } if (context.HandledResponse) { return(true); } else if (context.Skipped) { return(false); } else if (context.IsRejected) { Options.Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } // Reject grant_type=client_credentials requests if validation was skipped. else if (context.IsSkipped && request.IsClientCredentialsGrantType()) { Options.Logger.LogError("The token request must be fully validated to use the client_credentials grant type."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Client authentication is required when using client_credentials." })); } // Ensure that the client_id has been set from the ValidateTokenRequest event. else if (context.IsValidated && string.IsNullOrEmpty(request.ClientId)) { Options.Logger.LogError("The token request was validated but the client_id was not set."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." })); } // At this stage, client_id cannot be null for grant_type=authorization_code requests, // as it must either be set in the ValidateTokenRequest notification // by the developer or manually flowed by non-confidential client applications. // See https://tools.ietf.org/html/rfc6749#section-4.1.3 if (request.IsAuthorizationCodeGrantType() && string.IsNullOrEmpty(request.ClientId)) { Options.Logger.LogError("The token request was rejected because the mandatory 'client_id' was missing."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "client_id was missing from the token request" })); } AuthenticationTicket ticket = null; // See http://tools.ietf.org/html/rfc6749#section-4.1 // and http://tools.ietf.org/html/rfc6749#section-4.1.3 (authorization code grant). // See http://tools.ietf.org/html/rfc6749#section-6 (refresh token grant). if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) { ticket = request.IsAuthorizationCodeGrantType() ? await DeserializeAuthorizationCodeAsync(request.Code, request) : await DeserializeRefreshTokenAsync(request.RefreshToken, request); if (ticket == null) { Options.Logger.LogError("The token request was rejected because the " + "authorization code or the refresh token was invalid."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Invalid ticket" })); } // If the client was fully authenticated when retrieving its refresh token, // the current request must be rejected if client authentication was not enforced. if (request.IsRefreshTokenGrantType() && !context.IsValidated && ticket.IsConfidential()) { Options.Logger.LogError("The token request was rejected because client authentication " + "was required to use the confidential refresh token."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Client authentication is required to use this ticket" })); } if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Options.Logger.LogError("The token request was rejected because the " + "authorization code or the refresh token was expired."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Expired ticket" })); } // Note: presenters may be empty during a grant_type=refresh_token request if the refresh token // was issued to a public client but cannot be null for an authorization code grant request. var presenters = ticket.GetPresenters(); if (request.IsAuthorizationCodeGrantType() && !presenters.Any()) { Options.Logger.LogError("The token request was rejected because the authorization " + "code didn't contain any valid presenter."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." })); } // Ensure the authorization code/refresh token was issued to the client application making the token request. // Note: when using the refresh token grant, client_id is optional but must validated if present. // As a consequence, this check doesn't depend on the actual status of client authentication. // See https://tools.ietf.org/html/rfc6749#section-6 // and http://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken if (!string.IsNullOrEmpty(request.ClientId) && presenters.Any() && !presenters.Contains(request.ClientId, StringComparer.Ordinal)) { Options.Logger.LogError("The token request was rejected because the authorization " + "code was issued to a different client application."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Ticket does not contain matching client_id" })); } // Validate the redirect_uri flowed by the client application during this token request. // Note: for pure OAuth2 requests, redirect_uri is only mandatory if the authorization request // contained an explicit redirect_uri. OpenID Connect requests MUST include a redirect_uri // but the specifications allow proceeding the token request without returning an error // if the authorization request didn't contain an explicit redirect_uri. // See https://tools.ietf.org/html/rfc6749#section-4.1.3 // and http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation var address = ticket.GetProperty(OpenIdConnectConstants.Properties.RedirectUri); if (request.IsAuthorizationCodeGrantType() && !string.IsNullOrEmpty(address)) { ticket.SetProperty(OpenIdConnectConstants.Properties.RedirectUri, null); if (string.IsNullOrEmpty(request.RedirectUri)) { Options.Logger.LogError("The token request was rejected because the mandatory 'redirect_uri' " + "parameter was missing from the grant_type=authorization_code request."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "redirect_uri was missing from the token request" })); } else if (!string.Equals(address, request.RedirectUri, StringComparison.Ordinal)) { Options.Logger.LogError("The token request was rejected because the 'redirect_uri' " + "parameter didn't correspond to the expected value."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Authorization code does not contain matching redirect_uri" })); } } // If a code challenge was initially sent in the authorization request and associated with the // code, validate the code verifier to ensure the token request is sent by a legit caller. var challenge = ticket.GetProperty(OpenIdConnectConstants.Properties.CodeChallenge); if (request.IsAuthorizationCodeGrantType() && !string.IsNullOrEmpty(challenge)) { ticket.SetProperty(OpenIdConnectConstants.Properties.CodeChallenge, null); // Get the code verifier from the token request. // If it cannot be found, return an invalid_grant error. var verifier = request.CodeVerifier; if (string.IsNullOrEmpty(verifier)) { Options.Logger.LogError("The token request was rejected because the required 'code_verifier' " + "parameter was missing from the grant_type=authorization_code request."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The required 'code_verifier' was missing from the token request." })); } // Note: the code challenge method is always validated when receiving the authorization request. var method = ticket.GetProperty(OpenIdConnectConstants.Properties.CodeChallengeMethod); ticket.SetProperty(OpenIdConnectConstants.Properties.CodeChallengeMethod, null); Debug.Assert(string.IsNullOrEmpty(method) || string.Equals(method, OpenIdConnectConstants.CodeChallengeMethods.Plain, StringComparison.Ordinal) || string.Equals(method, OpenIdConnectConstants.CodeChallengeMethods.Sha256, StringComparison.Ordinal), "The specified code challenge method should be supported."); // If the S256 challenge method was used, compute the hash corresponding to the code verifier. if (string.Equals(method, OpenIdConnectConstants.CodeChallengeMethods.Sha256, StringComparison.Ordinal)) { using (var algorithm = SHA256.Create()) { // Compute the SHA-256 hash of the code verifier and encode it using base64-url. // See https://tools.ietf.org/html/rfc7636#section-4.6 for more information. var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(request.CodeVerifier)); verifier = Base64UrlEncoder.Encode(hash); } } // Compare the verifier and the code challenge: if the two don't match, return an error. // Note: to prevent timing attacks, a time-constant comparer is always used. if (!OpenIdConnectServerHelpers.AreEqual(verifier, challenge)) { Options.Logger.LogError("The token request was rejected because the 'code_verifier' was invalid."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "The specified 'code_verifier' was invalid." })); } } if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Resource)) { // When an explicit resource parameter has been included in the token request // but was missing from the initial request, the request MUST be rejected. var resources = ticket.GetResources(); if (!resources.Any()) { Options.Logger.LogError("The token request was rejected because the 'resource' parameter was not allowed."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request cannot contain a resource parameter " + "if the authorization request didn't contain one" })); } // When an explicit resource parameter has been included in the token request, // the authorization server MUST ensure that it doesn't contain resources // that were not allowed during the initial authorization/token request. else if (!new HashSet <string>(resources).IsSupersetOf(request.GetResources())) { Options.Logger.LogError("The token request was rejected because the 'resource' parameter was not valid."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request doesn't contain a valid resource parameter" })); } } if (request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(request.Scope)) { // When an explicit scope parameter has been included in the token request // but was missing from the initial request, the request MUST be rejected. // See http://tools.ietf.org/html/rfc6749#section-6 var scopes = ticket.GetScopes(); if (!scopes.Any()) { Options.Logger.LogError("The token request was rejected because the 'scope' parameter was not allowed."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request cannot contain a scope parameter " + "if the authorization request didn't contain one" })); } // When an explicit scope parameter has been included in the token request, // the authorization server MUST ensure that it doesn't contain scopes // that were not allowed during the initial authorization/token request. // See https://tools.ietf.org/html/rfc6749#section-6 else if (!new HashSet <string>(scopes).IsSupersetOf(request.GetScopes())) { Options.Logger.LogError("The token request was rejected because the 'scope' parameter was not valid."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Token request doesn't contain a valid scope parameter" })); } } } var notification = new HandleTokenRequestContext(Context, Options, request, ticket); await Options.Provider.HandleTokenRequest(notification); if (notification.HandledResponse) { return(true); } else if (notification.Skipped) { return(false); } else if (notification.IsRejected) { Options.Logger.LogError("The token request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, /* Description: */ notification.ErrorDescription); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidGrant, 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 grant type is not supported. if (ticket == null) { Options.Logger.LogError("The token request was rejected because no authentication " + "ticket was returned by application code."); return(await SendTokenResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.UnsupportedGrantType, ErrorDescription = "The specified grant_type parameter is not supported." })); } return(await HandleSignInAsync(ticket)); }
private async Task <bool> InvokeCryptographyEndpointAsync() { // Metadata requests must be made via GET. // See http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest if (!string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogError("The discovery request was rejected because an invalid " + "HTTP method was used: {Method}.", Request.Method); return(await SendCryptographyResponseAsync(null, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "Invalid HTTP method: make sure to use GET." })); } var request = new OpenIdConnectMessage(Request.Query); var context = new ValidateCryptographyRequestContext(Context, Options); await Options.Provider.ValidateCryptographyRequest(context); // Stop processing the request if Validated was not called. if (!context.IsValidated) { Options.Logger.LogError("The discovery request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendCryptographyResponseAsync(request, new OpenIdConnectMessage { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } var notification = new HandleCryptographyRequestContext(Context, Options, request); foreach (var credentials in Options.EncryptingCredentials) { // Ignore the key if it's not supported. if (!(credentials.SecurityKey is AsymmetricSecurityKey) || (!credentials.SecurityKey.IsSupportedAlgorithm(SecurityAlgorithms.RsaOaepKeyWrap) && !credentials.SecurityKey.IsSupportedAlgorithm(SecurityAlgorithms.RsaV15KeyWrap))) { Options.Logger.LogInformation("An unsupported encryption key was ignored and excluded " + "from the key set: {Type}. Only asymmetric security keys " + "supporting RSA1_5 or RSA-OAEP can be exposed via the JWKS " + "endpoint.", credentials.SecurityKey.GetType().Name); continue; } // Try to extract a key identifier from the credentials. LocalIdKeyIdentifierClause identifier = null; credentials.SecurityKeyIdentifier?.TryFind(out identifier); // Resolve the underlying algorithm from the security key. var algorithm = (RSA)((AsymmetricSecurityKey)credentials.SecurityKey) .GetAsymmetricAlgorithm( algorithm: SecurityAlgorithms.RsaOaepKeyWrap, privateKey: false); // Skip the key if a RSA instance cannot be retrieved. if (algorithm == null) { Options.Logger.LogError("An encryption key was ignored because it was unable " + "to provide the requested RSA instance."); continue; } // Export the RSA public key to create a new JSON Web Key // exposing the exponent and the modulus parameters. var parameters = algorithm.ExportParameters(includePrivateParameters: false); Debug.Assert(parameters.Exponent != null, "A null exponent was returned by RSA.ExportParameters()"); Debug.Assert(parameters.Modulus != null, "A null modulus was returned by RSA.ExportParameters()"); var key = new JsonWebKey { Use = JsonWebKeyUseNames.Enc, Kty = JsonWebAlgorithmsKeyTypes.RSA, // Resolve the JWA identifier from the algorithm specified in the credentials. Alg = OpenIdConnectServerHelpers.GetJwtAlgorithm(credentials.Algorithm), // Use the key identifier specified // in the signing credentials. Kid = identifier.LocalId, // Both E and N must be base64url-encoded. // See http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31#appendix-A.1 E = Base64UrlEncoder.Encode(parameters.Exponent), N = Base64UrlEncoder.Encode(parameters.Modulus) }; X509Certificate2 x509Certificate = null; // Determine whether the encrypting credentials are directly based on a X.509 certificate. var x509EncryptingCredentials = credentials as X509EncryptingCredentials; if (x509EncryptingCredentials != null) { x509Certificate = x509EncryptingCredentials.Certificate; } // Skip looking for a X509SecurityKey in EncryptingCredentials.SecurityKey // if a certificate has been found in the EncryptingCredentials instance. if (x509Certificate == null) { // Determine whether the security key is an asymmetric key embedded in a X.509 certificate. var x509SecurityKey = credentials.SecurityKey as X509SecurityKey; if (x509SecurityKey != null) { x509Certificate = x509SecurityKey.Certificate; } } // Skip looking for a X509AsymmetricSecurityKey in EncryptingCredentials.SecurityKey // if a certificate has been found in EncryptingCredentials or EncryptingCredentials.SecurityKey. if (x509Certificate == null) { // Determine whether the security key is an asymmetric key embedded in a X.509 certificate. var x509AsymmetricSecurityKey = credentials.SecurityKey as X509AsymmetricSecurityKey; if (x509AsymmetricSecurityKey != null) { // The X.509 certificate is not directly accessible when using X509AsymmetricSecurityKey. // Reflection is the only way to get the certificate used to create the security key. var field = typeof(X509AsymmetricSecurityKey).GetField( name: "certificate", bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic); Debug.Assert(field != null); x509Certificate = (X509Certificate2)field.GetValue(x509AsymmetricSecurityKey); } } // If the encryption key is embedded in a X.509 certificate, set // the x5t and x5c parameters using the certificate details. if (x509Certificate != null) { // x5t must be base64url-encoded. // See http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31#section-4.8 key.X5t = Base64UrlEncoder.Encode(x509Certificate.GetCertHash()); // Unlike E or N, the certificates contained in x5c // must be base64-encoded and not base64url-encoded. // See http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31#section-4.7 key.X5c.Add(Convert.ToBase64String(x509Certificate.RawData)); } notification.Keys.Add(key); } foreach (var credentials in Options.SigningCredentials) { // Ignore the key if it's not supported. if (!(credentials.SigningKey is AsymmetricSecurityKey) || !credentials.SigningKey.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256Signature)) { Options.Logger.LogInformation("An unsupported signing key was ignored and excluded " + "from the key set: {Type}. Only asymmetric security keys " + "supporting RS256, RS384 or RS512 can be exposed " + "via the JWKS endpoint.", credentials.SigningKey.GetType().Name); continue; } // Try to extract a key identifier from the credentials. LocalIdKeyIdentifierClause identifier = null; credentials.SigningKeyIdentifier?.TryFind(out identifier); // Resolve the underlying algorithm from the security key. var algorithm = (RSA)((AsymmetricSecurityKey)credentials.SigningKey) .GetAsymmetricAlgorithm( algorithm: SecurityAlgorithms.RsaOaepKeyWrap, privateKey: false); // Skip the key if a RSA instance cannot be retrieved. if (algorithm == null) { Options.Logger.LogError("A signing key was ignored because it was unable " + "to provide the requested RSA instance."); continue; } // Export the RSA public key to create a new JSON Web Key // exposing the exponent and the modulus parameters. var parameters = algorithm.ExportParameters(includePrivateParameters: false); Debug.Assert(parameters.Exponent != null, "A null exponent was returned by RSA.ExportParameters()"); Debug.Assert(parameters.Modulus != null, "A null modulus was returned by RSA.ExportParameters()"); var key = new JsonWebKey { Use = JsonWebKeyUseNames.Sig, Kty = JsonWebAlgorithmsKeyTypes.RSA, // Resolve the JWA identifier from the algorithm specified in the credentials. Alg = OpenIdConnectServerHelpers.GetJwtAlgorithm(credentials.SignatureAlgorithm), // Use the key identifier specified // in the signing credentials. Kid = identifier?.LocalId, // Both E and N must be base64url-encoded. // See http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31#appendix-A.1 E = Base64UrlEncoder.Encode(parameters.Exponent), N = Base64UrlEncoder.Encode(parameters.Modulus) }; X509Certificate2 x509Certificate = null; // Determine whether the signing credentials are directly based on a X.509 certificate. var x509SigningCredentials = credentials as X509SigningCredentials; if (x509SigningCredentials != null) { x509Certificate = x509SigningCredentials.Certificate; } // Skip looking for a X509SecurityKey in SigningCredentials.SigningKey // if a certificate has been found in the SigningCredentials instance. if (x509Certificate == null) { // Determine whether the security key is an asymmetric key embedded in a X.509 certificate. var x509SecurityKey = credentials.SigningKey as X509SecurityKey; if (x509SecurityKey != null) { x509Certificate = x509SecurityKey.Certificate; } } // Skip looking for a X509AsymmetricSecurityKey in SigningCredentials.SigningKey // if a certificate has been found in SigningCredentials or SigningCredentials.SigningKey. if (x509Certificate == null) { // Determine whether the security key is an asymmetric key embedded in a X.509 certificate. var x509AsymmetricSecurityKey = credentials.SigningKey as X509AsymmetricSecurityKey; if (x509AsymmetricSecurityKey != null) { // The X.509 certificate is not directly accessible when using X509AsymmetricSecurityKey. // Reflection is the only way to get the certificate used to create the security key. var field = typeof(X509AsymmetricSecurityKey).GetField( name: "certificate", bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic); Debug.Assert(field != null); x509Certificate = (X509Certificate2)field.GetValue(x509AsymmetricSecurityKey); } } // If the signing key is embedded in a X.509 certificate, set // the x5t and x5c parameters using the certificate details. if (x509Certificate != null) { // x5t must be base64url-encoded. // See http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31#section-4.8 key.X5t = Base64UrlEncoder.Encode(x509Certificate.GetCertHash()); // Unlike E or N, the certificates contained in x5c // must be base64-encoded and not base64url-encoded. // See http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31#section-4.7 key.X5c.Add(Convert.ToBase64String(x509Certificate.RawData)); } notification.Keys.Add(key); } await Options.Provider.HandleCryptographyRequest(notification); if (notification.HandledResponse) { return(true); } else if (notification.Skipped) { return(false); } var response = new JObject(); var keys = new JArray(); foreach (var key in notification.Keys) { var item = new JObject(); // Ensure a key type has been provided. // See http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31#section-4.1 if (string.IsNullOrEmpty(key.Kty)) { Options.Logger.LogError("A JSON Web Key was excluded from the key set because " + "it didn't contain the mandatory 'kid' parameter."); continue; } // Create a dictionary associating the // JsonWebKey components with their values. var parameters = new Dictionary <string, string> { { JsonWebKeyParameterNames.Kid, key.Kid }, { JsonWebKeyParameterNames.Use, key.Use }, { JsonWebKeyParameterNames.Kty, key.Kty }, { JsonWebKeyParameterNames.KeyOps, key.KeyOps }, { JsonWebKeyParameterNames.Alg, key.Alg }, { JsonWebKeyParameterNames.E, key.E }, { JsonWebKeyParameterNames.N, key.N }, { JsonWebKeyParameterNames.X5t, key.X5t }, { JsonWebKeyParameterNames.X5u, key.X5u } }; foreach (var parameter in parameters) { if (!string.IsNullOrEmpty(parameter.Value)) { item.Add(parameter.Key, parameter.Value); } } if (key.X5c.Count != 0) { item.Add(JsonWebKeyParameterNames.X5c, JArray.FromObject(key.X5c)); } keys.Add(item); } response.Add(JsonWebKeyParameterNames.Keys, keys); return(await SendCryptographyResponseAsync(request, response)); }
private async Task <bool> InvokeConfigurationEndpointAsync() { // Metadata requests must be made via GET. // See http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest if (!string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The configuration request was rejected because an invalid " + "HTTP method was specified: {Method}.", Request.Method); return(await SendConfigurationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified HTTP method is not valid." })); } var request = new OpenIdConnectRequest(Request.Query); // Note: set the message type before invoking the ExtractConfigurationRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.ConfigurationRequest); // Store the configuration request in the OWIN context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractConfigurationRequestContext(Context, Options, request); await Options.Provider.ExtractConfigurationRequest(@event); if (@event.HandledResponse) { Logger.LogDebug("The configuration request was handled in user code."); return(true); } else if (@event.Skipped) { Logger.LogDebug("The default configuration request handling was skipped from user code."); return(false); } else if (@event.IsRejected) { Logger.LogError("The configuration request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendConfigurationResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } Logger.LogInformation("The configuration request was successfully extracted " + "from the HTTP request: {Request}.", request); var context = new ValidateConfigurationRequestContext(Context, Options, request); await Options.Provider.ValidateConfigurationRequest(context); if (context.HandledResponse) { Logger.LogDebug("The configuration request was handled in user code."); return(true); } else if (context.Skipped) { Logger.LogDebug("The default configuration request handling was skipped from user code."); return(false); } else if (context.IsRejected) { Logger.LogError("The configuration request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendConfigurationResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } Logger.LogInformation("The configuration request was successfully validated."); var notification = new HandleConfigurationRequestContext(Context, Options, request) { Issuer = Context.GetIssuer(Options) }; if (Options.AuthorizationEndpointPath.HasValue) { notification.AuthorizationEndpoint = notification.Issuer.AddPath(Options.AuthorizationEndpointPath); } if (Options.CryptographyEndpointPath.HasValue) { notification.CryptographyEndpoint = notification.Issuer.AddPath(Options.CryptographyEndpointPath); } if (Options.IntrospectionEndpointPath.HasValue) { notification.IntrospectionEndpoint = notification.Issuer.AddPath(Options.IntrospectionEndpointPath); notification.IntrospectionEndpointAuthenticationMethods.Add( OpenIdConnectConstants.ClientAuthenticationMethods.ClientSecretBasic); notification.IntrospectionEndpointAuthenticationMethods.Add( OpenIdConnectConstants.ClientAuthenticationMethods.ClientSecretPost); } if (Options.LogoutEndpointPath.HasValue) { notification.LogoutEndpoint = notification.Issuer.AddPath(Options.LogoutEndpointPath); } if (Options.RevocationEndpointPath.HasValue) { notification.RevocationEndpoint = notification.Issuer.AddPath(Options.RevocationEndpointPath); notification.RevocationEndpointAuthenticationMethods.Add( OpenIdConnectConstants.ClientAuthenticationMethods.ClientSecretBasic); notification.RevocationEndpointAuthenticationMethods.Add( OpenIdConnectConstants.ClientAuthenticationMethods.ClientSecretPost); } if (Options.TokenEndpointPath.HasValue) { notification.TokenEndpoint = notification.Issuer.AddPath(Options.TokenEndpointPath); notification.TokenEndpointAuthenticationMethods.Add( OpenIdConnectConstants.ClientAuthenticationMethods.ClientSecretBasic); notification.TokenEndpointAuthenticationMethods.Add( OpenIdConnectConstants.ClientAuthenticationMethods.ClientSecretPost); } if (Options.UserinfoEndpointPath.HasValue) { notification.UserinfoEndpoint = notification.Issuer.AddPath(Options.UserinfoEndpointPath); } if (Options.AuthorizationEndpointPath.HasValue) { notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.Implicit); if (Options.TokenEndpointPath.HasValue) { // Only expose the code grant type and the code challenge methods // if both the authorization and the token endpoints are enabled. notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.AuthorizationCode); // Note: supporting S256 is mandatory for authorization servers that implement PKCE. // See https://tools.ietf.org/html/rfc7636#section-4.2 for more information. notification.CodeChallengeMethods.Add(OpenIdConnectConstants.CodeChallengeMethods.Plain); notification.CodeChallengeMethods.Add(OpenIdConnectConstants.CodeChallengeMethods.Sha256); } } if (Options.TokenEndpointPath.HasValue) { notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.RefreshToken); notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.ClientCredentials); notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.Password); } // Only populate response_modes_supported and response_types_supported // if the authorization endpoint is available. if (Options.AuthorizationEndpointPath.HasValue) { notification.ResponseModes.Add(OpenIdConnectConstants.ResponseModes.FormPost); notification.ResponseModes.Add(OpenIdConnectConstants.ResponseModes.Fragment); notification.ResponseModes.Add(OpenIdConnectConstants.ResponseModes.Query); notification.ResponseTypes.Add(OpenIdConnectConstants.ResponseTypes.Token); // Only expose response types containing code when // the token endpoint has not been explicitly disabled. if (Options.TokenEndpointPath.HasValue) { notification.ResponseTypes.Add(OpenIdConnectConstants.ResponseTypes.Code); notification.ResponseTypes.Add( OpenIdConnectConstants.ResponseTypes.Code + ' ' + OpenIdConnectConstants.ResponseTypes.Token); } // Only expose the response types containing id_token if an asymmetric signing key is available. if (Options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey)) { notification.ResponseTypes.Add(OpenIdConnectConstants.ResponseTypes.IdToken); notification.ResponseTypes.Add( OpenIdConnectConstants.ResponseTypes.IdToken + ' ' + OpenIdConnectConstants.ResponseTypes.Token); // Only expose response types containing code when // the token endpoint has not been explicitly disabled. if (Options.TokenEndpointPath.HasValue) { notification.ResponseTypes.Add( OpenIdConnectConstants.ResponseTypes.Code + ' ' + OpenIdConnectConstants.ResponseTypes.IdToken); notification.ResponseTypes.Add( OpenIdConnectConstants.ResponseTypes.Code + ' ' + OpenIdConnectConstants.ResponseTypes.IdToken + ' ' + OpenIdConnectConstants.ResponseTypes.Token); } } } notification.Scopes.Add(OpenIdConnectConstants.Scopes.OpenId); notification.Claims.Add(OpenIdConnectConstants.Claims.Audience); notification.Claims.Add(OpenIdConnectConstants.Claims.ExpiresAt); notification.Claims.Add(OpenIdConnectConstants.Claims.IssuedAt); notification.Claims.Add(OpenIdConnectConstants.Claims.Issuer); notification.Claims.Add(OpenIdConnectConstants.Claims.JwtId); notification.Claims.Add(OpenIdConnectConstants.Claims.Subject); notification.SubjectTypes.Add(OpenIdConnectConstants.SubjectTypes.Public); foreach (var credentials in Options.SigningCredentials) { // If the signing key is not an asymmetric key, ignore it. if (!(credentials.Key is AsymmetricSecurityKey)) { continue; } // Try to resolve the JWA algorithm short name. If a null value is returned, ignore it. var algorithm = OpenIdConnectServerHelpers.GetJwtAlgorithm(credentials.Algorithm); if (string.IsNullOrEmpty(algorithm)) { continue; } notification.IdTokenSigningAlgorithms.Add(algorithm); } await Options.Provider.HandleConfigurationRequest(notification); if (notification.HandledResponse) { Logger.LogDebug("The configuration request was handled in user code."); return(true); } else if (notification.Skipped) { Logger.LogDebug("The default configuration request handling was skipped from user code."); return(false); } else if (notification.IsRejected) { Logger.LogError("The configuration request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendConfigurationResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } var response = new OpenIdConnectResponse { [OpenIdConnectConstants.Metadata.Issuer] = notification.Issuer, [OpenIdConnectConstants.Metadata.AuthorizationEndpoint] = notification.AuthorizationEndpoint, [OpenIdConnectConstants.Metadata.TokenEndpoint] = notification.TokenEndpoint, [OpenIdConnectConstants.Metadata.IntrospectionEndpoint] = notification.IntrospectionEndpoint, [OpenIdConnectConstants.Metadata.EndSessionEndpoint] = notification.LogoutEndpoint, [OpenIdConnectConstants.Metadata.RevocationEndpoint] = notification.RevocationEndpoint, [OpenIdConnectConstants.Metadata.UserinfoEndpoint] = notification.UserinfoEndpoint, [OpenIdConnectConstants.Metadata.JwksUri] = notification.CryptographyEndpoint, [OpenIdConnectConstants.Metadata.GrantTypesSupported] = new JArray(notification.GrantTypes), [OpenIdConnectConstants.Metadata.ResponseTypesSupported] = new JArray(notification.ResponseTypes), [OpenIdConnectConstants.Metadata.ResponseModesSupported] = new JArray(notification.ResponseModes), [OpenIdConnectConstants.Metadata.ScopesSupported] = new JArray(notification.Scopes), [OpenIdConnectConstants.Metadata.ClaimsSupported] = new JArray(notification.Claims), [OpenIdConnectConstants.Metadata.IdTokenSigningAlgValuesSupported] = new JArray(notification.IdTokenSigningAlgorithms), [OpenIdConnectConstants.Metadata.CodeChallengeMethodsSupported] = new JArray(notification.CodeChallengeMethods), [OpenIdConnectConstants.Metadata.SubjectTypesSupported] = new JArray(notification.SubjectTypes), [OpenIdConnectConstants.Metadata.TokenEndpointAuthMethodsSupported] = new JArray(notification.TokenEndpointAuthenticationMethods), [OpenIdConnectConstants.Metadata.IntrospectionEndpointAuthMethodsSupported] = new JArray(notification.IntrospectionEndpointAuthenticationMethods), [OpenIdConnectConstants.Metadata.RevocationEndpointAuthMethodsSupported] = new JArray(notification.RevocationEndpointAuthenticationMethods) }; foreach (var metadata in notification.Metadata) { response.SetParameter(metadata.Key, metadata.Value); } return(await SendConfigurationResponseAsync(response)); }
/// <summary> /// Authorization Server middleware component which is added to an OWIN pipeline. This constructor is not /// called by application code directly, instead it is added by calling the the IAppBuilder UseOpenIdConnectServer /// extension method. /// </summary> public OpenIdConnectServerMiddleware(OwinMiddleware next, IAppBuilder app, OpenIdConnectServerOptions options) : base(next, options) { if (Options.HtmlEncoder == null) { throw new ArgumentException("The HTML encoder registered in the options " + "cannot be null.", nameof(options)); } if (Options.Provider == null) { throw new ArgumentException("The authorization provider registered in " + "the options cannot be null.", nameof(options)); } if (Options.SystemClock == null) { throw new ArgumentException("The system clock registered in the options " + "cannot be null.", nameof(options)); } if (Options.Issuer != null) { if (!Options.Issuer.IsAbsoluteUri) { throw new ArgumentException("The issuer registered in the options must be " + "a valid absolute URI.", nameof(options)); } // See http://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery if (!string.IsNullOrEmpty(Options.Issuer.Query) || !string.IsNullOrEmpty(Options.Issuer.Fragment)) { throw new ArgumentException("The issuer registered in the options must contain no query " + "and no fragment parts.", nameof(options)); } // Note: while the issuer parameter should be a HTTPS URI, making HTTPS mandatory // in Owin.Security.OpenIdConnect.Server would prevent the end developer from // running the different samples in test environments, where HTTPS is often disabled. // To mitigate this issue, AllowInsecureHttp can be set to true to bypass the HTTPS check. // See http://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery if (!Options.AllowInsecureHttp && string.Equals(Options.Issuer.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("The issuer registered in the options must be a HTTPS URI when " + "AllowInsecureHttp is not set to true.", nameof(options)); } } if (Options.AccessTokenHandler != null && !(Options.AccessTokenHandler is ISecurityTokenValidator)) { throw new ArgumentException("The access token handler must implement 'ISecurityTokenValidator'.", nameof(options)); } if (Options.Logger == null) { Options.Logger = new LoggerFactory().CreateLogger <OpenIdConnectServerMiddleware>(); } if (Options.DataProtectionProvider == null) { // Try to use the application name provided by // the OWIN host as the application discriminator. var discriminator = new AppProperties(app.Properties).AppName; // When an application discriminator cannot be resolved from // the OWIN host properties, generate a temporary identifier. if (string.IsNullOrEmpty(discriminator)) { discriminator = Guid.NewGuid().ToString(); } Options.DataProtectionProvider = DataProtectionProvider.Create(discriminator); } if (Options.AccessTokenFormat == null) { var protector = Options.DataProtectionProvider.CreateProtector( nameof(OpenIdConnectServerMiddleware), Options.AuthenticationType, "Access_Token", "v1"); Options.AccessTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(protector)); } if (Options.AuthorizationCodeFormat == null) { var protector = Options.DataProtectionProvider.CreateProtector( nameof(OpenIdConnectServerMiddleware), Options.AuthenticationType, "Authorization_Code", "v1"); Options.AuthorizationCodeFormat = new AspNetTicketDataFormat(new DataProtectorShim(protector)); } if (Options.RefreshTokenFormat == null) { var protector = Options.DataProtectionProvider.CreateProtector( nameof(OpenIdConnectServerMiddleware), Options.AuthenticationType, "Refresh_Token", "v1"); Options.RefreshTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(protector)); } var environment = new AppProperties(app.Properties).Get <string>("host.AppMode"); if (Options.AllowInsecureHttp && !string.Equals(environment, "Development", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogWarning("Disabling the transport security requirement is not recommended on production. " + "Consider setting 'OpenIdConnectServerOptions.AllowInsecureHttp' to 'false' " + "to prevent the OpenID Connect server middleware from serving non-HTTPS requests."); } // If no key has been explicitly added, use the fallback mode. if (Options.EncryptingCredentials.Count == 0 || Options.SigningCredentials.Count == 0) { // When detecting a non-development environment, log a warning to inform the developer // that registering a X.509 certificate is recommended when going live. if (!string.Equals(environment, "Development", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogWarning("No explicit signing credentials have been registered. " + "Using a X.509 certificate stored in the machine store " + "is recommended for production environments."); } var directory = OpenIdConnectServerHelpers.GetDefaultKeyStorageDirectory(); if (directory == null) { Options.Logger.LogError("No suitable directory can be found to store the dynamically-generated RSA keys."); return; } // Get a data protector from the services provider. var protector = Options.DataProtectionProvider.CreateProtector( nameof(OpenIdConnectServerMiddleware), Options.AuthenticationType, "Keys", "v1"); // Note: all the exceptions thrown when enumerating // existing keys or generating new ones must be caught. try { foreach (var key in OpenIdConnectServerHelpers.GetKeys(directory)) { // Extract the key material using the data protector. // Ignore the key if the decryption process failed. string usage; var parameters = OpenIdConnectServerHelpers.DecryptKey(protector, key.Value, out usage); if (parameters == null) { Options.Logger.LogDebug("An invalid/incompatible key was ignored: {Key}.", key.Key); continue; } if (string.Equals(usage, "Encryption", StringComparison.OrdinalIgnoreCase)) { var algorithm = RSA.Create(); algorithm.ImportParameters(parameters.Value); // Add the key to the encryption credentials list. Options.EncryptingCredentials.AddKey(new RsaSecurityKey(algorithm)); Options.Logger.LogInformation("An existing key was automatically added to the " + "encryption credentials list: {Key}.", key.Key); } else if (string.Equals(usage, "Signing", StringComparison.OrdinalIgnoreCase)) { var algorithm = RSA.Create(); algorithm.ImportParameters(parameters.Value); // Add the key to the signing credentials list. Options.SigningCredentials.AddKey(new RsaSecurityKey(algorithm)); Options.Logger.LogInformation("An existing key was automatically added to the " + "signing credentials list: {Key}.", key.Key); } } } catch (Exception exception) { Options.Logger.LogDebug("An error ocurred when retrieving the keys stored " + "on the disk: {Exception}.", exception.ToString()); } // Note: all the exceptions thrown when enumerating // existing keys or generating new ones must be caught. try { // If no encryption key has been found, generate and persist a new RSA key. if (Options.EncryptingCredentials.Count == 0) { // Generate a new RSA key and export its public/private parameters. var algorithm = OpenIdConnectServerHelpers.GenerateKey(size: 2048); var parameters = algorithm.ExportParameters(/* includePrivateParameters: */ true); // Add the encryption key to the credentials list before trying to persist it on the disk. Options.EncryptingCredentials.AddKey(new RsaSecurityKey(algorithm)); try { // Encrypt the key using the data protector and try to persist it on the disk. var key = OpenIdConnectServerHelpers.EncryptKey(protector, parameters, usage: "Encryption"); var path = OpenIdConnectServerHelpers.PersistKey(directory, key); Options.Logger.LogInformation("A new RSA key was automatically generated, added to the " + "encryption credentials list and persisted on the disk: {Path}.", path); } catch (Exception exception) { Options.Logger.LogWarning("An error ocurred when persisting a new key on the disk: {Exception}. " + "The key will be kept in memory and will be lost when the " + "application shuts down or restarts.", exception.ToString()); } } // If no signing key has been found, generate and persist a new RSA key. if (Options.SigningCredentials.Count == 0) { // Generate a new RSA key and export its public/private parameters. var algorithm = OpenIdConnectServerHelpers.GenerateKey(size: 2048); var parameters = algorithm.ExportParameters(/* includePrivateParameters: */ true); // Add the signing key to the credentials list before trying to persist it on the disk. Options.SigningCredentials.AddKey(new RsaSecurityKey(algorithm)); try { // Encrypt the key using the data protector and try to persist it on the disk. var key = OpenIdConnectServerHelpers.EncryptKey(protector, parameters, usage: "Signing"); var path = OpenIdConnectServerHelpers.PersistKey(directory, key); Options.Logger.LogInformation("A new RSA key was automatically generated, added to the " + "signing credentials list and persisted on the disk: {Path}.", path); } catch (Exception exception) { Options.Logger.LogWarning("An error ocurred when persisting a new key on the disk: {Exception}. " + "The key will be kept in memory and will be lost when the " + "application shuts down or restarts.", exception.ToString()); } } } catch (Exception exception) { Options.Logger.LogDebug("An error ocurred when generating a new key: {Exception}.", exception.ToString()); } } if (Options.SigningCredentials.Count == 0) { throw new ArgumentException("At least one signing key must be registered.", nameof(options)); } }
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 the subject claim. if (string.Equals(claim.Type, OpenIdConnectConstants.Claims.Subject, StringComparison.OrdinalIgnoreCase)) { return(true); } // Claims whose destination is not explicitly referenced or doesn't // contain "id_token" are not included in the identity token. if (!claim.HasDestination(OpenIdConnectConstants.Destinations.IdentityToken)) { Logger.LogDebug("'{Claim}' was excluded from the identity token claims.", claim.Type); return(false); } return(true); }); // Remove the destinations from the claim properties. foreach (var claim in identity.Claims) { claim.Properties.Remove(OpenIdConnectConstants.Properties.Destinations); } // Create a new ticket containing the updated properties and the filtered identity. var ticket = new AuthenticationTicket(identity, properties); ticket.Properties.IssuedUtc = Options.SystemClock.UtcNow; // Only set the expiration date if a lifetime was specified in either the ticket or the options. var lifetime = ticket.GetIdentityTokenLifetime() ?? Options.IdentityTokenLifetime; if (lifetime.HasValue) { ticket.Properties.ExpiresUtc = ticket.Properties.IssuedUtc + lifetime.Value; } // Associate a random identifier with the identity token. ticket.SetTokenId(Guid.NewGuid().ToString()); // Remove the unwanted properties from the authentication ticket. ticket.RemoveProperty(OpenIdConnectConstants.Properties.AccessTokenLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.AuthorizationCodeLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.CodeChallenge) .RemoveProperty(OpenIdConnectConstants.Properties.CodeChallengeMethod) .RemoveProperty(OpenIdConnectConstants.Properties.IdentityTokenLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.OriginalRedirectUri) .RemoveProperty(OpenIdConnectConstants.Properties.RefreshTokenLifetime) .RemoveProperty(OpenIdConnectConstants.Properties.TokenUsage); ticket.SetAudiences(ticket.GetPresenters()); var notification = new SerializeIdentityTokenContext(Context, Options, request, response, ticket) { Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.IdentityTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault( credentials => credentials.Key is AsymmetricSecurityKey) }; await Options.Provider.SerializeIdentityToken(notification); if (notification.IsHandled || !string.IsNullOrEmpty(notification.IdentityToken)) { return(notification.IdentityToken); } if (notification.SecurityTokenHandler == null) { throw new InvalidOperationException("A security token handler must be provided."); } if (string.IsNullOrEmpty(identity.GetClaim(OpenIdConnectConstants.Claims.Subject))) { throw new InvalidOperationException("The authentication ticket was rejected because " + "the mandatory subject claim was missing."); } // Note: identity tokens must be signed but an exception is made by the OpenID Connect specification // when they are returned from the token endpoint: in this case, signing is not mandatory, as the TLS // server validation can be used as a way to ensure an identity token was issued by a trusted party. // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. if (notification.SigningCredentials == null && request.IsAuthorizationRequest()) { throw new InvalidOperationException("A signing key must be provided."); } // Store the "usage" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.TokenUsage, OpenIdConnectConstants.TokenUsages.IdToken); // Store the "unique_id" property as a claim. ticket.Identity.AddClaim(OpenIdConnectConstants.Claims.JwtId, ticket.GetTokenId()); // 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); } if (notification.SigningCredentials != null && (!string.IsNullOrEmpty(response.Code) || !string.IsNullOrEmpty(response.AccessToken))) { using (var algorithm = OpenIdConnectServerHelpers.GetHashAlgorithm(notification.SigningCredentials.Algorithm)) { // 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: 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.CreateEncodedJwt(new SecurityTokenDescriptor { Subject = identity, Issuer = notification.Issuer, EncryptingCredentials = notification.EncryptingCredentials, SigningCredentials = notification.SigningCredentials, IssuedAt = notification.Ticket.Properties.IssuedUtc?.UtcDateTime, NotBefore = notification.Ticket.Properties.IssuedUtc?.UtcDateTime, Expires = notification.Ticket.Properties.ExpiresUtc?.UtcDateTime }); Logger.LogTrace("A new identity token was successfully generated using the specified " + "security token handler: {Token} ; {Claims} ; {Properties}.", token, ticket.Identity.Claims, ticket.Properties.Dictionary); return(token); }
private async Task <bool> InvokeConfigurationEndpointAsync() { // Metadata requests must be made via GET. // See http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest if (!string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogError("The discovery request was rejected because an invalid " + "HTTP method was used: {Method}.", Request.Method); return(await SendConfigurationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "Invalid HTTP method: make sure to use GET." })); } var request = new OpenIdConnectRequest(Request.Query); // Note: set the message type before invoking the ExtractConfigurationRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.Configuration); // Store the discovery request in the OWIN context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractConfigurationRequestContext(Context, Options, request); await Options.Provider.ExtractConfigurationRequest(@event); if (@event.HandledResponse) { return(true); } else if (@event.Skipped) { return(false); } else if (@event.IsRejected) { Options.Logger.LogError("The discovery request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendConfigurationResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } var context = new ValidateConfigurationRequestContext(Context, Options, request); await Options.Provider.ValidateConfigurationRequest(context); if (context.HandledResponse) { return(true); } else if (context.Skipped) { return(false); } else if (!context.IsValidated) { Options.Logger.LogError("The discovery request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendConfigurationResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } var notification = new HandleConfigurationRequestContext(Context, Options, request); notification.Issuer = Context.GetIssuer(Options); if (Options.AuthorizationEndpointPath.HasValue) { notification.AuthorizationEndpoint = notification.Issuer.AddPath(Options.AuthorizationEndpointPath); } if (Options.CryptographyEndpointPath.HasValue) { notification.CryptographyEndpoint = notification.Issuer.AddPath(Options.CryptographyEndpointPath); } if (Options.IntrospectionEndpointPath.HasValue) { notification.IntrospectionEndpoint = notification.Issuer.AddPath(Options.IntrospectionEndpointPath); } if (Options.LogoutEndpointPath.HasValue) { notification.LogoutEndpoint = notification.Issuer.AddPath(Options.LogoutEndpointPath); } if (Options.RevocationEndpointPath.HasValue) { notification.RevocationEndpoint = notification.Issuer.AddPath(Options.RevocationEndpointPath); } if (Options.TokenEndpointPath.HasValue) { notification.TokenEndpoint = notification.Issuer.AddPath(Options.TokenEndpointPath); } if (Options.UserinfoEndpointPath.HasValue) { notification.UserinfoEndpoint = notification.Issuer.AddPath(Options.UserinfoEndpointPath); } if (Options.AuthorizationEndpointPath.HasValue) { // Only expose the implicit grant type if the token // endpoint has not been explicitly disabled. notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.Implicit); if (Options.TokenEndpointPath.HasValue) { // Only expose the authorization code and refresh token grant types // if both the authorization and the token endpoints are enabled. notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.AuthorizationCode); } } if (Options.TokenEndpointPath.HasValue) { notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.RefreshToken); notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.ClientCredentials); notification.GrantTypes.Add(OpenIdConnectConstants.GrantTypes.Password); } // Only populate response_modes_supported and response_types_supported // if the authorization endpoint is available. if (Options.AuthorizationEndpointPath.HasValue) { notification.ResponseModes.Add(OpenIdConnectConstants.ResponseModes.FormPost); notification.ResponseModes.Add(OpenIdConnectConstants.ResponseModes.Fragment); notification.ResponseModes.Add(OpenIdConnectConstants.ResponseModes.Query); notification.ResponseTypes.Add(OpenIdConnectConstants.ResponseTypes.Token); notification.ResponseTypes.Add(OpenIdConnectConstants.ResponseTypes.IdToken); notification.ResponseTypes.Add( OpenIdConnectConstants.ResponseTypes.IdToken + ' ' + OpenIdConnectConstants.ResponseTypes.Token); // Only expose response types containing code when // the token endpoint has not been explicitly disabled. if (Options.TokenEndpointPath.HasValue) { notification.ResponseTypes.Add(OpenIdConnectConstants.ResponseTypes.Code); notification.ResponseTypes.Add( OpenIdConnectConstants.ResponseTypes.Code + ' ' + OpenIdConnectConstants.ResponseTypes.Token); notification.ResponseTypes.Add( OpenIdConnectConstants.ResponseTypes.Code + ' ' + OpenIdConnectConstants.ResponseTypes.IdToken); notification.ResponseTypes.Add( OpenIdConnectConstants.ResponseTypes.Code + ' ' + OpenIdConnectConstants.ResponseTypes.IdToken + ' ' + OpenIdConnectConstants.ResponseTypes.Token); } } notification.Scopes.Add(OpenIdConnectConstants.Scopes.OpenId); notification.SubjectTypes.Add(OpenIdConnectConstants.SubjectTypes.Public); // Note: supporting S256 is mandatory for authorization servers that implement PKCE. // See https://tools.ietf.org/html/rfc7636#section-4.2 for more information. notification.CodeChallengeMethods.Add(OpenIdConnectConstants.CodeChallengeMethods.Plain); notification.CodeChallengeMethods.Add(OpenIdConnectConstants.CodeChallengeMethods.Sha256); foreach (var credentials in Options.SigningCredentials) { // Try to resolve the JWA algorithm short name. If a null value is returned, ignore it. var algorithm = OpenIdConnectServerHelpers.GetJwtAlgorithm(credentials.SignatureAlgorithm); if (string.IsNullOrEmpty(algorithm)) { continue; } // If the algorithm is already listed, ignore it. if (notification.SigningAlgorithms.Contains(algorithm)) { continue; } notification.SigningAlgorithms.Add(algorithm); } await Options.Provider.HandleConfigurationRequest(notification); if (notification.HandledResponse) { return(true); } else if (notification.Skipped) { return(false); } else if (notification.IsRejected) { Options.Logger.LogError("The discovery request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendConfigurationResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } return(await SendConfigurationResponseAsync(new OpenIdConnectResponse(notification.Metadata))); }
/// <summary> /// Authorization Server middleware component which is added to an OWIN pipeline. This constructor is not /// called by application code directly, instead it is added by calling the the IAppBuilder UseOpenIdConnectServer /// extension method. /// </summary> public OpenIdConnectServerMiddleware(OwinMiddleware next, IAppBuilder app, OpenIdConnectServerOptions options) : base(next, options) { if (Options.HtmlEncoder == null) { throw new ArgumentException("The HTML encoder registered in the options " + "cannot be null.", nameof(options)); } if (Options.Provider == null) { throw new ArgumentException("The authorization provider registered in " + "the options cannot be null.", nameof(options)); } if (Options.RandomNumberGenerator == null) { throw new ArgumentException("The random number generator registered in " + "the options cannot be null.", nameof(options)); } if (Options.SystemClock == null) { throw new ArgumentException("The system clock registered in the options " + "cannot be null.", nameof(options)); } if (Options.Issuer != null) { if (!Options.Issuer.IsAbsoluteUri) { throw new ArgumentException("The issuer registered in the options must be " + "a valid absolute URI.", nameof(options)); } // See http://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery if (!string.IsNullOrEmpty(Options.Issuer.Query) || !string.IsNullOrEmpty(Options.Issuer.Fragment)) { throw new ArgumentException("The issuer registered in the options must contain no query " + "and no fragment parts.", nameof(options)); } // Note: while the issuer parameter should be a HTTPS URI, making HTTPS mandatory // in Owin.Security.OpenIdConnect.Server would prevent the end developer from // running the different samples in test environments, where HTTPS is often disabled. // To mitigate this issue, AllowInsecureHttp can be set to true to bypass the HTTPS check. // See http://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery if (!Options.AllowInsecureHttp && string.Equals(Options.Issuer.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("The issuer registered in the options must be a HTTPS URI when " + "AllowInsecureHttp is not set to true.", nameof(options)); } } if (Options.Logger == null) { Options.Logger = new LoggerFactory().CreateLogger <OpenIdConnectServerMiddleware>(); } if (Options.DataProtectionProvider == null) { // Create a new DI container and register // the data protection services. var services = new ServiceCollection(); services.AddDataProtection(configuration => { // Try to use the application name provided by // the OWIN host as the application discriminator. var discriminator = new AppProperties(app.Properties).AppName; // When an application discriminator cannot be resolved from // the OWIN host properties, generate a temporary identifier. if (string.IsNullOrEmpty(discriminator)) { discriminator = Guid.NewGuid().ToString(); } configuration.ApplicationDiscriminator = discriminator; }); var container = services.BuildServiceProvider(); // Resolve a data protection provider from the services container. Options.DataProtectionProvider = container.GetRequiredService <IDataProtectionProvider>(); } if (Options.AccessTokenFormat == null) { var protector = Options.DataProtectionProvider.CreateProtector( nameof(OpenIdConnectServerMiddleware), Options.AuthenticationType, "Access_Token", "v1"); Options.AccessTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(protector)); } if (Options.AuthorizationCodeFormat == null) { var protector = Options.DataProtectionProvider.CreateProtector( nameof(OpenIdConnectServerMiddleware), Options.AuthenticationType, "Authorization_Code", "v1"); Options.AuthorizationCodeFormat = new AspNetTicketDataFormat(new DataProtectorShim(protector)); } if (Options.RefreshTokenFormat == null) { var protector = Options.DataProtectionProvider.CreateProtector( nameof(OpenIdConnectServerMiddleware), Options.AuthenticationType, "Refresh_Token", "v1"); Options.RefreshTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(protector)); } // If no key has been explicitly added, use the fallback mode. if (Options.EncryptingCredentials.Count == 0 || Options.SigningCredentials.Count == 0) { var directory = OpenIdConnectServerHelpers.GetDefaultKeyStorageDirectory(); Debug.Assert(directory != null); // Get a data protector from the services provider. var protector = Options.DataProtectionProvider.CreateProtector( nameof(OpenIdConnectServerMiddleware), Options.AuthenticationType, "Keys", "v1"); foreach (var file in directory.EnumerateFiles("*.key")) { using (var buffer = new MemoryStream()) using (var stream = file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)) { // Copy the key content to the buffer. stream.CopyTo(buffer); // Extract the key material using the data protector. // Ignore the key if the decryption process failed. string usage; var parameters = OpenIdConnectServerHelpers.DecryptKey(protector, buffer.ToArray(), out usage); if (parameters == null) { Options.Logger.LogDebug("An invalid/incompatible key was ignored: {Key}.", file.FullName); continue; } if (string.Equals(usage, "Encryption", StringComparison.OrdinalIgnoreCase)) { // Only add the encryption key if none has been explictly added. if (Options.EncryptingCredentials.Count != 0) { continue; } var algorithm = RSA.Create(); algorithm.ImportParameters(parameters.Value); // Add the key to the encryption credentials list. Options.EncryptingCredentials.AddKey(new RsaSecurityKey(algorithm)); Options.Logger.LogInformation("An existing key was automatically added to the " + "encryption credentials list: {Key}.", file.FullName); } else if (string.Equals(usage, "Signing", StringComparison.OrdinalIgnoreCase)) { // Only add the signing key if none has been explictly added. if (Options.SigningCredentials.Count != 0) { continue; } var algorithm = RSA.Create(); algorithm.ImportParameters(parameters.Value); // Add the key to the signing credentials list. Options.SigningCredentials.AddKey(new RsaSecurityKey(algorithm)); Options.Logger.LogInformation("An existing key was automatically added to the " + "signing credentials list: {Key}.", file.FullName); } } } // If no encryption key has been found, generate and persist a new RSA key. if (Options.EncryptingCredentials.Count == 0) { // Generate a new RSA key and export its public/private parameters. var algorithm = OpenIdConnectServerHelpers.GenerateKey(size: 2048); var parameters = algorithm.ExportParameters(/* includePrivateParameters: */ true); // Generate a new file name for the key and determine its absolute path. var path = Path.Combine(directory.FullName, Guid.NewGuid().ToString() + ".key"); using (var stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write)) { // Encrypt the key using the data protector. var bytes = OpenIdConnectServerHelpers.EncryptKey(protector, parameters, usage: "Encryption"); // Write the encrypted key to the file stream. stream.Write(bytes, 0, bytes.Length); } Options.EncryptingCredentials.AddKey(new RsaSecurityKey(algorithm)); Options.Logger.LogInformation("A new RSA key was automatically generated, added to the " + "encryption credentials list and persisted on the disk: {Path}.", path); } // If no signing key has been found, generate and persist a new RSA key. if (Options.SigningCredentials.Count == 0) { // Generate a new RSA key and export its public/private parameters. var algorithm = OpenIdConnectServerHelpers.GenerateKey(size: 2048); var parameters = algorithm.ExportParameters(/* includePrivateParameters: */ true); // Generate a new file name for the key and determine its absolute path. var path = Path.Combine(directory.FullName, Guid.NewGuid().ToString() + ".key"); using (var stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write)) { // Encrypt the key using the data protector. var bytes = OpenIdConnectServerHelpers.EncryptKey(protector, parameters, usage: "Signing"); // Write the encrypted key to the file stream. stream.Write(bytes, 0, bytes.Length); } Options.SigningCredentials.AddKey(new RsaSecurityKey(algorithm)); Options.Logger.LogInformation("A new RSA key was automatically generated, added to the " + "signing credentials list and persisted on the disk: {Path}.", path); } } }
private async Task <bool> InvokeCryptographyEndpointAsync() { // Metadata requests must be made via GET. // See http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest if (!string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The discovery request was rejected because an invalid " + "HTTP method was used: {Method}.", Request.Method); return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "Invalid HTTP method: make sure to use GET." })); } var request = new OpenIdConnectRequest(Request.Query); // Note: set the message type before invoking the ExtractCryptographyRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.CryptographyRequest); // Store the discovery request in the OWIN context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractCryptographyRequestContext(Context, Options, request); await Options.Provider.ExtractCryptographyRequest(@event); if (@event.HandledResponse) { Logger.LogDebug("The discovery request was handled in user code."); return(true); } else if (@event.Skipped) { Logger.LogDebug("The default discovery request handling was skipped from user code."); return(false); } else if (@event.IsRejected) { Logger.LogError("The discovery request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } Logger.LogInformation("The discovery request was successfully extracted " + "from the HTTP request: {Request}", request); var context = new ValidateCryptographyRequestContext(Context, Options, request); await Options.Provider.ValidateCryptographyRequest(context); if (context.HandledResponse) { Logger.LogDebug("The discovery request was handled in user code."); return(true); } else if (context.Skipped) { Logger.LogDebug("The default discovery request handling was skipped from user code."); return(false); } else if (!context.IsValidated) { Logger.LogError("The discovery request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } var notification = new HandleCryptographyRequestContext(Context, Options, request); foreach (var credentials in Options.SigningCredentials) { // If the signing key is not an asymmetric key, ignore it. if (!(credentials.SigningKey is AsymmetricSecurityKey)) { continue; } if (!credentials.SigningKey.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256Signature)) { Logger.LogInformation("An unsupported signing key was ignored and excluded from the " + "key set: {Type}. Only RSA asymmetric security keys can be exposed " + "via the JWKS endpoint.", credentials.SigningKey.GetType().Name); continue; } // Try to extract a key identifier from the credentials. LocalIdKeyIdentifierClause identifier = null; credentials.SigningKeyIdentifier?.TryFind(out identifier); // Resolve the underlying algorithm from the security key. var algorithm = ((AsymmetricSecurityKey)credentials.SigningKey) .GetAsymmetricAlgorithm( algorithm: SecurityAlgorithms.RsaSha256Signature, privateKey: false) as RSA; // Skip the key if an algorithm instance cannot be extracted. if (algorithm == null) { Logger.LogWarning("A signing key was ignored because it was unable " + "to provide the requested algorithm instance."); continue; } // Export the RSA public key to create a new JSON Web Key // exposing the exponent and the modulus parameters. var parameters = algorithm.ExportParameters(includePrivateParameters: false); Debug.Assert(parameters.Exponent != null && parameters.Modulus != null, "RSA.ExportParameters() shouldn't return null parameters."); var key = new JsonWebKey { Use = JsonWebKeyUseNames.Sig, Kty = JsonWebAlgorithmsKeyTypes.RSA, // Resolve the JWA identifier from the algorithm specified in the credentials. Alg = OpenIdConnectServerHelpers.GetJwtAlgorithm(credentials.SignatureAlgorithm), // Use the key identifier specified // in the signing credentials. Kid = identifier?.LocalId, // Note: both E and N must be base64url-encoded. // See https://tools.ietf.org/html/rfc7518#section-6.2.1.2 E = Base64UrlEncoder.Encode(parameters.Exponent), N = Base64UrlEncoder.Encode(parameters.Modulus) }; X509Certificate2 certificate = null; // Determine whether the signing credentials are directly based on a X.509 certificate. var x509SigningCredentials = credentials as X509SigningCredentials; if (x509SigningCredentials != null) { certificate = x509SigningCredentials.Certificate; } // Skip looking for a X509SecurityKey in SigningCredentials.SigningKey // if a certificate has been found in the SigningCredentials instance. if (certificate == null) { // Determine whether the security key is an asymmetric key embedded in a X.509 certificate. var x509SecurityKey = credentials.SigningKey as X509SecurityKey; if (x509SecurityKey != null) { certificate = x509SecurityKey.Certificate; } } // Skip looking for a X509AsymmetricSecurityKey in SigningCredentials.SigningKey // if a certificate has been found in SigningCredentials or SigningCredentials.SigningKey. if (certificate == null) { // Determine whether the security key is an asymmetric key embedded in a X.509 certificate. var x509AsymmetricSecurityKey = credentials.SigningKey as X509AsymmetricSecurityKey; if (x509AsymmetricSecurityKey != null) { // The X.509 certificate is not directly accessible when using X509AsymmetricSecurityKey. // Reflection is the only way to get the certificate used to create the security key. var field = typeof(X509AsymmetricSecurityKey).GetField( name: "certificate", bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic); Debug.Assert(field != null); certificate = (X509Certificate2)field.GetValue(x509AsymmetricSecurityKey); } } // If the signing key is embedded in a X.509 certificate, set // the x5t and x5c parameters using the certificate details. if (certificate != null) { // x5t must be base64url-encoded. // See https://tools.ietf.org/html/rfc7517#section-4.8 key.X5t = Base64UrlEncoder.Encode(certificate.GetCertHash()); // Unlike E or N, the certificates contained in x5c // must be base64-encoded and not base64url-encoded. // See https://tools.ietf.org/html/rfc7517#section-4.7 key.X5c.Add(Convert.ToBase64String(certificate.RawData)); } notification.Keys.Add(key); } await Options.Provider.HandleCryptographyRequest(notification); if (notification.HandledResponse) { Logger.LogDebug("The discovery request was handled in user code."); return(true); } else if (notification.Skipped) { Logger.LogDebug("The default discovery request handling was skipped from user code."); return(false); } else if (notification.IsRejected) { Logger.LogError("The discovery request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } var keys = new JArray(); foreach (var key in notification.Keys) { var item = new JObject(); // Ensure a key type has been provided. // See https://tools.ietf.org/html/rfc7517#section-4.1 if (string.IsNullOrEmpty(key.Kty)) { Logger.LogError("A JSON Web Key was excluded from the key set because " + "it didn't contain the mandatory 'kid' parameter."); continue; } // Create a dictionary associating the // JsonWebKey components with their values. var parameters = new Dictionary <string, string> { [JsonWebKeyParameterNames.Kid] = key.Kid, [JsonWebKeyParameterNames.Use] = key.Use, [JsonWebKeyParameterNames.Kty] = key.Kty, [JsonWebKeyParameterNames.KeyOps] = key.KeyOps, [JsonWebKeyParameterNames.Alg] = key.Alg, [JsonWebKeyParameterNames.E] = key.E, [JsonWebKeyParameterNames.N] = key.N, [JsonWebKeyParameterNames.X5t] = key.X5t, [JsonWebKeyParameterNames.X5u] = key.X5u }; foreach (var parameter in parameters) { if (!string.IsNullOrEmpty(parameter.Value)) { item.Add(parameter.Key, parameter.Value); } } if (key.X5c.Count != 0) { item.Add(JsonWebKeyParameterNames.X5c, new JArray(key.X5c)); } keys.Add(item); } return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Keys] = keys })); }