From 831fd8a1cbc5982a48d9c4ce2e59d3b60d37a917 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 9 Sep 2026 14:47:56 +0800 Subject: [PATCH 1/2] docs: update Angular guide for SDK v2 --- .../quick-starts/framework/angular/README.mdx | 7 +- .../framework/angular/_api-resources.mdx | 59 ++++++- .../angular/_get-user-information.mdx | 157 ++++++++---------- .../framework/angular/_guide-tip.md | 4 +- .../framework/angular/_installation.mdx | 31 +--- .../framework/angular/_integration.mdx | 143 +++++++++++----- .../angular/code/_config-organization-code.md | 17 ++ .../angular/code/_config-resources-code.md | 19 ++- .../_config-resources-with-scopes-code.md | 19 ++- ...onfig-resources-with-shared-scopes-code.md | 18 ++ .../angular/code/_get-access-token-code.md | 28 ++++ .../_get-organization-access-token-code.md | 59 +++++++ 12 files changed, 367 insertions(+), 194 deletions(-) create mode 100644 docs/quick-starts/framework/angular/code/_config-organization-code.md create mode 100644 docs/quick-starts/framework/angular/code/_config-resources-with-shared-scopes-code.md create mode 100644 docs/quick-starts/framework/angular/code/_get-access-token-code.md create mode 100644 docs/quick-starts/framework/angular/code/_get-organization-access-token-code.md diff --git a/docs/quick-starts/framework/angular/README.mdx b/docs/quick-starts/framework/angular/README.mdx index 77996086e0b..80263827eea 100644 --- a/docs/quick-starts/framework/angular/README.mdx +++ b/docs/quick-starts/framework/angular/README.mdx @@ -2,7 +2,7 @@ slug: /quick-starts/angular sidebar_label: Angular sidebar_custom_props: - description: Angular is a JavaScript library for building user interfaces. + description: Angular is a framework for building web applications. --- import FurtherReadings from '../../fragments/_further-readings.md'; @@ -15,12 +15,15 @@ import Integration from './_integration.mdx'; # Add authentication to your Angular application +This guide will show you how to integrate Logto Angular SDK v2 into your application. + ## Prerequisites \{#prerequisites} - A [Logto Cloud](https://cloud.logto.io) account or a [self-hosted Logto](/introduction/set-up-logto-oss). -- A Logto single-page application created. +- A single-page application (SPA) created in Logto Console. +- An Angular 20 project. ## Installation \{#installation} diff --git a/docs/quick-starts/framework/angular/_api-resources.mdx b/docs/quick-starts/framework/angular/_api-resources.mdx index 8c88f2155ec..43eb08e9001 100644 --- a/docs/quick-starts/framework/angular/_api-resources.mdx +++ b/docs/quick-starts/framework/angular/_api-resources.mdx @@ -1,22 +1,67 @@ import ApiResourcesDescription from '../../fragments/_api-resources-description.md'; import ConfigApiResources from '../../fragments/_config-api-resources.mdx'; +import FetchAccessTokenForApiResources from '../../fragments/_fetch-access-token-for-api-resources.mdx'; +import FetchOrganizationTokenForUser from '../../fragments/_fetch-organization-token-for-user.mdx'; +import ConfigOrganizationCode from './code/_config-organization-code.md'; import ConfigResourcesCode from './code/_config-resources-code.md'; import ConfigResourcesWithScopesCode from './code/_config-resources-with-scopes-code.md'; - -### Configure `angular-auth-oidc-client` for API resource \{#configure-angular-auth-oidc-client-for-api-resource} +import ConfigResourcesWithSharedScopesCode from './code/_config-resources-with-shared-scopes-code.md'; +import GetAccessTokenCode from './code/_get-access-token-code.md'; +import GetOrganizationAccessTokenCode from './code/_get-organization-access-token-code.md'; +### Configure Logto client \{#configure-logto-client} + } configResourcesWithScopesCode={} + configResourcesWithSharedScopesCode={} /> -Now, the access token will be in the JSON Web Token (JWT) format instead of a random string (opaque token). +Sign in again after changing the resources or scopes so the user can authorize the updated configuration. -:::warning -Both `autoUserInfo` and `renewUserInfoAfterTokenRenew` will be disabled when `resource` is set. This is because the access token will be requested for the specific API resource and not for the user info endpoint. -::: +### Fetch access token for the API resource \{#fetch-access-token-for-the-api-resource} + +} +/> + +Use the exact resource identifier from your configuration. Call `getAccessToken(resource)` whenever you make an API request so the SDK can return a valid token, rather than keeping a token indefinitely in your component. + +### Fetch organization tokens \{#fetch-organization-tokens} + +} + getOrganizationAccessTokenCode={} +/> + +Merge `UserScope.Organizations` with any existing scopes, and sign in again after updating the configuration. `getOrganizationToken(organizationId)` returns a token for the selected Logto organization; use `getAccessToken(resource)` for an API resource token. + +### Attach access token to request headers \{#attach-access-token-to-request-headers} + +Put the token in the `Authorization` HTTP header using the Bearer format (`Bearer YOUR_TOKEN`). For example, add this method to an authenticated component that injects `LogtoService`: -Currently, only Logto official SDKs support the ability to request both user info and API resource access tokens. If you need to request both, please do not hesitate to contact us. +```ts +async fetchProducts() { + const accessToken = await this.logto.getAccessToken('https://shopping.your-app.com/api'); + const response = await fetch('https://shopping.your-app.com/api/products', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!response.ok) { + throw new Error(`Request failed: ${response.status}`); + } + + return response.json(); +} +``` + +:::note +The example uses `fetch`. If you use Angular `HttpClient`, set the same `Authorization` header in its request options. +::: diff --git a/docs/quick-starts/framework/angular/_get-user-information.mdx b/docs/quick-starts/framework/angular/_get-user-information.mdx index f8eaea133e0..015a693ddb3 100644 --- a/docs/quick-starts/framework/angular/_get-user-information.mdx +++ b/docs/quick-starts/framework/angular/_get-user-information.mdx @@ -5,132 +5,105 @@ import FindUserInfoMissing from '../../fragments/_find-user-info-missing.mdx'; import ScopesAndClaims from '../../fragments/_scopes-and-claims.mdx'; import ScopesAndClaimsIntroduction from '../../fragments/_scopes-claims-introduction.md'; -Once the user has successfully signed in, Logto will issue an [ID token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) that contains the user information claims. The ID token is a JSON Web Token (JWT). - -It's important to note that the user information claims that can be retrieved depending on the -scopes used by the user during signing-in, and considering performance and data size, the ID token -may not contain all user claims, some user claims are only available in the [userinfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) (see -the related list below). - -The `buildAngularAuthConfig()` utility will enable `autoUserInfo` and `renewUserInfoAfterTokenRenew` if there's no `resource` provided in the config. This means that Logto will automatically fetch the user information after the user signs in and renew the user information after the token is renewed. - -:::info -To learn more about configuring the `angular-auth-oidc-client` library, see the [official documentation](https://angular-auth-oidc-client.com/). -::: - ### Display user information \{#display-user-information} -The `OidcSecurityService` provides a convenient way to subscribe to the authentication state as well as user information: +To display the user's information, use `getIdTokenClaims()` to read claims from the ID token without an additional network request. Add an `effect` to your `AppComponent` to load the claims when `isAuthenticated()` becomes true, including when an existing session is restored. Import `JsonPipe` to display the result: ```ts title="app/app.component.ts" -import { OidcSecurityService } from 'angular-auth-oidc-client'; -import { decodeIdToken, type IdTokenClaims } from '@logto/js'; - -export class AppComponent implements OnInit { - isAuthenticated = false; - idTokenClaims?: IdTokenClaims; - accessToken?: string; - - constructor(public oidcSecurityService: OidcSecurityService) {} - - ngOnInit() { - this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, idToken, accessToken }) => { - console.log('app authenticated', isAuthenticated, idToken); - this.isAuthenticated = isAuthenticated; - this.idTokenClaims = decodeIdToken(idToken); - this.accessToken = accessToken; +import { JsonPipe } from '@angular/common'; +import { Component, effect, inject, signal } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { LogtoService, type IdTokenClaims } from '@logto/angular'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [JsonPipe, RouterOutlet], + templateUrl: './app.component.html', +}) +export class AppComponent { + readonly logto = inject(LogtoService); + readonly user = signal(undefined); + + constructor() { + effect(() => { + if (!this.logto.isAuthenticated()) { + this.user.set(undefined); + return; + } + + void this.logto + .getIdTokenClaims() + .then((claims) => { + this.user.set(claims); + }) + .catch(() => { + // The SDK exposes the error through logto.error() for the template. + }); }); } - // ...other methods + // ...keep the signIn() and signOut() methods from the previous step } ``` -And use it in the template: +Add the following inside the `logto.isAuthenticated()` branch of your template: ```html title="app/app.component.html" - - -
{{ idTokenClaims | json }}
-

