Skip to content

Commit a0b903a

Browse files
authored
Merge pull request #95 from fells-code/docs/angular-guide
docs: add the Angular SDK guide and list @seamless-auth/angular
2 parents 0b98696 + 113aa11 commit a0b903a

3 files changed

Lines changed: 296 additions & 17 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
---
2+
title: "Angular SDK"
3+
description: "@seamless-auth/angular adds Seamless Auth to an Angular 20, 21, or 22 application: a signal-based service, functional route guards, an HttpClient interceptor, and optional sign-in screens."
4+
sidebar:
5+
order: 4.7
6+
---
7+
8+
`@seamless-auth/angular` is the official Angular integration for Seamless Auth. It gives a
9+
standalone Angular application the same session state, auth actions, and optional sign-in screens
10+
the [React SDK](/build/react-sdk) gives a React app, in Angular's own idioms: an injectable service
11+
with signals and observables, a `provideSeamlessAuth()` provider, and functional route guards.
12+
13+
It is a thin binding over `@seamless-auth/client`, the framework-free core the React and React
14+
Native SDKs also build on, so sign-in behaves the same in every binding. The Angular screens keep
15+
the React screens' accessible names, and `seamless verify` runs the same browser specs against both.
16+
17+
---
18+
19+
## How It Fits
20+
21+
```txt
22+
Angular app (browser)
23+
-> your backend's /auth adapter routes, with signed httpOnly cookies
24+
-> Seamless Auth API
25+
```
26+
27+
The browser model is the same as React's: the [server adapter](/build/server-sdks) keeps the auth
28+
API's tokens and the browser holds only `httpOnly` cookies on your domain. The Angular SDK has no
29+
bearer mode at all. Its configuration has no transport option, so no token can reach page scripts,
30+
and nothing it writes to browser storage is a credential. See
31+
[Cookie, Session, And Token Reference](/reference/cookies-sessions-tokens).
32+
33+
---
34+
35+
## Requirements
36+
37+
- Angular 20, 21, or 22 (`@angular/core`, `@angular/common`, `@angular/router`) and `rxjs` 7
38+
- A standalone application. Zoneless and zone-based change detection both work, because state is
39+
held in signals
40+
- A backend mounting the Seamless Auth adapter routes at `/auth`, for example `@seamless-auth/express`
41+
42+
---
43+
44+
## Installation
45+
46+
```bash
47+
npm install @seamless-auth/angular
48+
```
49+
50+
`@seamless-auth/client` comes with it.
51+
52+
---
53+
54+
## Configure The Application
55+
56+
```ts
57+
// app.config.ts
58+
import { ApplicationConfig } from '@angular/core'
59+
import { provideHttpClient, withInterceptors } from '@angular/common/http'
60+
import { provideRouter } from '@angular/router'
61+
import { provideSeamlessAuth, seamlessAuthInterceptor } from '@seamless-auth/angular'
62+
63+
import { routes } from './app.routes'
64+
65+
export const appConfig: ApplicationConfig = {
66+
providers: [
67+
provideRouter(routes),
68+
provideSeamlessAuth({ apiHost: 'https://app.example.com' }),
69+
// Optional: sends the session cookies with HttpClient calls to apiHost, and nowhere else.
70+
provideHttpClient(withInterceptors([seamlessAuthInterceptor])),
71+
],
72+
}
73+
```
74+
75+
`apiHost` is the origin your backend serves `/auth` on. The session is read as soon as the
76+
application starts in the browser.
77+
78+
`provideSeamlessAuth` also takes a factory, which runs in an injection context. Use it to read
79+
configuration at runtime, for example from a `config.js` your container writes at startup:
80+
81+
```ts
82+
provideSeamlessAuth(() => ({ apiHost: window.__APP_CONFIG__.apiUrl }))
83+
```
84+
85+
| Option | Default | Purpose |
86+
| -------------------------- | ---------------- | ------------------------------------------------------------------- |
87+
| `apiHost` | required | Origin of the server adapter |
88+
| `basePath` | `/auth` | Where the adapter is mounted on `apiHost` |
89+
| `magicLinkRedirectUri` | the deployment's | Where an emailed magic link lands |
90+
| `autoDetectPreviousSignIn` | `true` | Open the login screen on Sign In for a browser that signed in before |
91+
| `initialSession` | none | A session your server already resolved, for server rendering |
92+
| `ports` | browser | `passkeys` and `oauthRedirect` ports |
93+
| `fetch` | global `fetch` | The fetch auth requests go through |
94+
| `signedInPath` | `/` | Where the bundled screens go after sign-in |
95+
| `loginPath` | `/login` | Where `requireAuth` sends someone who is signed out |
96+
97+
---
98+
99+
## Protect Routes And Mount The Screens
100+
101+
```ts
102+
// app.routes.ts
103+
import { Routes } from '@angular/router'
104+
import { authGuard, requireAuth } from '@seamless-auth/angular'
105+
106+
export const routes: Routes = [
107+
{ path: '', component: HomePage, canActivate: [authGuard] },
108+
{ path: 'admin', component: AdminPage, canActivate: [requireAuth({ roles: 'org:admin' })] },
109+
{
110+
path: '',
111+
loadChildren: () => import('@seamless-auth/angular/routes').then((m) => m.seamlessAuthRoutes),
112+
},
113+
]
114+
```
115+
116+
| Guard | Admits | Otherwise |
117+
| --------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------- |
118+
| `authGuard` / `requireAuth(options?)` | A signed-in user, with `roles` when given | `redirectTo` (default `loginPath`). A missing role goes to `forbiddenRedirectTo`, or is refused |
119+
| `guestGuard` / `requireGuest(options?)` | Someone signed out | `redirectTo` (default `signedInPath`) |
120+
121+
The guards work as `canActivate`, `canActivateChild`, and `canMatch`. They wait for the session to
122+
be read before deciding, so reloading a protected page does not bounce a signed-in user to the login
123+
screen. During a server render without `initialSession` they admit, and the browser runs them again
124+
when the application boots there.
125+
126+
`roles` uses the same matching as the auth API, scoped roles such as `org:admin` included. A guard
127+
decides what to render; it is not access control. Your backend still checks every request with
128+
`requireAuth`. See [Secure Routes](/build/secure-routes).
129+
130+
Do not put `guestGuard` on `register-passkey`, `oauth/callback`, or `verify-magiclink`. A user can
131+
already have a session by the time those render, and the guard would send them on before they
132+
finish.
133+
134+
---
135+
136+
## The `SeamlessAuth` Service
137+
138+
```ts
139+
import { Component, inject } from '@angular/core'
140+
import { Router } from '@angular/router'
141+
import { SeamlessAuth } from '@seamless-auth/angular'
142+
143+
@Component({
144+
selector: 'app-account-menu',
145+
template: `
146+
@if (auth.user(); as user) {
147+
<span>{{ user.email }}</span>
148+
<button type="button" (click)="signOut()">Logout</button>
149+
}
150+
`,
151+
})
152+
export class AccountMenu {
153+
protected readonly auth = inject(SeamlessAuth)
154+
private readonly router = inject(Router)
155+
156+
async signOut() {
157+
await this.auth.logout()
158+
await this.router.navigateByUrl('/login')
159+
}
160+
}
161+
```
162+
163+
- **Signals:** `state`, `user`, `isAuthenticated`, `loading`, `credentials`, `organizations`,
164+
`activeOrganization`, `stepUpStatus`, `hasSignedInBefore`, plus `loginMethods`,
165+
`loginMethodsLoading`, `passkeySupported`, and `passkeySupportLoading`.
166+
- **Observables:** `state$`, `user$`, `isAuthenticated$`. Each emits the current value on subscribe.
167+
- **Actions:** `login`, `handlePasskeyLogin`, `registerPasskey`, `refreshSession`, `logout`,
168+
`logoutAllSessions`, `deleteUser`, `updateCredential`, `deleteCredential`, `switchOrganization`,
169+
`listOAuthProviders`, `startOAuthLogin`, `finishOAuthLogin`, `refreshStepUpStatus`,
170+
`verifyStepUpWithPasskey`, `verifyStepUpWithPasskeyPrf`, and `verifyStepUpWithTotp`.
171+
- **Also:** `hasRole`, `hasScopedRole`, `markSignedIn`, `whenSettled()`, `loadLoginMethods()` and
172+
`checkPasskeySupport()` (each read once and cached), `authorizedFetch`, `client` (the headless
173+
client for OTP, magic link, organization, and TOTP calls), and `ports`.
174+
175+
Every request action resolves to a `SeamlessAuthResult<T>`, `{ data, error }`, and does not throw
176+
for an HTTP or network failure, the same convention as the React SDK.
177+
178+
---
179+
180+
## Built-In Screens
181+
182+
`@seamless-auth/angular/routes` exports `seamlessAuthRoutes` and the standalone components behind
183+
them. The screens move between each other relative to their parent route, so they work at the root
184+
or under a path such as `{ path: 'account', children: seamlessAuthRoutes }`. The array has no
185+
wildcard route, so mounting it at the root does not swallow your own unknown paths.
186+
187+
| Path | Component | Purpose |
188+
| ------------------ | ------------------- | ------------------------------------------------------------- |
189+
| `login` | `SaLogin` | Sign in with an email or phone number, or create an account |
190+
| `passkey-login` | `SaPasskeyLogin` | Sign in with a passkey alone |
191+
| `verify-email-otp` | `SaVerifyOtp` | Check an email code |
192+
| `verify-phone-otp` | `SaVerifyOtp` | Check a text message code |
193+
| `magic-link-sent` | `SaMagicLinkSent` | Wait for an emailed link to be used |
194+
| `verify-magiclink` | `SaVerifyMagicLink` | Where the emailed link lands. The auth API builds this URL |
195+
| `oauth/callback` | `SaOAuthCallback` | The OAuth redirect URI to register with your providers |
196+
| `register-passkey` | `SaRegisterPasskey` | Offer passkey enrolment after sign-up |
197+
198+
The building blocks are exported too: `SaOtpInput` (bind it with `[(value)]`), `SaFallbackOptions`,
199+
`SaOAuthProviderButtons`, `SaAuthLayout`, `authRoutePaths`, and `injectAuthNavigation`.
200+
201+
The magic link and OAuth callback screens remove the token or code from the address bar once they
202+
have read it, so a single-use secret does not stay in history or travel in a `Referer`.
203+
204+
### Styling
205+
206+
The screens render inside `.sa-auth` with plain `.sa-*` classes and one unencapsulated stylesheet,
207+
so your global styles can override any rule. Colors come from the same `--seamless-*` custom
208+
properties the React screens read, so one theme covers both:
209+
210+
```css
211+
:root {
212+
--seamless-accent: #1f3a34;
213+
--seamless-accent-hover: #16302b;
214+
--seamless-surface: #f7f5f0;
215+
--seamless-surface-raised: #ffffff;
216+
--seamless-text: #1a1a1a;
217+
--seamless-text-muted: #4b5563;
218+
}
219+
```
220+
221+
The full token list is in the
222+
[`@seamless-auth/react` README](https://github.com/fells-code/seamless-auth-react/tree/main/packages/react#theming-the-built-in-ui). The stylesheet also
223+
ships as `@seamless-auth/angular/seamless-auth.css` if you want a copy to start from.
224+
225+
---
226+
227+
## Custom Screens
228+
229+
Every bundled screen is a thin view over a flow in `@seamless-auth/client`: `beginSignIn`,
230+
`registerWithEmail`, `requestOtp` and `verifyOtp`, `watchMagicLink` and `finishMagicLinkSignIn`,
231+
`startOAuthSignIn` and `completeOAuthCallback`, `enrollPasskey`, plus helpers such as
232+
`loadLoginMethods`, `fallbackSignInOptions`, and `safeReturnPath`. A custom screen calls the same
233+
flows, passing the service as its actions:
234+
235+
```ts
236+
import { beginSignIn } from '@seamless-auth/client'
237+
238+
const step = await beginSignIn(auth, {
239+
identifier: email,
240+
passkeySupported: await auth.checkPasskeySupport(),
241+
configuredMethods: await auth.loadLoginMethods(),
242+
})
243+
244+
if (step.kind === 'signed_in') await router.navigateByUrl('/')
245+
if (step.kind === 'choose_method') showOtherMethods(step.loginMethods)
246+
if (step.kind === 'error') showError(step.message)
247+
```
248+
249+
[How To Build Your Own Auth UI](/build/custom-auth-ui) walks through the flows in order. Its
250+
examples use React hooks, but the client calls are the same.
251+
252+
---
253+
254+
## Server Rendering
255+
256+
On the server the service does not read the session or browser storage, and the screens start no
257+
requests: a server render has none of the browser's cookies, and a magic link or OAuth code is
258+
single use. `loading` stays true unless you pass `initialSession`. To render a known session on the
259+
first paint, resolve it on your server through your adapter and hand it over as `initialSession`;
260+
the browser then revalidates it in the background. Do not forward the browser's cookies to the
261+
adapter's `/auth/users/me` from a server.
262+
263+
---
264+
265+
## Related Docs
266+
267+
- [React SDK](/build/react-sdk)
268+
- [Server SDKs](/build/server-sdks)
269+
- [OAuth Login](/build/oauth-login)
270+
- [Secure Routes](/build/secure-routes)
271+
- [Ecosystem Overview](/build/ecosystem#compatibility-matrix)

0 commit comments

Comments
 (0)