@zshn-dev/auth-angular
v2.0.0
Published
Angular signals library for GitHub OAuth — guards, interceptor, and components
Maintainers
Readme
@zshn-dev/auth-angular
Signal-based GitHub OAuth authentication library for Angular 21+.
@zshn-dev/auth-angular provides a complete, opinionated GitHub OAuth flow for Angular applications. It ships with signal-based state, a functional HTTP interceptor, route guards, and ready-to-use login/callback components — all wired up through a single provideAuth() call.
Requirements
| Dependency | Version | | ---------- | -------- | | Angular | ≥ 21 | | TypeScript | ≥ 5.9 | | Node.js | ≥ 20 |
Installation
npm install @zshn-dev/auth-angularSetup
1. Register the providers — app.config.ts
Call provideAuth() inside ApplicationConfig. This registers AuthService and the authInterceptor for the entire application.
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideAuth } from '@zshn-dev/auth-angular';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi()),
provideAuth({
serverUrl: 'http://localhost:3000',
afterLoginUrl: '/dashboard',
afterLogoutUrl: '/login',
storageKey: 'auth_token',
}),
],
};2. Configure routes — app.routes.ts
Mount AuthLoginComponent behind guestGuard (redirects away if already logged in) and AuthCallbackComponent at the OAuth callback URL. Protect private routes with authGuard.
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import {
AuthLoginComponent,
AuthCallbackComponent,
authGuard,
guestGuard,
} from '@zshn-dev/auth-angular';
export const routes: Routes = [
{
path: 'login',
component: AuthLoginComponent,
canActivate: [guestGuard],
},
{
// Must match the callback URL configured in your GitHub OAuth App
path: 'auth/callback',
component: AuthCallbackComponent,
},
{
path: 'dashboard',
loadComponent: () =>
import('./dashboard/dashboard.component').then(m => m.DashboardComponent),
canActivate: [authGuard],
},
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
];3. Use AuthService in a component
Inject AuthService and read its signals directly in templates or component logic. Signals are synchronous — no async pipe or subscription needed.
// src/app/dashboard/dashboard.component.ts
import { Component, inject } from '@angular/core';
import { AuthService } from '@zshn-dev/auth-angular';
@Component({
selector: 'app-dashboard',
standalone: true,
template: `
<header>
<p>Welcome, {{ auth.user()?.name ?? 'stranger' }}</p>
<button (click)="auth.logout()">Sign out</button>
</header>
@if (auth.isAuthenticated()) {
<main>Protected content</main>
}
`,
})
export class DashboardComponent {
protected auth = inject(AuthService);
}API Reference
provideAuth(config: AuthConfig): EnvironmentProviders
Registers AuthService and authInterceptor via makeEnvironmentProviders(). Call once in app.config.ts.
function provideAuth(config: AuthConfig): EnvironmentProviders;AuthConfig
interface AuthConfig {
/** Base URL of the authentication server. Required. */
serverUrl: string;
/** Angular route to navigate to after a successful login. Default: '/' */
afterLoginUrl?: string;
/** Angular route to navigate to after logout. Default: '/login' */
afterLogoutUrl?: string;
/** localStorage key used to persist the JWT. Default: 'auth_token' */
storageKey?: string;
}AuthService
Signal-based service. Available via inject(AuthService) anywhere after provideAuth() has been registered.
class AuthService {
/** The currently authenticated user, or null when logged out. */
readonly user: Signal<User | null>;
/** True when a valid token is present in storage. */
readonly isAuthenticated: Signal<boolean>;
/** The raw JWT string, or null when logged out. */
readonly token: Signal<string | null>;
/** Redirects the browser to the GitHub OAuth authorization page. */
login(): void;
/** Clears the stored token and navigates to afterLogoutUrl. */
logout(): void;
/**
* Stores the given token and updates all signals.
* Called automatically by AuthCallbackComponent — you rarely need this directly.
*/
setToken(token: string): void;
}User
Shape of the decoded JWT payload exposed through AuthService.user.
interface User {
sub: string; // GitHub user ID
name: string; // GitHub display name
email: string; // GitHub email (if available)
avatar: string; // GitHub avatar URL
iat: number; // Issued-at timestamp (seconds)
exp: number; // Expiry timestamp (seconds)
}authGuard
Functional CanActivateFn. Redirects unauthenticated users to /login.
const authGuard: CanActivateFn;Usage:
{ path: 'settings', component: SettingsComponent, canActivate: [authGuard] }guestGuard
Functional CanActivateFn. Redirects already-authenticated users to /.
const guestGuard: CanActivateFn;Usage:
{ path: 'login', component: AuthLoginComponent, canActivate: [guestGuard] }authInterceptor
Functional HttpInterceptorFn. Auto-registered by provideAuth() — you do not need to add it manually. Attaches Authorization: Bearer <token> to every outbound HTTP request when a token is present.
const authInterceptor: HttpInterceptorFn;AuthLoginComponent
Renders a "Sign in with GitHub" button that calls AuthService.login().
@Component({ selector: 'auth-login', ... })
class AuthLoginComponent {}Use as a routed page or embed inline:
<!-- as a routed page (configured in routes) -->
<!-- inline -->
<auth-login />AuthCallbackComponent
Reads the ?token= query parameter, calls AuthService.setToken(), then navigates to afterLoginUrl. Mount at the route matching your GitHub OAuth App callback URL.
@Component({ selector: 'auth-callback', ... })
class AuthCallbackComponent {}// In app.routes.ts
{ path: 'auth/callback', component: AuthCallbackComponent }AUTH_CONFIG
InjectionToken<AuthConfig> — the token under which provideAuth() registers the configuration. Useful for reading config values in custom extensions.
const AUTH_CONFIG: InjectionToken<AuthConfig>;// Reading config in a custom service
readonly config = inject(AUTH_CONFIG);Design Notes
Signals instead of RxJS
AuthService exposes Signal<T> values rather than Observable<T>. Signals are synchronous and do not require a subscription or the async pipe — template reads like auth.user()?.name just work. They also integrate natively with Angular's effect() and computed() APIs introduced in Angular 17+.
makeEnvironmentProviders() instead of forRoot()
forRoot() is a legacy pattern from NgModule-based apps. makeEnvironmentProviders() is the modern equivalent for standalone Angular: it scopes providers to the environment injector, prevents accidental use in component or route injectors, and composes naturally with bootstrapApplication.
No providedIn: 'root'
AuthService is not decorated with providedIn: 'root'. This is intentional: the service depends on AUTH_CONFIG, which is only available after provideAuth() has been called. Making it tree-shakable via providedIn: 'root' would allow it to be injected without the required configuration, causing a runtime error. By requiring explicit registration through provideAuth(), the dependency is always satisfied and the misconfiguration is impossible.
License
MIT
