diff --git a/README.md b/README.md index 1241ea0..01ab6af 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The monorepo for Logto SDKs written in C#. - [src/Logto.AspNetCore.Authentication.Tests](./src/Logto.AspNetCore.Authentication.Tests): Tests for the ASP.NET Core authentication middleware. - [sample](./sample): Sample ASP.NET Core web application that shows how to use the ASP.NET Core authentication middleware. - [sample-mvc](./sample-mvc): Sample ASP.NET Core web MVC application that shows how to use the ASP.NET Core authentication middleware. +- [sample-wasm](./sample-wasm): Sample Blazor WebAssembly application that shows how to use Blorc.OpenIdConnect to authenticate users with Logto. ## Resources diff --git a/logto-csharp.sln b/logto-csharp.sln index f959424..88b5a42 100644 --- a/logto-csharp.sln +++ b/logto-csharp.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "logto-csharp-sample", "samp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sample-mvc", "sample-mvc\sample-mvc.csproj", "{2C4D9EC2-8697-4217-82E8-953835F990CA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sample-wasm", "sample-wasm\sample-wasm.csproj", "{A4D02F83-0AF6-4886-9DDC-D0E109780CD4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {2C4D9EC2-8697-4217-82E8-953835F990CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {2C4D9EC2-8697-4217-82E8-953835F990CA}.Release|Any CPU.ActiveCfg = Release|Any CPU {2C4D9EC2-8697-4217-82E8-953835F990CA}.Release|Any CPU.Build.0 = Release|Any CPU + {A4D02F83-0AF6-4886-9DDC-D0E109780CD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4D02F83-0AF6-4886-9DDC-D0E109780CD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4D02F83-0AF6-4886-9DDC-D0E109780CD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4D02F83-0AF6-4886-9DDC-D0E109780CD4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {D05B1B5F-D560-492A-A566-95888C8C5C43} = {D44D6C8F-1A29-4796-930F-B37ADB539EA3} diff --git a/sample-wasm/App.razor b/sample-wasm/App.razor new file mode 100644 index 0000000..e4461cf --- /dev/null +++ b/sample-wasm/App.razor @@ -0,0 +1,14 @@ +@using sample_wasm.Pages + + + + + + + + + Not found +

Sorry, there's nothing at this address.

+
+
+
diff --git a/sample-wasm/Pages/Home.razor b/sample-wasm/Pages/Home.razor new file mode 100644 index 0000000..9b2b9a8 --- /dev/null +++ b/sample-wasm/Pages/Home.razor @@ -0,0 +1,34 @@ +@page "/" + +
+

Logto Blazor WASM sample

+

This is the sample application for Logto integration with Blazor WASM.

+
+ + +

+ You are signed in as @(@User?.Profile?.Name ?? "(unknown name)"). +

+

Profile

+
    +
  • Email: @(@User?.Profile?.Email ?? "(null)")
  • +
  • Email verified: @(@User?.Profile?.EmailVerified ?? false)
  • +
+

Access token: @(@User?.AccessToken ?? "(null)")

+ +
+ +

+ You are not signed in. +

