Browse Source

Update the console sandbox to use the Google integration

pull/2349/head
Kévin Chalet 8 months ago
parent
commit
ea09c7f2fb
  1. 122
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  2. 29
      sandbox/OpenIddict.Sandbox.Console.Client/Program.cs
  3. 2
      sandbox/OpenIddict.Sandbox.Maui.Client/MauiProgram.cs
  4. 2
      sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs
  5. 2
      sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs
  6. 42
      src/OpenIddict.Server/OpenIddictServerHandlers.cs

122
sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs

@ -36,11 +36,12 @@ public class InteractiveService : BackgroundService
try try
{ {
var registration = await _service.GetClientRegistrationByProviderNameAsync(provider, stoppingToken);
var configuration = await _service.GetServerConfigurationByProviderNameAsync(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.[/]"); AnsiConsole.MarkupLine("[cyan]Launching the system browser.[/]");
@ -144,7 +145,7 @@ public class InteractiveService : BackgroundService
else else
{ {
var type = await GetSelectedGrantTypeAsync(configuration, stoppingToken); var type = await GetSelectedGrantTypeAsync(registration, configuration, stoppingToken);
if (type is GrantTypes.DeviceCode) if (type is GrantTypes.DeviceCode)
{ {
// Ask OpenIddict to send a device authorization request and write // Ask OpenIddict to send a device authorization request and write
@ -439,34 +440,48 @@ public class InteractiveService : BackgroundService
} }
Task<(string? GrantType, string? ResponseType)> GetSelectedFlowAsync( Task<(string? GrantType, string? ResponseType)> GetSelectedFlowAsync(
OpenIddictClientRegistration registration,
OpenIddictConfiguration configuration, CancellationToken cancellationToken) 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 = []; List<((string? GrantType, string? ResponseType), string DisplayName)> choices = [];
var types = configuration.ResponseTypesSupported.Select(type => var types = configuration.ResponseTypesSupported.Select(static type =>
new HashSet<string>(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))); new HashSet<string>(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)));
if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) && 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<string>(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)))
.Any(static type => type.Count is 1 && type.Contains(ResponseTypes.Code))))
{ {
choices.Add((( choices.Add(((
GrantType : GrantTypes.AuthorizationCode, GrantType : GrantTypes.AuthorizationCode,
ResponseType: ResponseTypes.Code), "Authorization code flow")); 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<string>(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)))
.Any(static type => type.Count is 1 && type.Contains(ResponseTypes.IdToken))))
{ {
choices.Add((( choices.Add(((
GrantType : GrantTypes.Implicit, GrantType : GrantTypes.Implicit,
ResponseType: ResponseTypes.IdToken), "Implicit flow (id_token)")); ResponseType: ResponseTypes.IdToken), "Implicit flow (id_token)"));
} }
if (types.Any(type => type.Count is 2 && type.Contains(ResponseTypes.IdToken) && if (types.Any(static type => type.Count is 2 && type.Contains(ResponseTypes.IdToken) &&
type.Contains(ResponseTypes.Token))) type.Contains(ResponseTypes.Token)) &&
(registration.ResponseTypes.Count is 0 || registration.ResponseTypes
.Select(static type => new HashSet<string>(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)))
.Any(static type => type.Count is 2 && type.Contains(ResponseTypes.IdToken) &&
type.Contains(ResponseTypes.Token))))
{ {
choices.Add((( choices.Add(((
GrantType : GrantTypes.Implicit, GrantType : GrantTypes.Implicit,
@ -475,19 +490,30 @@ public class InteractiveService : BackgroundService
} }
if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) && 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) && if (types.Any(static type => type.Count is 2 && type.Contains(ResponseTypes.Code) &&
type.Contains(ResponseTypes.IdToken))) type.Contains(ResponseTypes.IdToken)) &&
(registration.ResponseTypes.Count is 0 || registration.ResponseTypes
.Select(static type => new HashSet<string>(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)))
.Any(static type => type.Count is 2 && type.Contains(ResponseTypes.Code) &&
type.Contains(ResponseTypes.IdToken))))
{ {
choices.Add((( choices.Add(((
GrantType : GrantTypes.AuthorizationCode, GrantType : GrantTypes.AuthorizationCode,
ResponseType: ResponseTypes.Code + ' ' + ResponseTypes.IdToken), "Hybrid flow (code + id_token)")); 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.IdToken) &&
type.Contains(ResponseTypes.Token))) type.Contains(ResponseTypes.Token)) &&
(registration.ResponseTypes.Count is 0 || registration.ResponseTypes
.Select(static type => new HashSet<string>(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((( choices.Add(((
GrantType : GrantTypes.AuthorizationCode, GrantType : GrantTypes.AuthorizationCode,
@ -495,8 +521,12 @@ public class InteractiveService : BackgroundService
"Hybrid flow (code + id_token + token)")); "Hybrid flow (code + id_token + token)"));
} }
if (types.Any(type => type.Count is 2 && type.Contains(ResponseTypes.Code) && if (types.Any(static type => type.Count is 2 && type.Contains(ResponseTypes.Code) &&
type.Contains(ResponseTypes.Token))) type.Contains(ResponseTypes.Token)) &&
(registration.ResponseTypes.Count is 0 || registration.ResponseTypes
.Select(static type => new HashSet<string>(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)))
.Any(static type => type.Count is 2 && type.Contains(ResponseTypes.Code) &&
type.Contains(ResponseTypes.Token))))
{ {
choices.Add((( choices.Add(((
GrantType : GrantTypes.AuthorizationCode, 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<string>(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)))
.Any(static type => type.Count is 1 && type.Contains(ResponseTypes.None))))
{ {
choices.Add((( choices.Add(((
GrantType : null, GrantType : null,
@ -524,36 +557,42 @@ public class InteractiveService : BackgroundService
.UseConverter(choice => choice.DisplayName)).Item1; .UseConverter(choice => choice.DisplayName)).Item1;
} }
return WaitAsync(Task.Run(() => Prompt(configuration), cancellationToken), cancellationToken); return WaitAsync(Task.Run(() => Prompt(registration, configuration), cancellationToken), cancellationToken);
} }
Task<string> GetSelectedGrantTypeAsync(OpenIddictConfiguration configuration, CancellationToken cancellationToken) Task<string> 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 = []; List<(string GrantType, string DisplayName)> choices = [];
if (configuration.GrantTypesSupported.Contains(GrantTypes.DeviceCode) && if (configuration.GrantTypesSupported.Contains(GrantTypes.DeviceCode) &&
configuration.DeviceAuthorizationEndpoint is not null && 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")); choices.Add((GrantTypes.DeviceCode, "Device authorization code grant"));
} }
if (configuration.GrantTypesSupported.Contains(GrantTypes.Password) && 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")); choices.Add((GrantTypes.Password, "Resource owner password credentials grant"));
} }
if (configuration.GrantTypesSupported.Contains(GrantTypes.TokenExchange) && 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")); choices.Add((GrantTypes.TokenExchange, "Token exchange"));
} }
if (configuration.GrantTypesSupported.Contains(GrantTypes.ClientCredentials) && 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)")); choices.Add((GrantTypes.ClientCredentials, "Client credentials grant (application authentication only)"));
} }
@ -569,10 +608,11 @@ public class InteractiveService : BackgroundService
.UseConverter(choice => choice.DisplayName)).GrantType; .UseConverter(choice => choice.DisplayName)).GrantType;
} }
return WaitAsync(Task.Run(() => Prompt(configuration), cancellationToken), cancellationToken); return WaitAsync(Task.Run(() => Prompt(registration, configuration), cancellationToken), cancellationToken);
} }
Task<bool> AuthenticateUserInteractivelyAsync( Task<bool> AuthenticateUserInteractivelyAsync(
OpenIddictClientRegistration registration,
OpenIddictConfiguration configuration, CancellationToken cancellationToken) OpenIddictConfiguration configuration, CancellationToken cancellationToken)
{ {
static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt(
@ -583,10 +623,34 @@ public class InteractiveService : BackgroundService
ShowDefaultValue = true ShowDefaultValue = true
}); });
if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) || if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) &&
configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) (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); return Task.FromResult(false);

