Angular Quickstart 🅰️
Add a complete login experience to a standalone Angular app (Angular 17+). @faable/auth-js drives the Authorization Code flow with PKCE, stores the session, refreshes tokens, and syncs across tabs. You wire it to Angular’s reactivity with a single root service that exposes the session as a signal.
This is the framework-agnostic core pattern: createClient + onAuthStateChange + getSession. The JavaScript, Vue, and SvelteKit quickstarts are the same three calls wired into each framework — here they live in an injectable AuthService.
You need one package: @faable/auth-js — no Angular helper required.
✅ Prerequisites
In the Faable Dashboard , create a Client for your SPA and configure:
- Allowed Callback URLs:
http://localhost:4200/callback(add your production URL later). - Allowed Logout URLs:
http://localhost:4200. - Allowed Web Origins:
http://localhost:4200.
Note your auth domain (your-domain.auth.faable.link) and Client ID. SPAs are public clients — no client secret is involved.
🛠️ Step 1: Create the App and Install
npm install -g @angular/cli
ng new my-app
cd my-app
npm install @faable/auth-jsAnswer the ng new prompts however you like — this guide uses standalone components (the default since Angular 17) and the Angular Router.
Step 2: Create the Auth Service
The whole integration lives in one root-provided service. It creates the client once, seeds the session with getSession(), and keeps a session signal in sync with onAuthStateChange — login, logout, token refresh, or a change in another tab.
// src/app/auth.service.ts
import { Injectable, signal, computed } from "@angular/core";
import { createClient, type Session } from "@faable/auth-js";
@Injectable({ providedIn: "root" })
export class AuthService {
private auth = createClient({
domain: "your-domain.auth.faable.link",
clientId: "YOUR_CLIENT_ID",
redirectUri: window.location.origin + "/callback",
});
/** Current session, reactive. `null` when signed out. */
readonly session = signal<Session | null>(null);
/** Convenience: the signed-in user, or `undefined`. */
readonly user = computed(() => this.session()?.user);
constructor() {
// Keep the signal in sync with every auth change.
this.auth.onAuthStateChange((_event, session) => this.session.set(session));
// Seed the initial value (also auto-refreshes an expired session).
this.auth.getSession().then(({ data }) => this.session.set(data.session));
}
signIn() {
// Universal Login with every connection you've enabled.
return this.auth.signInWithOauthConnection({});
}
signOut() {
return this.auth.signOut({ returnTo: window.location.origin });
}
/** Finish the login on the /callback route. */
handleCallback() {
return this.auth.handleRedirectCallback();
}
/** A fresh access token for API calls (auto-refreshed). */
async accessToken() {
const { data } = await this.auth.getSession();
return data.session?.access_token ?? null;
}
}The client initializes itself on creation: it recovers an existing session from storage, or — on the callback URL — exchanges the PKCE ?code= for tokens.
Step 3: Wire It Up
There’s no extra wiring step — because AuthService is providedIn: "root", Angular constructs it lazily the first time you inject it, and the constructor above subscribes to onAuthStateChange and seeds the session signal. Every component that injects AuthService reads the same live signal.
signInWithOauthConnection({})sends the user to your tenant’s Universal Login with every connection you’ve enabled. Target one directly with{ connection_id: "connection_..." }.signOut({ returnTo })clears the local session and the SSO cookie on the auth server.returnTomust be in Allowed Logout URLs.- Both methods redirect the browser on success — the promise intentionally never resolves, so don’t put code after the
await.
Step 4: Login, User, and Logout
Inject the service and read the signal in the template with the new control flow (@if):
// src/app/app.component.ts
import { Component, inject } from "@angular/core";
import { RouterOutlet } from "@angular/router";
import { AuthService } from "./auth.service";
@Component({
selector: "app-root",
standalone: true,
imports: [RouterOutlet],
template: `
@if (!auth.session()) {
<button (click)="auth.signIn()">Sign in</button>
} @else {
<p>Hello {{ auth.user()?.email }}</p>
<button (click)="auth.signOut()">Sign out</button>
}
<router-outlet />
`,
})
export class AppComponent {
auth = inject(AuthService);
}Signals make this reactive with zero boilerplate: when onAuthStateChange updates session, the template re-renders. No async pipe, no manual subscription cleanup.
Step 5: The Callback Route
Add a callback route that runs handleRedirectCallback() once, then navigates home. It’s idempotent — the client already started the exchange — and returns { error, returnTo }, so deep links survive the login round-trip if you passed returnTo to signInWithOauthConnection({ returnTo }).
// src/app/callback.component.ts
import { Component, inject, signal, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { AuthService } from "./auth.service";
@Component({
selector: "app-callback",
standalone: true,
template: `<p>{{ message() }}</p>`,
})
export class CallbackComponent implements OnInit {
private auth = inject(AuthService);
private router = inject(Router);
message = signal("Signing you in…");
async ngOnInit() {
const { error, returnTo } = await this.auth.handleCallback();
if (error) this.message.set(error.message);
else this.router.navigateByUrl(returnTo ?? "/");
}
}Register the route and bootstrap the app:
// src/app/app.routes.ts
import { Routes } from "@angular/router";
import { CallbackComponent } from "./callback.component";
export const routes: Routes = [
{ path: "callback", component: CallbackComponent },
];// src/main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideRouter } from "@angular/router";
import { AppComponent } from "./app/app.component";
import { routes } from "./app/app.routes";
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)],
});Step 6: Call Your API
Read the token fresh before each call — AuthService.accessToken() wraps getSession(), which auto-refreshes an expired session:
// src/app/api.service.ts
import { Injectable, inject } from "@angular/core";
import { AuthService } from "./auth.service";
@Injectable({ providedIn: "root" })
export class ApiService {
private auth = inject(AuthService);
async fetch(path: string) {
const token = await this.auth.accessToken();
if (!token) throw new Error("Not signed in");
return fetch(`https://api.myapp.com${path}`, {
headers: { authorization: `Bearer ${token}` },
});
}
}If your backend validates the token’s audience, pass your API identifier when creating the client (createClient({ ..., audience: "https://api.myapp.com" })) — and see Validate Access Tokens for the Express middleware on the other side.
Optional: to attach the token to every
HttpClientrequest instead, register a functional HTTP interceptor that readsAuthService.accessToken()and sets theAuthorizationheader. Thefetchservice above keeps the example dependency-free.
❓ FAQ
Why expose the session as a signal instead of an Observable?
Signals are the modern Angular primitive for reactive state: they read like a plain value in templates (auth.session()), need no async pipe, and never leak subscriptions. onAuthStateChange pushes each change straight into session.set(...). If you prefer RxJS, wrap the signal with toObservable().
How do I get the access token?
From the session: const { data } = await auth.getSession(), then data.session?.access_token. The accessToken() helper on AuthService does exactly this. There is no separate getAccessToken() — getSession() already refreshes expired tokens before returning.
How do users pick a login method?
By default they choose on the Universal Login screen among the connections enabled for your client. To skip the screen and go straight to one provider, pass connection_id to signInWithOauthConnection — e.g. { connection_id: "connection_..." }.
Why doesn’t the sign-in promise resolve?
signInWithOauthConnection and signOut redirect the browser on success, so the page unloads before the promise settles. Let the onAuthStateChange subscription update the session signal — don’t put post-login logic after the await.
🔗 Related
- JavaScript · Vue · SvelteKit — the same pattern in a framework.
- React Quickstart · Next.js Quickstart — with the React helper hooks.
- Authorization Code Flow with PKCE — what the SDK does under the hood.
- Validate Access Tokens — verify these tokens in your backend.
- Connections — enable Google, GitHub, passwordless, and more.
Last updated on