-
-
Notifications
You must be signed in to change notification settings - Fork 301
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a new Sorgan.Console.Client sample and update Kalarba.Client to t…
…arget .NET Framework 4.8
- Loading branch information
1 parent
a85c962
commit fd3964d
Showing
9 changed files
with
372 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
231 changes: 231 additions & 0 deletions
231
samples/Sorgan/Sorgan.Console.Client/InteractiveService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
using System.Security.Claims; | ||
using Microsoft.Extensions.Hosting; | ||
using OpenIddict.Client; | ||
using Spectre.Console; | ||
using static OpenIddict.Abstractions.OpenIddictConstants; | ||
using static OpenIddict.Abstractions.OpenIddictExceptions; | ||
|
||
namespace Sorgan.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<bool>(); | ||
using (_lifetime.ApplicationStarted.Register(static state => ((TaskCompletionSource<bool>) state!).SetResult(true), source)) | ||
{ | ||
await source.Task; | ||
} | ||
|
||
while (!stoppingToken.IsCancellationRequested) | ||
{ | ||
var provider = await GetSelectedProviderAsync(stoppingToken); | ||
|
||
try | ||
{ | ||
// Resolve the server configuration and determine the type of flow | ||
// to use depending on the supported grants and the user selection. | ||
var configuration = await _service.GetServerConfigurationByProviderNameAsync(provider, stoppingToken); | ||
if (configuration.GrantTypesSupported.Contains(GrantTypes.DeviceCode) && | ||
configuration.DeviceAuthorizationEndpoint is not null && | ||
await UseDeviceAuthorizationGrantAsync(stoppingToken)) | ||
{ | ||
// 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 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 UseRefreshTokenGrantAsync(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 | ||
{ | ||
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() | ||
{ | ||
CancellationToken = stoppingToken, | ||
ProviderName = provider | ||
}); | ||
|
||
AnsiConsole.MarkupLine("[cyan]Waiting for the user to approve the authorization demand.[/]"); | ||
|
||
// Wait for the user to complete the authorization process. | ||
var response = await _service.AuthenticateInteractivelyAsync(new() | ||
{ | ||
CancellationToken = stoppingToken, | ||
Nonce = result.Nonce | ||
}); | ||
|
||
AnsiConsole.MarkupLine("[green]Interactive authentication successful:[/]"); | ||
AnsiConsole.Write(CreateClaimTable(response.Principal)); | ||
|
||
// 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 UseRefreshTokenGrantAsync(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)); | ||
} | ||
} | ||
} | ||
|
||
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 | ||
{ | ||
AnsiConsole.MarkupLine("[red]An error occurred while trying to authenticate the user.[/]"); | ||
} | ||
} | ||
|
||
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; | ||
} | ||
|
||
static Task<bool> UseDeviceAuthorizationGrantAsync(CancellationToken cancellationToken) | ||
{ | ||
static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt( | ||
"Would you like to authenticate using the device authorization grant?") | ||
{ | ||
Comparer = StringComparer.CurrentCultureIgnoreCase, | ||
DefaultValue = false, | ||
ShowDefaultValue = true | ||
}); | ||
|
||
return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken); | ||
} | ||
|
||
static Task<bool> UseRefreshTokenGrantAsync(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); | ||
} | ||
|
||
Task<string> GetSelectedProviderAsync(CancellationToken cancellationToken) | ||
{ | ||
async Task<string> PromptAsync() => AnsiConsole.Prompt(new SelectionPrompt<OpenIddictClientRegistration>() | ||
.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); | ||
} | ||
|
||
static async Task<T> WaitAsync<T>(Task<T> task, CancellationToken cancellationToken) | ||
{ | ||
#if SUPPORTS_TASK_WAIT_ASYNC | ||
return await task.WaitAsync(cancellationToken); | ||
#else | ||
var source = new TaskCompletionSource<bool>(TaskCreationOptions.None); | ||
|
||
using (cancellationToken.Register(static state => ((TaskCompletionSource<bool>) state!).SetResult(true), source)) | ||
{ | ||
if (await Task.WhenAny(task, source.Task) == source.Task) | ||
{ | ||
throw new OperationCanceledException(cancellationToken); | ||
} | ||
|
||
return await task; | ||
} | ||
#endif | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Hosting; | ||
using Microsoft.Extensions.Logging; | ||
using Sorgan.Console.Client; | ||
|
||
var host = new HostBuilder() | ||
// Note: applications for which a single instance is preferred can reference | ||
// the Dapplo.Microsoft.Extensions.Hosting.AppServices package and call this | ||
// method to automatically close extra instances based on the specified identifier: | ||
// | ||
// .ConfigureSingleInstance(options => options.MutexId = "{5519A32F-5B86-4CBB-A601-0CC7872A126A}") | ||
// | ||
.ConfigureLogging(options => options.AddDebug()) | ||
.ConfigureServices(services => | ||
{ | ||
services.AddDbContext<DbContext>(options => | ||
{ | ||
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sorgan-console-client.sqlite3")}"); | ||
options.UseOpenIddict(); | ||
}); | ||
|
||
services.AddOpenIddict() | ||
|
||
// Register the OpenIddict core components. | ||
.AddCore(options => | ||
{ | ||
// Configure OpenIddict to use the Entity Framework Core stores and models. | ||
// Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities. | ||
options.UseEntityFrameworkCore() | ||
.UseDbContext<DbContext>(); | ||
}) | ||
|
||
// Register the OpenIddict client components. | ||
.AddClient(options => | ||
{ | ||
// Note: this sample uses the authorization code, device authorization code | ||
// and refresh token flows, but you can enable the other flows if necessary. | ||
options.AllowAuthorizationCodeFlow() | ||
.AllowDeviceCodeFlow() | ||
.AllowRefreshTokenFlow(); | ||
|
||
// Register the signing and encryption credentials used to protect | ||
// sensitive data like the state tokens produced by OpenIddict. | ||
options.AddDevelopmentEncryptionCertificate() | ||
.AddDevelopmentSigningCertificate(); | ||
|
||
// Add the operating system integration. | ||
options.UseSystemIntegration(); | ||
|
||
// Register the System.Net.Http integration and use the identity of the current | ||
// assembly as a more specific user agent, which can be useful when dealing with | ||
// providers that use the user agent as a way to throttle requests (e.g Reddit). | ||
options.UseSystemNetHttp() | ||
.SetProductInformation(typeof(Program).Assembly); | ||
|
||
// Register the Web providers integrations. | ||
// | ||
// Note: to mitigate mix-up attacks, it's recommended to use a unique redirection endpoint | ||
// address per provider, unless all the registered providers support returning an "iss" | ||
// parameter containing their URL as part of authorization responses. For more information, | ||
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4. | ||
options.UseWebProviders() | ||
.AddGitHub(options => | ||
{ | ||
options.SetClientId("5c11c030ca570e8c5a16") | ||
.SetClientSecret("a5d36464b2ac2fe3e87fbfb95f0ebcf06c5992c1") | ||
.SetRedirectUri("callback/login/github"); | ||
}); | ||
}); | ||
|
||
// Register the worker responsible for creating the database used to store tokens | ||
// and adding the registry entries required to register the custom URI scheme. | ||
// | ||
// Note: in a real world application, this step should be part of a setup script. | ||
services.AddHostedService<Worker>(); | ||
|
||
// Register the background service responsible for handling the console interactions. | ||
services.AddHostedService<InteractiveService>(); | ||
}) | ||
.UseConsoleLifetime() | ||
.Build(); | ||
|
||
await host.RunAsync(); |
Oops, something went wrong.