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.Cryptography); // Store the discovery request in the ASP.NET context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractCryptographyRequestContext(Context, Options, request); await Options.Provider.ExtractCryptographyRequest(@event); if (@event.HandledResponse) { return(true); } else if (@event.Skipped) { 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 })); } var context = new ValidateCryptographyRequestContext(Context, Options, request); await Options.Provider.ValidateCryptographyRequest(context); if (context.HandledResponse) { return(true); } else if (context.Skipped) { 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) { // Ignore the key if it's not supported. #if SUPPORTS_ECDSA if (!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256Signature) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha256Signature) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384Signature) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512Signature)) { Logger.LogInformation("An unsupported signing key was ignored and excluded from the " + "key set: {Type}. 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.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.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.RsaSha256Signature)) { 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 var x509SecurityKey = credentials.Key as X509SecurityKey; if (x509SecurityKey != null) { algorithm = x509SecurityKey.PublicKey as RSA; } var rsaSecurityKey = credentials.Key as RsaSecurityKey; if (rsaSecurityKey != null) { 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.EcdsaSha256Signature) || credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha384Signature) || credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.EcdsaSha512Signature)) { ECDsa algorithm = null; var x509SecurityKey = credentials.Key as X509SecurityKey; if (x509SecurityKey != null) { algorithm = x509SecurityKey.PublicKey as ECDsa; } var ecdsaSecurityKey = credentials.Key as ECDsaSecurityKey; if (ecdsaSecurityKey != null) { 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) { return(true); } else if (notification.Skipped) { 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.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, JArray.FromObject(key.KeyOps)); } if (key.X5c.Count != 0) { item.Add(JsonWebKeyParameterNames.X5c, JArray.FromObject(key.X5c)); } keys.Add(item); } return(await SendCryptographyResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Keys] = keys })); }
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 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 ASP.NET 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) { 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) { 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.Algorithm); 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) { 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))); }
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 SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "Invalid HTTP method: make sure to use GET." })); } var validatingContext = new ValidateCryptographyRequestContext(Context, Options); await Options.Provider.ValidateCryptographyRequest(validatingContext); if (!validatingContext.IsValidated) { Logger.LogInformation("The discovery request was rejected by application code."); return(await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = validatingContext.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = validatingContext.ErrorDescription, ErrorUri = validatingContext.ErrorUri })); } var notification = new HandleCryptographyRequestContext(Context, Options); foreach (var credentials in Options.SigningCredentials) { // Ignore the key if it's not supported. if (!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256Signature) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha384Signature) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha512Signature)) { 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.Key.GetType().Name); continue; } // Determine whether the security key is a RSA key embedded in a X.509 certificate. var x509SecurityKey = credentials.Key as X509SecurityKey; if (x509SecurityKey != null) { // Create a new JSON Web Key exposing the // certificate instead of its public RSA key. notification.Keys.Add(new JsonWebKey { Use = JsonWebKeyUseNames.Sig, 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 = credentials.Kid, // x5t must be base64url-encoded. // See http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31#section-4.8 X5t = Base64UrlEncoder.Encode(x509SecurityKey.Certificate.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 X5c = { Convert.ToBase64String(x509SecurityKey.Certificate.RawData) } }); } var rsaSecurityKey = credentials.Key as RsaSecurityKey; if (rsaSecurityKey != null) { // Export the RSA public key. notification.Keys.Add(new JsonWebKey { Use = JsonWebKeyUseNames.Sig, 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 = credentials.Kid, // 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(rsaSecurityKey.Parameters.Exponent), N = Base64UrlEncoder.Encode(rsaSecurityKey.Parameters.Modulus) }); } } await Options.Provider.HandleCryptographyRequest(notification); if (notification.HandledResponse) { return(true); } else if (notification.Skipped) { return(false); } var payload = 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)) { 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.X5t] = key.X5t, [JsonWebKeyParameterNames.X5u] = key.X5u, [JsonWebKeyParameterNames.E] = key.E, [JsonWebKeyParameterNames.N] = key.N }; foreach (var parameter in parameters) { if (!string.IsNullOrEmpty(parameter.Value)) { item.Add(parameter.Key, parameter.Value); } } if (key.KeyOps.Any()) { item.Add(JsonWebKeyParameterNames.KeyOps, JArray.FromObject(key.KeyOps)); } if (key.X5c.Any()) { item.Add(JsonWebKeyParameterNames.X5c, JArray.FromObject(key.X5c)); } keys.Add(item); } payload.Add(JsonWebKeyParameterNames.Keys, keys); var context = new ApplyCryptographyResponseContext(Context, Options, payload); await Options.Provider.ApplyCryptographyResponse(context); if (context.HandledResponse) { return(true); } else if (context.Skipped) { return(false); } using (var buffer = new MemoryStream()) using (var writer = new JsonTextWriter(new StreamWriter(buffer))) { payload.WriteTo(writer); writer.Flush(); Response.ContentLength = buffer.Length; Response.ContentType = "application/json;charset=UTF-8"; buffer.Seek(offset: 0, loc: SeekOrigin.Begin); await buffer.CopyToAsync(Response.Body, 4096, Context.RequestAborted); return(true); } }
private async Task InvokeCryptographyEndpointAsync() { var notification = new CryptographyEndpointContext(Context, Options); // 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("Cryptography endpoint: invalid method used."); return; } foreach (var credentials in Options.SigningCredentials) { // Ignore the key if it's not supported. if (!credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256Signature) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha384Signature) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha512Signature)) { Logger.LogDebug("Cryptography endpoint: unsupported signing key ignored. " + "Only asymmetric security keys supporting RS256, RS384 " + "or RS512 can be exposed via the JWKS endpoint."); continue; } // Determine whether the security key is a RSA key embedded in a X.509 certificate. var x509SecurityKey = credentials.Key as X509SecurityKey; if (x509SecurityKey != null) { // Create a new JSON Web Key exposing the // certificate instead of its public RSA key. notification.Keys.Add(new JsonWebKey { Use = JsonWebKeyUseNames.Sig, Kty = JsonWebAlgorithmsKeyTypes.RSA, // Resolve the JWA identifier from the algorithm specified in the credentials. Alg = OpenIdConnectServerHelpers.GetJwtAlgorithm(credentials.Algorithm), // By default, use the key identifier specified in the signing credentials. If none is specified, // use the hexadecimal representation of the certificate's SHA-1 hash as the unique key identifier. Kid = credentials.Kid ?? x509SecurityKey.KeyId ?? x509SecurityKey.Certificate.Thumbprint, // x5t must be base64url-encoded. // See http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31#section-4.8 X5t = Base64UrlEncoder.Encode(x509SecurityKey.Certificate.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 X5c = { Convert.ToBase64String(x509SecurityKey.Certificate.RawData) } }); } var rsaSecurityKey = credentials.Key as RsaSecurityKey; if (rsaSecurityKey != null) { // Export the RSA public key. notification.Keys.Add(new JsonWebKey { Use = JsonWebKeyUseNames.Sig, Kty = JsonWebAlgorithmsKeyTypes.RSA, // Resolve the JWA identifier from the algorithm specified in the credentials. Alg = OpenIdConnectServerHelpers.GetJwtAlgorithm(credentials.Algorithm), // By default, use the key identifier specified in the signing credentials. // If none is specified, try to extract it from the security key itself or // create one using the base64url-encoded representation of the modulus. Kid = credentials.Kid ?? rsaSecurityKey.KeyId ?? // Note: use the first 40 chars to avoid using a too long identifier. Base64UrlEncoder.Encode(rsaSecurityKey.Parameters.Modulus) .Substring(0, 40).ToUpperInvariant(), // 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(rsaSecurityKey.Parameters.Exponent), N = Base64UrlEncoder.Encode(rsaSecurityKey.Parameters.Modulus) }); } } await Options.Provider.CryptographyEndpoint(notification); if (notification.HandledResponse) { return; } var payload = 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)) { Logger.LogWarning("Cryptography endpoint: a JSON Web Key didn't " + "contain the mandatory 'Kty' parameter and has been ignored."); 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.X5t] = key.X5t, [JsonWebKeyParameterNames.X5u] = key.X5u, [JsonWebKeyParameterNames.E] = key.E, [JsonWebKeyParameterNames.N] = key.N }; foreach (var parameter in parameters) { if (!string.IsNullOrEmpty(parameter.Value)) { item.Add(parameter.Key, parameter.Value); } } if (key.KeyOps.Any()) { item.Add(JsonWebKeyParameterNames.KeyOps, JArray.FromObject(key.KeyOps)); } if (key.X5c.Any()) { item.Add(JsonWebKeyParameterNames.X5c, JArray.FromObject(key.X5c)); } keys.Add(item); } payload.Add(JsonWebKeyParameterNames.Keys, keys); var context = new CryptographyEndpointResponseContext(Context, Options, payload); await Options.Provider.CryptographyEndpointResponse(context); if (context.HandledResponse) { return; } using (var buffer = new MemoryStream()) using (var writer = new JsonTextWriter(new StreamWriter(buffer))) { payload.WriteTo(writer); writer.Flush(); Response.ContentLength = buffer.Length; Response.ContentType = "application/json;charset=UTF-8"; buffer.Seek(offset: 0, loc: SeekOrigin.Begin); await buffer.CopyToAsync(Response.Body, 4096, Context.RequestAborted); } }
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 ASP.NET context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractConfigurationRequestContext(Context, Scheme, Options, request); await Provider.ExtractConfigurationRequest(@event); if (@event.Result != null) { if (@event.Result.Handled) { Logger.LogDebug("The configuration request was handled in user code."); return(true); } else if (@event.Result.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, Scheme, Options, request); await Provider.ValidateConfigurationRequest(context); if (context.Result != null) { if (context.Result.Handled) { Logger.LogDebug("The configuration request was handled in user code."); return(true); } else if (context.Result.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, Scheme, 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.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 Provider.HandleConfigurationRequest(notification); if (notification.Result != null) { if (notification.Result.Handled) { Logger.LogDebug("The configuration request was handled in user code."); return(true); } else if (notification.Result.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.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)); }
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(null, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "Invalid HTTP method: make sure to use GET." })); } var request = new OpenIdConnectMessage(Request.Query.ToDictionary()); var context = new ValidateCryptographyRequestContext(Context, Options); await Options.Provider.ValidateCryptographyRequest(context); 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(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.SigningCredentials) { // Ignore the key if it's not supported. if (!(credentials.Key is AsymmetricSecurityKey) && !credentials.Key.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256Signature)) { 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.Key.GetType().Name); continue; } 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 var x509SecurityKey = credentials.Key as X509SecurityKey; if (x509SecurityKey != null) { algorithm = (RSA)x509SecurityKey.PublicKey; } var rsaSecurityKey = credentials.Key as RsaSecurityKey; if (rsaSecurityKey != null) { algorithm = rsaSecurityKey.Rsa; // If no RSA instance can be found, create one using // the RSA parameters attached to the security key. if (algorithm == null) { algorithm = RSA.Create(); algorithm.ImportParameters(rsaSecurityKey.Parameters); } } // Skip the key if a RSA instance cannot be retrieved. if (algorithm == null) { 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.Algorithm), // Use the key identifier specified // in the signing credentials. Kid = credentials.Kid, // 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) }; // If the signing key is embedded in a X.509 certificate, set // the x5t and x5c parameters using the certificate details. var x509Certificate = x509SecurityKey?.Certificate; 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(x509SecurityKey.Certificate.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(x509SecurityKey.Certificate.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)) { 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.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.KeyOps.Count != 0) { item.Add(JsonWebKeyParameterNames.KeyOps, JArray.FromObject(key.KeyOps)); } 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)); }