From 154fa490b776bbb4423fa129941c0cdec18f2e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 20 Jun 2016 03:58:00 +0200 Subject: [PATCH] Implement automatic authorization code revocation --- .../OpenIddictProvider.Exchange.cs | 24 +++++++++++- .../OpenIddictProvider.Introspection.cs | 9 ++--- .../OpenIddictProvider.Revocation.cs | 33 +++++++++-------- .../OpenIddictProvider.Serialization.cs | 37 ++++++++++++++++--- src/OpenIddict.Mvc/OpenIddictExtensions.cs | 21 +++++++++++ 5 files changed, 97 insertions(+), 27 deletions(-) diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs index ecd4d02f..e57236b3 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs +++ b/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) { diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs index ff75310b..331f1f09 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs +++ b/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>(); + 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) { diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs index 87d5a7f5..fcd4be83 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs +++ b/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>(); - // 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>(); - // 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; } diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs index 67dd6a0c..3d2bbc6b 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs @@ -17,14 +17,41 @@ using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace OpenIddict.Infrastructure { public partial class OpenIddictProvider : 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>(); - 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>(); - 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. diff --git a/src/OpenIddict.Mvc/OpenIddictExtensions.cs b/src/OpenIddict.Mvc/OpenIddictExtensions.cs index 02f41db2..19128369 100644 --- a/src/OpenIddict.Mvc/OpenIddictExtensions.cs +++ b/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(); + 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();