0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): add .NET Blzor server tutorial

This commit is contained in:
Gao Sun 2024-02-04 18:26:06 +08:00
parent c98f7131ac
commit 9e6cc9ae3b
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
13 changed files with 289 additions and 38 deletions

View file

@ -15,8 +15,9 @@ import spaVanilla from './spa-vanilla/index';
import spaVue from './spa-vue/index';
import thirdPartyOidc from './third-party-oidc/index';
import { type Guide } from './types';
import webAspNetCore from './web-asp-net-core/index';
import webAspNetCoreMvc from './web-asp-net-core-mvc/index';
import webDotnetCore from './web-dotnet-core/index';
import webDotnetCoreBlazorServer from './web-dotnet-core-blazor-server/index';
import webDotnetCoreMvc from './web-dotnet-core-mvc/index';
import webExpress from './web-express/index';
import webGo from './web-go/index';
import webGptPlugin from './web-gpt-plugin/index';
@ -29,13 +30,6 @@ import webPython from './web-python/index';
import webRemix from './web-remix/index';
const guides: Readonly<Guide[]> = Object.freeze([
{
order: 1,
id: 'web-next',
Logo: lazy(async () => import('./web-next/logo.svg')),
Component: lazy(async () => import('./web-next/README.mdx')),
metadata: webNext,
},
{
order: 1.1,
id: 'spa-react',
@ -43,13 +37,6 @@ const guides: Readonly<Guide[]> = Object.freeze([
Component: lazy(async () => import('./spa-react/README.mdx')),
metadata: spaReact,
},
{
order: 1.1,
id: 'web-next-server-actions',
Logo: lazy(async () => import('./web-next-server-actions/logo.svg')),
Component: lazy(async () => import('./web-next-server-actions/README.mdx')),
metadata: webNextServerActions,
},
{
order: 1.1,
id: 'web-next-app-router',
@ -57,6 +44,13 @@ const guides: Readonly<Guide[]> = Object.freeze([
Component: lazy(async () => import('./web-next-app-router/README.mdx')),
metadata: webNextAppRouter,
},
{
order: 1.1,
id: 'web-next-server-actions',
Logo: lazy(async () => import('./web-next-server-actions/logo.svg')),
Component: lazy(async () => import('./web-next-server-actions/README.mdx')),
metadata: webNextServerActions,
},
{
order: 1.2,
id: 'm2m-general',
@ -71,6 +65,13 @@ const guides: Readonly<Guide[]> = Object.freeze([
Component: lazy(async () => import('./web-express/README.mdx')),
metadata: webExpress,
},
{
order: 1.2,
id: 'web-next',
Logo: lazy(async () => import('./web-next/logo.svg')),
Component: lazy(async () => import('./web-next/README.mdx')),
metadata: webNext,
},
{
order: 1.3,
id: 'web-go',
@ -150,17 +151,24 @@ const guides: Readonly<Guide[]> = Object.freeze([
},
{
order: 5,
id: 'web-asp-net-core',
Logo: lazy(async () => import('./web-asp-net-core/logo.svg')),
Component: lazy(async () => import('./web-asp-net-core/README.mdx')),
metadata: webAspNetCore,
id: 'web-dotnet-core',
Logo: lazy(async () => import('./web-dotnet-core/logo.svg')),
Component: lazy(async () => import('./web-dotnet-core/README.mdx')),
metadata: webDotnetCore,
},
{
order: 5.1,
id: 'web-asp-net-core-mvc',
Logo: lazy(async () => import('./web-asp-net-core-mvc/logo.svg')),
Component: lazy(async () => import('./web-asp-net-core-mvc/README.mdx')),
metadata: webAspNetCoreMvc,
id: 'web-dotnet-core-mvc',
Logo: lazy(async () => import('./web-dotnet-core-mvc/logo.svg')),
Component: lazy(async () => import('./web-dotnet-core-mvc/README.mdx')),
metadata: webDotnetCoreMvc,
},
{
order: 5.2,
id: 'web-dotnet-core-blazor-server',
Logo: lazy(async () => import('./web-dotnet-core-blazor-server/logo.svg')),
Component: lazy(async () => import('./web-dotnet-core-blazor-server/README.mdx')),
metadata: webDotnetCoreBlazorServer,
},
{
order: 6,

View file

@ -0,0 +1,225 @@
import UriInputField from '@/mdx-components/UriInputField';
import Steps from '@/mdx-components/Steps';
import Step from '@/mdx-components/Step';
<Steps>
<Step title="Get started">
This tutorial will show you how to use Logto ASP.NET Core authentication middleware to protect your web application.
<ul>
<li>It assumes your website is hosted on <code>{props.sampleUrls.origin}</code>.</li>
</ul>
### Installation
```bash
dotnet add package Logto.AspNetCore.Authentication
```
</Step>
<Step title="Add Logto authentication">
Open `Startup.cs` (or `Program.cs`) and add the following code to register Logto authentication middleware:
<pre>
<code className="language-csharp">
{`using Logto.AspNetCore.Authentication;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLogtoAuthentication(options =>
{
options.Endpoint = "${props.endpoint}";
options.AppId = "${props.app.id}";
options.AppSecret = "${props.app.secret}";
});
app.UseAuthentication();`}
</code>
</pre>
The `AddLogtoAuthentication` method will do the following things:
- Set the default authentication scheme to `LogtoDefaults.CookieScheme`.
- Set the default challenge scheme to `LogtoDefaults.AuthenticationScheme`.
- Set the default sign-out scheme to `LogtoDefaults.AuthenticationScheme`.
- Add cookie and OpenID Connect authentication handlers to the authentication scheme.
</Step>
<Step title="Add routes">
Since Blazor Server uses SignalR to communicate between the server and the client, this means methods that directly manipulate the HTTP context (like issuing challenges or redirects) don't work as expected when called from a Blazor component.
To make it right, we need to explicitly add two endpoints for sign-in and sign-out redirects:
```csharp
app.MapGet("/SignIn", async context =>
{
if (!(context.User?.Identity?.IsAuthenticated ?? false))
{
await context.ChallengeAsync(new AuthenticationProperties { RedirectUri = "/" });
} else {
context.Response.Redirect("/");
}
});
app.MapGet("/SignOut", async context =>
{
if (context.User?.Identity?.IsAuthenticated ?? false)
{
await context.SignOutAsync(new AuthenticationProperties { RedirectUri = "/" });
} else {
context.Response.Redirect("/");
}
});
```
Now we can redirect to these endpoints to trigger sign-in and sign-out.
</Step>
<Step title="Set up redirect URIs">
<p>
First, let's enter your redirect URI. E.g. <code>{props.sampleUrls.origin + 'Callback'}</code> (replace the endpoint with yours). This is where Logto will redirect users after they sign in.
</p>
<UriInputField name="redirectUris" />
Remember to keep the path `/Callback` in the URI as it's the default value for the Logto authentication middleware.
---
To clean up both ASP.NET session and Logto session, we can designate a post sign-out redierct URI. This is where Logto will redirect users after they sign out.
<p>
For example, set the URI to <code>{props.sampleUrls.origin + 'SignedOutCallback'}</code> (replace the endpoint with yours):
</p>
<UriInputField name="postLogoutRedirectUris" />
Remember to keep the path `/SignedOutCallback` in the URI as it's the default value for the Logto authentication middleware.
</Step>
<Step title="Sign-in and sign-out">
In the Razor component, add the following code:
```cshtml
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager
@* ... *@
<p>Is authenticated: @User.Identity?.IsAuthenticated</p>
@if (User.Identity?.IsAuthenticated == true)
{
<button @onclick="SignOut">Sign out</button>
}
else
{
<button @onclick="SignIn">Sign in</button>
}
@* ... *@
@code {
private ClaimsPrincipal? User { get; set; }
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
User = authState.User;
}
private void SignIn()
{
NavigationManager.NavigateTo("/SignIn", forceLoad: true);
}
private void SignOut()
{
NavigationManager.NavigateTo("/SignOut", forceLoad: true);
}
}
```
**Explanation**:
- The injected `AuthenticationStateProvider` is used to get the current user's authentication state, and populate the `User` property.
- The `SignIn` and `SignOut` methods are used to redirect the user to the sign-in and sign-out endpoints respectively. Since the nature of Blazor Server, we need to use `NavigationManager` with force load to trigger the redirection.
The page will show the "Sign in" button if the user is not authenticated, and show the "Sign out" button if the user is authenticated.
</Step>
<Step title="Checkpoint: Test your application">
Now you can run the web application and try to sign in and sign out with Logto:
1. Open the web application in your browser, you should see "Is authenticated: False" and the "Sign in" button.
2. Click the "Sign in" button, and you should be redirected to the Logto sign-in page.
3. After you have signed in, you should be redirected back to the web application, and you should see "Is authenticated: True" and the "Sign out" button.
4. Click the "Sign out" button, and you should be redirected to the Logto sign-out page, and then redirected back to the web application.
</Step>
<Step title="The user object">
To know if the user is authenticated, you can check the `User.Identity?.IsAuthenticated` property.
To get the user profile claims, you can use the `User.Claims` property:
```csharp
var claims = User.Claims;
// Get the user ID
var userId = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Subject)?.Value;
```
See the [full tutorial](https://docs.logto.io/sdk/dotnet-core/blazor-server/) for more details.
</Step>
<Step title="The `<AuthorizeView />` component">
Alternatively, you can use the `AuthorizeView` component to conditionally render content based on the user's authentication state. This component is useful when you want to show different content to authenticated and unauthenticated users.
In your Razor component, add the following code:
```cshtml
@using Microsoft.AspNetCore.Components.Authorization
@* ... *@
<AuthorizeView>
<Authorized>
<p>Name: @User?.Identity?.Name</p>
@* Content for authenticated users *@
</Authorized>
<NotAuthorized>
@* Content for unauthenticated users *@
</NotAuthorized>
</AuthorizeView>
@* ... *@
```
The `AuthorizeView` component requires a cascading parameter of type `Task<AuthenticationState>`. A direct way to get this parameter is to add the `<CascadingAuthenticationState>` component. However, due to the nature of Blazor Server, we cannot simply add the component to the layout or the root component (it may not work as expected). Instead, we can add the following code to the builder (`Program.cs` or `Startup.cs`) to provide the cascading parameter:
```csharp
builder.Services.AddCascadingAuthenticationState();
```
Then you can use the `AuthorizeView` component in every component that needs it.
</Step>
</Steps>

View file

@ -0,0 +1,3 @@
{
"order": 5.2
}

View file

@ -3,12 +3,12 @@ import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'ASP.NET Core',
description: 'ASP.NET Core is a cross-platform framework for building modern apps.',
name: '.NET Core (Blazor Server)',
description: 'Integrate Logto into your .NET Core Blazor Server app.',
target: ApplicationType.Traditional,
sample: {
repo: 'csharp',
path: 'sample',
path: '/',
},
});

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.991" fill="#5b2d91" d="M 999.5,224.5 C 999.5,247.5 999.5,270.5 999.5,293.5C 987.105,443.45 926.105,569.617 816.5,672C 683.561,790.753 527.894,841.086 349.5,823C 270.074,808.831 208.241,768.331 164,701.5C 125.156,635.721 118.49,566.721 144,494.5C 175.867,414.302 233.367,362.469 316.5,339C 413.785,318.432 496.285,344.266 564,416.5C 609.201,469.44 629.201,530.773 624,600.5C 621.636,620.75 612.469,636.917 596.5,649C 577.395,658.77 558.395,658.437 539.5,648C 525.633,638.124 517.466,624.624 515,607.5C 513.716,563.856 513.049,520.19 513,476.5C 507.273,447.106 489.773,429.273 460.5,423C 422.5,422.333 384.5,422.333 346.5,423C 283.864,433.306 241.364,468.139 219,527.5C 201.587,593.253 218.421,648.42 269.5,693C 321.584,731.019 376.584,735.686 434.5,707C 451.112,697.225 465.779,685.058 478.5,670.5C 495.368,696.172 519.035,711.672 549.5,717C 607.554,723.889 648.721,700.722 673,647.5C 684.436,618.479 688.103,588.479 684,557.5C 672.278,445.129 617.778,360.963 520.5,305C 430.417,259.489 339.084,256.822 246.5,297C 160.387,339.124 103.887,406.291 77,498.5C 51.6188,604.965 74.2855,699.632 145,782.5C 201.202,842.351 270.369,876.851 352.5,886C 503.388,899.11 642.721,865.443 770.5,785C 772.345,783.872 774.345,783.372 776.5,783.5C 776.657,784.873 776.49,786.207 776,787.5C 679.835,888.483 561.668,942.983 421.5,951C 348.701,958.22 278.701,947.553 211.5,919C 102.973,866.45 35.1393,781.283 8,663.5C 4.0882,644.511 1.25487,625.511 -0.5,606.5C -0.5,587.5 -0.5,568.5 -0.5,549.5C 12.4764,422.139 72.4764,323.972 179.5,255C 231.04,224.599 286.707,207.266 346.5,203C 397.833,202.667 449.167,202.333 500.5,202C 591.917,196.616 668.584,160.616 730.5,94C 735.357,92.2472 738.357,93.7472 739.5,98.5C 733.96,152.291 716.793,201.958 688,247.5C 686.835,250.814 688.002,252.814 691.5,253.5C 745.424,240.368 794.091,216.868 837.5,183C 868.397,156.65 894.231,126.15 915,91.5C 923.707,76.584 932.541,61.7507 941.5,47C 945.274,45.8175 948.441,46.6508 951,49.5C 979.811,104.421 995.978,162.754 999.5,224.5 Z"/></g>
<g><path style="opacity:0.991" fill="#5b2d91" d="M 348.5,482.5 C 382.835,482.333 417.168,482.5 451.5,483C 452.701,483.903 453.535,485.069 454,486.5C 454.667,518.167 454.667,549.833 454,581.5C 449.301,618.529 429.801,644.363 395.5,659C 350.606,672.59 314.106,660.757 286,623.5C 265.604,587.302 266.938,551.968 290,517.5C 305.312,498.763 324.812,487.097 348.5,482.5 Z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,7 +1,4 @@
import UriInputField from '@/mdx-components/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
import Steps from '@/mdx-components/Steps';
import Step from '@/mdx-components/Step';
@ -176,7 +173,7 @@ var claims = User.Claims;
var userId = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Subject)?.Value;
```
See the [full tutorial](https://github.com/logto-io/csharp/tree/HEAD/src/Logto.AspNetCore.Authentication/docs/tutorial.md) for more details.
See the [full tutorial](https://docs.logto.io/sdk/dotnet-core/mvc/) for more details.
</Step>

View file

@ -3,12 +3,12 @@ import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'ASP.NET Core (MVC)',
description: 'ASP.NET Core is a cross-platform framework for building modern apps.',
name: '.NET Core (MVC)',
description: 'Integrate Logto into your .NET Core web app with Model-View-Controller (MVC).',
target: ApplicationType.Traditional,
sample: {
repo: 'csharp',
path: 'sample-mvc',
path: '/',
},
});

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,7 +1,4 @@
import UriInputField from '@/mdx-components/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
import Steps from '@/mdx-components/Steps';
import Step from '@/mdx-components/Step';
@ -186,7 +183,7 @@ var claims = User.Claims;
var userId = claims.FirstOrDefault(c => c.Type == LogtoParameters.Claims.Subject)?.Value;
```
See the [full tutorial](https://github.com/logto-io/csharp/tree/HEAD/src/Logto.AspNetCore.Authentication/docs/tutorial.md) for more details.
See the [full tutorial](https://docs.logto.io/sdk/dotnet-core/razor/) for more details.
</Step>

View file

@ -0,0 +1,15 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: '.NET Core (Razor Pages)',
description: 'Integrate Logto into your .NET Core web app with Razor Pages and view models.',
target: ApplicationType.Traditional,
sample: {
repo: 'csharp',
path: '/',
},
});
export default metadata;

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB