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