Skip to content

Commit db682e2

Browse files
authored
feat(angular): rebuild SDK on browser client (#1165)
* feat(angular): rebuild SDK on browser client * fix(angular): refine authentication lifecycle * refactor(angular): simplify initialization cleanup * fix(angular): address initialization review feedback
1 parent be70274 commit db682e2

10 files changed

Lines changed: 858 additions & 315 deletions

File tree

.changeset/major-dolls-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@logto/angular": major
3+
---
4+
5+
rebuild the Angular SDK on the Logto Browser client

packages/angular/README.md

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,134 @@
1-
# Logto Angular helper
1+
# Logto Angular SDK
22

3-
This package provides a helper for Angular to use Logto.
3+
The Logto SDK for Angular applications. It is built on `@logto/browser` and exposes Angular-native dependency injection and Signals.
4+
5+
## Installation
6+
7+
```sh
8+
pnpm add @logto/angular
9+
```
10+
11+
## Configuration
12+
13+
Register Logto in the application config:
14+
15+
```ts
16+
import { type ApplicationConfig } from '@angular/core';
17+
import { provideRouter } from '@angular/router';
18+
import { provideLogto, UserScope } from '@logto/angular';
19+
20+
import { routes } from './app.routes';
21+
22+
export const appConfig: ApplicationConfig = {
23+
providers: [
24+
provideLogto({
25+
endpoint: 'https://your-tenant.logto.app',
26+
appId: 'your-app-id',
27+
scopes: [UserScope.Email, UserScope.Organizations],
28+
resources: ['https://api.example.com'],
29+
}),
30+
provideRouter(routes),
31+
],
32+
};
33+
```
34+
35+
## Sign in and sign out
36+
37+
Inject `LogtoService` and read its Signals directly from the template:
38+
39+
```ts
40+
import { Component, inject } from '@angular/core';
41+
import { LogtoService } from '@logto/angular';
42+
43+
@Component({
44+
selector: 'app-root',
45+
template: `
46+
@if (logto.isLoading()) {
47+
<p>Loading…</p>
48+
} @else if (logto.isAuthenticated()) {
49+
<button type="button" (click)="signOut()">Sign out</button>
50+
} @else {
51+
<button type="button" (click)="signIn()">Sign in</button>
52+
}
53+
`,
54+
})
55+
export class AppComponent {
56+
readonly logto = inject(LogtoService);
57+
58+
async signIn() {
59+
await this.logto.signIn({
60+
redirectUri: `${window.location.origin}/callback`,
61+
postRedirectUri: window.location.origin,
62+
});
63+
}
64+
65+
async signOut() {
66+
await this.logto.signOut(window.location.origin);
67+
}
68+
}
69+
```
70+
71+
## Handle the callback
72+
73+
Register a dedicated callback route and complete the sign-in flow after browser rendering:
74+
75+
```ts
76+
import { afterNextRender, Component, inject } from '@angular/core';
77+
import { LogtoService } from '@logto/angular';
78+
79+
@Component({
80+
standalone: true,
81+
template: '<p>Completing sign-in…</p>',
82+
})
83+
export class CallbackComponent {
84+
private readonly logto = inject(LogtoService);
85+
86+
constructor() {
87+
afterNextRender(() => {
88+
void (async () => {
89+
const callbackUri = window.location.href;
90+
91+
if (await this.logto.isSignInRedirected(callbackUri)) {
92+
await this.logto.handleSignInCallback(callbackUri);
93+
}
94+
})().catch(() => undefined);
95+
});
96+
}
97+
}
98+
```
99+
100+
`LogtoService` also exposes resource-aware access tokens, organization tokens, ID-token claims, userinfo, token clearing, and an `error` Signal. See the [Angular sample](../angular-sample/) for a complete integration.
101+
102+
## Server-side rendering
103+
104+
Authentication state is restored from browser storage after the first browser render. Tokens are not exposed during server rendering. Use a server or BFF SDK when authenticated data is required while rendering on the server.
105+
106+
## Migrating from v1
107+
108+
Version 2 replaces the `angular-auth-oidc-client` configuration helper with a first-party Logto SDK:
109+
110+
- Replace `provideAuth({ config: buildAngularAuthConfig(...) })` with `provideLogto(...)`.
111+
- Replace `OidcSecurityService` with `LogtoService`.
112+
- Pass redirect URIs to `signIn()` and `signOut()` instead of provider configuration.
113+
- Add a callback route that calls `handleSignInCallback()`.
114+
- Read `isLoading()`, `isAuthenticated()`, and `error()` Signals instead of subscribing to `checkAuth()`.
115+
116+
Version 1 accepted only one `resource` string. Applications that needed multiple resources sometimes used a comma-separated workaround:
117+
118+
```ts
119+
buildAngularAuthConfig({
120+
// ...
121+
resource: 'com.company.resource1,com.company.resource2',
122+
});
123+
```
124+
125+
Version 2 supports the Logto resource array directly. Replace the workaround with separate array entries:
126+
127+
```ts
128+
provideLogto({
129+
// ...
130+
resources: ['com.company.resource1', 'com.company.resource2'],
131+
});
132+
```
133+
134+
Existing third-party session data is not migrated; users need to sign in once after upgrading.

packages/angular/package.json

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,27 @@
2323
"precommit": "lint-staged",
2424
"check": "tsc --noEmit",
2525
"build": "rm -rf lib/ && tsc -p tsconfig.build.json --noEmit && rollup -c",
26-
"lint": "eslint --ext .ts --ext .tsx src",
27-
"prepack": "pnpm build"
26+
"lint": "eslint --ext .ts src",
27+
"test": "vitest",
28+
"test:coverage": "vitest --silent --coverage",
29+
"prepack": "pnpm build && pnpm test"
2830
},
2931
"dependencies": {
30-
"@logto/js": "workspace:^",
31-
"@silverhand/essentials": "^2.9.3"
32+
"@logto/browser": "workspace:^"
3233
},
3334
"devDependencies": {
3435
"@silverhand/eslint-config": "^6.0.1",
35-
"@silverhand/eslint-config-react": "^6.0.2",
3636
"@silverhand/ts-config": "^6.0.0",
37-
"@silverhand/ts-config-react": "^6.0.0",
38-
"angular-auth-oidc-client": "^20.0.3",
37+
"@vitest/coverage-v8": "3.2.6",
3938
"eslint": "^8.57.0",
39+
"happy-dom": "^20.0.8",
4040
"lint-staged": "^15.0.0",
4141
"prettier": "^3.0.0",
4242
"typescript": "^5.3.3",
4343
"vitest": "^3.2.6"
4444
},
4545
"peerDependencies": {
46-
"rxjs": "^6.5.3 || ^7.4.0",
47-
"@angular/core": "^20.0.0",
48-
"@angular/common": "^20.0.0",
49-
"@angular/router": "^20.0.0"
46+
"@angular/core": "^20.0.0"
5047
},
5148
"eslintConfig": {
5249
"extends": "@silverhand"

packages/angular/src/index.ts

Lines changed: 30 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,167 +1,32 @@
1-
import { Prompt, QueryKey, type SignInUriParameters, withReservedScopes } from '@logto/js';
2-
import { conditional } from '@silverhand/essentials';
3-
import { type OpenIdConfiguration } from 'angular-auth-oidc-client';
1+
export type {
2+
AccessTokenClaims,
3+
ClientAdapter,
4+
IdTokenClaims,
5+
InteractionMode,
6+
LogtoClientErrorCode,
7+
LogtoConfig,
8+
LogtoErrorCode,
9+
SignInOptions,
10+
Storage,
11+
UserInfoResponse,
12+
} from '@logto/browser';
413

5-
/** The Logto configuration object for Angular apps. */
6-
export type LogtoAngularConfig = {
7-
/**
8-
* The endpoint for the Logto server, you can get it from the integration guide
9-
* or the team settings page of the Logto Console.
10-
*
11-
* @example https://foo.logto.app
12-
*/
13-
endpoint: string;
14-
/**
15-
* The client ID of your application, you can get it from the integration guide
16-
* or the application details page of the Logto Console.
17-
*/
18-
appId: string;
19-
/**
20-
* The scopes (permissions) that your application needs to access.
21-
* Scopes that will be added by default: `openid`, `offline_access` and `profile`.
22-
*/
23-
scopes?: string[];
24-
/**
25-
* The API resource that your application needs to access.
26-
*
27-
* @see {@link https://docs.logto.io/docs/recipes/rbac/ | RBAC} to learn more about how to use
28-
* role-based access control (RBAC) to protect API resources.
29-
*/
30-
resource?: string;
31-
/**
32-
* @param redirectUri The redirect URI that the user will be redirected to after the sign-in flow
33-
* is completed.
34-
*/
35-
redirectUri: string;
36-
/**
37-
* @param postLogoutRedirectUri The URI that the user will be redirected to after the sign-out
38-
* flow is completed.
39-
*/
40-
postLogoutRedirectUri?: string;
41-
/**
42-
* The prompt parameter to be used for the authorization request.
43-
*
44-
* @default Prompt.Consent
45-
*/
46-
prompt?: Prompt | Prompt[];
47-
/**
48-
* Whether to include reserved scopes (`openid`, `offline_access` and `profile`) in the scopes.
49-
*
50-
* @default true
51-
*/
52-
includeReservedScopes?: boolean;
53-
/**
54-
* Login hint indicates the current user (usually an email address or a phone number).
55-
*
56-
* @link SignInUriParameters.loginHint
57-
*/
58-
loginHint?: SignInUriParameters['loginHint'];
59-
/**
60-
* The first screen to be shown in the sign-in experience.
61-
*
62-
* @link SignInUriParameters.firstScreen
63-
*/
64-
firstScreen?: SignInUriParameters['firstScreen'];
65-
/**
66-
* Identifiers used in the identifier sign-in, identifier register or reset password pages.
67-
*
68-
* Note: This parameter is applicable only when the `firstScreen` is set to either`identifierSignIn`
69-
* or `identifierRegister`.
70-
*
71-
* @link SignInUriParameters.identifiers
72-
*/
73-
identifiers?: SignInUriParameters['identifiers'];
74-
/**
75-
* Direct sign-in options.
76-
*
77-
* @link SignInUriParameters.directSignIn
78-
*/
79-
directSignIn?: SignInUriParameters['directSignIn'];
80-
/**
81-
* Extra parameters to be appended to the sign-in URI.
82-
*
83-
* Note: The parameters should be supported by the authorization server.
84-
*
85-
* @link SignInUriParameters.extraParams
86-
*/
87-
extraParams?: SignInUriParameters['extraParams'];
88-
};
14+
export {
15+
BrowserStorage,
16+
LogtoClientError,
17+
LogtoError,
18+
LogtoRequestError,
19+
OidcError,
20+
PersistKey,
21+
Prompt,
22+
ReservedResource,
23+
ReservedScope,
24+
UserScope,
25+
buildOrganizationUrn,
26+
getOrganizationIdFromUrn,
27+
isLogtoRequestError,
28+
organizationUrnPrefix,
29+
} from '@logto/browser';
8930

90-
/**
91-
* A helper function to build the OpenID Connect configuration for `angular-auth-oidc-client`
92-
* using a Logto-friendly way.
93-
*
94-
* @example
95-
* ```ts
96-
* // A minimal example
97-
* import { buildAngularAuthConfig } from '@logto/js';
98-
* import { provideAuth } from 'angular-auth-oidc-client';
99-
*
100-
* provideAuth({
101-
* config: buildAngularAuthConfig({
102-
* endpoint: '<your-logto-endpoint>',
103-
* appId: '<your-app-id>',
104-
* redirectUri: '<your-app-redirect-uri>',
105-
* }),
106-
* });
107-
* ```
108-
*
109-
* @param logtoConfig The Logto configuration object for Angular apps.
110-
* @returns The OpenID Connect configuration for `angular-auth-oidc-client`.
111-
* @see {@link https://angular-auth-oidc-client.com/ | angular-auth-oidc-client} to learn more
112-
* about how to use the library.
113-
*/
114-
export const buildAngularAuthConfig = (logtoConfig: LogtoAngularConfig): OpenIdConfiguration => {
115-
const {
116-
endpoint,
117-
appId: clientId,
118-
scopes,
119-
resource,
120-
redirectUri: redirectUrl,
121-
postLogoutRedirectUri,
122-
prompt = Prompt.Consent,
123-
includeReservedScopes = true,
124-
loginHint,
125-
identifiers,
126-
firstScreen,
127-
directSignIn,
128-
extraParams,
129-
} = logtoConfig;
130-
const scope = includeReservedScopes ? withReservedScopes(scopes) : scopes?.join(' ');
131-
const customParameters = {
132-
...conditional(resource && { [QueryKey.Resource]: resource }),
133-
...conditional(loginHint && { [QueryKey.LoginHint]: loginHint }),
134-
...conditional(firstScreen && { [QueryKey.FirstScreen]: firstScreen }),
135-
...conditional(identifiers && { [QueryKey.Identifier]: identifiers.join(' ') }),
136-
...conditional(
137-
directSignIn && { [QueryKey.DirectSignIn]: `${directSignIn.method}:${directSignIn.target}` }
138-
),
139-
...extraParams,
140-
};
141-
142-
return {
143-
authority: new URL('/oidc', endpoint).href,
144-
redirectUrl,
145-
postLogoutRedirectUri,
146-
clientId,
147-
scope,
148-
responseType: 'code',
149-
autoUserInfo: !resource,
150-
renewUserInfoAfterTokenRenew: !resource,
151-
silentRenew: true,
152-
useRefreshToken: true,
153-
customParamsAuthRequest: {
154-
prompt: Array.isArray(prompt) ? prompt.join(' ') : prompt,
155-
...customParameters,
156-
},
157-
customParamsCodeRequest: {
158-
...customParameters,
159-
},
160-
customParamsRefreshTokenRequest: {
161-
...customParameters,
162-
},
163-
};
164-
};
165-
166-
export type { UserInfoResponse } from '@logto/js';
167-
export { UserScope } from '@logto/js';
31+
export { LOGTO_CLIENT, provideLogto, type LogtoAngularOptions } from './provider.js';
32+
export { LogtoService } from './service.js';

0 commit comments

Comments
 (0)