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)); }
/// <summary> /// Sends a generic OpenID Connect request to the given endpoint and /// converts the returned response to an OpenID Connect response. /// </summary> /// <param name="method">The HTTP method used to send the OpenID Connect request.</param> /// <param name="uri">The endpoint to which the request is sent.</param> /// <param name="request">The OpenID Connect request to send.</param> /// <returns>The OpenID Connect response returned by the server.</returns> public virtual async Task <OpenIdConnectResponse> SendAsync( [NotNull] HttpMethod method, [NotNull] Uri uri, [NotNull] OpenIdConnectRequest request) { if (method == null) { throw new ArgumentNullException(nameof(method)); } if (uri == null) { throw new ArgumentNullException(nameof(uri)); } if (request == null) { throw new ArgumentNullException(nameof(request)); } if (HttpClient.BaseAddress == null && !uri.IsAbsoluteUri) { throw new ArgumentException("The address cannot be a relative URI when no base address " + "is associated with the HTTP client.", nameof(uri)); } var parameters = new Dictionary <string, string>(); foreach (var parameter in request.GetParameters()) { var value = (string)parameter.Value; if (string.IsNullOrEmpty(value)) { continue; } parameters.Add(parameter.Key, value); } if (method == HttpMethod.Get && parameters.Count != 0) { var builder = new StringBuilder(); foreach (var parameter in parameters) { if (builder.Length != 0) { builder.Append('&'); } builder.Append(UrlEncoder.Default.Encode(parameter.Key)); builder.Append('='); builder.Append(UrlEncoder.Default.Encode(parameter.Value)); } if (!uri.IsAbsoluteUri) { uri = new Uri(HttpClient.BaseAddress, uri); } uri = new UriBuilder(uri) { Query = builder.ToString() }.Uri; } var message = new HttpRequestMessage(method, uri); if (method != HttpMethod.Get) { message.Content = new FormUrlEncodedContent(parameters); } var response = await HttpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead); if (response.Headers.Location != null) { var payload = response.Headers.Location.Fragment; if (string.IsNullOrEmpty(payload)) { payload = response.Headers.Location.Query; } if (string.IsNullOrEmpty(payload)) { return(new OpenIdConnectResponse()); } var result = new OpenIdConnectResponse(); using (var tokenizer = new StringTokenizer(payload, OpenIdConnectConstants.Separators.Ampersand).GetEnumerator()) { while (tokenizer.MoveNext()) { var parameter = tokenizer.Current; if (parameter.Length == 0) { continue; } // Always skip the first char (# or ?). if (parameter.Offset == 0) { parameter = parameter.Subsegment(1, parameter.Length - 1); } var index = parameter.IndexOf('='); if (index == -1) { continue; } var name = parameter.Substring(0, index); if (string.IsNullOrEmpty(name)) { continue; } var value = parameter.Substring(index + 1, parameter.Length - (index + 1)); if (string.IsNullOrEmpty(value)) { continue; } result.AddParameter( Uri.UnescapeDataString(name.Replace('+', ' ')), Uri.UnescapeDataString(value.Replace('+', ' '))); } } return(result); } else if (string.Equals(response.Content?.Headers?.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase)) { using (var stream = await response.Content.ReadAsStreamAsync()) using (var reader = new JsonTextReader(new StreamReader(stream))) { var serializer = JsonSerializer.CreateDefault(); return(serializer.Deserialize <OpenIdConnectResponse>(reader)); } } else if (string.Equals(response.Content?.Headers?.ContentType?.MediaType, "text/html", StringComparison.OrdinalIgnoreCase)) { using (var stream = await response.Content.ReadAsStreamAsync()) { var result = new OpenIdConnectResponse(); var document = await new HtmlParser().ParseAsync(stream); foreach (var element in document.Body.GetElementsByTagName("input")) { var name = element.GetAttribute("name"); if (string.IsNullOrEmpty(name)) { continue; } var value = element.GetAttribute("value"); if (string.IsNullOrEmpty(value)) { continue; } result.AddParameter(name, value); } return(result); } } else if (string.Equals(response.Content?.Headers?.ContentType?.MediaType, "text/plain", StringComparison.OrdinalIgnoreCase)) { using (var stream = await response.Content.ReadAsStreamAsync()) using (var reader = new StreamReader(stream)) { var result = new OpenIdConnectResponse(); for (var line = await reader.ReadLineAsync(); line != null; line = await reader.ReadLineAsync()) { var index = line.IndexOf(':'); if (index == -1) { continue; } result.AddParameter(line.Substring(0, index), line.Substring(index + 1)); } return(result); } } return(new OpenIdConnectResponse()); }
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.SigningKey is AsymmetricSecurityKey)) { Logger.LogDebug("A non-asymmetric signing key of type '{Type}' was excluded " + "from the key set.", credentials.SigningKey.GetType().FullName); continue; } if (!credentials.SigningKey.IsSupportedAlgorithm(SecurityAlgorithms.RsaSha256Signature)) { 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.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 security key is an asymmetric key embedded in a X.509 certificate. if (credentials is X509SigningCredentials x509SigningCredentials) { certificate = x509SigningCredentials.Certificate; } else if (credentials.SigningKey is X509SecurityKey x509SecurityKey) { certificate = x509SecurityKey.Certificate; } else if (credentials.SigningKey is X509AsymmetricSecurityKey x509AsymmetricSecurityKey) { // 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 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.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); } // 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)); }