@ -1,5 +1,6 @@
using System.Security.Claims ;
using Microsoft.Extensions.Hosting ;
using OpenIddict.Abstractions ;
using OpenIddict.Client ;
using Spectre.Console ;
using static OpenIddict . Abstractions . OpenIddictConstants ;
@ -39,174 +40,21 @@ public class InteractiveService : BackgroundService
try
{
var type = await GetSelectedGrantTypeAsync ( provider , stoppingToken ) ;
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.[/]" ) ;
}
var configuration = await _ service . GetServerConfigurationByProviderNameAsync ( provider , stoppingToken ) ;
else if ( type is GrantTypes . Password )
if ( await AuthenticateUserInteractivelyAsync ( configuration , stoppingToken ) )
{
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.
var configuration = await _ service . GetServerConfigurationByProviderNameAsync ( provider , stoppingToken ) ;
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
} ) ;
var flow = await GetSelectedFlowAsync ( configuration , stoppingToken ) ;
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 . 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.
var configuration = await _ service . GetServerConfigurationByProviderNameAsync ( provider , stoppingToken ) ;
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 . AuthorizationCode )
{
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
ProviderName = provider ,
ResponseType = flow . ResponseType
} ) ;
AnsiConsole . MarkupLine ( "[cyan]Waiting for the user to approve the authorization demand.[/]" ) ;
@ -224,7 +72,6 @@ public class InteractiveService : BackgroundService
// 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.
var configuration = await _ service . GetServerConfigurationByProviderNameAsync ( provider , stoppingToken ) ;
if ( ! string . IsNullOrEmpty ( response . BackchannelAccessToken ) & &
configuration . IntrospectionEndpoint is not null & &
await IntrospectAccessTokenAsync ( stoppingToken ) )
@ -297,6 +144,166 @@ public class InteractiveService : BackgroundService
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 )
@ -350,20 +357,101 @@ public class InteractiveService : BackgroundService
return WaitAsync ( Task . Run ( PromptAsync , cancellationToken ) , cancellationToken ) ;
}
Task < string > GetSelectedGrantTypeAsync ( string provider , CancellationToken cancellationToken )
Task < ( string? GrantType , string? ResponseType ) > GetSelectedFlowAsync (
OpenIddictConfiguration configuration , CancellationToken cancellationToken )
{
async Task < string > PromptAsync ( )
static ( string? GrantType , string? ResponseType ) Prompt ( OpenIddictConfiguration configuration )
{
List < ( string GrantType , string DisplayName ) > choices = [ ] ;
List < ( ( string? GrantType , string? ResponseType ) , string DisplayName ) > choices = [ ] ;
var types = configuration . ResponseTypesSupported . Select ( type = >
new HashSet < string > ( type . Split ( Separators . Space , StringSplitOptions . RemoveEmptyEntries ) ) ) ;
var configuration = await _ service . GetServerConfigurationByProviderNameAsync ( provider , stoppingToken ) ;
if ( configuration . GrantTypesSupported . Contains ( GrantTypes . AuthorizationCode ) & &
configuration . AuthorizationEndpoint is not null & &
configuration . TokenEndpoint is not null )
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 ) )
{
choices . Add ( ( GrantTypes . AuthorizationCode , "Authorization code grant" ) ) ;
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 < string > 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 )
@ -385,12 +473,7 @@ public class InteractiveService : BackgroundService
if ( choices . Count is 0 )
{
choices . Add ( ( GrantTypes . AuthorizationCode , "Authorization code grant" ) ) ;
}
if ( choices . Count is 1 )
{
return choices [ 0 ] . GrantType ;
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 ) > ( )
@ -399,7 +482,27 @@ public class InteractiveService : BackgroundService
. UseConverter ( choice = > choice . DisplayName ) ) . GrantType ;
}
return WaitAsync ( Task . Run ( PromptAsync , cancellationToken ) , cancellationToken ) ;
return WaitAsync ( Task . Run ( ( ) = > Prompt ( configuration ) , cancellationToken ) , cancellationToken ) ;
}
Task < bool > 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 < string > GetUsernameAsync ( CancellationToken cancellationToken )
@ -424,7 +527,6 @@ public class InteractiveService : BackgroundService
return WaitAsync ( Task . Run ( Prompt , cancellationToken ) , cancellationToken ) ;
}
static Task < bool > RefreshTokenAsync ( CancellationToken cancellationToken )
{
static bool Prompt ( ) = > AnsiConsole . Prompt ( new ConfirmationPrompt (