Skip to content

Commit

Permalink
Add a Blazor Hybrid sample using GitHub authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinchalet committed Jan 12, 2024
1 parent 9ebebb6 commit e313185
Show file tree
Hide file tree
Showing 21 changed files with 469 additions and 11 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="8.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Wpf" Version="8.0.3" />
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.1" />
Expand Down
7 changes: 7 additions & 0 deletions OpenIddict.Samples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sorgan.WinForms.Client", "s
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sorgan.Wpf.Client", "samples\Sorgan\Sorgan.Wpf.Client\Sorgan.Wpf.Client.csproj", "{5132ABBD-6FC5-4232-B9E1-7F53EC52C826}"
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -284,6 +286,10 @@ Global
{5132ABBD-6FC5-4232-B9E1-7F53EC52C826}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5132ABBD-6FC5-4232-B9E1-7F53EC52C826}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5132ABBD-6FC5-4232-B9E1-7F53EC52C826}.Release|Any CPU.Build.0 = Release|Any CPU
{C392496F-B3E4-4B7C-97F3-66EB13206985}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -340,6 +346,7 @@ Global
{F2076FDE-06F9-441B-938E-97953A3C0906} = {8B467944-153B-4C90-BAB1-8F1B34C3075A}
{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}
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 @@ -12,7 +12,7 @@ This repository contains samples demonstrating **how to use [OpenIddict](https:/
- [Imynusoph](samples/Imynusoph): refresh token grant demo, with a .NET console acting as the client.
- [Matty](samples/Matty): device authorization flow demo, with a .NET console acting as the client.
- [Mimban](samples/Mimban): authorization code flow demo using minimal APIs and GitHub delegation for user authentication, with a .NET console acting as the client.
- [Sorgan](samples/Sorgan): Windows Forms and Windows Presentation Foundation clients using GitHub for user authentication.
- [Sorgan](samples/Sorgan): Windows Forms, Windows Presentation Foundation and Blazor Hybrid clients using GitHub for user authentication.
- [Velusia](samples/Velusia): authorization code flow demo, with an ASP.NET Core application acting as the client.
- [Weytta](samples/Weytta): authorization code flow with Integrated Windows Authentication support and a .NET console acting as the client.
- [Zirku](samples/Zirku): authorization code flow demo using minimal APIs with 2 hard-coded user identities, a .NET console and a SPA acting as the clients and two API projects using introspection (Api1) and local validation (Api2).
Expand Down
6 changes: 6 additions & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Application x:Class="Sorgan.BlazorHybrid.Client.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Sorgan.BlazorHybrid.Client"
StartupUri="MainWindow.xaml">
</Application>
7 changes: 7 additions & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Windows;

namespace Sorgan.BlazorHybrid.Client;

public partial class App : Application
{
}
70 changes: 70 additions & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/Login.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@using OpenIddict.Client;
@using System.Security.Claims;
@using System.Threading
@using System.Windows
@using static OpenIddict.Abstractions.OpenIddictExceptions
@using static OpenIddict.Abstractions.OpenIddictConstants
@inject OpenIddictClientService service;

<div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
<button disabled="@IsButtonDisabled" style="font-size: large" @onclick="LoginAsync">Log in using GitHub</button>
</div>

@code
{
private bool IsButtonDisabled;

public async Task LoginAsync()
{
// Disable the login button to prevent concurrent authentication operations.
IsButtonDisabled = true;

try
{
using var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(90));

try
{
// Ask OpenIddict to initiate the authentication flow (typically, by starting the system browser).
var result = await service.ChallengeInteractivelyAsync(new()
{
CancellationToken = source.Token
});

// Wait for the user to complete the authorization process.
var principal = (await service.AuthenticateInteractivelyAsync(new()
{
CancellationToken = source.Token,
Nonce = result.Nonce
})).Principal;

MessageBox.Show($"Welcome, {principal.FindFirst(ClaimTypes.Name)!.Value}.",
"Authentication successful", MessageBoxButton.OK, MessageBoxImage.Information);
}

catch (OperationCanceledException)
{
MessageBox.Show("The authentication process was aborted.",
"Authentication timed out", MessageBoxButton.OK, MessageBoxImage.Warning);
}

catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied)
{
MessageBox.Show("The authorization was denied by the end user.",
"Authorization denied", MessageBoxButton.OK, MessageBoxImage.Warning);
}

catch
{
MessageBox.Show("An error occurred while trying to authenticate the user.",
"Authentication failed", MessageBoxButton.OK, MessageBoxImage.Error);
}
}

finally
{
// Re-enable the login button to allow starting a new authentication operation.
IsButtonDisabled = false;
}
}
}
17 changes: 17 additions & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Window x:Class="Sorgan.BlazorHybrid.Client.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
xmlns:local="clr-namespace:Sorgan.BlazorHybrid.Client"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<blazor:BlazorWebView HostPage="wwwroot\index.html" Services="{DynamicResource services}">
<blazor:BlazorWebView.RootComponents>
<blazor:RootComponent Selector="#app" ComponentType="{x:Type local:Login}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
</Grid>
</Window>
15 changes: 15 additions & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Windows;
using Dapplo.Microsoft.Extensions.Hosting.Wpf;

