Skip to content

Commit 5cb0f17

Browse files
authored
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.
1 parent 53caf81 commit 5cb0f17

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+11130
-3
lines changed

.dockerignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
**/.classpath
2+
**/.dockerignore
3+
**/.env
4+
**/.git
5+
**/.gitignore
6+
**/.project
7+
**/.settings
8+
**/.toolstarget
9+
**/.vs
10+
**/.vscode
11+
**/*.*proj.user
12+
**/*.dbmdl
13+
**/*.jfm
14+
**/azds.yaml
15+
**/bin
16+
**/charts
17+
**/docker-compose*
18+
**/Dockerfile*
19+
**/node_modules
20+
**/npm-debug.log
21+
**/obj
22+
**/secrets.dev.yaml
23+
**/values.dev.yaml
24+
LICENSE
25+
README.md

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ bld/
2424
[Bb]in/
2525
[Oo]bj/
2626
[Ll]og/
27+
/Src/Fido2.BlazorWebAssembly/wwwroot/js/WebAuthn.js
28+
/Src/Fido2.BlazorWebAssembly/wwwroot/js/WebAuthn.js.map
2729

2830
# Visual Studio 2015/2017 cache/options directory
2931
.vs/

BlazorWasmDemo/Client/App.razor

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Router AppAssembly="@typeof(App).Assembly">
2+
<Found Context="routeData">
3+
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
4+
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
5+
</Found>
6+
<NotFound>
7+
<PageTitle>Not found</PageTitle>
8+
<LayoutView Layout="@typeof(MainLayout)">
9+
<p role="alert">Sorry, there's nothing at this address.</p>
10+
</LayoutView>
11+
</NotFound>
12+
</Router>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.13" />
11+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.13" PrivateAssets="all" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\..\Src\Fido2.BlazorWebAssembly\Fido2.BlazorWebAssembly.csproj" />
16+
<ProjectReference Include="..\..\Src\Fido2.Models\Fido2.Models.csproj" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
@page "/custom"
2+
@using BlazorWasmDemo.Client.Shared.Toasts
3+
@using Fido2NetLib.Objects
4+
@inject UserService UserService
5+
@inject ToastService Toasts
6+
7+
<h3>Custom</h3>
8+
9+
<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>
10+
<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>
11+
12+
@if (!WebAuthnSupported)
13+
{
14+
<div class="alert alert-danger">
15+
Please note: Your browser does not seem to support WebAuthn yet. <a href="https://caniuse.com/#search=webauthn" target="_blank">Supported browsers</a>
16+
</div>
17+
}
18+
19+
<section class="row">
20+
<div class="col">
21+
22+
<h3>Create an account</h3>
23+
<form>
24+
<label for="register-username">Username</label>
25+
<div class="input-group">
26+
<div class="input-group-text">
27+
<span class="fas fa-user"></span>
28+
</div>
29+
<input class="form-control" type="text" placeholder="abergs" id="register-username" @bind="RegisterUsername" required>
30+
</div>
31+
32+
<label for="displayName">Display name</label>
33+
<div class="input-group">
34+
<div class="input-group-text">
35+
<span class="fas fa-user">
36+
</span>
37+
</div>
38+
<input class="form-control" type="text" placeholder="Anders Åberg" id="displayName" @bind="RegisterDisplayName">
39+
</div>
40+
</form>
41+
<div class="input-group">
42+
<button class="btn btn-primary" disabled="@(!RegisterFormValid())" @onclick="Register">Create account</button>
43+
</div>
44+
</div>
45+
<div class="col-2"></div>
46+
<div class="col">
47+
48+
<h3>Sign in</h3>
49+
<form>
50+
<label for="login-username">Username</label>
51+
<div class="input-group">
52+
<div class="input-group-text">
53+
<span class="fas fa-user">
54+
</span>
55+
</div>
56+
<input class="form-control" type="text" placeholder="abergs" id="login-username" required @bind="LoginUsername">
57+
</div>
58+
</form>
59+
<div class="input-group">
60+
<button class="btn btn-primary" disabled="@(!LoginFormValid())" @onclick="Login">Sign in</button>
61+
</div>
62+
</div>
63+
</section>
64+
<section>
65+
<h6 class="fw-bold">Advanced settings</h6>
66+
<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>
67+
68+
<label>Attestation type</label>
69+
<div class="input-group">
70+
<select class="form-select w-auto" @bind="AttestationType">
71+
@foreach (var value in Enum.GetValues<AttestationConveyancePreference>())
72+
{
73+
<option value="@value">@value</option>
74+
}
75+
</select>
76+
</div>
77+
78+
<label>Authenticator</label>
79+
<div class="input-group">
80+
<select class="form-select w-auto" @bind="Authenticator">
81+
<option value="@(new AuthenticatorAttachment?())">Not specified</option>
82+
<option value="@((AuthenticatorAttachment?)AuthenticatorAttachment.CrossPlatform)">Cross-platform (Token)</option>
83+
<option value="@((AuthenticatorAttachment?)AuthenticatorAttachment.Platform)">Platform (TPM)</option>
84+
</select>
85+
</div>
86+
87+
<label>User verification</label>
88+
<div class="input-group">
89+
<select class="form-select w-auto" @bind="UserVerification">
90+
<option value="@UserVerificationRequirement.Discouraged">Discouraged</option>
91+
<option value="@UserVerificationRequirement.Preferred">Preferred</option>
92+
<option value="@UserVerificationRequirement.Required">Required</option>
93+
</select>
94+
</div>
95+
96+
<label>Resident key</label>
97+
<div class="input-group">
98+
<select class="form-select w-auto" @bind="ResidentKey">
99+
<option value="@ResidentKeyRequirement.Discouraged">Discouraged</option>
100+
<option value="@ResidentKeyRequirement.Preferred">Preferred</option>
101+
<option value="@ResidentKeyRequirement.Required">Required</option>
102+
</select>
103+
</div>
104+
</section>
105+
106+
<section class="pt-5">
107+
<p>
108+
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>
109+
</p>
110+
</section>
111+
@code {
112+
private bool WebAuthnSupported { get; set; } = true;
113+
114+
private string RegisterUsername { get; set; } = "";
115+
private string? RegisterDisplayName { get; set; }
116+
117+
private string LoginUsername { get; set; } = "";
118+
119+
private AttestationConveyancePreference AttestationType { get; set; }
120+
121+
private AuthenticatorAttachment? Authenticator { get; set; }
122+
123+
private UserVerificationRequirement UserVerification { get; set; } = UserVerificationRequirement.Discouraged;
124+
125+
private ResidentKeyRequirement ResidentKey { get; set; } = ResidentKeyRequirement.Preferred;
126+
127+
protected override async Task OnInitializedAsync()
128+
{
129+
WebAuthnSupported = await UserService.IsWebAuthnSupportedAsync();
130+
}
131+
132+
private bool RegisterFormValid() => !string.IsNullOrWhiteSpace(RegisterUsername);
133+
private async Task Register()
134+
{
135+
var username = RegisterUsername;
136+
var displayName = RegisterDisplayName;
137+
138+
var result = await UserService.RegisterAsync(
139+
username,
140+
displayName,
141+
AttestationType,
142+
Authenticator,
143+
UserVerification,
144+
ResidentKey);
145+
146+
if (result == "OK")
147+
{
148+
Toasts.ShowToast("Registration successful", ToastLevel.Success);
149+
}
150+
else
151+
{
152+
Toasts.ShowToast(result, ToastLevel.Error);
153+
}
154+
}
155+
156+
private bool LoginFormValid() => !string.IsNullOrWhiteSpace(LoginUsername);
157+
private async Task Login()
158+
{
159+
var result = await UserService.LoginAsync(LoginUsername);
160+
161+
if (result.StartsWith("Bearer"))
162+
{
163+
Toasts.ShowToast($"Login successful, JWT received", ToastLevel.Success);
164+
Console.WriteLine($"Token: {result.Replace("Bearer ", "")}");
165+
}
166+
else
167+
{
168+
Toasts.ShowToast(result, ToastLevel.Error);
169+
}
170+
}
171+
}

