From 39fd5763a9ef2775dbe8cb9692f7f366e31c86a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 28 Nov 2016 16:16:25 +0100 Subject: [PATCH] Update OpenIddictProvider to make refresh token requests pass-through --- .../Controllers/AuthorizationController.cs | 62 +++++++++++++++---- .../OpenIddictProvider.Exchange.cs | 10 --- .../OpenIddictProviderTests.Exchange.cs | 29 +++++++++ .../Infrastructure/OpenIddictProviderTests.cs | 3 +- 4 files changed, 79 insertions(+), 25 deletions(-) diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 29c469b6..35533e8a 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -35,6 +35,7 @@ namespace Mvc.Server { _userManager = userManager; } + #region Authorization code, implicit and implicit flows // Note: to support interactive flows like the code flow, // you must provide your own authorization endpoint action: @@ -108,7 +109,9 @@ namespace Mvc.Server { // to the post_logout_redirect_uri specified by the client application. return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme); } + #endregion + #region Password and refresh token flows // Note: to support non-interactive flows like password, // you must provide your own token endpoint action: @@ -169,13 +172,45 @@ namespace Mvc.Server { return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); } + else if (request.IsRefreshTokenGrantType()) { + // Retrieve the claims principal stored in the refresh token. + var info = await HttpContext.Authentication.GetAuthenticateInfoAsync( + OpenIdConnectServerDefaults.AuthenticationScheme); + + // Retrieve the user profile corresponding to the refresh token. + var user = await _userManager.GetUserAsync(info.Principal); + if (user == null) { + return BadRequest(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant, + ErrorDescription = "The refresh token is no longer valid." + }); + } + + // Ensure the user is still allowed to sign in. + if (!await _signInManager.CanSignInAsync(user)) { + return BadRequest(new OpenIdConnectResponse { + Error = OpenIdConnectConstants.Errors.InvalidGrant, + ErrorDescription = "The user is no longer allowed to sign in." + }); + } + + // Create a new authentication ticket, but reuse the properties stored + // in the refresh token, including the scopes originally granted. + var ticket = await CreateTicketAsync(request, user, info.Properties); + + return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); + } + return BadRequest(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.UnsupportedGrantType, ErrorDescription = "The specified grant type is not supported." }); } + #endregion - private async Task CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user) { + private async Task CreateTicketAsync( + OpenIdConnectRequest request, ApplicationUser user, + AuthenticationProperties properties = null) { // Create a new ClaimsPrincipal containing the claims that // will be used to create an id_token, a token or a code. var principal = await _signInManager.CreateUserPrincipalAsync(user); @@ -193,20 +228,21 @@ namespace Mvc.Server { } // Create a new authentication ticket holding the user identity. - var ticket = new AuthenticationTicket( - principal, new AuthenticationProperties(), + var ticket = new AuthenticationTicket(principal, properties, OpenIdConnectServerDefaults.AuthenticationScheme); - // Set the list of scopes granted to the client application. - // Note: the offline_access scope must be granted - // to allow OpenIddict to return a refresh token. - ticket.SetScopes(new[] { - OpenIdConnectConstants.Scopes.OpenId, - OpenIdConnectConstants.Scopes.Email, - OpenIdConnectConstants.Scopes.Profile, - OpenIdConnectConstants.Scopes.OfflineAccess, - OpenIddictConstants.Scopes.Roles - }.Intersect(request.GetScopes())); + if (!request.IsRefreshTokenGrantType()) { + // Set the list of scopes granted to the client application. + // Note: the offline_access scope must be granted + // to allow OpenIddict to return a refresh token. + ticket.SetScopes(new[] { + OpenIdConnectConstants.Scopes.OpenId, + OpenIdConnectConstants.Scopes.Email, + OpenIdConnectConstants.Scopes.Profile, + OpenIdConnectConstants.Scopes.OfflineAccess, + OpenIddictConstants.Scopes.Roles + }.Intersect(request.GetScopes())); + } return ticket; } diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs index 23aec84e..d4840607 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs @@ -180,9 +180,6 @@ namespace OpenIddict.Infrastructure { public override async Task HandleTokenRequest([NotNull] HandleTokenRequestContext context) { var services = context.HttpContext.RequestServices.GetRequiredService>(); - // Note: the OpenID Connect server middleware automatically reuses the authentication ticket - // 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 != null, "The authentication ticket shouldn't be null."); @@ -211,9 +208,6 @@ namespace OpenIddict.Infrastructure { return; } - // Note: the OpenID Connect server middleware automatically reuses the authentication ticket - // 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 != null, "The authentication ticket shouldn't be null."); @@ -240,10 +234,6 @@ namespace OpenIddict.Infrastructure { if (context.Options.UseSlidingExpiration) { await services.Tokens.RevokeAsync(token); } - - context.Validate(context.Ticket); - - return; } // Invoke the rest of the pipeline to allow diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Exchange.cs index 301452ed..402f44f7 100644 --- a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Exchange.cs +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Exchange.cs @@ -516,9 +516,33 @@ namespace OpenIddict.Core.Tests.Infrastructure { [Theory] [InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)] [InlineData(OpenIdConnectConstants.GrantTypes.Password)] + [InlineData(OpenIdConnectConstants.GrantTypes.RefreshToken)] [InlineData("urn:ietf:params:oauth:grant-type:custom_grant")] public async Task HandleTokenRequest_RequestsAreNotHandledLocally(string flow) { // Arrange + var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme); + identity.AddClaim(ClaimTypes.NameIdentifier, "Bob le Bricoleur"); + + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + OpenIdConnectServerDefaults.AuthenticationScheme); + + ticket.SetTicketId("60FFF7EA-F98E-437B-937E-5073CC313103"); + ticket.SetUsage(OpenIdConnectConstants.Usages.RefreshToken); + + var format = new Mock>(); + + format.Setup(mock => mock.Unprotect("8xLOxBtZp8")) + .Returns(ticket); + + var token = Mock.Of(); + + var manager = CreateTokenManager(instance => { + instance.Setup(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103")) + .ReturnsAsync(token); + }); + var server = CreateAuthorizationServer(builder => { builder.Services.AddSingleton(CreateApplicationManager(instance => { var application = Mock.Of(); @@ -534,6 +558,10 @@ namespace OpenIddict.Core.Tests.Infrastructure { })); builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); + + builder.Services.AddSingleton(manager); + + builder.Configure(options => options.RefreshTokenFormat = format.Object); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -543,6 +571,7 @@ namespace OpenIddict.Core.Tests.Infrastructure { ClientId = "Fabrikam", ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", GrantType = flow, + RefreshToken = "8xLOxBtZp8", Username = "johndoe", Password = "A3ddj3w" }); diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs index b29ea10a..f156c4ca 100644 --- a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs @@ -94,8 +94,7 @@ namespace OpenIddict.Core.Tests.Infrastructure { app.UseOpenIddict(); app.Run(context => { - if (context.Request.Path == AuthorizationEndpoint || - context.Request.Path == TokenEndpoint) { + if (context.Request.Path == AuthorizationEndpoint || context.Request.Path == TokenEndpoint) { var request = context.GetOpenIdConnectRequest(); var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);