+ +
+
+
+
diff --git a/sample-wasm/Pages/Home.razor.cs b/sample-wasm/Pages/Home.razor.cs new file mode 100644 index 0000000..dbd0638 --- /dev/null +++ b/sample-wasm/Pages/Home.razor.cs @@ -0,0 +1,56 @@ +namespace sample_wasm.Pages; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Blorc.OpenIdConnect; +using Microsoft.AspNetCore.Components.Authorization; + +[Authorize] +public partial class Home : ComponentBase +{ + [Inject] + public required IUserManager UserManager { get; set; } + public TimeSpan? SignOutTimeSpan { get; set; } + + public User? User { get; set; } + + [CascadingParameter] + protected Task? AuthenticationStateTask { get; set; } + + protected override async Task OnInitializedAsync() + { + User = await UserManager.GetUserAsync>(AuthenticationStateTask!); + + UserManager.UserActivity += OnUserManagerUserActivity; + UserManager.UserInactivity += OnUserManagerUserInactivity; + } + + private void OnUserManagerUserInactivity(object? sender, UserInactivityEventArgs args) + { + SignOutTimeSpan = args.SignOutTimeSpan; + StateHasChanged(); + } + + private void OnUserManagerUserActivity(object? sender, UserActivityEventArgs args) + { + SignOutTimeSpan = null; + StateHasChanged(); + } + + private async Task OnLoginButtonClickAsync(MouseEventArgs obj) + { + await UserManager.SignInRedirectAsync(); + } + + private async Task OnLogoutButtonClickAsync(MouseEventArgs obj) + { + await UserManager.SignOutRedirectAsync(); + } + + public void Dispose() + { + UserManager.UserActivity -= OnUserManagerUserActivity; + UserManager.UserInactivity -= OnUserManagerUserInactivity; + } +} diff --git a/sample-wasm/Program.cs b/sample-wasm/Program.cs new file mode 100644 index 0000000..8b5a9f7 --- /dev/null +++ b/sample-wasm/Program.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Blorc.OpenIdConnect; +using Blorc.Services; +using sample_wasm; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + +builder.Services.AddBlorcCore(); +builder.Services.AddAuthorizationCore(); +builder.Services.AddBlorcOpenIdConnect( + options => + { + builder.Configuration.Bind("IdentityServer", options); + }); + +var webAssemblyHost = builder.Build(); + +await webAssemblyHost + .ConfigureDocumentAsync(async documentService => + { + await documentService.InjectBlorcCoreJsAsync(); + await documentService.InjectOpenIdConnectAsync(); + }); + +await webAssemblyHost.RunAsync(); diff --git a/sample-wasm/Properties/launchSettings.json b/sample-wasm/Properties/launchSettings.json new file mode 100644 index 0000000..522e588 --- /dev/null +++ b/sample-wasm/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:40255", + "sslPort": 44360 + } + }, + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7119;http://localhost:5025", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/sample-wasm/README.md b/sample-wasm/README.md new file mode 100644 index 0000000..5bb34ed --- /dev/null +++ b/sample-wasm/README.md @@ -0,0 +1,75 @@ +# Logto ASP.NET Blazor WebAssembly sample project + +This sample project shows how to use the [Blorc.OpenIdConnect](https://github.com/WildGums/Blorc.OpenIdConnect) to authenticate users with Logto in a Blazor WebAssembly application. + +## Prerequisites + +- .NET 6.0 or higher +- A [Logto Cloud](https://logto.io/) account or a self-hosted Logto +- A Logto single-page application created + +### Optional + +- Set up an API resource in Logto + +If you don't have the Logto application created, please follow the [⚡ Get started](https://docs.logto.io/docs/tutorials/get-started/) guide to create one. + +## Configuration + +Create an `appsettings.Development.json` (or `appsettings.json`) with the following structure: + +```jsonc +{ + // ... + "IdentityServer": { + "Authority": "https:///oidc", + "ClientId": "", + "PostLogoutRedirectUri": "", // Remember to configure this in Logto + "RedirectUri": "", // Remember to configure this in Logto + "ResponseType": "code", + "Scope": "openid profile" // Add more scopes if needed + } +} +``` + +### Fetch user info + +For some special claims, such as `custom_data`, calling the `/userinfo` endpoint is required. To enable this feature, add the following configuration: + +```jsonc +{ + // ... + "IdentityServer": { + // ... + "LoadUserInfo": true + } +} +``` + +> [!Caution] +> Since WebAssembly is a client-side application, the token request will only be sent to the server-side once. Due to this nature, `LoadUserInfo` is conflict with fetching access token for API resources. + +### JWT access token + +If you need to fetch an access token in JWT format for an API resource, add the following configuration: + +```jsonc +{ + // ... + "IdentityServer": { + // ... + "Resource": "https://", + "ExtraTokenParams": { + "resource": "https://" // Ensure the key is lowercase + } + } +} +``` + +The value of `Resource` and `ExtraTokenParams.resource` should be the same. + +## Run the sample + +```bash +dotnet run # or `dotnet watch` to run in watch mode +``` diff --git a/sample-wasm/_Imports.razor b/sample-wasm/_Imports.razor new file mode 100644 index 0000000..ae4e992 --- /dev/null +++ b/sample-wasm/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using sample_wasm diff --git a/sample-wasm/sample-wasm.csproj b/sample-wasm/sample-wasm.csproj new file mode 100644 index 0000000..b7544a1 --- /dev/null +++ b/sample-wasm/sample-wasm.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/sample-wasm/wwwroot/css/app.css b/sample-wasm/wwwroot/css/app.css new file mode 100644 index 0000000..5181190 --- /dev/null +++ b/sample-wasm/wwwroot/css/app.css @@ -0,0 +1,101 @@ +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +h1:focus { + outline: none; +} + +a, .btn-link { + color: #0071c1; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } diff --git a/sample-wasm/wwwroot/favicon.png b/sample-wasm/wwwroot/favicon.png new file mode 100644 index 0000000..8422b59 Binary files /dev/null and b/sample-wasm/wwwroot/favicon.png differ diff --git a/sample-wasm/wwwroot/icon-192.png b/sample-wasm/wwwroot/icon-192.png new file mode 100644 index 0000000..166f56d Binary files /dev/null and b/sample-wasm/wwwroot/icon-192.png differ diff --git a/sample-wasm/wwwroot/index.html b/sample-wasm/wwwroot/index.html new file mode 100644 index 0000000..35b2a17 --- /dev/null +++ b/sample-wasm/wwwroot/index.html @@ -0,0 +1,33 @@ + + + + + + + Logto Blazor WASM sample + + + + + + + +
+ + + + +
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + diff --git a/src/Logto.AspNetCore.Authentication/Logto.AspNetCore.Authentication.csproj b/src/Logto.AspNetCore.Authentication/Logto.AspNetCore.Authentication.csproj index 6ca40b7..cdf2455 100644 --- a/src/Logto.AspNetCore.Authentication/Logto.AspNetCore.Authentication.csproj +++ b/src/Logto.AspNetCore.Authentication/Logto.AspNetCore.Authentication.csproj @@ -1,46 +1,49 @@ - - - - net6.0;net7.0;net8.0 - enable - true - - - - Logto.AspNetCore.Authentication - 0.1.1 - Logto ASP.Net Core authentication SDK. - Logto - Silverhand, Inc. - Silverhand, Inc. - logto;auth;authentication;identity;oauth2;openid-connect;oidc - README.md - Logto.png - https://logto.io/ - MIT - git - https://github.com/logto-io/csharp - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net7.0;net8.0 + enable + true + + + + Logto.AspNetCore.Authentication + 0.1.1 + Logto ASP.Net Core authentication SDK. + Logto + Silverhand, Inc. + Silverhand, Inc. + logto;auth;authentication;identity;oauth2;openid-connect;oidc + README.md + Logto.png + https://logto.io/ + MIT + git + https://github.com/logto-io/csharp + + + + + + + + + + + + + + + + + + + +