Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new Sorgan.Console.Client sample and update Kalarba.Client to target .NET Framework 4.8 #296

Merged
merged 1 commit into from
Feb 20, 2024
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
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, "[email protected]", "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
Loading