Skip to content

Commit 01c2569

Browse files
committed
feat: add support for roles, culture, timezone and email
- only doc and demo changes
1 parent 536c382 commit 01c2569

File tree

9 files changed

+178
-19
lines changed

9 files changed

+178
-19
lines changed

README.md

+80-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Add the following to the appsettings.json with the scopes you made above and you
4949
"Authority": "https://mysite.catglobe.com/",
5050
"ClientId": "Production id",
5151
"ResponseType": "code",
52-
"DefaultScopes": [ "email", "offline_access", "roles", "and others from above, except profile and openid " ],
52+
"Scope": [ "email", "offline_access", "roles", "and others from above, except profile and openid " ],
5353
"SaveTokens": true
5454
},
5555
"CatglobeApi": {
@@ -103,7 +103,6 @@ services.AddAuthentication(SCHEMENAME)
103103
builder.Configuration.GetSection(SCHEMENAME).Bind(oidcOptions);
104104
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
105105
oidcOptions.TokenValidationParameters.NameClaimType = "name";
106-
oidcOptions.TokenValidationParameters.RoleClaimType = "cg_roles";
107106
})
108107
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
109108
services.AddCgScript(builder.Configuration.GetSection("CatglobeApi"), builder.Environment.IsDevelopment());
@@ -221,7 +220,6 @@ services.AddAuthentication(SCHEMENAME)
221220
builder.Configuration.GetSection(SCHEMENAME).Bind(oidcOptions);
222221
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
223222
oidcOptions.TokenValidationParameters.NameClaimType = "name";
224-
oidcOptions.TokenValidationParameters.RoleClaimType = "cg_roles";
225223

