Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 93 additions & 29 deletions sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)

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.[/]");

Expand Down Expand Up @@ -144,7 +145,7 @@ await _service.AuthenticateInteractivelyAsync(new()

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
Expand Down Expand Up @@ -439,34 +440,48 @@ async Task<string> PromptAsync() => AnsiConsole.Prompt(new SelectionPrompt<OpenI
}

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<string>(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<string>(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<string>(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<string>(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,
Expand All @@ -475,36 +490,54 @@ async Task<string> PromptAsync() => AnsiConsole.Prompt(new SelectionPrompt<OpenI
}

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<string>(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<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(((
GrantType : GrantTypes.AuthorizationCode,
ResponseType: ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token),
"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<string>(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,
ResponseType: ResponseTypes.Code + ' ' + ResponseTypes.Token), "Hybrid flow (code + token)"));
}
}

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(((
GrantType : null,
Expand All @@ -524,36 +557,42 @@ async Task<string> PromptAsync() => AnsiConsole.Prompt(new SelectionPrompt<OpenI
.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 = [];

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)"));
}
Expand All @@ -569,10 +608,11 @@ configuration.DeviceAuthorizationEndpoint is not null &&
.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(
OpenIddictClientRegistration registration,
OpenIddictConfiguration configuration, CancellationToken cancellationToken)
{
static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt(
Expand All @@ -583,10 +623,34 @@ static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt(
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);
Expand Down
29 changes: 29 additions & 0 deletions sandbox/OpenIddict.Sandbox.Console.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,38 @@
.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")
Expand Down
2 changes: 1 addition & 1 deletion sandbox/OpenIddict.Sandbox.Maui.Client/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public static MauiApp CreateMauiApp()
.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");
});
});
Expand Down
2 changes: 2 additions & 0 deletions sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@
.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");
Expand Down
Loading