public void IsAccessToken_ReturnsExpectedResult(string usage, bool result) { // Arrange var ticket = new AuthenticationTicket( new ClaimsIdentity(), new AuthenticationProperties()); ticket.Properties.Dictionary[OpenIdConnectConstants.Properties.Usage] = usage; // Act and assert Assert.Equal(result, ticket.IsAccessToken()); }
private async Task <bool> InvokeRevocationEndpointAsync() { if (!string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The revocation request was rejected because an invalid " + "HTTP method was specified: {Method}.", Request.Method); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified HTTP method is not valid." })); } // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrEmpty(Request.ContentType)) { Logger.LogError("The revocation request was rejected because " + "the mandatory 'Content-Type' header was missing."); return(await SendRevocationResponseAsync(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 revocation request was rejected because an invalid 'Content-Type' " + "header was specified: {ContentType}.", Request.ContentType); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "The specified 'Content-Type' header is not valid." })); } var request = new OpenIdConnectRequest(await Request.ReadFormAsync(Context.RequestAborted)); // Note: set the message type before invoking the ExtractRevocationRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.RevocationRequest); // Insert the revocation request in the ASP.NET context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractRevocationRequestContext(Context, Options, request); await Options.Provider.ExtractRevocationRequest(@event); if (@event.HandledResponse) { Logger.LogDebug("The revocation request was handled in user code."); return(true); } else if (@event.Skipped) { Logger.LogDebug("The default revocation request handling was skipped from user code."); return(false); } else if (@event.IsRejected) { Logger.LogError("The revocation request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } Logger.LogInformation("The revocation request was successfully extracted " + "from the HTTP request: {Request}.", request); if (string.IsNullOrEmpty(request.Token)) { return(await SendRevocationResponseAsync(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 revocation request was rejected because " + "multiple client credentials were specified."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "Multiple client credentials cannot be specified." })); } request.ClientId = credentials?.Key; request.ClientSecret = credentials?.Value; } var context = new ValidateRevocationRequestContext(Context, Options, request); await Options.Provider.ValidateRevocationRequest(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 revocation request was handled in user code."); return(true); } else if (context.Skipped) { Logger.LogDebug("The default revocation request handling was skipped from user code."); return(false); } else if (context.IsRejected) { Logger.LogError("The revocation request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendRevocationResponseAsync(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 revocation 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/rfc7009#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/rfc7009#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 revocation request was ignored because the token was invalid."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse())); } // If the ticket is already expired, directly return a 200 response. else if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Logger.LogInformation("The revocation request was ignored because the token was already expired."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse())); } // Note: unlike refresh tokens that can only be revoked by client applications, // access tokens can be revoked 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 revocation request was rejected because the caller was not authenticated."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } // 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 revocation request was rejected because the " + "authorization code was issued to a different client."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } // 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 revocation request was rejected because the access token " + "was issued to a different client or for another resource server."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } // 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 revocation request was rejected because the " + "identity token was issued to a different client."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } // 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 revocation request was rejected because the " + "refresh token was issued to a different client."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } } var notification = new HandleRevocationRequestContext(Context, Options, request, ticket); await Options.Provider.HandleRevocationRequest(notification); if (notification.HandledResponse) { Logger.LogDebug("The revocation request was handled in user code."); return(true); } else if (notification.Skipped) { Logger.LogDebug("The default revocation request handling was skipped from user code."); return(false); } else if (notification.IsRejected) { Logger.LogError("The revocation request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } if (!notification.Revoked) { return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.UnsupportedTokenType, ErrorDescription = "The specified token cannot be revoked." })); } return(await SendRevocationResponseAsync(new OpenIdConnectResponse())); }
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 <AuthenticationTicket> DeserializeAccessTokenAsync(string token, OpenIdConnectRequest request) { var notification = new DeserializeAccessTokenContext(Context, Options, request, token) { DataFormat = Options.AccessTokenFormat, SecurityTokenHandler = Options.AccessTokenHandler }; // Note: ValidateAudience and ValidateLifetime are always set to false: // if necessary, the audience and the expiration can be validated // in InvokeIntrospectionEndpointAsync or InvokeTokenEndpointAsync. notification.TokenValidationParameters = new TokenValidationParameters { IssuerSigningKeys = Options.SigningCredentials.Select(credentials => credentials.Key), NameClaimType = OpenIdConnectConstants.Claims.Name, RoleClaimType = OpenIdConnectConstants.Claims.Role, ValidIssuer = Context.GetIssuer(Options), ValidateAudience = false, ValidateLifetime = false }; await Options.Provider.DeserializeAccessToken(notification); if (notification.HandledResponse || notification.Ticket != null) { notification.Ticket.SetUsage(OpenIdConnectConstants.Usages.AccessToken); return(notification.Ticket); } else if (notification.Skipped) { return(null); } var handler = notification.SecurityTokenHandler as ISecurityTokenValidator; if (handler == null) { return(notification.DataFormat?.Unprotect(token)); } SecurityToken securityToken; ClaimsPrincipal principal; try { if (!handler.CanReadToken(token)) { Logger.LogDebug("The access token handler refused to read the token: {Token}", token); return(null); } principal = handler.ValidateToken(token, notification.TokenValidationParameters, out securityToken); } catch (Exception exception) { Logger.LogDebug("An exception occured when deserializing an identity token: {Message}.", exception.Message); return(null); } // Parameters stored in AuthenticationProperties are lost // when the identity token is serialized using a security token handler. // To mitigate that, they are inferred from the claims or the security token. var properties = new AuthenticationProperties { ExpiresUtc = securityToken.ValidTo, IssuedUtc = securityToken.ValidFrom }; var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); var audiences = principal.FindAll(OpenIdConnectConstants.Claims.Audience); if (audiences.Any()) { ticket.SetAudiences(audiences.Select(claim => claim.Value)); } var presenters = principal.FindAll(OpenIdConnectConstants.Claims.AuthorizedParty); if (presenters.Any()) { ticket.SetPresenters(presenters.Select(claim => claim.Value)); } var scopes = principal.FindAll(OpenIdConnectConstants.Claims.Scope); if (scopes.Any()) { ticket.SetScopes(scopes.Select(claim => claim.Value)); } var identifier = principal.FindFirst(OpenIdConnectConstants.Claims.JwtId); if (identifier != null) { ticket.SetTicketId(identifier.Value); } var usage = principal.FindFirst(OpenIdConnectConstants.Claims.Usage); if (usage != null) { ticket.SetUsage(usage.Value); } var confidentiality = principal.FindFirst(OpenIdConnectConstants.Claims.ConfidentialityLevel); if (confidentiality != null) { ticket.SetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel, confidentiality.Value); } // Ensure that e received ticket is an access token. if (!ticket.IsAccessToken()) { Logger.LogDebug("The received token was not an access token: {Token}.", token); return(null); } return(ticket); }
private async Task <bool> InvokeRevocationEndpointAsync() { if (!string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { Logger.LogError("The revocation request was rejected because an invalid " + "HTTP method was received: {Method}.", Request.Method); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed revocation request has been received: " + "make sure to use either GET or POST." })); } // See http://openid.net/specs/openid-connect-core-1_0.html#FormSerialization if (string.IsNullOrEmpty(Request.ContentType)) { Logger.LogError("The revocation request was rejected because " + "the mandatory 'Content-Type' header was missing."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed revocation 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 revocation request was rejected because an invalid 'Content-Type' " + "header was received: {ContentType}.", Request.ContentType); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed revocation request has been received: " + "the 'Content-Type' header contained an unexcepted value. " + "Make sure to use 'application/x-www-form-urlencoded'." })); } var request = new OpenIdConnectRequest(await Request.ReadFormAsync(Context.RequestAborted)); // Note: set the message type before invoking the ExtractRevocationRequest event. request.SetProperty(OpenIdConnectConstants.Properties.MessageType, OpenIdConnectConstants.MessageTypes.Revocation); // Insert the revocation request in the ASP.NET context. Context.SetOpenIdConnectRequest(request); var @event = new ExtractRevocationRequestContext(Context, Options, request); await Options.Provider.ExtractRevocationRequest(@event); if (@event.HandledResponse) { return(true); } else if (@event.Skipped) { return(false); } else if (@event.IsRejected) { Logger.LogError("The revocation request was rejected with the following error: {Error} ; {Description}", /* Error: */ @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ @event.ErrorDescription); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = @event.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = @event.ErrorDescription, ErrorUri = @event.ErrorUri })); } if (string.IsNullOrWhiteSpace(request.Token)) { return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed revocation request has been received: " + "a 'token' parameter with an access or refresh token is required." })); } // When client_id and client_secret are both null, try to extract them from the Authorization header. // See http://tools.ietf.org/html/rfc6749#section-2.3.1 and // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication if (string.IsNullOrEmpty(request.ClientId) && string.IsNullOrEmpty(request.ClientSecret)) { string header = Request.Headers[HeaderNames.Authorization]; if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { try { var value = header.Substring("Basic ".Length).Trim(); var data = Encoding.UTF8.GetString(Convert.FromBase64String(value)); var index = data.IndexOf(':'); if (index >= 0) { request.ClientId = data.Substring(0, index); request.ClientSecret = data.Substring(index + 1); } } catch (FormatException) { } catch (ArgumentException) { } } } var context = new ValidateRevocationRequestContext(Context, Options, request); await Options.Provider.ValidateRevocationRequest(context); // If the validation context was set as fully validated, // mark the OpenID Connect request as confidential. if (context.IsValidated) { request.SetProperty(OpenIdConnectConstants.Properties.ConfidentialityLevel, OpenIdConnectConstants.ConfidentialityLevels.Private); } if (context.HandledResponse) { return(true); } else if (context.Skipped) { return(false); } else if (context.IsRejected) { Logger.LogError("The revocation request was rejected with the following error: {Error} ; {Description}", /* Error: */ context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ context.ErrorDescription); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } // Ensure that the client_id has been set from the ValidateRevocationRequest event. else if (context.IsValidated && string.IsNullOrEmpty(request.ClientId)) { Logger.LogError("The revocation request was validated but the client_id was not set."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." })); } 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/rfc7009#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/rfc7009#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 revocation request was ignored because the token was invalid."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse())); } // If the ticket is already expired, directly return a 200 response. else if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Logger.LogInformation("The revocation request was ignored because the token was already expired."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse())); } // Note: unlike refresh tokens that can only be revoked by client applications, // access tokens can be revoked 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 revocation request was rejected because the caller was not authenticated."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } // When a client_id can be inferred from the introspection request, // ensure that the client application is a valid audience/presenter. if (!string.IsNullOrEmpty(request.ClientId)) { if (ticket.IsAuthorizationCode() && ticket.HasPresenter() && !ticket.HasPresenter(request.ClientId)) { Logger.LogError("The revocation request was rejected because the " + "authorization code was issued to a different client."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } // Ensure the caller is listed as a valid audience or authorized presenter. else if (ticket.IsAccessToken() && ticket.HasAudience() && !ticket.HasAudience(request.ClientId) && ticket.HasPresenter() && !ticket.HasPresenter(request.ClientId)) { Logger.LogError("The revocation request was rejected because the access token " + "was issued to a different client or for another resource server."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } // Reject the request if the caller is not listed as a valid audience. else if (ticket.IsIdentityToken() && ticket.HasAudience() && !ticket.HasAudience(request.ClientId)) { Logger.LogError("The revocation request was rejected because the " + "identity token was issued to a different client."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } // 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(request.ClientId)) { Logger.LogError("The revocation request was rejected because the " + "refresh token was issued to a different client."); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidRequest })); } } var notification = new HandleRevocationRequestContext(Context, Options, request, ticket); await Options.Provider.HandleRevocationRequest(notification); if (notification.HandledResponse) { return(true); } else if (notification.Skipped) { return(false); } else if (notification.IsRejected) { Logger.LogError("The revocation request was rejected with the following error: {Error} ; {Description}", /* Error: */ notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, /* Description: */ notification.ErrorDescription); return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = notification.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = notification.ErrorDescription, ErrorUri = notification.ErrorUri })); } if (!notification.Revoked) { return(await SendRevocationResponseAsync(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.UnsupportedTokenType, ErrorDescription = "The token cannot be revoked." })); } return(await SendRevocationResponseAsync(new OpenIdConnectResponse())); }
private async Task <bool> InvokeIntrospectionEndpointAsync() { OpenIdConnectMessage 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 OpenIdConnectMessage(Request.Query) { RequestType = OpenIdConnectRequestType.AuthenticationRequest }; } 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)) { Options.Logger.LogError("The introspection request was rejected because " + "the mandatory 'Content-Type' header was missing."); return(await SendIntrospectionResponseAsync(null, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed introspection request has been received: " + "the mandatory 'Content-Type' header was missing from the POST request." })); } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { Options.Logger.LogError("The introspection request was rejected because an invalid 'Content-Type' " + "header was received: {ContentType}.", Request.ContentType); return(await SendIntrospectionResponseAsync(null, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed introspection request has been received: " + "the 'Content-Type' header contained an unexcepted value. " + "Make sure to use 'application/x-www-form-urlencoded'." })); } request = new OpenIdConnectMessage(await Request.ReadFormAsync()) { RequestType = OpenIdConnectRequestType.AuthenticationRequest }; } else { Options.Logger.LogError("The introspection request was rejected because an invalid " + "HTTP method was received: {Method}.", Request.Method); return(await SendIntrospectionResponseAsync(null, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed introspection request has been received: " + "make sure to use either GET or POST." })); } if (string.IsNullOrWhiteSpace(request.Token)) { return(await SendIntrospectionResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed introspection request has been received: " + "a 'token' parameter with an access, refresh, or identity token is required." })); } // Insert the introspection request in the OWIN context. Context.SetOpenIdConnectRequest(request); // When client_id and client_secret are both null, try to extract them from the Authorization header. // See http://tools.ietf.org/html/rfc6749#section-2.3.1 and // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication if (string.IsNullOrEmpty(request.ClientId) && string.IsNullOrEmpty(request.ClientSecret)) { var header = Request.Headers.Get("Authorization"); if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { try { var value = header.Substring("Basic ".Length).Trim(); var data = Encoding.UTF8.GetString(Convert.FromBase64String(value)); var index = data.IndexOf(':'); if (index >= 0) { request.ClientId = data.Substring(0, index); request.ClientSecret = data.Substring(index + 1); } } catch (FormatException) { } catch (ArgumentException) { } } } var context = new ValidateIntrospectionRequestContext(Context, Options, request); await Options.Provider.ValidateIntrospectionRequest(context); if (context.IsRejected) { Options.Logger.LogInformation("The introspection request was rejected by application code."); return(await SendIntrospectionResponseAsync(request, new OpenIdConnectMessage { Error = context.Error ?? OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = context.ErrorDescription, ErrorUri = context.ErrorUri })); } // Ensure that the client_id has been set from the ValidateIntrospectionRequest event. else if (context.IsValidated && string.IsNullOrEmpty(request.ClientId)) { Options.Logger.LogError("The introspection request was validated but the client_id was not set."); return(await SendIntrospectionResponseAsync(request, new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." })); } 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.GetTokenTypeHint()) { case OpenIdConnectConstants.Usages.AccessToken: ticket = await DeserializeAccessTokenAsync(request.Token, request); break; case OpenIdConnectConstants.Usages.RefreshToken: ticket = await DeserializeRefreshTokenAsync(request.Token, request); break; case OpenIdConnectConstants.Usages.IdToken: ticket = await DeserializeIdentityTokenAsync(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 DeserializeIdentityTokenAsync(request.Token, request) ?? await DeserializeRefreshTokenAsync(request.Token, request); } if (ticket == null) { Options.Logger.LogInformation("The introspection request was rejected because the token was invalid."); return(await SendIntrospectionResponseAsync(request, new JObject { [OpenIdConnectConstants.Claims.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()) { Options.Logger.LogWarning("The introspection request was rejected because the caller was not authenticated."); return(await SendIntrospectionResponseAsync(request, new JObject { [OpenIdConnectConstants.Claims.Active] = false })); } // If the ticket is already expired, directly return active=false. if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Options.Logger.LogInformation("The introspection request was rejected because the token was expired."); return(await SendIntrospectionResponseAsync(request, new JObject { [OpenIdConnectConstants.Claims.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(request.ClientId)) { // Ensure the caller is listed as a valid audience or authorized presenter. if (ticket.IsAccessToken() && ticket.HasAudience() && !ticket.HasAudience(request.ClientId) && ticket.HasPresenter() && !ticket.HasPresenter(request.ClientId)) { Options.Logger.LogWarning("The introspection request was rejected because the access token " + "was issued to a different client or for another resource server."); return(await SendIntrospectionResponseAsync(request, new JObject { [OpenIdConnectConstants.Claims.Active] = false })); } // Reject the request if the caller is not listed as a valid audience. else if (ticket.IsIdentityToken() && ticket.HasAudience() && !ticket.HasAudience(request.ClientId)) { Options.Logger.LogWarning("The introspection request was rejected because the " + "identity token was issued to a different client."); return(await SendIntrospectionResponseAsync(request, new JObject { [OpenIdConnectConstants.Claims.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(request.ClientId)) { Options.Logger.LogWarning("The introspection request was rejected because the " + "refresh token was issued to a different client."); return(await SendIntrospectionResponseAsync(request, new JObject { [OpenIdConnectConstants.Claims.Active] = false })); } } var notification = new HandleIntrospectionRequestContext(Context, Options, request, ticket); notification.Active = true; // Use the unique ticket identifier to populate the "jti" claim. notification.TokenId = ticket.GetTicketId(); // 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.Issuer = Context.GetIssuer(Options); notification.Subject = ticket.Identity.GetClaim(ClaimTypes.NameIdentifier); notification.IssuedAt = ticket.Properties.IssuedUtc; notification.ExpiresAt = ticket.Properties.ExpiresUtc; // Copy the audiences extracted from the "aud" claim. foreach (var audience in ticket.GetAudiences()) { notification.Audiences.Add(audience); } // Note: non-metadata claims are only added if the caller is authenticated // AND is in the specified audiences, unless there's so explicit audience. if (!ticket.HasAudience() || (!string.IsNullOrEmpty(request.ClientId) && ticket.HasAudience(request.ClientId))) { notification.Username = ticket.Identity.Name; notification.Scope = ticket.GetProperty(OpenIdConnectConstants.Properties.Scopes); // Potentially sensitive claims are only exposed to trusted callers // if the ticket corresponds to an access or identity token. if (ticket.IsAccessToken() || ticket.IsIdentityToken()) { foreach (var claim in ticket.Identity.Claims) { // 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. if (string.Equals(claim.Type, ticket.Identity.NameClaimType, StringComparison.Ordinal) || string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal)) { continue; } if (string.Equals(claim.Type, OpenIdConnectConstants.Claims.Audience, StringComparison.Ordinal) || string.Equals(claim.Type, OpenIdConnectConstants.Claims.ExpiresAt, StringComparison.Ordinal) || string.Equals(claim.Type, OpenIdConnectConstants.Claims.IssuedAt, StringComparison.Ordinal) || string.Equals(claim.Type, OpenIdConnectConstants.Claims.Issuer, StringComparison.Ordinal) || string.Equals(claim.Type, OpenIdConnectConstants.Claims.NotBefore, StringComparison.Ordinal) || string.Equals(claim.Type, OpenIdConnectConstants.Claims.Scope, StringComparison.Ordinal) || string.Equals(claim.Type, OpenIdConnectConstants.Claims.Subject, StringComparison.Ordinal) || string.Equals(claim.Type, OpenIdConnectConstants.Claims.TokenType, StringComparison.Ordinal)) { continue; } string type; // Try to resolve the short name associated with the claim type: // if none can be found, the claim type is used as-is. if (!JwtSecurityTokenHandler.OutboundClaimTypeMap.TryGetValue(claim.Type, out type)) { type = claim.Type; } // Note: make sure to use the indexer // syntax to avoid duplicate properties. notification.Claims[type] = claim.Value; } } } await Options.Provider.HandleIntrospectionRequest(notification); if (notification.HandledResponse) { return(true); } else if (notification.Skipped) { return(false); } var response = new JObject(); response.Add(OpenIdConnectConstants.Claims.Active, notification.Active); // Only add the other properties if // the token is considered as active. if (notification.Active) { if (!string.IsNullOrEmpty(notification.Issuer)) { response.Add(OpenIdConnectConstants.Claims.Issuer, notification.Issuer); } if (!string.IsNullOrEmpty(notification.Username)) { response.Add(OpenIdConnectConstants.Claims.Username, notification.Username); } if (!string.IsNullOrEmpty(notification.Subject)) { response.Add(OpenIdConnectConstants.Claims.Subject, notification.Subject); } if (!string.IsNullOrEmpty(notification.Scope)) { response.Add(OpenIdConnectConstants.Claims.Scope, notification.Scope); } if (notification.IssuedAt.HasValue) { response.Add(OpenIdConnectConstants.Claims.IssuedAt, EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime)); response.Add(OpenIdConnectConstants.Claims.NotBefore, EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime)); } if (notification.ExpiresAt.HasValue) { response.Add(OpenIdConnectConstants.Claims.ExpiresAt, EpochTime.GetIntDate(notification.ExpiresAt.Value.UtcDateTime)); } if (!string.IsNullOrEmpty(notification.TokenId)) { response.Add(OpenIdConnectConstants.Claims.JwtId, notification.TokenId); } if (!string.IsNullOrEmpty(notification.TokenType)) { response.Add(OpenIdConnectConstants.Claims.TokenType, notification.TokenType); } switch (notification.Audiences.Count) { case 0: break; case 1: response.Add(OpenIdConnectConstants.Claims.Audience, notification.Audiences[0]); break; default: response.Add(OpenIdConnectConstants.Claims.Audience, JArray.FromObject(notification.Audiences)); break; } foreach (var claim in notification.Claims) { // Ignore claims whose value is null. if (claim.Value == null) { continue; } // Note: make sure to use the indexer // syntax to avoid duplicate properties. response[claim.Key] = claim.Value; } } return(await SendIntrospectionResponseAsync(request, response)); }
private async Task <AuthenticationTicket> DeserializeAccessTokenAsync(string token, OpenIdConnectRequest request) { var notification = new DeserializeAccessTokenContext(Context, Scheme, Options, request, token) { DataFormat = Options.AccessTokenFormat, SecurityTokenHandler = Options.AccessTokenHandler }; // Note: ValidateAudience and ValidateLifetime are always set to false: // if necessary, the audience and the expiration can be validated // in InvokeIntrospectionEndpointAsync or InvokeTokenEndpointAsync. notification.TokenValidationParameters = new TokenValidationParameters { IssuerSigningKeys = Options.SigningCredentials.Select(credentials => credentials.Key), NameClaimType = OpenIdConnectConstants.Claims.Name, RoleClaimType = OpenIdConnectConstants.Claims.Role, TokenDecryptionKeys = Options.EncryptingCredentials.Select(credentials => credentials.Key) .Where(key => key is SymmetricSecurityKey), ValidIssuer = Context.GetIssuer(Options), ValidateAudience = false, ValidateLifetime = false }; await Provider.DeserializeAccessToken(notification); if (notification.IsHandled || notification.Ticket != null) { notification.Ticket?.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); return(notification.Ticket); } var handler = notification.SecurityTokenHandler as ISecurityTokenValidator; if (handler == null) { if (notification.DataFormat == null) { throw new InvalidOperationException("A security token handler or data formatter must be provided."); } var value = notification.DataFormat.Unprotect(token); if (value == null) { Logger.LogTrace("The received token was invalid or malformed: {Token}.", token); return(null); } // Note: since the data formatter relies on a data protector using different "purposes" strings // per token type, the ticket returned by Unprotect() is guaranteed to be an access token. value.SetTokenUsage(OpenIdConnectConstants.TokenUsages.AccessToken); Logger.LogTrace("The access token '{Token}' was successfully validated using " + "the specified token data format: {Claims} ; {Properties}.", token, value.Principal.Claims, value.Properties.Items); return(value); } SecurityToken securityToken; ClaimsPrincipal principal; try { if (!handler.CanReadToken(token)) { Logger.LogTrace("The access token '{Token}' was rejected by the security token handler.", token); return(null); } principal = handler.ValidateToken(token, notification.TokenValidationParameters, out securityToken); } catch (Exception exception) { Logger.LogDebug("An exception occured while deserializing an identity token: {Exception}.", exception); return(null); } // Parameters stored in AuthenticationProperties are lost // when the identity token is serialized using a security token handler. // To mitigate that, they are inferred from the claims or the security token. var properties = new AuthenticationProperties { ExpiresUtc = securityToken.ValidTo, IssuedUtc = securityToken.ValidFrom }; var ticket = new AuthenticationTicket(principal, properties, Scheme.Name) .SetAudiences(principal.FindAll(OpenIdConnectConstants.Claims.Audience).Select(claim => claim.Value)) .SetConfidentialityLevel(principal.GetClaim(OpenIdConnectConstants.Claims.ConfidentialityLevel)) .SetPresenters(principal.FindAll(OpenIdConnectConstants.Claims.AuthorizedParty).Select(claim => claim.Value)) .SetScopes(principal.FindAll(OpenIdConnectConstants.Claims.Scope).Select(claim => claim.Value)) .SetTokenId(principal.GetClaim(OpenIdConnectConstants.Claims.JwtId)) .SetTokenUsage(principal.GetClaim(OpenIdConnectConstants.Claims.TokenUsage)); // Ensure that the received ticket is an access token. if (!ticket.IsAccessToken()) { Logger.LogTrace("The received token was not an access token: {Token}.", token); return(null); } Logger.LogTrace("The access token '{Token}' was successfully validated using " + "the specified security token handler: {Claims} ; {Properties}.", token, ticket.Principal.Claims, ticket.Properties.Items); return(ticket); }
private async Task <AuthenticationTicket> DeserializeAccessTokenAsync(string token, OpenIdConnectMessage request) { var notification = new DeserializeAccessTokenContext(Context, Options, request, token) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SignatureProvider = Options.SignatureProvider, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.DeserializeAccessToken(notification); // Directly return the authentication ticket if one // has been provided by DeserializeAccessToken. if (notification.AuthenticationTicket != null) { return(notification.AuthenticationTicket); } var handler = notification.SecurityTokenHandler as ISecurityTokenValidator; if (handler == null) { return(notification.DataFormat?.Unprotect(token)); } // Create new validation parameters to validate the security token. // ValidateAudience and ValidateLifetime are always set to false: // if necessary, the audience and the expiration can be validated // in InvokeValidationEndpointAsync or InvokeTokenEndpointAsync. var parameters = new TokenValidationParameters { IssuerSigningKey = notification.SigningCredentials.Key, ValidIssuer = notification.Issuer, ValidateAudience = false, ValidateLifetime = false }; SecurityToken securityToken; ClaimsPrincipal principal; try { principal = handler.ValidateToken(token, parameters, out securityToken); } catch (Exception exception) { Logger.LogVerbose("An exception occured when deserializing an identity token: {Message}.", exception.Message); return(null); } // Parameters stored in AuthenticationProperties are lost // when the identity token is serialized using a security token handler. // To mitigate that, they are inferred from the claims or the security token. var properties = new AuthenticationProperties { ExpiresUtc = securityToken.ValidTo, IssuedUtc = securityToken.ValidFrom }; var audiences = principal.FindAll(JwtRegisteredClaimNames.Aud); if (audiences.Any()) { properties.SetAudiences(audiences.Select(claim => claim.Value)); } var usage = principal.FindFirst(OpenIdConnectConstants.Extra.Usage); if (usage != null) { properties.SetUsage(usage.Value); } if (principal.Claims.Any(claim => claim.Type == OpenIdConnectConstants.Extra.Confidential)) { properties.Items[OpenIdConnectConstants.Extra.Confidential] = "true"; } // Ensure the received ticket is an access token. var ticket = new AuthenticationTicket(principal, properties, Options.AuthenticationScheme); if (!ticket.IsAccessToken()) { Logger.LogVerbose("The received token was not an access token: {Token}.", token); return(null); } return(ticket); }
private async Task <AuthenticationTicket> DeserializeAccessTokenAsync(string token, OpenIdConnectMessage request) { var notification = new DeserializeAccessTokenContext(Context, Options, request, token) { DataFormat = Options.AccessTokenFormat, Issuer = Context.GetIssuer(Options), SecurityTokenHandler = Options.AccessTokenHandler, SigningCredentials = Options.SigningCredentials.FirstOrDefault() }; await Options.Provider.DeserializeAccessToken(notification); // Directly return the authentication ticket if one // has been provided by DeserializeAccessToken. if (notification.Ticket != null) { return(notification.Ticket); } var handler = notification.SecurityTokenHandler as ISecurityTokenValidator; if (handler == null) { return(notification.DataFormat?.Unprotect(token)); } // Create new validation parameters to validate the security token. // ValidateAudience and ValidateLifetime are always set to false: // if necessary, the audience and the expiration can be validated // in InvokeIntrospectionEndpointAsync or InvokeTokenEndpointAsync. var parameters = new TokenValidationParameters { IssuerSigningKey = notification.SigningCredentials.SigningKey, ValidIssuer = notification.Issuer, ValidateAudience = false, ValidateLifetime = false }; SecurityToken securityToken; ClaimsPrincipal principal; try { principal = handler.ValidateToken(token, parameters, out securityToken); } catch (Exception exception) { Options.Logger.LogInformation("An exception occured when deserializing an access token: {Message}", exception.Message); return(null); } // Parameters stored in AuthenticationProperties are lost // when the identity token is serialized using a security token handler. // To mitigate that, they are inferred from the claims or the security token. var properties = new AuthenticationProperties { ExpiresUtc = securityToken.ValidTo, IssuedUtc = securityToken.ValidFrom }; var ticket = new AuthenticationTicket((ClaimsIdentity)principal.Identity, properties); var audiences = principal.FindAll(OpenIdConnectConstants.Claims.Audience); if (audiences.Any()) { ticket.SetAudiences(audiences.Select(claim => claim.Value)); } var presenters = principal.FindAll(OpenIdConnectConstants.Claims.AuthorizedParty); if (presenters.Any()) { ticket.SetPresenters(presenters.Select(claim => claim.Value)); } var scopes = principal.FindAll(OpenIdConnectConstants.Claims.Scope); if (scopes.Any()) { ticket.SetScopes(scopes.Select(claim => claim.Value)); } // Note: the token identifier may be stored in either the token_id claim // or in the jti claim, which is the standard name used by JWT tokens. var identifier = principal.FindFirst(OpenIdConnectConstants.Claims.JwtId) ?? principal.FindFirst(OpenIdConnectConstants.Claims.TokenId); if (identifier != null) { ticket.SetTicketId(identifier.Value); } var usage = principal.FindFirst(OpenIdConnectConstants.Claims.Usage); if (usage != null) { ticket.SetUsage(usage.Value); } var confidential = principal.FindFirst(OpenIdConnectConstants.Claims.Confidential); if (confidential != null && string.Equals(confidential.Value, "true", StringComparison.OrdinalIgnoreCase)) { ticket.Properties.Dictionary[OpenIdConnectConstants.Properties.Confidential] = "true"; } // Ensure the received ticket is an access token. if (!ticket.IsAccessToken()) { Options.Logger.LogWarning("The received token was not an access token: {Token}.", token); return(null); } return(ticket); }
private async Task InvokeValidationEndpointAsync() { OpenIdConnectMessage 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 OpenIdConnectMessage(Request.Query.ToDictionary()) { RequestType = OpenIdConnectRequestType.AuthenticationRequest }; } 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)) { await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed validation request has been received: " + "the mandatory 'Content-Type' header was missing from the POST request." }); return; } // May have media/type; charset=utf-8, allow partial match. if (!Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) { await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed validation request has been received: " + "the 'Content-Type' header contained an unexcepted value. " + "Make sure to use 'application/x-www-form-urlencoded'." }); return; } var form = await Request.ReadFormAsync(Context.RequestAborted); request = new OpenIdConnectMessage(form.ToDictionary()) { RequestType = OpenIdConnectRequestType.AuthenticationRequest }; } else { Logger.LogInformation("A malformed request has been received by the validation endpoint."); await SendErrorPageAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed validation request has been received: " + "make sure to use either GET or POST." }); return; } if (string.IsNullOrWhiteSpace(request.GetToken())) { await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidRequest, ErrorDescription = "A malformed validation request has been received: " + "a 'token' parameter with an access, refresh, or identity token is required." }); return; } // When client_id and client_secret are both null, try to extract them from the Authorization header. // See http://tools.ietf.org/html/rfc6749#section-2.3.1 and // http://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication if (string.IsNullOrEmpty(request.ClientId) && string.IsNullOrEmpty(request.ClientSecret)) { string header = Request.Headers[HeaderNames.Authorization]; if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase)) { try { var value = header.Substring("Basic ".Length).Trim(); var data = Encoding.UTF8.GetString(Convert.FromBase64String(value)); var index = data.IndexOf(':'); if (index >= 0) { request.ClientId = data.Substring(0, index); request.ClientSecret = data.Substring(index + 1); } } catch (FormatException) { } catch (ArgumentException) { } } } var clientNotification = new ValidateClientAuthenticationContext(Context, Options, request); await Options.Provider.ValidateClientAuthentication(clientNotification); // Reject the request if client authentication was rejected. if (clientNotification.IsRejected) { Logger.LogError("The validation request was rejected " + "because client authentication was invalid."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } // Ensure that the client_id has been set from the ValidateClientAuthentication event. else if (clientNotification.IsValidated && string.IsNullOrEmpty(request.ClientId)) { Logger.LogError("Client authentication was validated but the client_id was not set."); await SendErrorPayloadAsync(new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, ErrorDescription = "An internal server error occurred." }); return; } 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.GetTokenTypeHint()) { case OpenIdConnectConstants.Usages.AccessToken: ticket = await DeserializeAccessTokenAsync(request.GetToken(), request); break; case OpenIdConnectConstants.Usages.RefreshToken: ticket = await DeserializeRefreshTokenAsync(request.GetToken(), request); break; case OpenIdConnectConstants.Usages.IdToken: ticket = await DeserializeIdentityTokenAsync(request.GetToken(), 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.GetToken(), request) ?? await DeserializeIdentityTokenAsync(request.GetToken(), request) ?? await DeserializeRefreshTokenAsync(request.GetToken(), request); } if (ticket == null) { Logger.LogInformation("The validation request was rejected because the token was invalid."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } // 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 (clientNotification.IsSkipped && ticket.IsConfidential()) { Logger.LogWarning("The validation request was rejected " + "because the caller was not authenticated."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } // If the ticket is already expired, directly return active=false. if (ticket.Properties.ExpiresUtc.HasValue && ticket.Properties.ExpiresUtc < Options.SystemClock.UtcNow) { Logger.LogDebug("expired token"); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } switch (ticket.GetUsage()) { case OpenIdConnectConstants.Usages.AccessToken: { // When the caller is authenticated, ensure it is // listed as a valid audience or authorized presenter. if (clientNotification.IsValidated && !ticket.HasAudience(clientNotification.ClientId) && !ticket.HasPresenter(clientNotification.ClientId)) { Logger.LogWarning("The validation request was rejected because the access token " + "was issued to a different client or for another resource server."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } break; } case OpenIdConnectConstants.Usages.IdToken: { // When the caller is authenticated, reject the validation // request if the caller is not listed as a valid audience. if (clientNotification.IsValidated && !ticket.HasAudience(clientNotification.ClientId)) { Logger.LogWarning("The validation request was rejected because the " + "identity token was issued to a different client."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } break; } case OpenIdConnectConstants.Usages.RefreshToken: { // When the caller is authenticated, reject the validation request if the caller // doesn't correspond to the client application the token was issued to. if (clientNotification.IsValidated && !ticket.HasPresenter(clientNotification.ClientId)) { Logger.LogWarning("The validation request was rejected because the " + "refresh token was issued to a different client."); await SendPayloadAsync(new JObject { [OpenIdConnectConstants.Claims.Active] = false }); return; } break; } } // Insert the validation request in the ASP.NET context. Context.SetOpenIdConnectRequest(request); var notification = new ValidationEndpointContext(Context, Options, request, ticket); notification.Active = true; // Note: "token_type" may be null when the received token is not an access token. // See https://tools.ietf.org/html/rfc7662#section-2.2 and https://tools.ietf.org/html/rfc6749#section-5.1 notification.TokenType = ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.TokenType); notification.Issuer = Context.GetIssuer(Options); notification.Subject = ticket.Principal.GetClaim(ClaimTypes.NameIdentifier); notification.IssuedAt = ticket.Properties.IssuedUtc; notification.ExpiresAt = ticket.Properties.ExpiresUtc; // Copy the audiences extracted from the "aud" claim. foreach (var audience in ticket.GetAudiences()) { notification.Audiences.Add(audience); } // Note: non-metadata claims are only added if the caller is authenticated AND is in the specified audiences. if (clientNotification.IsValidated && notification.Audiences.Contains(clientNotification.ClientId)) { // Extract the main identity associated with the principal. var identity = (ClaimsIdentity)ticket.Principal.Identity; notification.Username = identity.Name; notification.Scope = ticket.GetProperty(OpenIdConnectConstants.Properties.Scopes); // Potentially sensitive claims are only exposed to trusted callers // if the ticket corresponds to an access or identity token. if (ticket.IsAccessToken() || ticket.IsIdentityToken()) { foreach (var claim in ticket.Principal.Claims) { // 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. if (string.Equals(claim.Type, identity.NameClaimType, StringComparison.Ordinal) || string.Equals(claim.Type, ClaimTypes.NameIdentifier, StringComparison.Ordinal)) { continue; } if (string.Equals(claim.Type, JwtRegisteredClaimNames.Aud, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Exp, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Iat, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Iss, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Nbf, StringComparison.Ordinal) || string.Equals(claim.Type, JwtRegisteredClaimNames.Sub, StringComparison.Ordinal)) { continue; } if (string.Equals(claim.Type, OpenIdConnectConstants.Claims.TokenType, StringComparison.Ordinal) || string.Equals(claim.Type, OpenIdConnectConstants.Claims.Scope, StringComparison.Ordinal)) { continue; } string type; // Try to resolve the short name associated with the claim type: // if none can be found, the claim type is used as-is. if (!JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.TryGetValue(claim.Type, out type)) { type = claim.Type; } // Note: make sure to use the indexer // syntax to avoid duplicate properties. notification.Claims[type] = claim.Value; } } } await Options.Provider.ValidationEndpoint(notification); // Flow the changes made to the authentication ticket. ticket = notification.AuthenticationTicket; if (notification.HandledResponse) { return; } var payload = new JObject(); payload.Add(OpenIdConnectConstants.Claims.Active, notification.Active); // Only add the other properties if // the token is considered as active. if (notification.Active) { if (!string.IsNullOrEmpty(notification.Issuer)) { payload.Add(JwtRegisteredClaimNames.Iss, notification.Issuer); } if (!string.IsNullOrEmpty(notification.Username)) { payload.Add(OpenIdConnectConstants.Claims.Username, notification.Username); } if (!string.IsNullOrEmpty(notification.Subject)) { payload.Add(JwtRegisteredClaimNames.Sub, notification.Subject); } if (!string.IsNullOrEmpty(notification.Scope)) { payload.Add(OpenIdConnectConstants.Claims.Scope, notification.Scope); } if (notification.IssuedAt.HasValue) { payload.Add(JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime)); payload.Add(JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(notification.IssuedAt.Value.UtcDateTime)); } if (notification.ExpiresAt.HasValue) { payload.Add(JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(notification.ExpiresAt.Value.UtcDateTime)); } if (!string.IsNullOrEmpty(notification.TokenType)) { payload.Add(OpenIdConnectConstants.Claims.TokenType, notification.TokenType); } switch (notification.Audiences.Count) { case 0: break; case 1: payload.Add(JwtRegisteredClaimNames.Aud, notification.Audiences[0]); break; default: payload.Add(JwtRegisteredClaimNames.Aud, JArray.FromObject(notification.Audiences)); break; } foreach (var claim in notification.Claims) { // Ignore claims whose value is null. if (claim.Value == null) { continue; } // Note: make sure to use the indexer // syntax to avoid duplicate properties. payload[claim.Key] = claim.Value; } } var context = new ValidationEndpointResponseContext(Context, Options, payload); await Options.Provider.ValidationEndpointResponse(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"; Response.Headers[HeaderNames.CacheControl] = "no-cache"; Response.Headers[HeaderNames.Pragma] = "no-cache"; Response.Headers[HeaderNames.Expires] = "-1"; buffer.Seek(offset: 0, loc: SeekOrigin.Begin); await buffer.CopyToAsync(Response.Body, 4096, Context.RequestAborted); } }