226224
oidcOptions.Events.OnRedirectToIdentityProvider = context => {
227225
if (context.Properties.Items.TryGetValue("respondent", out var resp) &&
@@ -252,6 +250,85 @@ services.AddAuthentication(SCHEMENAME)
252250
gotoUrl("https://siteurl.com/authentication/login?respondent=" + User_getCurrentUser().ResourceGuid + "&respondent_secret=" + qas.AccessCode);");
253251
```
254252

253+
## I18n and email
254+
255+
Users language, culture, timezone and email is stored in Catglobe. To use in your app, add the following:
256+
257+
```csharp
258+
.AddOpenIdConnect(SCHEMENAME, oidcOptions => {
259+
...
260+
// to get the locale/culture
261+
oidcOptions.ClaimActions.MapUniqueJsonKey("locale", "locale");
262+
oidcOptions.ClaimActions.MapUniqueJsonKey("culture", "culture");
263+
264+
// must be true to get the zoneinfo and email claims
265+
oidcOptions.GetClaimsFromUserInfoEndpoint = true;
266+
oidcOptions.ClaimActions.MapUniqueJsonKey("zoneinfo", "zoneinfo");
267+
})
268+
```
269+
270+
If you use Blazor WASM, you need to also send these claims to the WASM and parse them:
271+
272+
```csharp
273+
//in SERVER program.cs:
274+
...
275+
.AddAuthenticationStateSerialization(o=>o.SerializeAllClaims=true);
276+
```
277+
278+
```csharp
279+
builder.Services.AddAuthenticationStateDeserialization(o=>o.DeserializationCallback = ProcessLanguageAndCultureFromClaims(o.DeserializationCallback));
280+
281+
static Func<AuthenticationStateData?, Task<AuthenticationState>> ProcessLanguageAndCultureFromClaims(Func<AuthenticationStateData?, Task<AuthenticationState>> authenticationStateData) =>
282+
state => {
283+
var tsk = authenticationStateData(state);
284+
if (!tsk.IsCompletedSuccessfully) return tsk;
285+
var authState = tsk.Result;
286+
if (authState?.User is not { } user) return tsk;
287+
var userCulture = user.FindFirst("culture")?.Value;
288+
var userUiCulture = user.FindFirst("locale")?.Value ?? userCulture;
289+
if (userUiCulture == null) return tsk;
290+
291+
CultureInfo.DefaultThreadCurrentCulture = new(userCulture ?? userUiCulture);
292+
CultureInfo.DefaultThreadCurrentUICulture = new(userUiCulture);
293+
return tsk;
294+
};
295+
296+
```
297+
298+
You can adapt something like https://www.meziantou.net/convert-datetime-to-user-s-time-zone-with-server-side-blazor-time-provider.htm for timezone
299+
300+
## Role based authorization in your app
301+
302+
If you want to use roles in your app, you need to request roles from oidc:
303+
```json
304+
"CatglobeOidc": {
305+
...
306+
"Scope": [ ... "roles", ],
307+
},
308+
```
309+
310+
Next, you need to make a script that detect the users roles:
311+
312+
```cgscript
313+
array scopesRequested = Workflow_getParameters()[0]["scopes"];
314+
...do some magic to figure out the roles...
315+
return {"thisUserIsAdmin"};
316+
```
317+
318+
You can make this script public.
319+
320+
```cgscript
321+
OidcAuthenticationFlow client = OidcAuthenticationFlow_createOrUpdate("some id, a guid works, but any string is acceptable");
322+
client.AppRolesScriptId = 424242; // the script that returns the roles
323+
...
324+
```
325+
326+
and finally in any page, you can add either `@attribute [Authorize(Roles = "thisUserIsAdmin")]` or `<AuthorizeView Roles="thisUserIsAdmin">Only visible to admins<AuthorizeView>`.
327+
328+
Why can the script NOT be in the app? Because it needs to run __before__ the app is ever deployed.
329+
330+
**NOTICE!** We may change the way to setup the script in the future to avoid the bootstrapping issue.
331+
255332
# Usage of the library
256333

257334
## Development

demos/BlazorWebApp/BlazorWebApp.Client/Pages/Auth.razor

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99

1010
<h1>You are authenticated</h1>
1111

12-
<AuthorizeView>
12+
<AuthorizeView Roles="thisUserIsAdmin">
1313
Hello @context.User.Identity?.Name!
1414
</AuthorizeView>
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
@page "/counter"
22
@rendermode InteractiveAuto
3+
@using System.Globalization
34

45
<PageTitle>Counter</PageTitle>
56

6-
<h1>Counter</h1>
7+
<h1>Counter @@ @RendererInfo.Name</h1>
78

89
<p role="status">Current count: @currentCount</p>
910

1011
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
1112

13+
<p>
14+
Culture: <span class="">@CultureInfo.CurrentCulture.Name @CultureInfo.CurrentUICulture.Name</span>
15+
</p>
16+
17+
1218
@code {
13-
private int currentCount = 0;
19+
private int currentCount = 0;
1420

15-
private void IncrementCount()
16-
{
17-
currentCount++;
18-
}
21+
private void IncrementCount()
22+
{
23+
currentCount++;
24+
}
1925
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
using Microsoft.AspNetCore.Components.Authorization;
12
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
3+
using System.Globalization;
24

35
var builder = WebAssemblyHostBuilder.CreateDefault(args);
46

57
builder.Services.AddAuthorizationCore();
68
builder.Services.AddCascadingAuthenticationState();
7-
builder.Services.AddAuthenticationStateDeserialization();
9+
builder.Services.AddAuthenticationStateDeserialization(o=>o.DeserializationCallback = ProcessLanguageAndCultureFromClaims(o.DeserializationCallback));
10+
11+
static Func<AuthenticationStateData?, Task<AuthenticationState>> ProcessLanguageAndCultureFromClaims(Func<AuthenticationStateData?, Task<AuthenticationState>> authenticationStateData) =>
12+
state => {
13+
var tsk = authenticationStateData(state);
14+
if (!tsk.IsCompletedSuccessfully) return tsk;
15+
var authState = tsk.Result;
16+
if (authState?.User is not { } user) return tsk;
17+
var userCulture = user.FindFirst("culture")?.Value;
18+
//Console.WriteLine($"New culture = {userCulture ?? "unset"}. Old = {CultureInfo.DefaultThreadCurrentCulture?.Name ?? "unset"}");
19+
var userUiCulture = user.FindFirst("locale")?.Value ?? userCulture;
20+
//Console.WriteLine($"New locale = {userUiCulture ?? "unset"}. Old = {CultureInfo.DefaultThreadCurrentUICulture?.Name ?? "unset"}");
21+
if (userUiCulture == null) return tsk;
22+
23+
CultureInfo.DefaultThreadCurrentCulture = new(userCulture ?? userUiCulture);
24+
CultureInfo.DefaultThreadCurrentUICulture = new(userUiCulture);
25+
return tsk;
26+
};
827

928
await builder.Build().RunAsync();

demos/BlazorWebApp/BlazorWebApp/Components/Layout/NavMenu.razor

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
@implements IDisposable
1+
@using System.Security.Claims
2+
@implements IDisposable
23

34
@inject NavigationManager NavigationManager
45

56
<div class="top-row ps-3 navbar navbar-dark">
67
<div class="container-fluid">
78
<a class="navbar-brand" href="">BlazorWebApp</a>
8-
</div>
9+
<AuthorizeView>
10+
<span class="navbar-text">Welcome, @context.User.Identity!.Name (@GetEmail(context))!</span>
11+
</AuthorizeView>
12+
<span class="navbar-text">@Thread.CurrentThread.CurrentCulture.Name @Thread.CurrentThread.CurrentUICulture.Name</span>
13+
</div>
914
</div>
1015

1116
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
@@ -84,5 +89,8 @@
8489
{
8590
NavigationManager.LocationChanged -= OnLocationChanged;
8691
}
92+
93+
private static string? GetEmail(AuthenticationState context) => context.User.FindFirstValue("email");
94+
8795
}
8896

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Security.Claims;
2+
using Microsoft.AspNetCore.Localization;
3+
4+
namespace BlazorWebApp.DemoUsage;
5+
6+
/// <summary>
7+
/// Pull the "locale" and "culture" claims from the user and use that as the uiCulture and culture.
8+
/// If either is missing, the other is used as a fallback. If both are missing, or the locale is not in the list of known cultures, it does nothing.
9+
/// <example><code>
10+
/// host.UseRequestLocalization(o =&gt; {
11+
/// var cultures = ...;
12+
/// o.AddSupportedCultures(cultures)
13+
/// .AddSupportedUICultures(cultures)
14+
/// .SetDefaultCulture(cultures[0]);
15+
/// //insert before the final default provider (the AcceptLanguageHeaderRequestCultureProvider)
16+
/// o.RequestCultureProviders.Insert(o.RequestCultureProviders.Count - 1, new OidcClaimsCultureProvider {Options = o});
17+
/// });
18+
/// </code></example>
19+
/// </summary>
20+
public class OidcClaimsCultureProvider : RequestCultureProvider
21+
{
22+
///<inheritdoc/>
23+
public override Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext) => Task.FromResult(GetCultureFromClaims(httpContext));
24+
25+
private static ProviderCultureResult? GetCultureFromClaims(HttpContext ctx)
26+
{
27+
var userCulture = ctx.User.FindFirstValue("culture");
28+
var userUiCulture = ctx.User.FindFirstValue("locale") ?? userCulture;
29+
if (userUiCulture == null) goto noneFound;
30+
31+
return new(userCulture ?? userUiCulture, userUiCulture);
32+
noneFound:
33+
return null;
34+
}
35+
}

demos/BlazorWebApp/BlazorWebApp/DemoUsage/SetupRuntime.cs

+18-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ public static void Configure(WebApplicationBuilder builder)
2323
// user credentials across requests.
2424
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
2525
oidcOptions.TokenValidationParameters.NameClaimType = "name";
26-
oidcOptions.TokenValidationParameters.RoleClaimType = "cg_roles";
26+
27+
// to get the locale/culture
28+
oidcOptions.ClaimActions.MapUniqueJsonKey("locale", "locale");
29+
oidcOptions.ClaimActions.MapUniqueJsonKey("culture", "culture");
30+
31+
// must be true to get the zoneinfo and email claims
32+
oidcOptions.GetClaimsFromUserInfoEndpoint = true;
33+
oidcOptions.ClaimActions.MapUniqueJsonKey("zoneinfo", "zoneinfo");
2734
})
2835
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
2936

@@ -51,6 +58,15 @@ public static void Configure(WebApplicationBuilder builder)
5158

5259
public static void Use(WebApplication app)
5360
{
61+
string[] culture = ["da-DK", "en-US", "en-GB"];
62+
app.UseRequestLocalization(o => {
63+
o.AddSupportedCultures(culture)
64+
.AddSupportedUICultures(culture)
65+
.SetDefaultCulture(culture[0]);
66+
//insert before the final default provider (the AcceptLanguageHeaderRequestCultureProvider)
67+
o.RequestCultureProviders.Insert(o.RequestCultureProviders.Count - 1, new OidcClaimsCultureProvider {Options = o});
68+
});
69+
5470
app.MapGroup("/authentication").MapLoginAndLogout();
5571

5672
//Add this, if you need the browser (blazor wasm or javascript) to be able to call CgScript
@@ -63,5 +79,4 @@ public static void Use(WebApplication app)
6379
});
6480
}).RequireAuthorization();
6581
}
66-
}
67-
82+
}

demos/BlazorWebApp/BlazorWebApp/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
builder.Services.AddRazorComponents()
2020
.AddInteractiveServerComponents()
2121
.AddInteractiveWebAssemblyComponents()
22-
.AddAuthenticationStateSerialization();
22+
.AddAuthenticationStateSerialization(o=>o.SerializeAllClaims=true);
2323

2424
builder.Services.AddCascadingAuthenticationState();
2525
/***********************

demos/BlazorWebApp/BlazorWebApp/appsettings.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"ConnectionStrings": {
3-
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-BlazorWebApp-058011ee-34cd-4482-9d5a-96535612df93;Trusted_Connection=True;MultipleActiveResultSets=true"
43
},
54
"Logging": {
65
"LogLevel": {
@@ -14,7 +13,7 @@
1413
"ClientId": "13BAC6C1-8DEC-46E2-B378-90E0325F8132",
1514
"ClientSecret": "secret",
1615
"ResponseType": "code",
17-
"DefaultScopes": [ "email", "offline_access", "roles" ],
16+
"Scope": [ "email", "offline_access", "roles" ],
1817
"SaveTokens": true
1918
},
2019
"CatglobeApi": {

0 commit comments

Comments
 (0)