Skip to content

Commit

Permalink
Adding Blazor examples wasm/server (#379)
Browse files Browse the repository at this point in the history
* Add Blazor WebAuthn lib

* Razor lib: Add transports to attestation response.

Note that this is not yet implemented by Firefox, so we have to return empty-listed if we can't get transports.

* Blazor: Create basic structure, style, and MFA page

* Add footer

* Add passwordless

* Add usernameless

* Add custom demo

* Blazor Client: Update userservice for new featues

* Add docker support for onrender deployment

* Less SouceLink but more nodejs in docker builder

* Return actual errors, use render as origin

* Add Blazor demo to pipeline config

Also publishWebProjects has to be explicitly false to honour the 'projects' parameter.
  • Loading branch information
Regenhardt committed Jul 28, 2023
1 parent 53caf81 commit 5cb0f17
Show file tree
Hide file tree
Showing 71 changed files with 11,130 additions and 3 deletions.
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ bld/
[Bb]in/
[Oo]bj/
[Ll]og/
/Src/Fido2.BlazorWebAssembly/wwwroot/js/WebAuthn.js
/Src/Fido2.BlazorWebAssembly/wwwroot/js/WebAuthn.js.map

# Visual Studio 2015/2017 cache/options directory
.vs/
Expand Down
12 changes: 12 additions & 0 deletions BlazorWasmDemo/Client/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
19 changes: 19 additions & 0 deletions BlazorWasmDemo/Client/BlazorWasmDemo.Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.13" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Src\Fido2.BlazorWebAssembly\Fido2.BlazorWebAssembly.csproj" />
<ProjectReference Include="..\..\Src\Fido2.Models\Fido2.Models.csproj" />
</ItemGroup>

</Project>
171 changes: 171 additions & 0 deletions BlazorWasmDemo/Client/Pages/Custom.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
@page "/custom"
@using BlazorWasmDemo.Client.Shared.Toasts
@using Fido2NetLib.Objects
@inject UserService UserService
@inject ToastService Toasts

<h3>Custom</h3>

<p>In this scenario we have removed the need for passwords. We will use the settings set by you when registering your credentials. This is useful if you want to try differences or browser support etc.</p>
<p>Note: When we say passwordless, what we mean is that no password is sent over the internet or stored in a database. Password, PINs or Biometrics might be used by the authenticator on the client</p>

@if (!WebAuthnSupported)
{
<div class="alert alert-danger">
Please note: Your browser does not seem to support WebAuthn yet. <a href="https://caniuse.com/#search=webauthn" target="_blank">Supported browsers</a>
</div>
}

<section class="row">
<div class="col">

<h3>Create an account</h3>
<form>
<label for="register-username">Username</label>
<div class="input-group">
<div class="input-group-text">
<span class="fas fa-user"></span>
</div>
<input class="form-control" type="text" placeholder="abergs" id="register-username" @bind="RegisterUsername" required>
</div>

<label for="displayName">Display name</label>
<div class="input-group">
<div class="input-group-text">
<span class="fas fa-user">
</span>
</div>
<input class="form-control" type="text" placeholder="Anders Åberg" id="displayName" @bind="RegisterDisplayName">
</div>
</form>
<div class="input-group">
<button class="btn btn-primary" disabled="@(!RegisterFormValid())" @onclick="Register">Create account</button>
</div>
</div>
<div class="col-2"></div>
<div class="col">

<h3>Sign in</h3>
<form>
<label for="login-username">Username</label>
<div class="input-group">
<div class="input-group-text">
<span class="fas fa-user">
</span>
</div>
<input class="form-control" type="text" placeholder="abergs" id="login-username" required @bind="LoginUsername">
</div>
</form>
<div class="input-group">
<button class="btn btn-primary" disabled="@(!LoginFormValid())" @onclick="Login">Sign in</button>
</div>
</div>
</section>
<section>
<h6 class="fw-bold">Advanced settings</h6>
<p>These settings are typically administred by the RP but for demo purposes we expose them to you for testing behaviours and browser support</p>

<label>Attestation type</label>
<div class="input-group">
<select class="form-select w-auto" @bind="AttestationType">
@foreach (var value in Enum.GetValues<AttestationConveyancePreference>())
{
<option value="@value">@value</option>
}
</select>
</div>

<label>Authenticator</label>
<div class="input-group">
<select class="form-select w-auto" @bind="Authenticator">
<option value="@(new AuthenticatorAttachment?())">Not specified</option>
<option value="@((AuthenticatorAttachment?)AuthenticatorAttachment.CrossPlatform)">Cross-platform (Token)</option>
<option value="@((AuthenticatorAttachment?)AuthenticatorAttachment.Platform)">Platform (TPM)</option>
</select>
</div>

<label>User verification</label>
<div class="input-group">
<select class="form-select w-auto" @bind="UserVerification">
<option value="@UserVerificationRequirement.Discouraged">Discouraged</option>
<option value="@UserVerificationRequirement.Preferred">Preferred</option>
<option value="@UserVerificationRequirement.Required">Required</option>
</select>
</div>

<label>Resident key</label>
<div class="input-group">
<select class="form-select w-auto" @bind="ResidentKey">
<option value="@ResidentKeyRequirement.Discouraged">Discouraged</option>
<option value="@ResidentKeyRequirement.Preferred">Preferred</option>
<option value="@ResidentKeyRequirement.Required">Required</option>
</select>
</div>
</section>

<section class="pt-5">
<p>
Read the source code for this demo here: <a href="@(Constants.GithubBaseUrl+"BlazorWasmDemo/Client/Pages/Custom.razor")">Custom.razor</a> and <a href="@(Constants.GithubBaseUrl+"BlazorWasmDemo/Client/Shared/UserService.cs")">UserService.cs</a>
</p>
</section>
@code {
private bool WebAuthnSupported { get; set; } = true;

private string RegisterUsername { get; set; } = "";
private string? RegisterDisplayName { get; set; }

private string LoginUsername { get; set; } = "";

private AttestationConveyancePreference AttestationType { get; set; }

private AuthenticatorAttachment? Authenticator { get; set; }

private UserVerificationRequirement UserVerification { get; set; } = UserVerificationRequirement.Discouraged;

private ResidentKeyRequirement ResidentKey { get; set; } = ResidentKeyRequirement.Preferred;

protected override async Task OnInitializedAsync()
{
WebAuthnSupported = await UserService.IsWebAuthnSupportedAsync();
}

private bool RegisterFormValid() => !string.IsNullOrWhiteSpace(RegisterUsername);
private async Task Register()
{
var username = RegisterUsername;
var displayName = RegisterDisplayName;

var result = await UserService.RegisterAsync(
username,
displayName,
AttestationType,
Authenticator,
UserVerification,
ResidentKey);

if (result == "OK")
{
Toasts.ShowToast("Registration successful", ToastLevel.Success);
}
else
{
Toasts.ShowToast(result, ToastLevel.Error);
}
}

private bool LoginFormValid() => !string.IsNullOrWhiteSpace(LoginUsername);
private async Task Login()
{
var result = await UserService.LoginAsync(LoginUsername);

if (result.StartsWith("Bearer"))
{
Toasts.ShowToast($"Login successful, JWT received", ToastLevel.Success);
Console.WriteLine($"Token: {result.Replace("Bearer ", "")}");
}
else
{
Toasts.ShowToast(result, ToastLevel.Error);
}
}
}
150 changes: 150 additions & 0 deletions BlazorWasmDemo/Client/Pages/Mfa.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
@page "/mfa"
@using BlazorWasmDemo.Client.Shared.Toasts
@inject UserService UserService
@inject ToastService Toasts

<h1>Scenario: 2FA/MFA</h1>
<div class="content">
<p>This is scenario where we just want to use FIDO as the MFA. The user register and logins with their username and password. For demo purposes, we trigger the MFA registering on sign up.</p>
</div>
@if (!WebAuthnSupported)
{
<div class="alert alert-danger">
Please note: Your browser does not seem to support WebAuthn yet. <a href="https://caniuse.com/#search=webauthn" target="_blank">Supported browsers</a>
</div>
}

<section class="row">
<div class="col">

<h3>Create an account</h3>
<form>
<label for="register-username">Username</label>
<div class="input-group">
<div class="input-group-text">
<span class="fas fa-user"></span>
</div>
<input class="form-control" type="text" placeholder="abergs" id="register-username" @bind="RegisterUsername" required>
</div>

<label for="displayName">Display name</label>
<div class="input-group">
<div class="input-group-text">
<span class="fas fa-user">
</span>
</div>
<input class="form-control" type="text" placeholder="Anders Åberg" id="displayName" @bind="RegisterDisplayName">
</div>

<label for="register-password">Password</label>
<div class="input-group">
<div class="input-group-text">
<span class="fas fa-user">
</span>
</div>
<input class="form-control" type="password" placeholder="Do not use something secret" id="register-password">
</div>
<p>
<small>For demo purposes the password will not be used or stored</small>
</p>

<label class="checkbox">
<input type="checkbox" disabled checked readonly>
Register MFA on registration
</label>
</form>
<div class="input-group">
<button class="btn btn-primary" disabled="@(!RegisterFormValid())" @onclick="Register">Create account</button>
</div>
</div>
<div class="col-2"></div>
<div class="col">

<h3>Sign in</h3>
<form>
<label for="login-username">Username</label>
<div class="input-group">
<div class="input-group-text">
<span class="fas fa-user">
</span>
</div>
<input class="form-control" type="text" placeholder="abergs" id="login-username" required @bind="LoginUsername">
</div>

<label for="login-password">Password</label>
<div class="input-group">
<div class="input-group-text">
<span class="fas fa-user">
</span>
</div>
<input class="form-control" type="password" placeholder="Do not use something secret" id="login-password">
</div>
<p><small>For demo purposes the password will not be used or stored</small></p>
</form>
<div class="input-group">
<button class="btn btn-primary" disabled="@(!LoginFormValid())" @onclick="Login">Sign in</button>
</div>
</div>
</section>

<section class="pt-5">
<h1>Explanation: 2FA/MFA with FIDO2</h1>
<p>
In this scenario, WebAuthn is only used as second factor mechanism. MFA stands for Multi Factor Authentication which generally means it relies on <i>something the user knows</i> (username &amp; password) and <i>something the user has</i> (Authenticator Private key).
The flow is visualized in the figure below.
</p>
<img src="images/scenario1.png" alt="figure visualizing username and password sent together with assertion" />
<p>In this flow the Relying Party does not necessarily need to tell the Authenticator device to verify the human identity (we could set UserVerification to discourage) to minimize user interactions needed to sign in. More on UserVerification in the other scenarios.</p>

<p>
Read the source code for this demo here: <a href="@(Constants.GithubBaseUrl+"BlazorWasmDemo/Client/Pages/Mfa.razor")">Mfa.razor</a> and <a href="@(Constants.GithubBaseUrl+"BlazorWasmDemo/Client/Shared/UserService.cs")">UserService.cs</a>
</p>
</section>

@code
{
private bool WebAuthnSupported { get; set; } = true;

private string RegisterUsername { get; set; } = "";
private string? RegisterDisplayName { get; set; }

private string LoginUsername { get; set; } = "";

protected override async Task OnInitializedAsync()
{
WebAuthnSupported = await UserService.IsWebAuthnSupportedAsync();
}

private bool RegisterFormValid() => !string.IsNullOrWhiteSpace(RegisterUsername);
private async Task Register()
{
var username = RegisterUsername;
var displayName = RegisterDisplayName;

var result = await UserService.RegisterAsync(username, displayName);

if (result == "OK")
{
Toasts.ShowToast("Registration successful", ToastLevel.Success);
}
else
{
Toasts.ShowToast(result, ToastLevel.Error);
}
}

private bool LoginFormValid() => !string.IsNullOrWhiteSpace(LoginUsername);
private async Task Login()
{
var result = await UserService.LoginAsync(LoginUsername);

if (result.StartsWith("Bearer"))
{
Toasts.ShowToast($"Login successful, token:{Environment.NewLine}{result.Replace("Bearer ", string.Empty)}", ToastLevel.Success);
}
else
{
Toasts.ShowToast(result, ToastLevel.Error);
}
}
}
Loading

0 comments on commit 5cb0f17

Please sign in to comment.