namespace Sorgan.BlazorHybrid.Client;

public partial class MainWindow : Window, IWpfShell
{
public MainWindow(IServiceProvider provider)
{
InitializeComponent();

Resources.Add("services", provider);
}
}
95 changes: 95 additions & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.IO;
using System.Windows;
using Dapplo.Microsoft.Extensions.Hosting.Wpf;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Sorgan.BlazorHybrid.Client;

[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]

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 = "{6FBAFC6B-528A-4CB7-A99A-B5DDF5812943}")
//
.ConfigureLogging(options => options.AddDebug())
.ConfigureServices(services =>
{
services.AddDbContext<DbContext>(options =>
{
options.UseSqlite($"Filename={Path.Combine(Path.GetTempPath(), "openiddict-sorgan-blazorhybrid-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 and refresh token
// flows, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow()
.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("2388b26eab831adab80d")
// Note: GitHub doesn't allow creating public clients and requires using a client secret.
.SetClientSecret("5115eeb4c840aeaaa19f7be7ea8b13b992dca765")
// Note: GitHub doesn't support the recommended ":/" syntax and requires using "://", but allows
// using a dynamic/random port that will be dynamically chosen by the OpenIddict system integration.
.SetRedirectUri("http://localhost/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>();

services.AddWpfBlazorWebView();
})
.ConfigureWpf(options =>
{
options.UseApplication<App>();
options.UseWindow<MainWindow>();
})
.UseWpfLifetime()
.Build();

await host.RunAsync();
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<RootNamespace>Sorgan.BlazorHybrid.Client</RootNamespace>
<EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapplo.Microsoft.Extensions.Hosting.Wpf" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Wpf" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="OpenIddict.Client.SystemIntegration" />
<PackageReference Include="OpenIddict.Client.SystemNetHttp" />
<PackageReference Include="OpenIddict.Client.WebIntegration" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/Worker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Sorgan.BlazorHybrid.Client;

public class Worker : IHostedService
{
private readonly IServiceProvider _provider;

public Worker(IServiceProvider provider)
=> _provider = provider;

public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _provider.CreateScope();

var context = scope.ServiceProvider.GetRequiredService<DbContext>();
await context.Database.EnsureCreatedAsync(cancellationToken);
}

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
1 change: 1 addition & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/_Imports.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@using Microsoft.AspNetCore.Components.Web
25 changes: 25 additions & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/wwwroot/Index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenIddict Sorgan Blazor Hybrid client</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="Sorgan.BlazorHybrid.Client.styles.css" rel="stylesheet" />
</head>

<body>
<div id="app">Loading...</div>

<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webview.js"></script>
</body>

</html>
48 changes: 48 additions & 0 deletions samples/Sorgan/Sorgan.BlazorHybrid.Client/wwwroot/css/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1:focus {
outline: none;
}

a, .btn-link {
color: #0071c1;
}

.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}

.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}

.invalid {
outline: 1px solid red;
}

.validation-message {
color: red;
}

#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}

#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
Loading

0 comments on commit e313185

Please sign in to comment.