Access token: {{ accessToken }}

- - -
+@if (user(); as claims) { +
{{ claims | json }}
+} ``` ### Request additional claims \{#request-additional-claims} - + -To request additional scopes, you can configure the auth provider configs: +Add the scopes to your `provideLogto` configuration: -```tsx title="app/app.config.ts" -import { UserScope, buildAngularAuthConfig } from '@logto/js'; +```ts title="app/app.config.ts" +import { type ApplicationConfig } from '@angular/core'; +import { provideLogto, UserScope } from '@logto/angular'; export const appConfig: ApplicationConfig = { providers: [ - provideHttpClient(withFetch()), - provideAuth({ - config: buildAngularAuthConfig({ - // ...other configs - // highlight-start - scopes: [ - UserScope.Email, - UserScope.Phone, - UserScope.CustomData, - UserScope.Identities, - UserScope.Organizations, - ], - // highlight-end - }), + provideLogto({ + endpoint: '', + appId: '', + scopes: [ + UserScope.Email, + UserScope.Phone, + UserScope.CustomData, + UserScope.Identities, + UserScope.Organizations, + ], }), // ...other providers ], }; ``` -Then you can access the additional claims in the return value of `idToken`. +Sign in again after changing the scopes. The additional ID token claims, such as `email` and `phone_number`, will be available from `getIdTokenClaims()` and displayed by the example above. -{/* eslint-disable prettier/prettier */} {`import { OidcSecurityService } from 'angular-auth-oidc-client'; -// highlight-next-line -import { type UserInfoResponse } from '@logto/js'; - -export class AppComponent implements OnInit { - isAuthenticated = false; - // highlight-next-line - userData?: UserInfoResponse; - accessToken?: string; - - constructor(public oidcSecurityService: OidcSecurityService) {} - - ngOnInit() { - this.oidcSecurityService - .checkAuth() - // highlight-next-line - .subscribe(({ isAuthenticated, userData, accessToken }) => { - console.log('app authenticated', isAuthenticated, idToken); - this.isAuthenticated = isAuthenticated; - // highlight-next-line - this.userData = userData; - this.accessToken = accessToken; - }); + {`// Add this method to AppComponent and call it after sign-in. +async loadUserInfo() { + const userInfo = await this.logto.fetchUserInfo(); + // Now you can access userInfo.custom_data, userInfo.identities, etc. + return userInfo; +}`} } - - // ...other methods -} - -// Now you can access the claim \`userData.custom_data\``} -} /> -{/* eslint-enable prettier/prettier */} + +`fetchUserInfo()` can be used alongside API resource access tokens. Configuring `resources` does not prevent the SDK from requesting user information. ### Scopes and claims \{#scopes-and-claims} diff --git a/docs/quick-starts/framework/angular/_guide-tip.md b/docs/quick-starts/framework/angular/_guide-tip.md index 0146b49d657..7ef48064400 100644 --- a/docs/quick-starts/framework/angular/_guide-tip.md +++ b/docs/quick-starts/framework/angular/_guide-tip.md @@ -1,6 +1,6 @@ :::tip -- The following demonstration is built on Angular 18.0.0 and [angular-auth-oidc-client](https://github.com/damienbod/angular-auth-oidc-client). -- The sample project is available in the [GitHub repository](https://github.com/logto-io/js/tree/master/packages/angular-sample). +- This guide uses the first-party `@logto/angular` v2 SDK, which supports Angular 20 and provides dependency injection and Signals. +- The sample project is available in our [SDK repository](https://github.com/logto-io/js/tree/master/packages/angular-sample). ::: diff --git a/docs/quick-starts/framework/angular/_installation.mdx b/docs/quick-starts/framework/angular/_installation.mdx index 9bc1115d9d6..41aabde1f14 100644 --- a/docs/quick-starts/framework/angular/_installation.mdx +++ b/docs/quick-starts/framework/angular/_installation.mdx @@ -1,30 +1,3 @@ -import TabItem from '@theme/TabItem'; -import Tabs from '@theme/Tabs'; +import NpmLikeInstallation from '../../fragments/_npm-like-installation.mdx'; -Install Logto JS core SDK and Angular OIDC client library: - - - - - -```bash -npm i @logto/js angular-auth-oidc-client -``` - - - - -```bash -pnpm add @logto/js angular-auth-oidc-client -``` - - - - -```bash -yarn add @logto/js angular-auth-oidc-client -``` - - - - + diff --git a/docs/quick-starts/framework/angular/_integration.mdx b/docs/quick-starts/framework/angular/_integration.mdx index f2136ba0f86..654dbea1337 100644 --- a/docs/quick-starts/framework/angular/_integration.mdx +++ b/docs/quick-starts/framework/angular/_integration.mdx @@ -1,33 +1,41 @@ import Checkpoint from '../../fragments/_checkpoint-test-your-application.md'; +import SignInNote from '../../fragments/_sign-in-note.mdx'; import AssumingUrl from '../../fragments/_web-assuming-url.mdx'; import WebConfigureRedirectUris from '../../fragments/_web-configure-redirect-uris.mdx'; import SignInFlowSummary from '../../fragments/_web-sign-in-flow-summary.mdx'; import SignOutNote from '../../fragments/_web-sign-out-note.md'; -### Configure application \{#configure-application} +### Init Logto provider \{#init-logto-provider} -In your Angular project, add the auth provider in your `app.config.ts`: +In your Angular project, register `provideLogto` and your application routes in `app.config.ts`: ```ts title="app/app.config.ts" -import { buildAngularAuthConfig } from '@logto/js'; -import { provideAuth } from 'angular-auth-oidc-client'; +import { type ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { provideLogto } from '@logto/angular'; + +import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ - provideHttpClient(withFetch()), - provideAuth({ - config: buildAngularAuthConfig({ - endpoint: '', - appId: '', - redirectUri: 'http://localhost:3000/callback', - postLogoutRedirectUri: 'http://localhost:3000/', - }), + provideLogto({ + endpoint: '', + appId: '', }), + provideRouter(routes), // ...other providers ], }; ``` +`provideLogto` restores authentication state automatically after the first browser render. You do not need to call an initialization method in your components. + +:::note +When using server-side rendering (SSR), authentication state and tokens are available only in the +browser. Use `isLoading()` to show a loading state until initialization finishes. If you need +authenticated data during server rendering, use a server or BFF SDK. +::: + ### Configure redirect URIs \{#configure-redirect-uris} @@ -38,55 +46,102 @@ export const appConfig: ApplicationConfig = { ### Handle redirect \{#handle-redirect} -Since we use `http://localhost:3000/callback` as the redirect URI, now we need to handle it properly. The `angular-auth-oidc-client` library provides built-in support for handling the redirect. You can just configure the auth provider configs properly and the library will handle the rest. +Create a callback component to complete sign-in after Logto redirects the user back to your application. Use `afterNextRender` so callback handling runs only in the browser: + +```ts title="app/callback.component.ts" +import { afterNextRender, Component, inject } from '@angular/core'; +import { LogtoService } from '@logto/angular'; + +@Component({ + selector: 'app-callback', + standalone: true, + template: ` + @if (logto.error(); as error) { +

{{ error.message }}

+ } @else { +

Completing sign-in...

+ } + `, +}) +export class CallbackComponent { + readonly logto = inject(LogtoService); + + constructor() { + afterNextRender(() => { + void this.logto.handleSignInCallback(window.location.href).catch(() => { + // The SDK exposes the error through logto.error() for the template. + }); + }); + } +} +``` -```ts title="app/app.config.ts" -export const appConfig: ApplicationConfig = { - providers: [ - provideAuth({ - config: buildAngularAuthConfig({ - // ...other config - // highlight-start - redirectUri: 'http://localhost:3000/callback', - postLogoutRedirectUri: 'http://localhost:3000/', - // highlight-end - }), - }), - // ...other providers - ], -}; +Register the callback route in `app.routes.ts`. It must match the path of your redirect URI and must not require authentication. For example, use `callback` for a redirect URI ending in `/callback`: + +```ts title="app/app.routes.ts" +import { type Routes } from '@angular/router'; + +import { CallbackComponent } from './callback.component'; + +export const routes: Routes = [ + { path: 'callback', component: CallbackComponent }, + // ...other routes +]; ``` -### Implement sign-in and sign-out \{#implement-sign-in-and-sign-out} +The root component needs a `` to render this route, as shown in the next step. -In the component where you want to implement sign-in and sign-out (for example, `app.component.ts`), inject the `OidcSecurityService` and use it to sign in and sign out. +### Implement sign-in and sign-out \{#implement-sign-in-and-sign-out} -```ts title="app/app.component.ts" -import { OidcSecurityService } from 'angular-auth-oidc-client'; +Inject `LogtoService` to start sign-in and sign-out. Pass the registered redirect URIs to these methods. The `postRedirectUri` tells the SDK where to navigate after successfully handling the sign-in callback: -export class AppComponent implements OnInit { - constructor(public oidcSecurityService: OidcSecurityService) {} + - signIn() { - this.oidcSecurityService.authorize(); +```ts title="app/app.component.ts" +import { Component, inject } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { LogtoService } from '@logto/angular'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + templateUrl: './app.component.html', +}) +export class AppComponent { + readonly logto = inject(LogtoService); + + async signIn() { + await this.logto.signIn({ + redirectUri: 'http://localhost:3000/callback', + postRedirectUri: window.location.origin, + }); } - signOut() { - this.oidcSecurityService.logoff().subscribe((result) => { - console.log('app sign-out', result); - }); + async signOut() { + await this.logto.signOut('http://localhost:3000/'); } } ``` -Then, in the template, add buttons to sign in and sign out: +Read the `isLoading()`, `isAuthenticated()`, and `error()` Signals directly in the template: ```html title="app/app.component.html" - -
- +@if (logto.error(); as error) { +

{{ error.message }}

+} @if (logto.isLoading()) { +

Loading...

+} @else if (logto.isAuthenticated()) { + +} @else { + +} + + ``` +Keep `` outside the authentication conditions so the callback can render before the user is signed in. + diff --git a/docs/quick-starts/framework/angular/code/_config-organization-code.md b/docs/quick-starts/framework/angular/code/_config-organization-code.md new file mode 100644 index 00000000000..63c1a120189 --- /dev/null +++ b/docs/quick-starts/framework/angular/code/_config-organization-code.md @@ -0,0 +1,17 @@ +```ts title="app/app.config.ts" +import { type ApplicationConfig } from '@angular/core'; +import { provideLogto, UserScope } from '@logto/angular'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideLogto({ + endpoint: '', + appId: '', + // highlight-start + scopes: [UserScope.Organizations], + // highlight-end + }), + // ...other providers + ], +}; +``` diff --git a/docs/quick-starts/framework/angular/code/_config-resources-code.md b/docs/quick-starts/framework/angular/code/_config-resources-code.md index d680e1b878a..7d6a1a15342 100644 --- a/docs/quick-starts/framework/angular/code/_config-resources-code.md +++ b/docs/quick-starts/framework/angular/code/_config-resources-code.md @@ -1,14 +1,15 @@ -```ts title="/app/app.config.ts" +```ts title="app/app.config.ts" +import { type ApplicationConfig } from '@angular/core'; +import { provideLogto } from '@logto/angular'; + export const appConfig: ApplicationConfig = { providers: [ - provideHttpClient(withFetch()), - provideAuth({ - config: buildAngularAuthConfig({ - // ...other config - // highlight-start - resource: 'https://your-api-resource.com', - // highlight-end - }), + provideLogto({ + endpoint: '', + appId: '', + // highlight-start + resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], + // highlight-end }), // ...other providers ], diff --git a/docs/quick-starts/framework/angular/code/_config-resources-with-scopes-code.md b/docs/quick-starts/framework/angular/code/_config-resources-with-scopes-code.md index 135f1616058..d2c0c7bba17 100644 --- a/docs/quick-starts/framework/angular/code/_config-resources-with-scopes-code.md +++ b/docs/quick-starts/framework/angular/code/_config-resources-with-scopes-code.md @@ -1,15 +1,16 @@ ```ts title="app/app.config.ts" +import { type ApplicationConfig } from '@angular/core'; +import { provideLogto } from '@logto/angular'; + export const appConfig: ApplicationConfig = { providers: [ - provideHttpClient(withFetch()), - provideAuth({ - config: buildAngularAuthConfig({ - // ...other config - // highlight-start - resource: 'https://your-api-resource.com', - scopes: ['openid', 'profile', 'offline_access', 'read', 'write'], - // highlight-end - }), + provideLogto({ + endpoint: '', + appId: '', + // highlight-start + scopes: ['shopping:read', 'shopping:write', 'store:read', 'store:write'], + resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], + // highlight-end }), // ...other providers ], diff --git a/docs/quick-starts/framework/angular/code/_config-resources-with-shared-scopes-code.md b/docs/quick-starts/framework/angular/code/_config-resources-with-shared-scopes-code.md new file mode 100644 index 00000000000..18f488dfd41 --- /dev/null +++ b/docs/quick-starts/framework/angular/code/_config-resources-with-shared-scopes-code.md @@ -0,0 +1,18 @@ +```ts title="app/app.config.ts" +import { type ApplicationConfig } from '@angular/core'; +import { provideLogto } from '@logto/angular'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideLogto({ + endpoint: '', + appId: '', + // highlight-start + scopes: ['read', 'write'], + resources: ['https://shopping.your-app.com/api', 'https://store.your-app.com/api'], + // highlight-end + }), + // ...other providers + ], +}; +``` diff --git a/docs/quick-starts/framework/angular/code/_get-access-token-code.md b/docs/quick-starts/framework/angular/code/_get-access-token-code.md new file mode 100644 index 00000000000..fc2d97d66e3 --- /dev/null +++ b/docs/quick-starts/framework/angular/code/_get-access-token-code.md @@ -0,0 +1,28 @@ +```ts title="app/api-resource.component.ts" +import { Component, inject, signal } from '@angular/core'; +import { LogtoService } from '@logto/angular'; + +@Component({ + selector: 'app-api-resource', + standalone: true, + template: ` + @if (logto.error(); as error) { +

{{ error.message }}

+ } + @if (logto.isAuthenticated()) { + +
{{ accessToken() }}
+ } + `, +}) +export class ApiResourceComponent { + readonly logto = inject(LogtoService); + readonly accessToken = signal(''); + + async loadAccessToken() { + this.accessToken.set(await this.logto.getAccessToken('https://shopping.your-app.com/api')); + } +} +``` diff --git a/docs/quick-starts/framework/angular/code/_get-organization-access-token-code.md b/docs/quick-starts/framework/angular/code/_get-organization-access-token-code.md new file mode 100644 index 00000000000..11083d2cbae --- /dev/null +++ b/docs/quick-starts/framework/angular/code/_get-organization-access-token-code.md @@ -0,0 +1,59 @@ +```ts title="app/organizations.component.ts" +import { Component, effect, inject, signal } from '@angular/core'; +import { LogtoService } from '@logto/angular'; + +@Component({ + selector: 'app-organizations', + standalone: true, + template: ` + @if (logto.error(); as error) { +

{{ error.message }}

+ } + @if (logto.isAuthenticated()) { +
    + @for (organizationId of organizationIds(); track organizationId) { +
  • + {{ organizationId }} + +
  • + } +
+
{{ organizationToken() }}
+ } + `, +}) +export class OrganizationsComponent { + readonly logto = inject(LogtoService); + readonly organizationIds = signal([]); + readonly organizationToken = signal(''); + + constructor() { + effect(() => { + if (!this.logto.isAuthenticated()) { + this.organizationIds.set([]); + this.organizationToken.set(''); + return; + } + + void this.logto + .getIdTokenClaims() + .then((claims) => { + this.organizationIds.set(claims.organizations ?? []); + }) + .catch(() => { + // The SDK exposes the error through logto.error() for the template. + }); + }); + } + + async loadOrganizationToken(organizationId: string) { + this.organizationToken.set(await this.logto.getOrganizationToken(organizationId)); + } +} +``` From 63a316b242f08911d02211e415e70ca8b1199870 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 10 Sep 2026 12:48:28 +0800 Subject: [PATCH 2/2] docs: guard Angular sign-in callback handling --- .../framework/angular/_integration.mdx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/quick-starts/framework/angular/_integration.mdx b/docs/quick-starts/framework/angular/_integration.mdx index 654dbea1337..b29707fc61c 100644 --- a/docs/quick-starts/framework/angular/_integration.mdx +++ b/docs/quick-starts/framework/angular/_integration.mdx @@ -68,14 +68,25 @@ export class CallbackComponent { constructor() { afterNextRender(() => { - void this.logto.handleSignInCallback(window.location.href).catch(() => { - // The SDK exposes the error through logto.error() for the template. + void (async () => { + const callbackUri = window.location.href; + + if (!(await this.logto.isSignInRedirected(callbackUri))) { + window.location.replace(window.location.origin); + return; + } + + await this.logto.handleSignInCallback(callbackUri); + })().catch(() => { + // The SDK exposes callback errors through logto.error() for the template. }); }); } } ``` +`isSignInRedirected()` checks whether the URL matches an active sign-in session. If someone visits the callback route without one, this example returns them to the application home page instead of attempting to complete sign-in. + Register the callback route in `app.routes.ts`. It must match the path of your redirect URI and must not require authentication. For example, use `callback` for a redirect URI ending in `/callback`: ```ts title="app/app.routes.ts"