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

Feature/blazor wasm #379

Merged
merged 12 commits into from
Jul 28, 2023
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
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