BlazorWasmDemo/Client/Pages/Mfa.razor

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
@page "/mfa"
2+
@using BlazorWasmDemo.Client.Shared.Toasts
3+
@inject UserService UserService
4+
@inject ToastService Toasts
5+
6+
<h1>Scenario: 2FA/MFA</h1>
7+
<div class="content">
8+
<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>
9+
</div>
10+
@if (!WebAuthnSupported)
11+
{
12+
<div class="alert alert-danger">
13+
Please note: Your browser does not seem to support WebAuthn yet. <a href="https://caniuse.com/#search=webauthn" target="_blank">Supported browsers</a>
14+
</div>
15+
}
16+
17+
<section class="row">
18+
<div class="col">
19+
20+
<h3>Create an account</h3>
21+
<form>
22+
<label for="register-username">Username</label>
23+
<div class="input-group">
24+
<div class="input-group-text">
25+
<span class="fas fa-user"></span>
26+
</div>
27+
<input class="form-control" type="text" placeholder="abergs" id="register-username" @bind="RegisterUsername" required>
28+
</div>
29+
30+
<label for="displayName">Display name</label>
31+
<div class="input-group">
32+
<div class="input-group-text">
33+
<span class="fas fa-user">
34+
</span>
35+
</div>
36+
<input class="form-control" type="text" placeholder="Anders Åberg" id="displayName" @bind="RegisterDisplayName">
37+
</div>
38+
39+
<label for="register-password">Password</label>
40+
<div class="input-group">
41+
<div class="input-group-text">
42+
<span class="fas fa-user">
43+
</span>
44+
</div>
45+
<input class="form-control" type="password" placeholder="Do not use something secret" id="register-password">
46+
</div>
47+
<p>
48+
<small>For demo purposes the password will not be used or stored</small>
49+
</p>
50+
51+
<label class="checkbox">
52+
<input type="checkbox" disabled checked readonly>
53+
Register MFA on registration
54+
</label>
55+
</form>
56+
<div class="input-group">
57+
<button class="btn btn-primary" disabled="@(!RegisterFormValid())" @onclick="Register">Create account</button>
58+
</div>
59+
</div>
60+
<div class="col-2"></div>
61+
<div class="col">
62+
63+
<h3>Sign in</h3>
64+
<form>
65+
<label for="login-username">Username</label>
66+
<div class="input-group">
67+
<div class="input-group-text">
68+
<span class="fas fa-user">
69+
</span>
70+
</div>
71+
<input class="form-control" type="text" placeholder="abergs" id="login-username" required @bind="LoginUsername">
72+
</div>
73+
74+
<label for="login-password">Password</label>
75+
<div class="input-group">
76+
<div class="input-group-text">
77+
<span class="fas fa-user">
78+
</span>
79+
</div>
80+
<input class="form-control" type="password" placeholder="Do not use something secret" id="login-password">
81+
</div>
82+
<p><small>For demo purposes the password will not be used or stored</small></p>
83+
</form>
84+
<div class="input-group">
85+
<button class="btn btn-primary" disabled="@(!LoginFormValid())" @onclick="Login">Sign in</button>
86+
</div>
87+
</div>
88+
</section>
89+
90+
<section class="pt-5">
91+
<h1>Explanation: 2FA/MFA with FIDO2</h1>
92+
<p>
93+
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).
94+
The flow is visualized in the figure below.
95+
</p>
96+
<img src="images/scenario1.png" alt="figure visualizing username and password sent together with assertion" />
97+
<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>
98+
99+
<p>
100+
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>
101+
</p>
102+
</section>
103+
104+
@code
105+
{
106+
private bool WebAuthnSupported { get; set; } = true;
107+
108+
private string RegisterUsername { get; set; } = "";
109+
private string? RegisterDisplayName { get; set; }
110+
111+
private string LoginUsername { get; set; } = "";
112+
113+
protected override async Task OnInitializedAsync()
114+
{
115+
WebAuthnSupported = await UserService.IsWebAuthnSupportedAsync();
116+
}
117+
118+
private bool RegisterFormValid() => !string.IsNullOrWhiteSpace(RegisterUsername);
119+
private async Task Register()
120+
{
121+
var username = RegisterUsername;
122+
var displayName = RegisterDisplayName;
123+
124+
var result = await UserService.RegisterAsync(username, displayName);
125+
126+
if (result == "OK")
127+
{
128+
Toasts.ShowToast("Registration successful", ToastLevel.Success);
129+
}
130+
else
131+
{
132+
Toasts.ShowToast(result, ToastLevel.Error);
133+
}
134+
}
135+
136+
private bool LoginFormValid() => !string.IsNullOrWhiteSpace(LoginUsername);
137+
private async Task Login()
138+
{
139+
var result = await UserService.LoginAsync(LoginUsername);
140+
141+
if (result.StartsWith("Bearer"))
142+
{
143+
Toasts.ShowToast($"Login successful, token:{Environment.NewLine}{result.Replace("Bearer ", string.Empty)}", ToastLevel.Success);
144+
}
145+
else
146+
{
147+
Toasts.ShowToast(result, ToastLevel.Error);
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)