29
sandbox/OpenIddict.Sandbox.Console.Client/Program.cs

@ -89,9 +89,38 @@ builder.Services.AddOpenIddict()
.AddGitHub(options => .AddGitHub(options =>
{ {
options.SetClientId("992372d088f8676a7945") 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") .SetClientSecret("1f18c22f766e44d7bd4ea4a6510b9e337d48ab38")
.SetRedirectUri("callback/login/github"); .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 => .AddTwitter(options =>
{ {
options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")

2
sandbox/OpenIddict.Sandbox.Maui.Client/MauiProgram.cs

@ -92,7 +92,7 @@ public static class MauiProgram
.AddTwitter(options => .AddTwitter(options =>
{ {
options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") 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"); .SetRedirectUri("com.openiddict.sandbox.maui.client://callback/login/twitter");
}); });
}); });

2
sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs

@ -89,6 +89,8 @@ var host = new HostBuilder()
.AddGitHub(options => .AddGitHub(options =>
{ {
options.SetClientId("cf8efb4d76c0cb7109d3") 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") .SetClientSecret("e8c0f6b869164411bb9052e42414cbcc52d518cd")
// Note: GitHub doesn't support the recommended ":/" syntax and requires using "://". // Note: GitHub doesn't support the recommended ":/" syntax and requires using "://".
.SetRedirectUri("com.openiddict.sandbox.winforms.client://callback/login/github"); .SetRedirectUri("com.openiddict.sandbox.winforms.client://callback/login/github");

2
sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs

@ -86,6 +86,8 @@ var host = new HostBuilder()
.AddGitHub(options => .AddGitHub(options =>
{ {
options.SetClientId("8abc54b6d5f4e39d78aa") 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") .SetClientSecret("f37ef38bdb18a0f5f2d430a8edbed4353c012dc3")
// Note: GitHub doesn't support the recommended ":/" syntax and requires using "://". // Note: GitHub doesn't support the recommended ":/" syntax and requires using "://".
.SetRedirectUri("com.openiddict.sandbox.wpf.client://callback/login/github"); .SetRedirectUri("com.openiddict.sandbox.wpf.client://callback/login/github");

42
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: // 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 // 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 // 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), 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 // 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) _ => (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 (context.GenerateRefreshToken, context.IncludeRefreshToken) = context.EndpointType switch
{ {
// For token exchange requests, do not generate and return a second refresh // For token exchange requests, do not generate and return a second refresh
@ -3245,6 +3225,26 @@ public static partial class OpenIddictServerHandlers
_ => (false, false) _ => (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 (context.GenerateUserCode, context.IncludeUserCode) = context.EndpointType switch
{ {
// Only generate and return a user code if the request is a device authorization request. // Only generate and return a user code if the request is a device authorization request.

Loading…
Cancel
Save