HazelJS OAuth Package
@hazeljs/oauth provides OAuth 2.0 social login and native SAML SSO for HazelJS applications — Google, Microsoft, GitHub, Facebook, and Twitter out of the box, plus multi-IdP enterprise SSO.
Quick Reference
- Purpose:
@hazeljs/oauthprovides OAuth 2.0 social login and SAML SP flows with authorization URL generation, state/PKCE validation, token exchange, SAML ACS handling, and user/profile extraction. - When to use: Use
@hazeljs/oauthwhen a HazelJS application needs social login and/or enterprise SSO (SAML). Use@hazeljs/authalongside it to issue and validate your app JWTs. - Key concepts: OAuth 2.0 flow, SAML SP flow, authorization URL, callback handler, state validation, PKCE, ACS callback, IdP metadata,
OAuthModule, provider configuration. - Dependencies:
@hazeljs/core,@hazeljs/auth(for JWT after OAuth). - Common patterns: Register
OAuthModulewith provider credentials → redirect user to authorization URL → handle callback → exchange code for tokens → fetch user profile → issue JWT. - Common mistakes: Not validating OAuth state parameter (CSRF vulnerability); not setting correct callback URLs; hardcoding client secrets instead of using environment variables.
Purpose
Implementing OAuth from scratch involves handling authorization URLs, state validation, PKCE for some providers, token exchange, and user profile fetching. The @hazeljs/oauth package simplifies this by providing:
- Multi-Provider Support: Google, Microsoft, GitHub, Facebook, Twitter with a unified API
- Native SAML SP: Multi-IdP SAML entry/callback/metadata support
- PKCE Handling: Automatic code verifier generation and validation for Google, Microsoft, and Twitter
- User Profile Fetching: Fetches user id, email, name, and picture from provider APIs
- Optional Controller: Ready-made
/auth/:providerand/auth/:provider/callbackroutes callbackHandlerhook: Inject your own handler class to issue a JWT, look up the user in your DB, and return{ token, user }— all wired through the DI container
Architecture
flowchart TB
subgraph UserFlow [OAuth Flow]
A[User clicks Login] --> B[GET /auth/google]
B --> C[Redirect to provider]
C --> D[User authenticates]
D --> E[Callback with code]
E --> F[OAuthService.handleCallback]
F --> G[OAuthCallbackHandler.handle]
G --> H["Return { token, user }"]
end
subgraph Package [@hazeljs/oauth]
OAuthModule
OAuthService
OAuthController
OAuthCallbackHandler
end
subgraph App [Your Application]
Handler[YourCallbackHandler]
DB[(Database)]
JWT[JwtService]
end
OAuthService --> Arctic[Arctic Library]
OAuthModule --> OAuthService
G --> Handler
Handler --> DB
Handler --> JWTKey Components
- OAuthService:
getAuthorizationUrl(),handleCallback(),executeCallback(),validateState(), plus SAML helpers - OAuthController: Optional OAuth routes and SAML routes (
/auth/saml/:idp, callback, metadata) - OAuthCallbackHandler: Interface — implement this to bridge the OAuth result into your JWT system
- OAuthStateGuard: Validates state parameter on callback (CSRF protection)
Installation
npm install @hazeljs/oauth
Or with the CLI:
hazel add oauth
Quick Start
Basic Setup
Configure providers and register the module. Pass a callbackHandler class to receive a { token, user } response instead of raw OAuth tokens:
import { HazelModule } from '@hazeljs/core';
import { OAuthModule } from '@hazeljs/oauth';
import { MyOAuthCallbackHandler } from './auth/oauth-callback.handler';
@HazelModule({
imports: [
OAuthModule.forRoot({
providers: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectUri: process.env.GITHUB_REDIRECT_URI!,
},
},
// Optional — when set, OAuthController calls handler.handle() before responding.
// The handler is resolved from the DI container so it can inject any service.
callbackHandler: MyOAuthCallbackHandler,
}),
],
})
export class AppModule {}
Using the Built-in Controller
The OAuthController is included by default. It provides:
- GET /auth/:provider — Redirects user to the OAuth provider (google, microsoft, github, facebook, twitter)
- GET /auth/:provider/callback — Handles the callback, returns JSON with
accessToken,user, etc. - GET /auth/saml/:idp — Initiates SAML login and redirects to IdP
- POST /auth/saml/:idp/callback — Handles SAML ACS callback (
SAMLResponse) - GET /auth/saml/:idp/metadata — Returns SP metadata XML
Example flow:
- User visits
GET /auth/google→ redirected to Google - After auth, Google redirects to
GET /auth/google/callback?code=...&state=... - Callback returns
{ accessToken, refreshToken?, expiresAt?, user: { id, email, name, picture } }
For redirect-based flows, use query params:
?successRedirect=https://yourapp.com/dashboard— redirect after success?errorRedirect=https://yourapp.com/login— redirect on error
Using OAuthService Directly
For custom flows, inject OAuthService:
import { Service } from '@hazeljs/core';
import { OAuthService } from '@hazeljs/oauth';
@Service()
export class CustomAuthController {
constructor(private readonly oauthService: OAuthService) {}
@Get('login/google')
async loginGoogle(@Res() res: Response) {
const { url, state, codeVerifier } = this.oauthService.getAuthorizationUrl('google');
// Store state and codeVerifier in cookies/session
res.redirect(url);
}
@Get('oauth/callback')
async callback(
@Query() query: { code: string; state: string },
@Req() req: Request
) {
const storedState = getStoredState(req); // from cookie/session
const codeVerifier = getStoredCodeVerifier(req); // for Google, Microsoft, Twitter
if (!this.oauthService.validateState(query.state, storedState)) {
throw new UnauthorizedError('Invalid state');
}
const result = await this.oauthService.handleCallback(
'google',
query.code,
query.state,
codeVerifier
);
// result: { accessToken, refreshToken?, expiresAt?, user }
// Create/update user in DB, issue JWT via JwtService, etc.
return result;
}
}
Provider Configuration
google: {
clientId: string;
clientSecret: string;
redirectUri: string;
}
Default scopes: openid, profile, email
Microsoft Entra ID
microsoft: {
clientId: string;
clientSecret: string;
redirectUri: string;
tenant?: string; // default: 'common' (multi-tenant)
}
Default scopes: openid, profile, email
GitHub
github: {
clientId: string;
clientSecret: string;
redirectUri: string;
}
Default scopes: user:email
facebook: {
clientId: string;
clientSecret: string;
redirectUri: string;
}
Default scopes: email, public_profile
twitter: {
clientId: string;
clientSecret?: string | null; // optional for public clients (PKCE-only)
redirectUri: string;
}
Default scopes: users.read, tweet.read
Note: Twitter API v2 does not provide user email. Add offline.access scope for refresh tokens.
SAML (Multi-IdP)
saml: {
oktaMain: {
idpKey: 'okta-main',
ssoUrl: 'https://your-org.okta.com/app/abc/sso/saml',
issuer: 'https://api.example.com',
acsUrl: 'https://api.example.com/auth/saml/okta-main/callback',
audience: 'https://api.example.com',
relayState: 'app=dashboard',
},
azureMain: {
idpKey: 'azure-main',
ssoUrl: 'https://login.microsoftonline.com/.../saml2',
issuer: 'https://api.example.com',
acsUrl: 'https://api.example.com/auth/saml/azure-main/callback',
},
}
SAML flow:
GET /auth/saml/:idpgeneratesSAMLRequestand redirects to IdP- IdP posts
SAMLResponsetoPOST /auth/saml/:idp/callback - Package parses assertion and invokes optional callback handler
Custom Scopes
Override default scopes via defaultScopes or pass to getAuthorizationUrl:
OAuthModule.forRoot({
providers: { google: {...}, github: {...} },
defaultScopes: {
google: ['openid', 'profile', 'email', 'https://www.googleapis.com/auth/calendar.readonly'],
github: ['user:email', 'repo'],
},
});
// Or per-request:
const { url } = this.oauthService.getAuthorizationUrl('google', undefined, [
'openid',
'profile',
'email',
'https://www.googleapis.com/auth/drive.readonly',
]);
Integration with @hazeljs/auth
Using OAuthCallbackHandler (recommended)
Implement the OAuthCallbackHandler interface to handle OAuth or SAML callback results in a class that can inject any service via the DI container. The built-in OAuthController calls handler.handle() automatically after successful callbacks.
import { Injectable } from '@hazeljs/core';
import { JwtService } from '@hazeljs/auth';
import type {
OAuthCallbackHandler,
OAuthProtocolCallbackResult,
SupportedProvider,
} from '@hazeljs/oauth';
@Injectable()
export class MyOAuthCallbackHandler implements OAuthCallbackHandler {
constructor(
private readonly jwtService: JwtService,
private readonly usersRepo: UsersRepository,
private readonly orgsRepo: OrganizationsRepository,
) {}
async handle(
result: OAuthProtocolCallbackResult,
_provider: SupportedProvider | `saml:${string}`,
) {
const oauthUser = result.user;
// 1. Find or create the application user
let user = await this.usersRepo.findByEmail(oauthUser.email);
const isNewUser = !user;
if (!user) {
const org = await this.orgsRepo.findOrCreate(oauthUser.email.split('@')[1]);
user = await this.usersRepo.create({
email: oauthUser.email,
name: oauthUser.name ?? '',
role: 'user',
organizationId: org.id,
});
}
// 2. Issue a HazelJS JWT — same shape JwtAuthGuard, RoleGuard, TenantGuard all expect
const token = this.jwtService.sign({
sub: user.id,
email: user.email,
role: user.role,
tenantId: user.organizationId,
});
return { token, user, isNewUser };
}
}
Register the handler as a provider and pass it to OAuthModule.forRoot():
// auth.module.ts
@Module({
providers: [MyOAuthCallbackHandler],
exports: [MyOAuthCallbackHandler],
})
export class AuthModule {}
// app.module.ts
OAuthModule.forRoot({
providers: { google: {...}, github: {...} },
callbackHandler: MyOAuthCallbackHandler,
})
After this, OAuth and SAML callbacks can both return { token, user, isNewUser } (or your own shape) — using the same JWT payload consumed by JwtAuthGuard, RoleGuard, and TenantGuard.
Using OAuthService directly
For complete control, skip the built-in OAuthController and call OAuthService from your own controller:
import { OAuthService } from '@hazeljs/oauth';
import { JwtService } from '@hazeljs/auth';
@Controller('/auth')
export class AuthController {
constructor(
private readonly oauthService: OAuthService,
private readonly jwtService: JwtService,
) {}
@Get('/google')
loginWithGoogle(@Res() res: Response) {
const { url, state, codeVerifier } = this.oauthService.getAuthorizationUrl('google');
// store state + codeVerifier in httpOnly cookies
res.status(302);
res.setHeader('Location', url);
res.end();
}
@Get('/google/callback')
async googleCallback(
@Query() query: { code: string; state: string },
@Req() req: { headers?: Record<string, string> },
@Res() res: Response,
) {
const storedState = getCookie(req, 'oauth_state');
const codeVerifier = getCookie(req, 'oauth_code_verifier');
if (query.state !== storedState) {
res.status(400).json({ error: 'Invalid state' });
return;
}
const result = await this.oauthService.handleCallback(
'google', query.code, storedState, codeVerifier,
);
// result: { accessToken, user: { id, email, name, picture } }
// → look up user in DB, issue JWT, etc.
const token = this.jwtService.sign({ sub: result.user.id, email: result.user.email });
res.status(200).json({ token });
}
}
Environment Variables
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
MICROSOFT_CLIENT_ID=your-microsoft-client-id
MICROSOFT_CLIENT_SECRET=your-microsoft-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
FACEBOOK_CLIENT_ID=your-facebook-client-id
FACEBOOK_CLIENT_SECRET=your-facebook-client-secret
TWITTER_CLIENT_ID=your-twitter-client-id
TWITTER_CLIENT_SECRET=your-twitter-client-secret
OAUTH_REDIRECT_URI=http://localhost:3000/auth/google/callback
Best Practices
- State validation: Always validate the
stateparameter on callback to prevent CSRF. - HTTPS in production: Use HTTPS for redirect URIs in production.
- Secure cookie storage: Store state and codeVerifier in httpOnly, SameSite cookies.
- Token storage: Store access/refresh tokens securely (encrypted if in DB). The package returns them; storage is your responsibility.
- Combine with JWT: Use OAuth for login, then issue your own JWT for API authentication.
What's Next?
- Learn about Auth for JWT, role guards, and tenant isolation
- Explore Config for managing OAuth secrets
- Check out TypeORM or Prisma for user storage
Recipes
Recipe: Google OAuth Login
// File: src/app.module.ts
import { HazelModule } from '@hazeljs/core';
import { OAuthModule } from '@hazeljs/oauth';
@HazelModule({
imports: [
OAuthModule.register({
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackUrl: '/auth/google/callback',
scope: ['email', 'profile'],
},
}),
],
})
export class AppModule {}
// File: src/auth/oauth.controller.ts
import { Controller, Get, UseGuards, Req, Res } from '@hazeljs/core';
import { OAuthGuard, OAuthCallback } from '@hazeljs/oauth';
import { AuthService } from '@hazeljs/auth';
import { Response } from 'express';
@Controller('auth')
export class OAuthController {
constructor(private readonly auth: AuthService) {}
@Get('google')
@UseGuards(OAuthGuard('google'))
googleLogin() {}
@Get('google/callback')
@OAuthCallback('google')
async googleCallback(@Req() req: any, @Res() res: Response) {
const jwt = await this.auth.login(req.user);
res.redirect(`/dashboard?token=${jwt.accessToken}`);
}
}
Related Resources
- Auth Package – JWT authentication and guards
- CASL Package – Authorization and permissions
- Config Package – Secret management for OAuth keys
- Audit Package – Login audit logging