Skip to content

Commit

Permalink
feat: add support for roles, culture, timezone and email
Browse files Browse the repository at this point in the history
- only doc and demo changes
  • Loading branch information
davhdavh committed Dec 12, 2024
1 parent 536c382 commit 01c2569
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 19 deletions.
83 changes: 80 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Add the following to the appsettings.json with the scopes you made above and you
"Authority": "https://mysite.catglobe.com/",
"ClientId": "Production id",
"ResponseType": "code",
"DefaultScopes": [ "email", "offline_access", "roles", "and others from above, except profile and openid " ],
"Scope": [ "email", "offline_access", "roles", "and others from above, except profile and openid " ],
"SaveTokens": true
},
"CatglobeApi": {
Expand Down Expand Up @@ -103,7 +103,6 @@ services.AddAuthentication(SCHEMENAME)
builder.Configuration.GetSection(SCHEMENAME).Bind(oidcOptions);
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "cg_roles";
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
services.AddCgScript(builder.Configuration.GetSection("CatglobeApi"), builder.Environment.IsDevelopment());
Expand Down Expand Up @@ -221,7 +220,6 @@ services.AddAuthentication(SCHEMENAME)
builder.Configuration.GetSection(SCHEMENAME).Bind(oidcOptions);
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "cg_roles";

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

## I18n and email

Users language, culture, timezone and email is stored in Catglobe. To use in your app, add the following:

```csharp
.AddOpenIdConnect(SCHEMENAME, oidcOptions => {
...
// to get the locale/culture
oidcOptions.ClaimActions.MapUniqueJsonKey("locale", "locale");
oidcOptions.ClaimActions.MapUniqueJsonKey("culture", "culture");

// must be true to get the zoneinfo and email claims
oidcOptions.GetClaimsFromUserInfoEndpoint = true;
oidcOptions.ClaimActions.MapUniqueJsonKey("zoneinfo", "zoneinfo");
})
```

If you use Blazor WASM, you need to also send these claims to the WASM and parse them:

```csharp
//in SERVER program.cs:
...
.AddAuthenticationStateSerialization(o=>o.SerializeAllClaims=true);
```

```csharp
builder.Services.AddAuthenticationStateDeserialization(o=>o.DeserializationCallback = ProcessLanguageAndCultureFromClaims(o.DeserializationCallback));

static Func<AuthenticationStateData?, Task<AuthenticationState>> ProcessLanguageAndCultureFromClaims(Func<AuthenticationStateData?, Task<AuthenticationState>> authenticationStateData) =>
state => {
var tsk = authenticationStateData(state);
if (!tsk.IsCompletedSuccessfully) return tsk;
var authState = tsk.Result;
if (authState?.User is not { } user) return tsk;
var userCulture = user.FindFirst("culture")?.Value;
var userUiCulture = user.FindFirst("locale")?.Value ?? userCulture;
if (userUiCulture == null) return tsk;

CultureInfo.DefaultThreadCurrentCulture = new(userCulture ?? userUiCulture);
CultureInfo.DefaultThreadCurrentUICulture = new(userUiCulture);
return tsk;
};

```

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

## Role based authorization in your app

If you want to use roles in your app, you need to request roles from oidc:
```json
"CatglobeOidc": {
...
"Scope": [ ... "roles", ],
},
```

Next, you need to make a script that detect the users roles:

```cgscript
array scopesRequested = Workflow_getParameters()[0]["scopes"];
...do some magic to figure out the roles...
return {"thisUserIsAdmin"};
```

You can make this script public.

```cgscript
OidcAuthenticationFlow client = OidcAuthenticationFlow_createOrUpdate("some id, a guid works, but any string is acceptable");
client.AppRolesScriptId = 424242; // the script that returns the roles
...
```

and finally in any page, you can add either `@attribute [Authorize(Roles = "thisUserIsAdmin")]` or `<AuthorizeView Roles="thisUserIsAdmin">Only visible to admins<AuthorizeView>`.

Why can the script NOT be in the app? Because it needs to run __before__ the app is ever deployed.

**NOTICE!** We may change the way to setup the script in the future to avoid the bootstrapping issue.

# Usage of the library

## Development
Expand Down
2 changes: 1 addition & 1 deletion demos/BlazorWebApp/BlazorWebApp.Client/Pages/Auth.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@

<h1>You are authenticated</h1>

<AuthorizeView>
<AuthorizeView Roles="thisUserIsAdmin">
Hello @context.User.Identity?.Name!
</AuthorizeView>
18 changes: 12 additions & 6 deletions demos/BlazorWebApp/BlazorWebApp.Client/Pages/Counter.razor
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
@page "/counter"
@rendermode InteractiveAuto
@using System.Globalization

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>
<h1>Counter @@ @RendererInfo.Name</h1>

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

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

<p>
Culture: <span class="">@CultureInfo.CurrentCulture.Name @CultureInfo.CurrentUICulture.Name</span>
</p>


@code {
private int currentCount = 0;
private int currentCount = 0;

private void IncrementCount()
{
currentCount++;
}
private void IncrementCount()
{
currentCount++;
}
}
21 changes: 20 additions & 1 deletion demos/BlazorWebApp/BlazorWebApp.Client/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using System.Globalization;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization();
builder.Services.AddAuthenticationStateDeserialization(o=>o.DeserializationCallback = ProcessLanguageAndCultureFromClaims(o.DeserializationCallback));

static Func<AuthenticationStateData?, Task<AuthenticationState>> ProcessLanguageAndCultureFromClaims(Func<AuthenticationStateData?, Task<AuthenticationState>> authenticationStateData) =>
state => {
var tsk = authenticationStateData(state);
if (!tsk.IsCompletedSuccessfully) return tsk;
var authState = tsk.Result;
if (authState?.User is not { } user) return tsk;
var userCulture = user.FindFirst("culture")?.Value;
//Console.WriteLine($"New culture = {userCulture ?? "unset"}. Old = {CultureInfo.DefaultThreadCurrentCulture?.Name ?? "unset"}");
var userUiCulture = user.FindFirst("locale")?.Value ?? userCulture;
//Console.WriteLine($"New locale = {userUiCulture ?? "unset"}. Old = {CultureInfo.DefaultThreadCurrentUICulture?.Name ?? "unset"}");
if (userUiCulture == null) return tsk;

CultureInfo.DefaultThreadCurrentCulture = new(userCulture ?? userUiCulture);
CultureInfo.DefaultThreadCurrentUICulture = new(userUiCulture);
return tsk;
};

await builder.Build().RunAsync();
12 changes: 10 additions & 2 deletions demos/BlazorWebApp/BlazorWebApp/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
@implements IDisposable
@using System.Security.Claims
@implements IDisposable

@inject NavigationManager NavigationManager

<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">BlazorWebApp</a>
</div>
<AuthorizeView>
<span class="navbar-text">Welcome, @context.User.Identity!.Name (@GetEmail(context))!</span>
</AuthorizeView>
<span class="navbar-text">@Thread.CurrentThread.CurrentCulture.Name @Thread.CurrentThread.CurrentUICulture.Name</span>
</div>
</div>

<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
Expand Down Expand Up @@ -84,5 +89,8 @@
{
NavigationManager.LocationChanged -= OnLocationChanged;
}

private static string? GetEmail(AuthenticationState context) => context.User.FindFirstValue("email");

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Localization;

namespace BlazorWebApp.DemoUsage;

/// <summary>
/// Pull the "locale" and "culture" claims from the user and use that as the uiCulture and culture.
/// 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.
/// <example><code>
/// host.UseRequestLocalization(o =&gt; {
/// var cultures = ...;
/// o.AddSupportedCultures(cultures)
/// .AddSupportedUICultures(cultures)
/// .SetDefaultCulture(cultures[0]);
/// //insert before the final default provider (the AcceptLanguageHeaderRequestCultureProvider)
/// o.RequestCultureProviders.Insert(o.RequestCultureProviders.Count - 1, new OidcClaimsCultureProvider {Options = o});
/// });
/// </code></example>
/// </summary>
public class OidcClaimsCultureProvider : RequestCultureProvider
{
///<inheritdoc/>
public override Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext) => Task.FromResult(GetCultureFromClaims(httpContext));

private static ProviderCultureResult? GetCultureFromClaims(HttpContext ctx)
{
var userCulture = ctx.User.FindFirstValue("culture");
var userUiCulture = ctx.User.FindFirstValue("locale") ?? userCulture;
if (userUiCulture == null) goto noneFound;

return new(userCulture ?? userUiCulture, userUiCulture);
noneFound:
return null;
}
}
21 changes: 18 additions & 3 deletions demos/BlazorWebApp/BlazorWebApp/DemoUsage/SetupRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,14 @@ public static void Configure(WebApplicationBuilder builder)
// user credentials across requests.
oidcOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
oidcOptions.TokenValidationParameters.NameClaimType = "name";
oidcOptions.TokenValidationParameters.RoleClaimType = "cg_roles";

