Skip to content

Commit

Permalink
Add a new Sorgan.Console.Client sample and update Kalarba.Client to t…
Browse files Browse the repository at this point in the history
…arget .NET Framework 4.8
  • Loading branch information
kevinchalet committed Feb 20, 2024
1 parent a85c962 commit fd3964d
Show file tree
Hide file tree
Showing 9 changed files with 372 additions and 5 deletions.
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
<PackageVersion Include="OpenIddict.Validation.AspNetCore" Version="5.2.0" />
<PackageVersion Include="OpenIddict.Validation.SystemNetHttp" Version="5.2.0" />
<PackageVersion Include="Quartz.Extensions.Hosting" Version="3.5.0" />
<PackageVersion Include="Spectre.Console" Version="0.46.0" />
<PackageVersion Include="Spectre.Console" Version="0.48.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="Yarp.ReverseProxy" Version="2.0.1" />
</ItemGroup>
Expand Down
7 changes: 7 additions & 0 deletions OpenIddict.Samples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sorgan.Wpf.Client", "sample
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sorgan.BlazorHybrid.Client", "samples\Sorgan\Sorgan.BlazorHybrid.Client\Sorgan.BlazorHybrid.Client.csproj", "{C392496F-B3E4-4B7C-97F3-66EB13206985}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sorgan.Console.Client", "samples\Sorgan\Sorgan.Console.Client\Sorgan.Console.Client.csproj", "{A2B093AC-6044-467E-B94F-936343DCD11B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -290,6 +292,10 @@ Global
{C392496F-B3E4-4B7C-97F3-66EB13206985}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C392496F-B3E4-4B7C-97F3-66EB13206985}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C392496F-B3E4-4B7C-97F3-66EB13206985}.Release|Any CPU.Build.0 = Release|Any CPU
{A2B093AC-6044-467E-B94F-936343DCD11B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A2B093AC-6044-467E-B94F-936343DCD11B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A2B093AC-6044-467E-B94F-936343DCD11B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A2B093AC-6044-467E-B94F-936343DCD11B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -347,6 +353,7 @@ Global
{6E1B3224-B529-4B45-AD66-969BBBA08F63} = {F2076FDE-06F9-441B-938E-97953A3C0906}
{5132ABBD-6FC5-4232-B9E1-7F53EC52C826} = {F2076FDE-06F9-441B-938E-97953A3C0906}
{C392496F-B3E4-4B7C-97F3-66EB13206985} = {F2076FDE-06F9-441B-938E-97953A3C0906}
{A2B093AC-6044-467E-B94F-936343DCD11B} = {F2076FDE-06F9-441B-938E-97953A3C0906}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F3ECDD26-F40D-4AB4-BC48-8DF143F98FAE}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This repository contains samples demonstrating **how to use [OpenIddict](https:/

## .NET samples

- [Sorgan](samples/Sorgan): Windows Forms, Windows Presentation Foundation and Blazor Hybrid clients using GitHub for user authentication.
- [Sorgan](samples/Sorgan): console, Windows Forms, Windows Presentation Foundation and Blazor Hybrid clients using GitHub for user authentication.

## OWIN/ASP.NET 4.8 samples
- [Fornax](samples/Fornax): authorization code flow demo using ASP.NET Web Forms 4.8 and OWIN/Katana, with a .NET Framework 4.8 console acting as the client.
Expand Down
3 changes: 2 additions & 1 deletion samples/Kalarba/Kalarba.Client/Kalarba.Client.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net48</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="OpenIddict.Client.SystemNetHttp" />
</ItemGroup>

Expand Down
5 changes: 3 additions & 2 deletions samples/Kalarba/Kalarba.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
});
});

await using var provider = services.BuildServiceProvider();
using var provider = services.BuildServiceProvider();

var token = await GetTokenAsync(provider, "alice@wonderland.com", "P@ssw0rd");
Console.WriteLine("Access token: {0}", token);
Expand All @@ -58,7 +58,8 @@ static async Task<string> GetTokenAsync(IServiceProvider provider, string email,

static async Task<string> GetResourceAsync(IServiceProvider provider, string token)
{
using var client = provider.GetRequiredService<HttpClient>();
var factory = provider.GetRequiredService<IHttpClientFactory>();
using var client = factory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:58779/api/message");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);

Expand Down
231 changes: 231 additions & 0 deletions samples/Sorgan/Sorgan.Console.Client/InteractiveService.cs
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
}
}
}
84 changes: 84 additions & 0 deletions samples/Sorgan/Sorgan.Console.Client/Program.cs
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();
Loading

0 comments on commit fd3964d

Please sign in to comment.