From ea09c7f2fbaa39141ed7d2ed41b63f389a88b25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 13 Jun 2025 08:14:03 +0200 Subject: [PATCH] Update the console sandbox to use the Google integration --- .../InteractiveService.cs | 122 +++++++++++++----- .../Program.cs | 29 +++++ .../MauiProgram.cs | 2 +- .../Program.cs | 2 + .../OpenIddict.Sandbox.Wpf.Client/Program.cs | 2 + .../OpenIddictServerHandlers.cs | 42 +++--- 6 files changed, 148 insertions(+), 51 deletions(-) diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs index 288d1140..9fb74c50 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs +++ b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs @@ -36,11 +36,12 @@ public class InteractiveService : BackgroundService try { + var registration = await _service.GetClientRegistrationByProviderNameAsync(provider, stoppingToken); var configuration = await _service.GetServerConfigurationByProviderNameAsync(provider, stoppingToken); - if (await AuthenticateUserInteractivelyAsync(configuration, stoppingToken)) + if (await AuthenticateUserInteractivelyAsync(registration, configuration, stoppingToken)) { - var flow = await GetSelectedFlowAsync(configuration, stoppingToken); + var flow = await GetSelectedFlowAsync(registration, configuration, stoppingToken); AnsiConsole.MarkupLine("[cyan]Launching the system browser.[/]"); @@ -144,7 +145,7 @@ public class InteractiveService : BackgroundService else { - var type = await GetSelectedGrantTypeAsync(configuration, stoppingToken); + var type = await GetSelectedGrantTypeAsync(registration, configuration, stoppingToken); if (type is GrantTypes.DeviceCode) { // Ask OpenIddict to send a device authorization request and write @@ -439,34 +440,48 @@ public class InteractiveService : BackgroundService } Task<(string? GrantType, string? ResponseType)> GetSelectedFlowAsync( + OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, CancellationToken cancellationToken) { - static (string? GrantType, string? ResponseType) Prompt(OpenIddictConfiguration configuration) + static (string? GrantType, string? ResponseType) Prompt( + OpenIddictClientRegistration registration, OpenIddictConfiguration configuration) { List<((string? GrantType, string? ResponseType), string DisplayName)> choices = []; - var types = configuration.ResponseTypesSupported.Select(type => + var types = configuration.ResponseTypesSupported.Select(static type => new HashSet(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))); if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) && - types.Any(type => type.Count is 1 && type.Contains(ResponseTypes.Code))) + (registration.GrantTypes.Count is 0 || registration.GrantTypes.Contains(GrantTypes.AuthorizationCode)) && + types.Any(static type => type.Count is 1 && type.Contains(ResponseTypes.Code)) && + (registration.ResponseTypes.Count is 0 || registration.ResponseTypes + .Select(static type => new HashSet(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))) + .Any(static type => type.Count is 1 && type.Contains(ResponseTypes.Code)))) { choices.Add((( GrantType : GrantTypes.AuthorizationCode, ResponseType: ResponseTypes.Code), "Authorization code flow")); } - if (configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) + if (configuration.GrantTypesSupported.Contains(GrantTypes.Implicit) && + (registration.GrantTypes.Count is 0 || registration.GrantTypes.Contains(GrantTypes.Implicit))) { - if (types.Any(type => type.Count is 1 && type.Contains(ResponseTypes.IdToken))) + if (types.Any(static type => type.Count is 1 && type.Contains(ResponseTypes.IdToken)) && + (registration.ResponseTypes.Count is 0 || registration.ResponseTypes + .Select(static type => new HashSet(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))) + .Any(static type => type.Count is 1 && type.Contains(ResponseTypes.IdToken)))) { choices.Add((( GrantType : GrantTypes.Implicit, ResponseType: ResponseTypes.IdToken), "Implicit flow (id_token)")); } - if (types.Any(type => type.Count is 2 && type.Contains(ResponseTypes.IdToken) && - type.Contains(ResponseTypes.Token))) + if (types.Any(static type => type.Count is 2 && type.Contains(ResponseTypes.IdToken) && + type.Contains(ResponseTypes.Token)) && + (registration.ResponseTypes.Count is 0 || registration.ResponseTypes + .Select(static type => new HashSet(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))) + .Any(static type => type.Count is 2 && type.Contains(ResponseTypes.IdToken) && + type.Contains(ResponseTypes.Token)))) { choices.Add((( GrantType : GrantTypes.Implicit, @@ -475,19 +490,30 @@ public class InteractiveService : BackgroundService } if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) && - configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) + configuration.GrantTypesSupported.Contains(GrantTypes.Implicit) && + (registration.GrantTypes.Count is 0 || (registration.GrantTypes.Contains(GrantTypes.AuthorizationCode) && + registration.GrantTypes.Contains(GrantTypes.Implicit)))) { - if (types.Any(type => type.Count is 2 && type.Contains(ResponseTypes.Code) && - type.Contains(ResponseTypes.IdToken))) + if (types.Any(static type => type.Count is 2 && type.Contains(ResponseTypes.Code) && + type.Contains(ResponseTypes.IdToken)) && + (registration.ResponseTypes.Count is 0 || registration.ResponseTypes + .Select(static type => new HashSet(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))) + .Any(static type => type.Count is 2 && type.Contains(ResponseTypes.Code) && + type.Contains(ResponseTypes.IdToken)))) { choices.Add((( GrantType : GrantTypes.AuthorizationCode, ResponseType: ResponseTypes.Code + ' ' + ResponseTypes.IdToken), "Hybrid flow (code + id_token)")); } - if (types.Any(type => type.Count is 3 && type.Contains(ResponseTypes.Code) && + if (types.Any(static type => type.Count is 3 && type.Contains(ResponseTypes.Code) && type.Contains(ResponseTypes.IdToken) && - type.Contains(ResponseTypes.Token))) + type.Contains(ResponseTypes.Token)) && + (registration.ResponseTypes.Count is 0 || registration.ResponseTypes + .Select(static type => new HashSet(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))) + .Any(static type => type.Count is 3 && type.Contains(ResponseTypes.Code) && + type.Contains(ResponseTypes.IdToken) && + type.Contains(ResponseTypes.Token)))) { choices.Add((( GrantType : GrantTypes.AuthorizationCode, @@ -495,8 +521,12 @@ public class InteractiveService : BackgroundService "Hybrid flow (code + id_token + token)")); } - if (types.Any(type => type.Count is 2 && type.Contains(ResponseTypes.Code) && - type.Contains(ResponseTypes.Token))) + if (types.Any(static type => type.Count is 2 && type.Contains(ResponseTypes.Code) && + type.Contains(ResponseTypes.Token)) && + (registration.ResponseTypes.Count is 0 || registration.ResponseTypes + .Select(static type => new HashSet(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))) + .Any(static type => type.Count is 2 && type.Contains(ResponseTypes.Code) && + type.Contains(ResponseTypes.Token)))) { choices.Add((( GrantType : GrantTypes.AuthorizationCode, @@ -504,7 +534,10 @@ public class InteractiveService : BackgroundService } } - if (types.Any(type => type.Count is 1 && type.Contains(ResponseTypes.None))) + if (types.Any(static type => type.Count is 1 && type.Contains(ResponseTypes.None)) && + (registration.ResponseTypes.Count is 0 || registration.ResponseTypes + .Select(static type => new HashSet(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))) + .Any(static type => type.Count is 1 && type.Contains(ResponseTypes.None)))) { choices.Add((( GrantType : null, @@ -524,36 +557,42 @@ public class InteractiveService : BackgroundService .UseConverter(choice => choice.DisplayName)).Item1; } - return WaitAsync(Task.Run(() => Prompt(configuration), cancellationToken), cancellationToken); + return WaitAsync(Task.Run(() => Prompt(registration, configuration), cancellationToken), cancellationToken); } - Task GetSelectedGrantTypeAsync(OpenIddictConfiguration configuration, CancellationToken cancellationToken) + Task GetSelectedGrantTypeAsync( + OpenIddictClientRegistration registration, + OpenIddictConfiguration configuration, CancellationToken cancellationToken) { - static string Prompt(OpenIddictConfiguration configuration) + static string Prompt(OpenIddictClientRegistration registration, OpenIddictConfiguration configuration) { List<(string GrantType, string DisplayName)> choices = []; if (configuration.GrantTypesSupported.Contains(GrantTypes.DeviceCode) && configuration.DeviceAuthorizationEndpoint is not null && - configuration.TokenEndpoint is not null) + configuration.TokenEndpoint is not null && + (registration.GrantTypes.Count is 0 || registration.GrantTypes.Contains(GrantTypes.DeviceCode))) { choices.Add((GrantTypes.DeviceCode, "Device authorization code grant")); } if (configuration.GrantTypesSupported.Contains(GrantTypes.Password) && - configuration.TokenEndpoint is not null) + configuration.TokenEndpoint is not null && + (registration.GrantTypes.Count is 0 || registration.GrantTypes.Contains(GrantTypes.Password))) { choices.Add((GrantTypes.Password, "Resource owner password credentials grant")); } if (configuration.GrantTypesSupported.Contains(GrantTypes.TokenExchange) && - configuration.TokenEndpoint is not null) + configuration.TokenEndpoint is not null && + (registration.GrantTypes.Count is 0 || registration.GrantTypes.Contains(GrantTypes.TokenExchange))) { choices.Add((GrantTypes.TokenExchange, "Token exchange")); } if (configuration.GrantTypesSupported.Contains(GrantTypes.ClientCredentials) && - configuration.TokenEndpoint is not null) + configuration.TokenEndpoint is not null && + (registration.GrantTypes.Count is 0 || registration.GrantTypes.Contains(GrantTypes.ClientCredentials))) { choices.Add((GrantTypes.ClientCredentials, "Client credentials grant (application authentication only)")); } @@ -569,10 +608,11 @@ public class InteractiveService : BackgroundService .UseConverter(choice => choice.DisplayName)).GrantType; } - return WaitAsync(Task.Run(() => Prompt(configuration), cancellationToken), cancellationToken); + return WaitAsync(Task.Run(() => Prompt(registration, configuration), cancellationToken), cancellationToken); } Task AuthenticateUserInteractivelyAsync( + OpenIddictClientRegistration registration, OpenIddictConfiguration configuration, CancellationToken cancellationToken) { static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( @@ -583,10 +623,34 @@ public class InteractiveService : BackgroundService ShowDefaultValue = true }); - if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) || - configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) + if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) && + (registration.GrantTypes.Count is 0 || registration.GrantTypes.Contains(GrantTypes.AuthorizationCode))) + { + if (configuration.GrantTypesSupported.Any(static type => type is not ( + GrantTypes.AuthorizationCode or GrantTypes.Implicit or GrantTypes.RefreshToken)) && + (registration.GrantTypes.Count is 0 || + registration.GrantTypes.Any(static type => type is not ( + GrantTypes.AuthorizationCode or GrantTypes.Implicit or GrantTypes.RefreshToken)))) + { + return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); + } + + return Task.FromResult(true); + } + + if (configuration.GrantTypesSupported.Contains(GrantTypes.Implicit) && + (registration.GrantTypes.Count is 0 || registration.GrantTypes.Contains(GrantTypes.Implicit))) { - return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); + if (configuration.GrantTypesSupported.Any(static type => type is not ( + GrantTypes.AuthorizationCode or GrantTypes.Implicit or GrantTypes.RefreshToken)) && + (registration.GrantTypes.Count is 0 || + registration.GrantTypes.Any(static type => type is not ( + GrantTypes.AuthorizationCode or GrantTypes.Implicit or GrantTypes.RefreshToken)))) + { + return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); + } + + return Task.FromResult(true); } return Task.FromResult(false); diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs b/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs index 80c8e5cf..e42f6c6b 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs +++ b/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs @@ -89,9 +89,38 @@ builder.Services.AddOpenIddict() .AddGitHub(options => { options.SetClientId("992372d088f8676a7945") + // Note: GitHub doesn't allow creating public clients and requires using a secret. While this + // is a discouraged practice, it is the only option to use this provider in a desktop client. .SetClientSecret("1f18c22f766e44d7bd4ea4a6510b9e337d48ab38") .SetRedirectUri("callback/login/github"); }) + // Note: Google requires using separate client registrations to be able to use the authorization code flow + // and device flow in the same application. To work around this limitation, two registrations are used but + // each one explicitly restricts the grant types that OpenIddict is allowed to negotiate dynamically. + .AddGoogle(options => + { + options.SetClientId("1016114395689-arf09f1g51hadci5p5hn6lpp798k8rql.apps.googleusercontent.com") + // Note: Google doesn't allow creating public clients and requires using a secret. While this + // is discouraged practice, it is the only option to use this provider in a desktop client. + .SetClientSecret("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") + .SetRedirectUri("callback/login/google") + .SetAccessType("offline") + .AddScopes(Scopes.Profile) + .AddGrantTypes(GrantTypes.AuthorizationCode) + .SetProviderName("Google [code flow]") + .SetProviderDisplayName("Google (authorization code grant-only)"); + }) + .AddGoogle(options => + { + options.SetClientId("1016114395689-le5kvnikv5hhg3otvn1tgs2aogpkpvff.apps.googleusercontent.com") + .SetClientSecret("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") + .SetRedirectUri("callback/login/google") + .SetAccessType("offline") + .AddScopes(Scopes.Profile) + .AddGrantTypes(GrantTypes.DeviceCode) + .SetProviderName("Google [device flow]") + .SetProviderDisplayName("Google (device code grant-only)"); + }) .AddTwitter(options => { options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") diff --git a/sandbox/OpenIddict.Sandbox.Maui.Client/MauiProgram.cs b/sandbox/OpenIddict.Sandbox.Maui.Client/MauiProgram.cs index 74fa0f3f..2cbaf100 100644 --- a/sandbox/OpenIddict.Sandbox.Maui.Client/MauiProgram.cs +++ b/sandbox/OpenIddict.Sandbox.Maui.Client/MauiProgram.cs @@ -92,7 +92,7 @@ public static class MauiProgram .AddTwitter(options => { options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") - // Note: Twitter doesn't support the recommended ":/" syntax and requires using "://". + // Note: Twitter doesn't support the recommended ":/" syntax and requires using "://". .SetRedirectUri("com.openiddict.sandbox.maui.client://callback/login/twitter"); }); }); diff --git a/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs b/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs index 0b8ca44f..6107b61d 100644 --- a/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs +++ b/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs @@ -89,6 +89,8 @@ var host = new HostBuilder() .AddGitHub(options => { options.SetClientId("cf8efb4d76c0cb7109d3") + // Note: GitHub doesn't allow creating public clients and requires using a secret. While this + // is discouraged practice, it is the only option to use this provider in a desktop client. .SetClientSecret("e8c0f6b869164411bb9052e42414cbcc52d518cd") // Note: GitHub doesn't support the recommended ":/" syntax and requires using "://". .SetRedirectUri("com.openiddict.sandbox.winforms.client://callback/login/github"); diff --git a/sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs b/sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs index 5a9a4879..03d91103 100644 --- a/sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs +++ b/sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs @@ -86,6 +86,8 @@ var host = new HostBuilder() .AddGitHub(options => { options.SetClientId("8abc54b6d5f4e39d78aa") + // Note: GitHub doesn't allow creating public clients and requires using a secret. While this + // is discouraged practice, it is the only option to use this provider in a desktop client. .SetClientSecret("f37ef38bdb18a0f5f2d430a8edbed4353c012dc3") // Note: GitHub doesn't support the recommended ":/" syntax and requires using "://". .SetRedirectUri("com.openiddict.sandbox.wpf.client://callback/login/github"); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 266aaf5d..9000429e 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -3177,7 +3177,7 @@ public static partial class OpenIddictServerHandlers // For token requests using the OAuth 2.0 Token Exchange grant, never return an identity token as-is: // clients that need to retrieve an identity token can explicitly request an identity token using the // standard "requested_token_type" parameter. In that case, the identity token will be returned via - // the "access_token", as defined and required by the OAuth 2.0 Token Exchange specification. + // the "access_token" parameter, as defined and required by the OAuth 2.0 Token Exchange specification. OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType() => (false, false), // For token requests using other grant types (even for those that don't define the id_token as a @@ -3202,26 +3202,6 @@ public static partial class OpenIddictServerHandlers _ => (false, false, null) }; - (context.GenerateRequestToken, context.IncludeRequestToken) = context.EndpointType switch - { - // Always generate a request token if request caching was enabled and the - // authorization request doesn't already contain a request_uri parameter. - OpenIddictServerEndpointType.Authorization when - context.Options.EnableAuthorizationRequestCaching && - string.IsNullOrEmpty(context.Request.RequestUri) => (true, true), - - // Always generate a request token if request caching was enabled and the - // end session request doesn't already contain a request_uri parameter. - OpenIddictServerEndpointType.EndSession when - context.Options.EnableEndSessionRequestCaching && - string.IsNullOrEmpty(context.Request.RequestUri) => (true, true), - - // Always generate and return a request token if the request is a PAR request. - OpenIddictServerEndpointType.PushedAuthorization => (true, true), - - _ => (false, false) - }; - (context.GenerateRefreshToken, context.IncludeRefreshToken) = context.EndpointType switch { // For token exchange requests, do not generate and return a second refresh @@ -3245,6 +3225,26 @@ public static partial class OpenIddictServerHandlers _ => (false, false) }; + (context.GenerateRequestToken, context.IncludeRequestToken) = context.EndpointType switch + { + // Always generate a request token if request caching was enabled and the + // authorization request doesn't already contain a request_uri parameter. + OpenIddictServerEndpointType.Authorization when + context.Options.EnableAuthorizationRequestCaching && + string.IsNullOrEmpty(context.Request.RequestUri) => (true, true), + + // Always generate a request token if request caching was enabled and the + // end session request doesn't already contain a request_uri parameter. + OpenIddictServerEndpointType.EndSession when + context.Options.EnableEndSessionRequestCaching && + string.IsNullOrEmpty(context.Request.RequestUri) => (true, true), + + // Always generate and return a request token if the request is a PAR request. + OpenIddictServerEndpointType.PushedAuthorization => (true, true), + + _ => (false, false) + }; + (context.GenerateUserCode, context.IncludeUserCode) = context.EndpointType switch { // Only generate and return a user code if the request is a device authorization request.