OAuth Package
The @hazeljs/oauth package provides OAuth 2.0 social login for HazelJS applications. It supports Google, Microsoft Entra ID, GitHub, Facebook, and Twitter out of the box, with a consistent API built on Arctic (50+ providers available).
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
- 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 - Integration with @hazeljs/auth: Use OAuth for login, then issue JWT via
JwtService
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[Return tokens + user]
end
subgraph Package [@hazeljs/oauth]
OAuthModule
OAuthService
OAuthController
end
OAuthService --> Arctic[Arctic Library]
OAuthModule --> OAuthServiceKey Components
- OAuthService:
getAuthorizationUrl(),handleCallback(),validateState() - OAuthController: Optional routes for
/auth/:providerand/auth/:provider/callback - 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:
import { HazelModule } from '@hazeljs/core';
import { OAuthModule } from '@hazeljs/oauth';
@HazelModule({
imports: [
OAuthModule.forRoot({
providers: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.OAUTH_REDIRECT_URI!,
},
microsoft: {
clientId: process.env.MICROSOFT_CLIENT_ID!,
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!,
redirectUri: process.env.OAUTH_REDIRECT_URI!,
tenant: 'common', // or your Azure AD tenant ID
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectUri: process.env.OAUTH_REDIRECT_URI!,
},
facebook: {
clientId: process.env.FACEBOOK_CLIENT_ID!,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
redirectUri: process.env.OAUTH_REDIRECT_URI!,
},
twitter: {
clientId: process.env.TWITTER_CLIENT_ID!,
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
redirectUri: process.env.OAUTH_REDIRECT_URI!,
},
},
}),
],
})
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.
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 { Injectable } from '@hazeljs/core';
import { OAuthService } from '@hazeljs/oauth';
@Injectable()
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.
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
After OAuth callback, create/update the user and issue a JWT:
import { JwtService } from '@hazeljs/auth';
import { OAuthService } from '@hazeljs/oauth';
@Injectable()
export class AuthService {
constructor(
private readonly oauthService: OAuthService,
private readonly jwtService: JwtService,
private readonly prisma: PrismaService
) {}
async handleOAuthCallback(provider: 'google' | 'microsoft' | 'github' | 'facebook' | 'twitter', code: string, state: string, codeVerifier?: string) {
const { user, accessToken } = await this.oauthService.handleCallback(
provider,
code,
state,
codeVerifier
);
let dbUser = await this.prisma.user.findUnique({ where: { email: user.email } });
if (!dbUser) {
dbUser = await this.prisma.user.create({
data: {
email: user.email,
name: user.name,
picture: user.picture,
provider,
providerId: user.id,
},
});
}
const jwt = this.jwtService.sign({
sub: dbUser.id,
email: dbUser.email,
role: dbUser.role,
});
return { user: dbUser, accessToken: jwt };
}
}
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.