using System.Security.Claims; using Microsoft.Extensions.Hosting; using OpenIddict.Abstractions; using OpenIddict.Client; using Spectre.Console; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictExceptions; #if !SUPPORTS_HOST_APPLICATION_LIFETIME using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime; #endif namespace OpenIddict.Sandbox.Console.Client; public class InteractiveService : BackgroundService { private readonly IHostApplicationLifetime _lifetime; private readonly OpenIddictClientService _service; public InteractiveService( IHostApplicationLifetime lifetime, OpenIddictClientService service) { _lifetime = lifetime; _service = service; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Wait for the host to confirm that the application has started. var source = new TaskCompletionSource(); using (_lifetime.ApplicationStarted.Register(static state => ((TaskCompletionSource) state!).SetResult(true), source)) { await source.Task; } while (!stoppingToken.IsCancellationRequested) { var provider = await GetSelectedProviderAsync(stoppingToken); try { var configuration = await _service.GetServerConfigurationByProviderNameAsync(provider, stoppingToken); if (await AuthenticateUserInteractivelyAsync(configuration, stoppingToken)) { var flow = await GetSelectedFlowAsync(configuration, stoppingToken); AnsiConsole.MarkupLine("[cyan]Launching the system browser.[/]"); // Ask OpenIddict to initiate the authentication flow (typically, by starting the system browser). var result = await _service.ChallengeInteractivelyAsync(new() { GrantType = flow.GrantType, CancellationToken = stoppingToken, ProviderName = provider, ResponseType = flow.ResponseType }); AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the authorization demand.[/]"); // Wait for the user to complete the authorization process and authenticate the callback request, // which allows resolving all the claims contained in the merged principal created by OpenIddict. var response = await _service.AuthenticateInteractivelyAsync(new() { CancellationToken = stoppingToken, Nonce = result.Nonce }); AnsiConsole.MarkupLine("[green]Interactive authentication successful:[/]"); AnsiConsole.Write(CreateClaimTable(response.Principal)); // If an access token was returned by the authorization server and introspection is // supported by the server, ask the user if the access token should be introspected. if (!string.IsNullOrEmpty(response.BackchannelAccessToken) && configuration.IntrospectionEndpoint is not null && await IntrospectAccessTokenAsync(stoppingToken)) { AnsiConsole.MarkupLine("[steelblue]Claims extracted from the token introspection response:[/]"); AnsiConsole.Write(CreateClaimTable((await _service.IntrospectTokenAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, Token = response.BackchannelAccessToken, TokenTypeHint = TokenTypeHints.AccessToken })).Principal)); } // If an access token was returned by the authorization server and revocation is // supported by the server, ask the user if the access token should be revoked. if (!string.IsNullOrEmpty(response.BackchannelAccessToken) && configuration.RevocationEndpoint is not null && await RevokeAccessTokenAsync(stoppingToken)) { await _service.RevokeTokenAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, Token = response.BackchannelAccessToken, TokenTypeHint = TokenTypeHints.AccessToken }); AnsiConsole.MarkupLine("[steelblue]Access token revoked.[/]"); } // If a refresh token was returned by the authorization server, ask the user // if the access token should be refreshed using the refresh_token grant. if (!string.IsNullOrEmpty(response.RefreshToken) && await RefreshTokenAsync(stoppingToken)) { AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity:[/]"); AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, RefreshToken = response.RefreshToken })).Principal)); } // If the authorization server supports RP-initiated logout, // ask the user if a logout operation should be started. if (configuration.EndSessionEndpoint is not null && await LogOutAsync(stoppingToken)) { AnsiConsole.MarkupLine("[cyan]Launching the system browser.[/]"); // Ask OpenIddict to initiate the logout flow (typically, by starting the system browser). var nonce = (await _service.SignOutInteractivelyAsync(new() { CancellationToken = stoppingToken, ProviderName = provider })).Nonce; AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the logout demand.[/]"); // Wait for the user to complete the logout process and authenticate the callback request. // // Note: in this case, only the claims contained in the state token can be resolved since // the authorization server doesn't return any other user identity during a logout dance. await _service.AuthenticateInteractivelyAsync(new() { CancellationToken = stoppingToken, Nonce = nonce }); AnsiConsole.MarkupLine("[green]Interactive logout successful.[/]"); } } else { var type = await GetSelectedGrantTypeAsync(configuration, stoppingToken); if (type is GrantTypes.DeviceCode) { // Ask OpenIddict to send a device authorization request and write // the complete verification endpoint URI to the console output. var result = await _service.ChallengeUsingDeviceAsync(new() { CancellationToken = stoppingToken, ProviderName = provider }); if (result.VerificationUriComplete is not null) { AnsiConsole.MarkupLineInterpolated($""" [yellow]Please visit [link]{result.VerificationUriComplete}[/] and confirm the displayed code is '{result.UserCode}' to complete the authentication demand.[/] """); } else { AnsiConsole.MarkupLineInterpolated($""" [yellow]Please visit [link]{result.VerificationUri}[/] and enter '{result.UserCode}' to complete the authentication demand.[/] """); } AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the authorization demand.[/]"); // Wait for the user to complete the demand on the other device. var response = await _service.AuthenticateWithDeviceAsync(new() { CancellationToken = stoppingToken, DeviceCode = result.DeviceCode, Interval = result.Interval, ProviderName = provider, Timeout = result.ExpiresIn < TimeSpan.FromMinutes(5) ? result.ExpiresIn : TimeSpan.FromMinutes(5) }); AnsiConsole.MarkupLine("[green]Device authentication successful:[/]"); AnsiConsole.Write(CreateClaimTable(response.Principal)); // If introspection is supported by the server, ask the user if the access token should be introspected. if (configuration.IntrospectionEndpoint is not null && await IntrospectAccessTokenAsync(stoppingToken)) { AnsiConsole.MarkupLine("[steelblue]Claims extracted from the token introspection response:[/]"); AnsiConsole.Write(CreateClaimTable((await _service.IntrospectTokenAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, Token = response.AccessToken, TokenTypeHint = TokenTypeHints.AccessToken })).Principal)); } // If revocation is supported by the server, ask the user if the access token should be revoked. if (configuration.RevocationEndpoint is not null && await RevokeAccessTokenAsync(stoppingToken)) { await _service.RevokeTokenAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, Token = response.AccessToken, TokenTypeHint = TokenTypeHints.AccessToken }); AnsiConsole.MarkupLine("[steelblue]Access token revoked.[/]"); } // If a refresh token was returned by the authorization server, ask the user // if the access token should be refreshed using the refresh_token grant. if (!string.IsNullOrEmpty(response.RefreshToken) && await RefreshTokenAsync(stoppingToken)) { AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity:[/]"); AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, RefreshToken = response.RefreshToken })).Principal)); } } else if (type is GrantTypes.Password) { var (username, password) = (await GetUsernameAsync(stoppingToken), await GetPasswordAsync(stoppingToken)); AnsiConsole.MarkupLine("[cyan]Sending the token request.[/]"); // Ask OpenIddict to authenticate the user using the resource owner password credentials grant. var response = await _service.AuthenticateWithPasswordAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, Username = username, Password = password, Scopes = [Scopes.OfflineAccess] }); AnsiConsole.MarkupLine("[green]Resource owner password credentials authentication successful:[/]"); AnsiConsole.Write(CreateClaimTable(response.Principal)); // If introspection is supported by the server, ask the user if the access token should be introspected. if (configuration.IntrospectionEndpoint is not null && await IntrospectAccessTokenAsync(stoppingToken)) { AnsiConsole.MarkupLine("[steelblue]Claims extracted from the token introspection response:[/]"); AnsiConsole.Write(CreateClaimTable((await _service.IntrospectTokenAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, Token = response.AccessToken, TokenTypeHint = TokenTypeHints.AccessToken })).Principal)); } // If revocation is supported by the server, ask the user if the access token should be revoked. if (configuration.RevocationEndpoint is not null && await RevokeAccessTokenAsync(stoppingToken)) { await _service.RevokeTokenAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, Token = response.AccessToken, TokenTypeHint = TokenTypeHints.AccessToken }); AnsiConsole.MarkupLine("[steelblue]Access token revoked.[/]"); } // If a refresh token was returned by the authorization server, ask the user // if the access token should be refreshed using the refresh_token grant. if (!string.IsNullOrEmpty(response.RefreshToken) && await RefreshTokenAsync(stoppingToken)) { AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity:[/]"); AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new() { CancellationToken = stoppingToken, ProviderName = provider, RefreshToken = response.RefreshToken })).Principal)); } } else if (type is GrantTypes.ClientCredentials) { AnsiConsole.MarkupLine("[cyan]Sending the token request.[/]"); // Ask OpenIddict to authenticate the client application using the client credentials grant. await _service.AuthenticateWithClientCredentialsAsync(new() { CancellationToken = stoppingToken, ProviderName = provider }); AnsiConsole.MarkupLine("[green]Client credentials authentication successful.[/]"); } } } catch (OperationCanceledException) { AnsiConsole.MarkupLine("[red]The authentication process was aborted.[/]"); } catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied) { AnsiConsole.MarkupLine("[yellow]The authorization was denied by the end user.[/]"); } catch (Exception exception) { AnsiConsole.MarkupLine("[red]An error occurred while trying to authenticate the user:[/]"); AnsiConsole.WriteException(exception); } } static Table CreateClaimTable(ClaimsPrincipal principal) { var table = new Table() .LeftAligned() .AddColumn("Claim type") .AddColumn("Claim value type") .AddColumn("Claim value") .AddColumn("Claim issuer"); foreach (var claim in principal.Claims) { table.AddRow( claim.Type.EscapeMarkup(), claim.ValueType.EscapeMarkup(), claim.Value.EscapeMarkup(), claim.Issuer.EscapeMarkup()); } return table; } Task GetSelectedProviderAsync(CancellationToken cancellationToken) { async Task PromptAsync() => AnsiConsole.Prompt(new SelectionPrompt() .Title("Select the authentication provider you'd like to log in with.") .AddChoices(from registration in await _service.GetClientRegistrationsAsync(stoppingToken) where !string.IsNullOrEmpty(registration.ProviderName) where !string.IsNullOrEmpty(registration.ProviderDisplayName) select registration) .UseConverter(registration => registration.ProviderDisplayName!)).ProviderName!; return WaitAsync(Task.Run(PromptAsync, cancellationToken), cancellationToken); } Task<(string? GrantType, string? ResponseType)> GetSelectedFlowAsync( OpenIddictConfiguration configuration, CancellationToken cancellationToken) { static (string? GrantType, string? ResponseType) Prompt(OpenIddictConfiguration configuration) { List<((string? GrantType, string? ResponseType), string DisplayName)> choices = []; var types = configuration.ResponseTypesSupported.Select(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))) { choices.Add((( GrantType : GrantTypes.AuthorizationCode, ResponseType: ResponseTypes.Code), "Authorization code flow")); } if (configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) { if (types.Any(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))) { choices.Add((( GrantType : GrantTypes.Implicit, ResponseType: ResponseTypes.IdToken + ' ' + ResponseTypes.Token), "Implicit flow (id_token + token)")); } } if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) && configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) { if (types.Any(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) && 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))) { 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))) { choices.Add((( GrantType : null, ResponseType: ResponseTypes.None), "\"None flow\" (no token is returned)")); } if (choices.Count is 0) { throw new NotSupportedException("The selected provider doesn't support any of the flows implemented by this sample."); } choices.Insert(0, ((null, null), "Let OpenIddict negotiate the best authentication flow")); return AnsiConsole.Prompt(new SelectionPrompt<((string? GrantType, string? ResponseType), string DisplayName)>() .Title("Select the user interactive grant type you'd like to use.") .AddChoices(choices) .UseConverter(choice => choice.DisplayName)).Item1; } return WaitAsync(Task.Run(() => Prompt(configuration), cancellationToken), cancellationToken); } Task GetSelectedGrantTypeAsync(OpenIddictConfiguration configuration, CancellationToken cancellationToken) { static string Prompt(OpenIddictConfiguration configuration) { List<(string GrantType, string DisplayName)> choices = []; if (configuration.GrantTypesSupported.Contains(GrantTypes.DeviceCode) && configuration.DeviceAuthorizationEndpoint is not null && configuration.TokenEndpoint is not null) { choices.Add((GrantTypes.DeviceCode, "Device authorization code grant")); } if (configuration.GrantTypesSupported.Contains(GrantTypes.Password) && configuration.TokenEndpoint is not null) { choices.Add((GrantTypes.Password, "Resource owner password credentials grant")); } if (configuration.GrantTypesSupported.Contains(GrantTypes.ClientCredentials) && configuration.TokenEndpoint is not null) { choices.Add((GrantTypes.ClientCredentials, "Client credentials grant (application authentication only)")); } if (choices.Count is 0) { throw new NotSupportedException("The selected provider doesn't support any of the grant types implemented by this sample."); } return AnsiConsole.Prompt(new SelectionPrompt<(string GrantType, string DisplayName)>() .Title("Select the grant type you'd like to use.") .AddChoices(choices) .UseConverter(choice => choice.DisplayName)).GrantType; } return WaitAsync(Task.Run(() => Prompt(configuration), cancellationToken), cancellationToken); } Task AuthenticateUserInteractivelyAsync( OpenIddictConfiguration configuration, CancellationToken cancellationToken) { static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( "Would you like to use a user-interactive authentication method?") { Comparer = StringComparer.CurrentCultureIgnoreCase, DefaultValue = true, ShowDefaultValue = true }); if (configuration.GrantTypesSupported.Contains(GrantTypes.AuthorizationCode) || configuration.GrantTypesSupported.Contains(GrantTypes.Implicit)) { return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } return Task.FromResult(false); } Task GetUsernameAsync(CancellationToken cancellationToken) { static string Prompt() => AnsiConsole.Prompt(new TextPrompt("Please enter your username:") { AllowEmpty = false, IsSecret = false }); return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } Task GetPasswordAsync(CancellationToken cancellationToken) { static string Prompt() => AnsiConsole.Prompt(new TextPrompt("Please enter your password:") { AllowEmpty = false, IsSecret = true }); return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } static Task RefreshTokenAsync(CancellationToken cancellationToken) { static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( "Would you like to refresh the user authentication using the refresh token grant?") { Comparer = StringComparer.CurrentCultureIgnoreCase, DefaultValue = false, ShowDefaultValue = true }); return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } static Task IntrospectAccessTokenAsync(CancellationToken cancellationToken) { static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( "Would you like to introspect the access token?") { Comparer = StringComparer.CurrentCultureIgnoreCase, DefaultValue = false, ShowDefaultValue = true }); return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } static Task LogOutAsync(CancellationToken cancellationToken) { static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt("Would you like to log out?") { Comparer = StringComparer.CurrentCultureIgnoreCase, DefaultValue = false, ShowDefaultValue = true }); return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } static Task RevokeAccessTokenAsync(CancellationToken cancellationToken) { static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( "Would you like to revoke the access token?") { Comparer = StringComparer.CurrentCultureIgnoreCase, DefaultValue = false, ShowDefaultValue = true }); return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); } static async Task WaitAsync(Task task, CancellationToken cancellationToken) { #if SUPPORTS_TASK_WAIT_ASYNC return await task.WaitAsync(cancellationToken); #else var source = new TaskCompletionSource(TaskCreationOptions.None); using (cancellationToken.Register(static state => ((TaskCompletionSource) state!).SetResult(true), source)) { if (await Task.WhenAny(task, source.Task) == source.Task) { throw new OperationCanceledException(cancellationToken); } return await task; } #endif } } }