/// <summary> /// Parses a URL and turns it into authority and discovery endpoint URL. /// </summary> /// <param name="input">The input.</param> /// <returns></returns> /// <exception cref="System.InvalidOperationException"> /// Malformed URL /// </exception> public static DiscoveryEndpoint ParseUrl(string input) { var success = Uri.TryCreate(input, UriKind.Absolute, out var uri); if (success == false) { throw new InvalidOperationException("Malformed URL"); } if (!DiscoveryUrlHelper.IsValidScheme(uri)) { throw new InvalidOperationException("Malformed URL"); } var url = input.RemoveTrailingSlash(); if (url.EndsWith(OidcConstants.Discovery.DiscoveryEndpoint, StringComparison.OrdinalIgnoreCase)) { return(new DiscoveryEndpoint(url.Substring(0, url.Length - OidcConstants.Discovery.DiscoveryEndpoint.Length - 1), url)); } else { return(new DiscoveryEndpoint(url, url.EnsureTrailingSlash() + OidcConstants.Discovery.DiscoveryEndpoint)); } }
/// <summary> /// Initializes a new instance of the <see cref="DiscoveryClient"/> class. /// </summary> /// <param name="authority">The authority.</param> /// <param name="innerHandler">The inner handler.</param> /// <exception cref="System.InvalidOperationException"> /// Malformed authority URL /// or /// Malformed authority URL /// </exception> public DiscoveryClient(string authority, HttpMessageHandler innerHandler = null) { var handler = innerHandler ?? new HttpClientHandler(); var success = Uri.TryCreate(authority, UriKind.Absolute, out var uri); if (success == false) { throw new InvalidOperationException("Malformed authority URL"); } if (!DiscoveryUrlHelper.IsValidScheme(uri)) { throw new InvalidOperationException("Malformed authority URL"); } var url = authority.RemoveTrailingSlash(); if (url.EndsWith(OidcConstants.Discovery.DiscoveryEndpoint, StringComparison.OrdinalIgnoreCase)) { Url = url; Authority = url.Substring(0, url.Length - OidcConstants.Discovery.DiscoveryEndpoint.Length - 1); } else { Authority = url; Url = url.EnsureTrailingSlash() + OidcConstants.Discovery.DiscoveryEndpoint; } _client = new HttpClient(handler); }
/// <summary> /// Retrieves the discovery document. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> public async Task <DiscoveryResponse> GetAsync(CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(Policy.Authority)) { Policy.Authority = Authority; } string jwkUrl = ""; if (!DiscoveryUrlHelper.IsSecureScheme(new Uri(Url), Policy)) { return(new DiscoveryResponse(new InvalidOperationException("HTTPS required"), $"Error connecting to {Url}")); } try { var response = await _client.GetAsync(Url, cancellationToken).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { return(new DiscoveryResponse(response.StatusCode, $"Error connecting to {Url}: {response.ReasonPhrase}")); } var disco = new DiscoveryResponse(await response.Content.ReadAsStringAsync().ConfigureAwait(false), Policy); if (disco.IsError) { return(disco); } try { jwkUrl = disco.JwksUri; if (jwkUrl != null) { response = await _client.GetAsync(jwkUrl, cancellationToken).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { return(new DiscoveryResponse(response.StatusCode, $"Error connecting to {jwkUrl}: {response.ReasonPhrase}")); } var jwk = await response.Content.ReadAsStringAsync().ConfigureAwait(false); disco.KeySet = new JsonWebKeySet(jwk); } return(disco); } catch (Exception ex) { return(new DiscoveryResponse(ex, $"Error connecting to {jwkUrl}")); } } catch (Exception ex) { return(new DiscoveryResponse(ex, $"Error connecting to {Url}")); } }
public string ValidateEndoints(JObject json, DiscoveryPolicy policy) { var authorityHost = new Uri(policy.Authority).Authority; foreach (var element in json) { if (element.Key.EndsWith("Endpoint", StringComparison.OrdinalIgnoreCase) || element.Key.Equals(OidcConstants.Discovery.JwksUri, StringComparison.OrdinalIgnoreCase)) { var endpoint = element.Value.ToString(); Uri uri; var isValidUri = Uri.TryCreate(endpoint, UriKind.Absolute, out uri); if (!isValidUri) { return($"Malformed endpoint: {endpoint}"); } if (!DiscoveryUrlHelper.IsValidScheme(uri)) { return($"Malformed endpoint: {endpoint}"); } if (!DiscoveryUrlHelper.IsSecureScheme(uri, policy)) { return($"Endpoint does not use HTTPS: {endpoint}"); } if (policy.ValidateEndpoints) { if (!string.Equals(authorityHost, uri.Host)) { return($"Endpoint is on a different host than authority: {endpoint}"); } if (!endpoint.StartsWith(policy.Authority, StringComparison.Ordinal)) { return($"Endpoint belongs to different authority: {endpoint}"); } } } } if (policy.RequireKeySet) { if (string.IsNullOrWhiteSpace(JwksUri)) { return("Keyset is missing"); } } return(string.Empty); }
/// <summary> /// Validates the endoints and jwks_uri according to the security policy. /// </summary> /// <param name="json">The json.</param> /// <param name="policy">The policy.</param> /// <returns></returns> public string ValidateEndpoints(JObject json, DiscoveryPolicy policy) { // allowed hosts var allowedHosts = new HashSet <string>(policy.AdditionalEndpointBaseAddresses.Select(e => new Uri(e).Authority)) { new Uri(policy.Authority).Authority }; // allowed authorities (hosts + base address) var allowedAuthorities = new HashSet <string>(policy.AdditionalEndpointBaseAddresses) { policy.Authority }; foreach (var element in json) { if (element.Key.EndsWith("endpoint", StringComparison.OrdinalIgnoreCase) || element.Key.Equals(OidcConstants.Discovery.JwksUri, StringComparison.OrdinalIgnoreCase) || element.Key.Equals(OidcConstants.Discovery.CheckSessionIframe, StringComparison.OrdinalIgnoreCase)) { var endpoint = element.Value.ToString(); var isValidUri = Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uri); if (!isValidUri) { return($"Malformed endpoint: {endpoint}"); } if (!DiscoveryUrlHelper.IsValidScheme(uri)) { return($"Malformed endpoint: {endpoint}"); } if (!DiscoveryUrlHelper.IsSecureScheme(uri, policy)) { return($"Endpoint does not use HTTPS: {endpoint}"); } if (policy.ValidateEndpoints) { // if endpoint is on exclude list, don't validate if (policy.EndpointValidationExcludeList.Contains(element.Key)) { continue; } bool isAllowed = false; foreach (var host in allowedHosts) { if (string.Equals(host, uri.Authority)) { isAllowed = true; } } if (!isAllowed) { return($"Endpoint is on a different host than authority: {endpoint}"); } isAllowed = false; foreach (var authority in allowedAuthorities) { if (endpoint.StartsWith(authority, policy.AuthorityNameComparison)) { isAllowed = true; } } if (!isAllowed) { return($"Endpoint belongs to different authority: {endpoint}"); } } } } if (policy.RequireKeySet) { if (string.IsNullOrWhiteSpace(JwksUri)) { return("Keyset is missing"); } } return(string.Empty); }