// to get the locale/culture
oidcOptions.ClaimActions.MapUniqueJsonKey("locale", "locale");
oidcOptions.ClaimActions.MapUniqueJsonKey("culture", "culture");

// must be true to get the zoneinfo and email claims
oidcOptions.GetClaimsFromUserInfoEndpoint = true;
oidcOptions.ClaimActions.MapUniqueJsonKey("zoneinfo", "zoneinfo");
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);

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

public static void Use(WebApplication app)
{
string[] culture = ["da-DK", "en-US", "en-GB"];
app.UseRequestLocalization(o => {
o.AddSupportedCultures(culture)
.AddSupportedUICultures(culture)
.SetDefaultCulture(culture[0]);
//insert before the final default provider (the AcceptLanguageHeaderRequestCultureProvider)
o.RequestCultureProviders.Insert(o.RequestCultureProviders.Count - 1, new OidcClaimsCultureProvider {Options = o});
});

app.MapGroup("/authentication").MapLoginAndLogout();

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

}
2 changes: 1 addition & 1 deletion demos/BlazorWebApp/BlazorWebApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents()
.AddAuthenticationStateSerialization();
.AddAuthenticationStateSerialization(o=>o.SerializeAllClaims=true);

builder.Services.AddCascadingAuthenticationState();
/***********************
Expand Down
3 changes: 1 addition & 2 deletions demos/BlazorWebApp/BlazorWebApp/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-BlazorWebApp-058011ee-34cd-4482-9d5a-96535612df93;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
Expand All @@ -14,7 +13,7 @@
"ClientId": "13BAC6C1-8DEC-46E2-B378-90E0325F8132",
"ClientSecret": "secret",
"ResponseType": "code",
"DefaultScopes": [ "email", "offline_access", "roles" ],
"Scope": [ "email", "offline_access", "roles" ],
"SaveTokens": true
},
"CatglobeApi": {
Expand Down

0 comments on commit 01c2569

Please sign in to comment.