From f063cf56e23950bd98ffdbd8c1376dab53c68051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sun, 27 Nov 2022 06:26:01 +0100 Subject: [PATCH] Update the Google integration to support the access_type parameter --- .../Controllers/HomeController.cs | 52 ++++++++++++++++-- .../Startup.cs | 7 ++- .../Views/Home/Index.cshtml | 19 ++++++- .../Controllers/HomeController.cs | 54 ++++++++++++++++--- .../Startup.cs | 7 ++- .../Views/Home/Index.cshtml | 13 ++++- .../OpenIddictClientWebIntegrationHandlers.cs | 41 +++++++++++++- ...penIddictClientWebIntegrationProviders.xml | 3 ++ 8 files changed, 175 insertions(+), 21 deletions(-) diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs index 15ccdb36..dd99411a 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs @@ -1,10 +1,14 @@ -using System.Net.Http; +using System; +using System.Linq; +using System.Net.Http; using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; +using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; +using OpenIddict.Client; using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants; namespace OpenIddict.Sandbox.AspNet.Client.Controllers @@ -12,14 +16,20 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers public class HomeController : Controller { private readonly IHttpClientFactory _httpClientFactory; + private readonly OpenIddictClientService _service; - public HomeController(IHttpClientFactory httpClientFactory) - => _httpClientFactory = httpClientFactory; + public HomeController( + IHttpClientFactory httpClientFactory, + OpenIddictClientService service) + { + _httpClientFactory = httpClientFactory; + _service = service; + } [HttpGet, Route("~/")] public ActionResult Index() => View(); - [Authorize, HttpPost, Route("~/")] + [Authorize, HttpPost, Route("~/message")] [ValidateAntiForgeryToken] public async Task Index(CancellationToken cancellationToken) { @@ -38,5 +48,39 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers return View(model: await response.Content.ReadAsStringAsync()); } + + [Authorize, HttpPost, Route("~/refresh-token")] + [ValidateAntiForgeryToken] + public async Task RefreshToken(CancellationToken cancellationToken) + { + var context = HttpContext.GetOwinContext(); + + var result = await context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType); + if (!result.Properties.Dictionary.TryGetValue(Tokens.RefreshToken, out string token)) + { + return new HttpStatusCodeResult(400); + } + + var (response, principal) = await _service.AuthenticateWithRefreshTokenAsync( + issuer: new Uri(result.Identity.Claims.Select(claim => claim.Issuer).First(), UriKind.Absolute), + token: token, + cancellationToken: cancellationToken); + + var properties = new AuthenticationProperties(result.Properties.Dictionary) + { + RedirectUri = null + }; + + properties.Dictionary[Tokens.BackchannelAccessToken] = response.AccessToken; + + if (!string.IsNullOrEmpty(response.RefreshToken)) + { + properties.Dictionary[Tokens.RefreshToken] = response.RefreshToken; + } + + context.Authentication.SignIn(properties, result.Identity); + + return View("Index", model: response.AccessToken); + } } } diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs index f78bc613..4ad35853 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs @@ -82,8 +82,10 @@ namespace OpenIddict.Sandbox.AspNet.Client options.SetPostLogoutRedirectionEndpointUris( "/callback/logout/local"); - // Note: this sample uses the code flow, but you can enable the other flows if necessary. - options.AllowAuthorizationCodeFlow(); + // Note: this sample uses the authorization code and refresh token + // flows, but you can enable the other flows if necessary. + options.AllowAuthorizationCodeFlow() + .AllowRefreshTokenFlow(); // Register the signing and encryption credentials used to protect // sensitive data like the state tokens produced by OpenIddict. @@ -128,6 +130,7 @@ namespace OpenIddict.Sandbox.AspNet.Client options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com") .SetClientSecret("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") .SetRedirectUri("https://localhost:44378/callback/login/google") + .SetAccessType("offline") .AddScopes(Scopes.Profile); }) .UseTwitter(options => diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml b/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml index 2115a85a..b4f102d0 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml @@ -1,4 +1,8 @@ @using System.Security.Claims +@using Microsoft.Owin +@using Microsoft.Owin.Security +@using Microsoft.Owin.Security.Cookies +@using OpenIddict.Client.Owin @model string
@@ -15,19 +19,30 @@ if (!string.IsNullOrEmpty(Model)) { -

Message received from the resource controller: @Model

+

Payload returned by the controller: @Model

} if (User is ClaimsPrincipal principal && principal.FindFirst(ClaimTypes.NameIdentifier)?.Issuer is "https://localhost:44349/") { -
+ @Html.AntiForgeryToken()
} + if (Context.GetOwinContext() is IOwinContext context && + context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType).Result is AuthenticateResult result && + result.Properties.Dictionary.ContainsKey(OpenIddictClientOwinConstants.Tokens.RefreshToken)) + { +
+ @Html.AntiForgeryToken() + + +
+ } +
@Html.AntiForgeryToken() diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs index aea5f97c..a402bb99 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs @@ -3,25 +3,31 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using OpenIddict.Client.AspNetCore; +using OpenIddict.Client; +using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants; namespace OpenIddict.Sandbox.AspNetCore.Client.Controllers; public class HomeController : Controller { private readonly IHttpClientFactory _httpClientFactory; + private readonly OpenIddictClientService _service; - public HomeController(IHttpClientFactory httpClientFactory) - => _httpClientFactory = httpClientFactory; + public HomeController( + IHttpClientFactory httpClientFactory, + OpenIddictClientService service) + { + _httpClientFactory = httpClientFactory; + _service = service; + } [HttpGet("~/")] public ActionResult Index() => View(); - [Authorize, HttpPost("~/"), ValidateAntiForgeryToken] - public async Task Index(CancellationToken cancellationToken) + [Authorize, HttpPost("~/message"), ValidateAntiForgeryToken] + public async Task GetMessage(CancellationToken cancellationToken) { - var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, - OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken); + var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, Tokens.BackchannelAccessToken); using var client = _httpClientFactory.CreateClient(); @@ -31,6 +37,38 @@ public class HomeController : Controller using var response = await client.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); - return View(model: await response.Content.ReadAsStringAsync(cancellationToken)); + return View("Index", model: await response.Content.ReadAsStringAsync(cancellationToken)); + } + + [Authorize, HttpPost("~/refresh-token"), ValidateAntiForgeryToken] + public async Task RefreshToken(CancellationToken cancellationToken) + { + var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + var token = result?.Properties.GetTokenValue(Tokens.RefreshToken); + if (string.IsNullOrEmpty(token)) + { + return BadRequest(); + } + + var (response, principal) = await _service.AuthenticateWithRefreshTokenAsync( + issuer: new Uri(result.Principal.Claims.Select(claim => claim.Issuer).First(), UriKind.Absolute), + token: token, + cancellationToken: cancellationToken); + + var properties = new AuthenticationProperties(result.Properties.Items) + { + RedirectUri = null + }; + + properties.UpdateTokenValue(Tokens.BackchannelAccessToken, response.AccessToken); + + if (!string.IsNullOrEmpty(response.RefreshToken)) + { + properties.UpdateTokenValue(Tokens.RefreshToken, response.RefreshToken); + } + + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, result.Principal, properties); + + return View("Index", model: response.AccessToken); } } diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs index d9bcc474..5a1c3703 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs @@ -91,8 +91,10 @@ public class Startup options.SetPostLogoutRedirectionEndpointUris( "/callback/logout/local"); - // Note: this sample uses the code flow, but you can enable the other flows if necessary. - options.AllowAuthorizationCodeFlow(); + // Note: this sample uses the authorization code and refresh token + // flows, but you can enable the other flows if necessary. + options.AllowAuthorizationCodeFlow() + .AllowRefreshTokenFlow(); // Register the signing and encryption credentials used to protect // sensitive data like the state tokens produced by OpenIddict. @@ -138,6 +140,7 @@ public class Startup options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com") .SetClientSecret("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") .SetRedirectUri("https://localhost:44381/callback/login/google") + .SetAccessType("offline") .AddScopes(Scopes.Profile); }) .UseReddit(options => diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml index 4c97a1d7..f9e9fd73 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml @@ -1,4 +1,6 @@ @using System.Security.Claims +@using Microsoft.AspNetCore.Authentication; +@using OpenIddict.Client.AspNetCore; @model string
@@ -15,16 +17,23 @@ if (!string.IsNullOrEmpty(Model)) { -

Message received from the resource controller: @Model

+

Payload returned by the controller: @Model

} if (User.FindFirst(ClaimTypes.NameIdentifier)?.Issuer is "https://localhost:44395/") { - + } + if (!string.IsNullOrEmpty(await Context.GetTokenAsync(OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken))) + { +
+ +
+ } +
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 96879f94..fc5a9c90 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -32,7 +32,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers */ OverrideResponseMode.Descriptor, FormatNonStandardScopeParameter.Descriptor, - IncludeStateParameterInRedirectUri.Descriptor) + IncludeStateParameterInRedirectUri.Descriptor, + AttachAdditionalChallengeParameters.Descriptor) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Protection.DefaultHandlers) @@ -509,4 +510,42 @@ public static partial class OpenIddictClientWebIntegrationHandlers return default; } } + + /// + /// Contains the logic responsible for attaching additional parameters + /// to the authorization request for the providers that require it. + /// + public sealed class AttachAdditionalChallengeParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachChallengeParameters.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // By default, Google doesn't return a refresh token but allows sending an "access_type" + // parameter to retrieve one (but it is only returned during the first authorization dance). + if (context.Registration.ProviderName is Providers.Google) + { + var options = context.Registration.GetGoogleOptions(); + + context.Request["access_type"] = options.AccessType; + } + + return default; + } + } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index 1eec13c8..3a6e979c 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -39,6 +39,9 @@ + +