public void PropertyGetter_ReturnsExpectedParameter(string property, string name, OpenIdConnectParameter value) { // Arrange var response = new OpenIdConnectResponse(); response.SetParameter(name, value); // Act and assert Assert.Equal(value.Value, typeof(OpenIdConnectResponse).GetProperty(property).GetValue(response)); }
private async Task <bool> InvokeIntrospectionEndpointAsync() { OpenIdConnectRequest request; // See https://tools.ietf.org/html/rfc7662#section-2.1 // and https://tools.ietf.org/html/rfc7662#section-4 if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { request = new OpenIdConnectRequest(Request.Query); } else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrEmpty(Request.ContentType)) { Logger.LogError("The introspection request was rejected because " + "the mandatory 'Content-Type' header was missing."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'Content-Type' header must be specified." })); } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The introspection request was rejected because an invalid 'Content-Type' " + "header was specified: {ContentType}.", Request.ContentType); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified 'Content-Type' header is not valid." })); } request = new OpenIdConnectRequest(await Request.ReadFormAsync(Context.RequestAborted)); } else { Logger.LogError("The introspection request was rejected because an invalid " + "HTTP method was specified: {Method}.", Request.Method); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified HTTP method is not valid." })); } // Note: set the message type before invoking the ExtractIntrospectionRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.IntrospectionRequest); // Store the introspection request in the ASP.NET context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractIntrospectionRequestContext(Context, Options, request); await Options.Provider.ExtractIntrospectionRequest(@event); if (@event.HandledResponse) { Logger.LogDebug("The introspection request was handled in user code."); return(true); } else if (@event.Skipped) { Logger.LogDebug("The default introspection request handling was skipped from user code."); return(false); } else if (@event.IsRejected) { Logger.LogError("The introspection request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } Logger.LogInformation("The introspection request was successfully extracted " + "from the HTTP request: {Request}.", request); if (string.IsNullOrEmpty(request.Token)) { return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'token' parameter is missing." })); } // Try to resolve the client credentials specified in the 'Authorization' header. // If they cannot be extracted, fallback to the client_id/client_secret parameters. var credentials = Request.Headers.GetClientCredentials(); if (credentials != null) { // Reject requests that use multiple client authentication methods. // See https://tools.ietf.org/html/rfc6749#section-2.3 for more information. if (!string.IsNullOrEmpty(request.ClientSecret)) { Logger.LogError("The introspection request was rejected because " + "multiple client credentials were specified."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "Multiple client credentials cannot be specified." })); } request.ClientId = credentials?.Key; request.ClientSecret = credentials?.Value; } var context = new ValidateIntrospectionRequestContext(Context, Options, request); await Options.Provider.ValidateIntrospectionRequest(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) { Logger.LogDebug("The introspection request was handled in user code."); return(true); } else if (context.Skipped) { Logger.LogDebug("The default introspection request handling was skipped from user code."); return(false); } else if (context.IsRejected) { Logger.LogError("The introspection request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } // Store the validated client_id as a request property. request.SetProperty(OpenIdConnectConstants.Properties.ValidatedClientId, context.ClientId); Logger.LogInformation("The introspection request was successfully validated."); AuthenticationTicket ticket = null; // Note: use the "token_type_hint" parameter to determine // the type of the token sent by the client application. // See https://tools.ietf.org/html/rfc7662#section-2.1 switch (request.TokenTypeHint) { case OpenIdConnectConstants.TokenTypeHints.AccessToken: ticket = await DeserializeAccessTokenAsync(request.Token, request); break; case OpenIdConnectConstants.TokenTypeHints.AuthorizationCode: ticket = await DeserializeAuthorizationCodeAsync(request.Token, request); break; case OpenIdConnectConstants.TokenTypeHints.IdToken: ticket = await DeserializeIdentityTokenAsync(request.Token, request); break; case OpenIdConnectConstants.TokenTypeHints.RefreshToken: ticket = await DeserializeRefreshTokenAsync(request.Token, request); break; } // Note: if the token can't be found using "token_type_hint", // the search must be extended to all supported token types. // See https://tools.ietf.org/html/rfc7662#section-2.1 if (ticket == null) { ticket = await DeserializeAccessTokenAsync(request.Token, request) ?? await DeserializeAuthorizationCodeAsync(request.Token, request) ?? await DeserializeIdentityTokenAsync(request.Token, request) ?? await DeserializeRefreshTokenAsync(request.Token, request); } if (ticket == null) { Logger.LogInformation("The introspection request was rejected because the token was invalid."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // Note: unlike refresh or identity tokens that can only be validated by client applications, // access tokens can be validated by either resource servers or client applications: // in both cases, the caller must be authenticated if the ticket is marked as confidential. if (context.IsSkipped && ticket.IsConfidential()) { Logger.LogError("The introspection request was rejected because the caller was not authenticated."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // If the ticket is already expired, directly return active=false. if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Logger.LogInformation("The introspection request was rejected because the token was expired."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // When a client_id can be inferred from the introspection request, // ensure that the client application is a valid audience/presenter. if (!string.IsNullOrEmpty(context.ClientId)) { if (ticket.IsAuthorizationCode() && ticket.HasPresenter() && !ticket.HasPresenter(context.ClientId)) { Logger.LogError("The introspection request was rejected because the " + "authorization code was issued to a different client."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // Ensure the caller is listed as a valid audience or authorized presenter. else if (ticket.IsAccessToken() && ticket.HasAudience() && !ticket.HasAudience(context.ClientId) && ticket.HasPresenter() && !ticket.HasPresenter(context.ClientId)) { Logger.LogError("The introspection request was rejected because the access token " + "was issued to a different client or for another resource server."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // Reject the request if the caller is not listed as a valid audience. else if (ticket.IsIdentityToken() && ticket.HasAudience() && !ticket.HasAudience(context.ClientId)) { Logger.LogError("The introspection request was rejected because the " + "identity token was issued to a different client."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } // Reject the introspection request if the caller doesn't // correspond to the client application the token was issued to. else if (ticket.IsRefreshToken() && ticket.HasPresenter() && !ticket.HasPresenter(context.ClientId)) { Logger.LogError("The introspection request was rejected because the " + "refresh token was issued to a different client."); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { [OpenIdConnectConstants.Parameters.Active] = false })); } } var notification = new HandleIntrospectionRequestContext(Context, Options, request, ticket) { Active = true, Issuer = Context.GetIssuer(Options), TokenId = ticket.GetTokenId(), TokenUsage = ticket.GetProperty(OpenIdConnectConstants.Properties.TokenUsage), Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject) }; // Note: only set "token_type" when the received token is an access token. // See https://tools.ietf.org/html/rfc7662#section-2.2 // and https://tools.ietf.org/html/rfc6749#section-5.1 if (ticket.IsAccessToken()) { notification.TokenType = OpenIdConnectConstants.TokenTypes.Bearer; } notification.IssuedAt = ticket.Properties.IssuedUtc; notification.NotBefore = ticket.Properties.IssuedUtc; notification.ExpiresAt = ticket.Properties.ExpiresUtc; // Infer the audiences/client_id claims from the properties stored in the authentication ticket. // Note: the client_id claim must be a unique string so multiple presenters cannot be returned. // To work around this limitation, only the first one is returned if multiple values are listed. notification.Audiences.UnionWith(ticket.GetAudiences()); notification.ClientId = ticket.GetPresenters().FirstOrDefault(); // Note: non-metadata claims are only added if the caller's client_id is known // AND is in the specified audiences, unless there's no explicit audience. if (!ticket.HasAudience() || (!string.IsNullOrEmpty(context.ClientId) && ticket.HasAudience(context.ClientId))) { notification.Username = ticket.Principal.Identity?.Name; notification.Scopes.UnionWith(ticket.GetScopes()); // Potentially sensitive claims are only exposed if the client was authenticated // and if the authentication ticket corresponds to an identity or access token. if (context.IsValidated && (ticket.IsAccessToken() || ticket.IsIdentityToken())) { foreach (var grouping in ticket.Principal.Claims.GroupBy(claim => claim.Type)) { // Exclude standard claims, that are already handled via strongly-typed properties. // Make sure to always update this list when adding new built-in claim properties. var type = grouping.Key; switch (type) { case OpenIdConnectConstants.Claims.Audience: case OpenIdConnectConstants.Claims.ExpiresAt: case OpenIdConnectConstants.Claims.IssuedAt: case OpenIdConnectConstants.Claims.Issuer: case OpenIdConnectConstants.Claims.NotBefore: case OpenIdConnectConstants.Claims.Scope: case OpenIdConnectConstants.Claims.Subject: case OpenIdConnectConstants.Claims.TokenType: case OpenIdConnectConstants.Claims.TokenUsage: continue; } var claims = grouping.ToArray(); switch (claims.Length) { case 0: continue; // When there's only one claim with the same type, directly // convert the claim as an OpenIdConnectParameter instance, // whose token type is determined from the claim value type. case 1: { notification.Claims[type] = claims[0].AsParameter(); continue; } // When multiple claims share the same type, convert all the claims // to OpenIdConnectParameter instances, retrieve the underlying // JSON values and add everything to a new JSON array. default: { notification.Claims[type] = new JArray(claims.Select(claim => claim.AsParameter().Value)); continue; } } } } } await Options.Provider.HandleIntrospectionRequest(notification); if (notification.HandledResponse) { Logger.LogDebug("The introspection request was handled in user code."); return(true); } else if (notification.Skipped) { Logger.LogDebug("The default introspection request handling was skipped from user code."); return(false); } else if (notification.IsRejected) { Logger.LogError("The introspection request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendIntrospectionResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } var response = new OpenIdConnectResponse { [OpenIdConnectConstants.Claims.Active] = notification.Active }; // Only add the other properties if // the token is considered as active. if (notification.Active) { response[OpenIdConnectConstants.Claims.Issuer] = notification.Issuer; response[OpenIdConnectConstants.Claims.Username] = notification.Username; response[OpenIdConnectConstants.Claims.Subject] = notification.Subject; response[OpenIdConnectConstants.Claims.Scope] = string.Join(" ", notification.Scopes); response[OpenIdConnectConstants.Claims.JwtId] = notification.TokenId; response[OpenIdConnectConstants.Claims.TokenType] = notification.TokenType; response[OpenIdConnectConstants.Claims.TokenUsage] = notification.TokenUsage; response[OpenIdConnectConstants.Claims.ClientId] = notification.ClientId; if (notification.IssuedAt != null) { response[OpenIdConnectConstants.Claims.IssuedAt] = EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime); } if (notification.NotBefore != null) { response[OpenIdConnectConstants.Claims.NotBefore] = EpochTime.GetIntDate(notification.NotBefore.Value.UtcDateTime); } if (notification.ExpiresAt != null) { response[OpenIdConnectConstants.Claims.ExpiresAt] = EpochTime.GetIntDate(notification.ExpiresAt.Value.UtcDateTime); } switch (notification.Audiences.Count) { case 0: break; case 1: response[OpenIdConnectConstants.Claims.Audience] = notification.Audiences.ElementAt(0); break; default: response[OpenIdConnectConstants.Claims.Audience] = new JArray(notification.Audiences); break; } foreach (var claim in notification.Claims) { response.SetParameter(claim.Key, claim.Value); } } return(await SendIntrospectionResponseAsync(response)); }
private async Task <bool> InvokeUserinfoEndpointAsync() { OpenIdConnectRequest request; if (HttpMethods.IsGet(Request.Method)) { request = new OpenIdConnectRequest(Request.Query); } else if (HttpMethods.IsPost(Request.Method)) { // Note: if no Content-Type header was specified, assume the userinfo request // doesn't contain any parameter and create an empty OpenIdConnectRequest. if (string.IsNullOrEmpty(Request.ContentType)) { request = new OpenIdConnectRequest(); } else { // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The userinfo request was rejected because an invalid 'Content-Type' " + "header was specified: {ContentType}.", Request.ContentType); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified 'Content-Type' header is not valid." })); } request = new OpenIdConnectRequest(await Request.ReadFormAsync()); } } else { Logger.LogError("The userinfo request was rejected because an invalid " + "HTTP method was specified: {Method}.", Request.Method); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified HTTP method is not valid." })); } // Note: set the message type before invoking the ExtractUserinfoRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.UserinfoRequest); // Insert the userinfo request in the ASP.NET context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractUserinfoRequestContext(Context, Scheme, Options, request); await Provider.ExtractUserinfoRequest(@event); if (@event.Result != null) { if (@event.Result.Handled) { Logger.LogDebug("The userinfo request was handled in user code."); return(true); } else if (@event.Result.Skipped) { Logger.LogDebug("The default userinfo request handling was skipped from user code."); return(false); } } else if (@event.IsRejected) { Logger.LogError("The userinfo request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } Logger.LogInformation("The userinfo request was successfully extracted " + "from the HTTP request: {Request}.", request); string token = null; if (!string.IsNullOrEmpty(request.AccessToken)) { token = request.AccessToken; } else { string header = Request.Headers[HeaderNames.Authorization]; if (!string.IsNullOrEmpty(header)) { if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The userinfo request was rejected because the " + "'Authorization' header was invalid: {Header}.", header); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified 'Authorization' header is invalid." })); } token = header.Substring("Bearer ".Length); } } if (string.IsNullOrEmpty(token)) { Logger.LogError("The userinfo request was rejected because the access token was missing."); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The mandatory 'access_token' parameter is missing." })); } var context = new ValidateUserinfoRequestContext(Context, Scheme, Options, request); await Provider.ValidateUserinfoRequest(context); if (context.Result != null) { if (context.Result.Handled) { Logger.LogDebug("The userinfo request was handled in user code."); return(true); } else if (context.Result.Skipped) { Logger.LogDebug("The default userinfo request handling was skipped from user code."); return(false); } } else if (context.IsRejected) { Logger.LogError("The userinfo request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } Logger.LogInformation("The userinfo request was successfully validated."); var ticket = await DeserializeAccessTokenAsync(token, request); if (ticket == null) { Logger.LogError("The userinfo request was rejected because the access token was invalid."); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidToken, ErrorDescription = "The specified access token is not valid." })); } if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Logger.LogError("The userinfo request was rejected because the access token was expired."); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidToken, ErrorDescription = "The specified access token is no longer valid." })); } var notification = new HandleUserinfoRequestContext(Context, Scheme, Options, request, ticket) { Issuer = Context.GetIssuer(Options), Subject = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject) }; // Note: when receiving an access token, its audiences list cannot be used for the "aud" claim // as the client application is not the intented audience but only an authorized presenter. // See http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse notification.Audiences.UnionWith(ticket.GetPresenters()); // The following claims are all optional and should be excluded when // no corresponding value has been found in the authentication ticket. if (ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) { notification.FamilyName = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.FamilyName); notification.GivenName = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.GivenName); notification.BirthDate = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Birthdate); } if (ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) { notification.Email = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Email); } if (ticket.HasScope(OpenIdConnectConstants.Scopes.Phone)) { notification.PhoneNumber = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.PhoneNumber); } await Provider.HandleUserinfoRequest(notification); if (notification.Result != null) { if (notification.Result.Handled) { Logger.LogDebug("The userinfo request was handled in user code."); return(true); } else if (notification.Result.Skipped) { Logger.LogDebug("The default userinfo request handling was skipped from user code."); return(false); } } else if (notification.IsRejected) { Logger.LogError("The userinfo request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } // Ensure the "sub" claim has been correctly populated. if (string.IsNullOrEmpty(notification.Subject)) { throw new InvalidOperationException("The subject claim cannot be null or empty."); } var response = new OpenIdConnectResponse { [OpenIdConnectConstants.Claims.Subject] = notification.Subject, [OpenIdConnectConstants.Claims.Address] = notification.Address, [OpenIdConnectConstants.Claims.Birthdate] = notification.BirthDate, [OpenIdConnectConstants.Claims.Email] = notification.Email, [OpenIdConnectConstants.Claims.EmailVerified] = notification.EmailVerified, [OpenIdConnectConstants.Claims.FamilyName] = notification.FamilyName, [OpenIdConnectConstants.Claims.GivenName] = notification.GivenName, [OpenIdConnectConstants.Claims.Issuer] = notification.Issuer, [OpenIdConnectConstants.Claims.PhoneNumber] = notification.PhoneNumber, [OpenIdConnectConstants.Claims.PhoneNumberVerified] = notification.PhoneNumberVerified, [OpenIdConnectConstants.Claims.PreferredUsername] = notification.PreferredUsername, [OpenIdConnectConstants.Claims.Profile] = notification.Profile, [OpenIdConnectConstants.Claims.Website] = notification.Website }; switch (notification.Audiences.Count) { case 0: break; case 1: response[OpenIdConnectConstants.Claims.Audience] = notification.Audiences.ElementAt(0); break; default: response[OpenIdConnectConstants.Claims.Audience] = new JArray(notification.Audiences); break; } foreach (var claim in notification.Claims) { response.SetParameter(claim.Key, claim.Value); } return(await SendUserinfoResponseAsync(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> /// 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 = parameter.Value as JValue; if (value == null) { continue; } parameters.Add(parameter.Key, (string) parameter.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.SetParameter( 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 payload = JToken.ReadFrom(reader) as JObject; if (payload == null) { throw new InvalidOperationException("The JSON payload returned by the server was invalid."); } return new OpenIdConnectResponse(payload); } } 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.SetParameter(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.SetParameter(line.Substring(0, index), line.Substring(index + 1)); } return result; } } throw new InvalidOperationException("The server returned an unexpected response."); }
private async Task <bool> InvokeUserinfoEndpointAsync() { OpenIdConnectRequest request; if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) { request = new OpenIdConnectRequest(Request.Query); } else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrWhiteSpace(Request.ContentType)) { Logger.LogError("The userinfo request was rejected because " + "the mandatory 'Content-Type' header was missing."); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed userinfo 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)) { Logger.LogError("The userinfo request was rejected because an invalid 'Content-Type' " + "header was received: {ContentType}.", Request.ContentType); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed userinfo request has been received: " + "the 'Content-Type' header contained an unexcepted value. " + "Make sure to use 'application/x-www-form-urlencoded'." })); } request = new OpenIdConnectRequest(await Request.ReadFormAsync()); } else { Logger.LogError("The userinfo request was rejected because an invalid " + "HTTP method was received: {Method}.", Request.Method); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed userinfo request has been received: " + "make sure to use either GET or POST." })); } // Note: set the message type before invoking the ExtractUserinfoRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.UserinfoRequest); // Insert the userinfo request in the OWIN context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractUserinfoRequestContext(Context, Options, request); await Options.Provider.ExtractUserinfoRequest(@event); if (@event.HandledResponse) { Logger.LogDebug("The userinfo request was handled in user code."); return(true); } else if (@event.Skipped) { Logger.LogDebug("The default userinfo request handling was skipped from user code."); return(false); } else if (@event.IsRejected) { Logger.LogError("The userinfo request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } Logger.LogInformation("The userinfo request was successfully extracted " + "from the HTTP request: {Request}", request); string token; if (!string.IsNullOrEmpty(request.AccessToken)) { token = request.AccessToken; } else { var header = Request.Headers.Get("Authorization"); if (string.IsNullOrEmpty(header)) { Logger.LogError("The userinfo request was rejected because " + "the 'Authorization' header was missing."); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed userinfo request has been received." })); } if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The userinfo request was rejected because the " + "'Authorization' header was invalid: {Header}.", header); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed userinfo request has been received." })); } token = header.Substring("Bearer ".Length); if (string.IsNullOrEmpty(token)) { Logger.LogError("The userinfo request was rejected because the access token was missing."); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed userinfo request has been received." })); } } var context = new ValidateUserinfoRequestContext(Context, Options, request); await Options.Provider.ValidateUserinfoRequest(context); if (context.HandledResponse) { Logger.LogDebug("The userinfo request was handled in user code."); return(true); } else if (context.Skipped) { Logger.LogDebug("The default userinfo request handling was skipped from user code."); return(false); } else if (!context.IsValidated) { Logger.LogError("The userinfo request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } Logger.LogInformation("The userinfo request was successfully validated."); var ticket = await DeserializeAccessTokenAsync(token, request); if (ticket == null) { Logger.LogError("The userinfo request was rejected because the access token was invalid."); // Note: an invalid token should result in an unauthorized response // but returning a 401 status would invoke the previously registered // authentication middleware and potentially replace it by a 302 response. // To work around this limitation, a 400 error is returned instead. // See http://openid.net/specs/openid-connect-core-1_0.html#UserInfoError return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Invalid token." })); } if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Logger.LogError("The userinfo request was rejected because the access token was expired."); // Note: an invalid token should result in an unauthorized response // but returning a 401 status would invoke the previously registered // authentication middleware and potentially replace it by a 302 response. // To work around this limitation, a 400 error is returned instead. // See http://openid.net/specs/openid-connect-core-1_0.html#UserInfoError return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, ErrorDescription = "Expired token." })); } var notification = new HandleUserinfoRequestContext(Context, Options, request, ticket); notification.Issuer = Context.GetIssuer(Options); notification.Subject = ticket.Identity.GetClaim(OpenIdConnectConstants.Claims.Subject); // Note: when receiving an access token, its audiences list cannot be used for the "aud" claim // as the client application is not the intented audience but only an authorized presenter. // See http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse notification.Audiences.UnionWith(ticket.GetPresenters()); // The following claims are all optional and should be excluded when // no corresponding value has been found in the authentication ticket. if (ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) { notification.FamilyName = ticket.Identity.GetClaim(OpenIdConnectConstants.Claims.FamilyName); notification.GivenName = ticket.Identity.GetClaim(OpenIdConnectConstants.Claims.GivenName); notification.BirthDate = ticket.Identity.GetClaim(OpenIdConnectConstants.Claims.Birthdate); } if (ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) { notification.Email = ticket.Identity.GetClaim(OpenIdConnectConstants.Claims.Email); } if (ticket.HasScope(OpenIdConnectConstants.Scopes.Phone)) { notification.PhoneNumber = ticket.Identity.GetClaim(OpenIdConnectConstants.Claims.PhoneNumber); } await Options.Provider.HandleUserinfoRequest(notification); if (notification.HandledResponse) { Logger.LogDebug("The userinfo request was handled in user code."); return(true); } else if (notification.Skipped) { Logger.LogDebug("The default userinfo request handling was skipped from user code."); return(false); } else if (notification.IsRejected) { Logger.LogError("The userinfo request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendUserinfoResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } // Ensure the "sub" claim has been correctly populated. if (string.IsNullOrEmpty(notification.Subject)) { throw new InvalidOperationException("The subject claim cannot be null or empty."); } var response = new OpenIdConnectResponse { [OpenIdConnectConstants.Claims.Subject] = notification.Subject, [OpenIdConnectConstants.Claims.Address] = notification.Address, [OpenIdConnectConstants.Claims.Birthdate] = notification.BirthDate, [OpenIdConnectConstants.Claims.Email] = notification.Email, [OpenIdConnectConstants.Claims.EmailVerified] = notification.EmailVerified, [OpenIdConnectConstants.Claims.FamilyName] = notification.FamilyName, [OpenIdConnectConstants.Claims.GivenName] = notification.GivenName, [OpenIdConnectConstants.Claims.Issuer] = notification.Issuer, [OpenIdConnectConstants.Claims.PhoneNumber] = notification.PhoneNumber, [OpenIdConnectConstants.Claims.PhoneNumberVerified] = notification.PhoneNumberVerified, [OpenIdConnectConstants.Claims.PreferredUsername] = notification.PreferredUsername, [OpenIdConnectConstants.Claims.Profile] = notification.Profile, [OpenIdConnectConstants.Claims.Website] = notification.Website }; switch (notification.Audiences.Count) { case 0: break; case 1: response[OpenIdConnectConstants.Claims.Audience] = notification.Audiences.ElementAt(0); break; default: response[OpenIdConnectConstants.Claims.Audience] = new JArray(notification.Audiences); break; } foreach (var claim in notification.Claims) { response.SetParameter(claim.Key, claim.Value); } return(await SendUserinfoResponseAsync(response)); }