diff --git a/packages/console/src/assets/docs/guides/index.ts b/packages/console/src/assets/docs/guides/index.ts index bb239f38d..1b14290d9 100644 --- a/packages/console/src/assets/docs/guides/index.ts +++ b/packages/console/src/assets/docs/guides/index.ts @@ -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 = 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 = 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 = 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 = 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 = 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, diff --git a/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/README.mdx b/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/README.mdx new file mode 100644 index 000000000..006165530 --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/README.mdx @@ -0,0 +1,225 @@ +import UriInputField from '@/mdx-components/UriInputField'; +import Steps from '@/mdx-components/Steps'; +import Step from '@/mdx-components/Step'; + + + + + +This tutorial will show you how to use Logto ASP.NET Core authentication middleware to protect your web application. + +
    +
  • It assumes your website is hosted on {props.sampleUrls.origin}.
  • +
+ +### Installation + +```bash +dotnet add package Logto.AspNetCore.Authentication +``` + +
+ + + +Open `Startup.cs` (or `Program.cs`) and add the following code to register Logto authentication middleware: + +
+  
+{`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();`}
+  
+
+ +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. + +
+ + + +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. + + + + + +

+First, let's enter your redirect URI. E.g. {props.sampleUrls.origin + 'Callback'} (replace the endpoint with yours). This is where Logto will redirect users after they sign in. +

+ + + +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. + +

+For example, set the URI to {props.sampleUrls.origin + 'SignedOutCallback'} (replace the endpoint with yours): +

+ + + +Remember to keep the path `/SignedOutCallback` in the URI as it's the default value for the Logto authentication middleware. + +
+ + + +In the Razor component, add the following code: + +```cshtml +@using Microsoft.AspNetCore.Components.Authorization +@using System.Security.Claims +@inject AuthenticationStateProvider AuthenticationStateProvider +@inject NavigationManager NavigationManager + +@* ... *@ + +

Is authenticated: @User.Identity?.IsAuthenticated

+@if (User.Identity?.IsAuthenticated == true) +{ + +} +else +{ + +} + +@* ... *@ + +@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. + +
+ + + +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. + + + + + +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. + + + + + +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 + +@* ... *@ + + + +

Name: @User?.Identity?.Name

+ @* Content for authenticated users *@ +
+ + @* Content for unauthenticated users *@ + +
+ +@* ... *@ +``` + +The `AuthorizeView` component requires a cascading parameter of type `Task`. A direct way to get this parameter is to add the `` 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. + +
+ +
diff --git a/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/config.json b/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/config.json new file mode 100644 index 000000000..f6c789108 --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/config.json @@ -0,0 +1,3 @@ +{ + "order": 5.2 +} diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core/index.ts b/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/index.ts similarity index 66% rename from packages/console/src/assets/docs/guides/web-asp-net-core/index.ts rename to packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/index.ts index 35dedf614..bee98707b 100644 --- a/packages/console/src/assets/docs/guides/web-asp-net-core/index.ts +++ b/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/index.ts @@ -3,12 +3,12 @@ import { ApplicationType } from '@logto/schemas'; import { type GuideMetadata } from '../types'; const metadata: Readonly = 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: '/', }, }); diff --git a/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/logo.svg b/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/logo.svg new file mode 100644 index 000000000..29dc61d07 --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-dotnet-core-blazor-server/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/README.mdx b/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/README.mdx similarity index 94% rename from packages/console/src/assets/docs/guides/web-asp-net-core-mvc/README.mdx rename to packages/console/src/assets/docs/guides/web-dotnet-core-mvc/README.mdx index d52a4bd35..7dead35f2 100644 --- a/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/README.mdx +++ b/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/README.mdx @@ -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. diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/config.json b/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/config.json similarity index 100% rename from packages/console/src/assets/docs/guides/web-asp-net-core-mvc/config.json rename to packages/console/src/assets/docs/guides/web-dotnet-core-mvc/config.json diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/index.ts b/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/index.ts similarity index 65% rename from packages/console/src/assets/docs/guides/web-asp-net-core-mvc/index.ts rename to packages/console/src/assets/docs/guides/web-dotnet-core-mvc/index.ts index 0bdf883a3..d86e0ff55 100644 --- a/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/index.ts +++ b/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/index.ts @@ -3,12 +3,12 @@ import { ApplicationType } from '@logto/schemas'; import { type GuideMetadata } from '../types'; const metadata: Readonly = 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: '/', }, }); diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core-mvc/logo.svg b/packages/console/src/assets/docs/guides/web-dotnet-core-mvc/logo.svg similarity index 100% rename from packages/console/src/assets/docs/guides/web-asp-net-core-mvc/logo.svg rename to packages/console/src/assets/docs/guides/web-dotnet-core-mvc/logo.svg diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx b/packages/console/src/assets/docs/guides/web-dotnet-core/README.mdx similarity index 94% rename from packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx rename to packages/console/src/assets/docs/guides/web-dotnet-core/README.mdx index 28c25efe7..bff61cdfc 100644 --- a/packages/console/src/assets/docs/guides/web-asp-net-core/README.mdx +++ b/packages/console/src/assets/docs/guides/web-dotnet-core/README.mdx @@ -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. diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core/config.json b/packages/console/src/assets/docs/guides/web-dotnet-core/config.json similarity index 100% rename from packages/console/src/assets/docs/guides/web-asp-net-core/config.json rename to packages/console/src/assets/docs/guides/web-dotnet-core/config.json diff --git a/packages/console/src/assets/docs/guides/web-dotnet-core/index.ts b/packages/console/src/assets/docs/guides/web-dotnet-core/index.ts new file mode 100644 index 000000000..550ead971 --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-dotnet-core/index.ts @@ -0,0 +1,15 @@ +import { ApplicationType } from '@logto/schemas'; + +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = 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; diff --git a/packages/console/src/assets/docs/guides/web-asp-net-core/logo.svg b/packages/console/src/assets/docs/guides/web-dotnet-core/logo.svg similarity index 100% rename from packages/console/src/assets/docs/guides/web-asp-net-core/logo.svg rename to packages/console/src/assets/docs/guides/web-dotnet-core/logo.svg