-
-
Notifications
You must be signed in to change notification settings - Fork 157
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding Blazor examples wasm/server (#379)
* 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
1 parent
53caf81
commit 5cb0f17
Showing
71 changed files
with
11,130 additions
and
3 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
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 |
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
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> |
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,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> |
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,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); | ||
} | ||
} | ||
} |
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,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 & 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); | ||
} | ||
} | ||
} |
Oops, something went wrong.