Browse Source

Implement automatic authorization code revocation

pull/147/head
Kévin Chalet 10 years ago
parent
commit
154fa490b7
  1. 24
      src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs
  2. 9
      src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs
  3. 33
      src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs
  4. 37
      src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs
  5. 21
      src/OpenIddict.Mvc/OpenIddictExtensions.cs

24
src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs

@ -138,7 +138,7 @@ namespace OpenIddict.Infrastructure {
// stored in the authorization code to create a new identity. To ensure the user was not removed
// after the authorization code was issued, a new check is made before validating the request.
if (context.Request.IsAuthorizationCodeGrantType()) {
Debug.Assert(context.Ticket.Principal != null, "The authentication ticket shouldn't be null.");
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
var user = await services.Users.GetUserAsync(context.Ticket.Principal);
if (user == null) {
@ -153,6 +153,26 @@ namespace OpenIddict.Infrastructure {
return;
}
// Extract the token identifier from the authorization code.
var identifier = context.Ticket.GetTicketId();
Debug.Assert(!string.IsNullOrEmpty(identifier),
"The authorization code should contain a ticket identifier.");
// Retrieve the token from the database and ensure it is still valid.
var token = await services.Tokens.FindByIdAsync(identifier);
if (token == null) {
services.Logger.LogError("The token request was rejected because the authorization code was revoked.");
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "The authorization code is no longer valid.");
return;
}
// Revoke the authorization code to prevent token reuse.
await services.Tokens.RevokeAsync(token);
context.Validate(context.Ticket);
}
@ -160,7 +180,7 @@ namespace OpenIddict.Infrastructure {
// stored in the refresh token to create a new identity. To ensure the user was not removed
// after the refresh token was issued, a new check is made before validating the request.
else if (context.Request.IsRefreshTokenGrantType()) {
Debug.Assert(context.Ticket.Principal != null, "The authentication ticket shouldn't be null.");
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
var user = await services.Users.GetUserAsync(context.Ticket.Principal);
if (user == null) {

9
src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs

@ -85,6 +85,7 @@ namespace OpenIddict.Infrastructure {
public override async Task HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client_id parameter shouldn't be null.");
// Note: the OpenID Connect server middleware allows authorized presenters (e.g relying parties) to introspect access tokens
@ -111,11 +112,9 @@ namespace OpenIddict.Infrastructure {
return;
}
// When the received ticket is a refresh token, ensure it is still valid.
// Note: the OpenID Connect server middleware automatically ensures only
// authorized presenters are allowed to introspect refresh tokens.
if (context.Ticket.IsRefreshToken()) {
// Retrieve the token from the database using the unique identifier stored in the refresh token:
// When the received ticket is revocable, ensure it is still valid.
if (context.Ticket.IsAuthorizationCode() || context.Ticket.IsRefreshToken()) {
// Retrieve the token from the database using the unique identifier stored in the authentication ticket:
// if the corresponding entry cannot be found, return Active = false to indicate that is is no longer valid.
var token = await services.Tokens.FindByIdAsync(context.Ticket.GetTicketId());
if (token == null) {

33
src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs

@ -19,14 +19,14 @@ namespace OpenIddict.Infrastructure {
public override async Task ValidateRevocationRequest([NotNull] ValidateRevocationRequestContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
// When token_type_hint is specified, reject the request
// if token_type_hint is not equal to "refresh_token".
// When token_type_hint is specified, reject the request if it doesn't correspond to a revocable token.
if (!string.IsNullOrEmpty(context.Request.GetTokenTypeHint()) &&
!string.Equals(context.Request.GetTokenTypeHint(), OpenIdConnectConstants.TokenTypeHints.AuthorizationCode) &&
!string.Equals(context.Request.GetTokenTypeHint(), OpenIdConnectConstants.TokenTypeHints.RefreshToken)) {
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedTokenType,
description: "Only refresh tokens can be revoked. When specifying a token_type_hint " +
"parameter, its value must be equal to 'refresh_token'.");
description: "Only authorization codes and refresh tokens can be revoked. When specifying a token_type_hint " +
"parameter, its value must be equal to 'authorization_code' or 'refresh_token'.");
return;
}
@ -90,36 +90,39 @@ namespace OpenIddict.Infrastructure {
public override async Task HandleRevocationRequest([NotNull] HandleRevocationRequestContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
// If the received token is not a refresh token, set Revoked
// to false to indicate that the token cannot be revoked.
if (!context.Ticket.IsRefreshToken()) {
services.Logger.LogError("The revocation request was rejected because the token was not a refresh token.");
Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null.");
context.Revoked = false;
// If the received token is not an authorization code or a refresh token,
// return an error to indicate that the token cannot be revoked.
if (!context.Ticket.IsAuthorizationCode() && !context.Ticket.IsRefreshToken()) {
services.Logger.LogError("The revocation request was rejected because the token was not revocable.");
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedTokenType,
description: "Only authorization codes and refresh tokens can be revoked.");
return;
}
// Extract the token identifier from the refresh token.
// Extract the token identifier from the authentication ticket.
var identifier = context.Ticket.GetTicketId();
Debug.Assert(!string.IsNullOrEmpty(identifier),
"The refresh token should contain a ticket identifier.");
Debug.Assert(!string.IsNullOrEmpty(identifier), "The token should contain a ticket identifier.");
// Retrieve the token from the database. If the token cannot be found,
// assume it is invalid and consider the revocation as successful.
var token = await services.Tokens.FindByIdAsync(identifier);
if (token == null) {
services.Logger.LogInformation("The refresh token '{Identifier}' was already revoked.", identifier);
services.Logger.LogInformation("The token '{Identifier}' was already revoked.", identifier);
context.Revoked = true;
return;
}
// Revoke the refresh token.
// Revoke the token.
await services.Tokens.RevokeAsync(token);
services.Logger.LogInformation("The refresh token '{Identifier}' was successfully revoked.", identifier);
services.Logger.LogInformation("The token '{Identifier}' was successfully revoked.", identifier);
context.Revoked = true;
}

37
src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs

@ -17,14 +17,41 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace OpenIddict.Infrastructure {
public partial class OpenIddictProvider<TUser, TApplication, TAuthorization, TScope, TToken> : OpenIdConnectServerProvider
where TUser : class where TApplication : class where TAuthorization : class where TScope : class where TToken : class {
public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) {
public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
Debug.Assert(context.Request.RequestType == OpenIdConnectRequestType.Token,
"The request should be a token request.");
Debug.Assert(context.Request.RequestType == OpenIdConnectRequestType.Authentication, "The request should be an authorization request.");
Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client_id parameter shouldn't be null or empty.");
// Note: a null value could be returned by FindByIdAsync. In this case, throw an exception to abort the token request.
var user = await services.Users.FindByIdAsync(context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier));
if (user == null) {
throw new InvalidOperationException("The token request was aborted because the user associated " +
"with the authorization code was not found in the database.");
}
var application = await services.Applications.FindByClientIdAsync(context.Request.ClientId);
if (application == null) {
throw new InvalidOperationException("The application cannot be retrieved from the database.");
}
// Persist a new token entry in the database and attach it to the user and the client application it is issued to.
var identifier = await services.Users.CreateTokenAsync(user, context.Request.ClientId, OpenIdConnectConstants.TokenTypeHints.AuthorizationCode);
if (string.IsNullOrEmpty(identifier)) {
throw new InvalidOperationException("The unique key associated with a authorization code cannot be null or empty.");
}
// Attach the key returned by the underlying store
// to the authorization code to override the default GUID
// generated by the OpenID Connect server middleware.
context.Ticket.SetTicketId(identifier);
}
public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TUser, TApplication, TAuthorization, TScope, TToken>>();
Debug.Assert(!context.Request.IsClientCredentialsGrantType(),
"A refresh token should not be issued when using grant_type=client_credentials.");
Debug.Assert(context.Request.RequestType == OpenIdConnectRequestType.Token, "The request should be a token request.");
Debug.Assert(!context.Request.IsClientCredentialsGrantType(), "A refresh token should not be issued when using grant_type=client_credentials.");
// Note: a null value could be returned by FindByIdAsync if the user was removed after the initial
// check made by GrantAuthorizationCode/GrantRefreshToken/GrantResourceOwnerCredentials.

21
src/OpenIddict.Mvc/OpenIddictExtensions.cs

@ -21,6 +21,7 @@ using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using OpenIddict;
using OpenIddict.Infrastructure;
using OpenIddict.Mvc;
namespace Microsoft.AspNetCore.Builder {
@ -159,6 +160,26 @@ namespace Microsoft.AspNetCore.Builder {
return provider.GetRequiredService(typeof(OpenIddictUserManager<>).MakeGenericType(builder.UserType));
});
// Register the OpenIddict services in the isolated container.
services.AddScoped(typeof(OpenIddictServices<,,,,>).MakeGenericType(
/* TUser: */ builder.UserType,
/* TApplication: */ builder.ApplicationType,
/* TAuthorization: */ builder.AuthorizationType,
/* TScope: */ builder.ScopeType,
/* TToken: */ builder.TokenType), provider => {
var accessor = provider.GetRequiredService<IHttpContextAccessor>();
var container = (IServiceProvider) accessor.HttpContext.Items[typeof(IServiceProvider)];
Debug.Assert(container != null, "The parent DI container cannot be resolved from the HTTP context.");
// Resolve the OpenIddict services from the parent container.
return container.GetRequiredService(typeof(OpenIddictServices<,,,,>).MakeGenericType(
/* TUser: */ builder.UserType,
/* TApplication: */ builder.ApplicationType,
/* TAuthorization: */ builder.AuthorizationType,
/* TScope: */ builder.ScopeType,
/* TToken: */ builder.TokenType));
});
// Register the options in the isolated container.
services.AddSingleton(provider => {
var accessor = provider.GetRequiredService<IHttpContextAccessor>();

Loading…
Cancel
Save