Add social login to your HazelJS apps in minutes—not hours.
Building OAuth from scratch is painful. Authorization URLs, state validation, PKCE for some providers, token exchange, user profile fetching—each provider has its own quirks. We built @hazeljs/oauth so you can add "Sign in with Google" (and four other providers) with a single package and a few lines of config.
The Problem with DIY OAuth
If you've ever implemented OAuth yourself, you know the drill:
- Provider-specific flows — Google and Microsoft require PKCE; GitHub and Facebook don't. Twitter uses OAuth 2.0 with different scopes.
-
State management — You need to generate, store, and validate a cryptographically secure
stateto prevent CSRF. For PKCE providers, you also store acode_verifier. - Token exchange — Exchange the authorization code for tokens. Handle errors. Parse responses.
-
User profile — Each provider has a different API: Google's
userinfo, Microsoft Graph/me, GitHub/user, Facebook Graph/me, Twitter API v2/users/me. -
Scopes — Different scope formats:
openid profile emailvsuser:emailvsemail,public_profile.
Getting all of this right—and keeping it maintained as providers change their APIs—is a lot of work.
What @hazeljs/oauth Does
@hazeljs/oauth gives you a unified API across five major providers:
| Provider | PKCE | Default Scopes |
|---|---|---|
| Yes | openid, profile, email | |
| Microsoft Entra ID | Yes | openid, profile, email |
| GitHub | No | user:email |
| No | email, public_profile | |
| Yes | users.read, tweet.read |
It's built on Arctic, a lightweight OAuth library that supports 50+ providers. We've wrapped it in a HazelJS module with:
-
OAuthService —
getAuthorizationUrl(),handleCallback(),validateState(),generateState() -
OAuthController — Ready-made routes:
GET /auth/:providerandGET /auth/:provider/callback - OAuthStateGuard — CSRF protection for callbacks
-
User profile fetching — Normalized
{ id, email, name, picture }from each provider
Quick Start
1. Install
npm install @hazeljs/oauth
# or
hazel add oauth
2. Configure
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!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectUri: process.env.OAUTH_REDIRECT_URI!,
},
},
}),
],
})
export class AppModule {}
3. Use It
The built-in controller gives you:
- GET /auth/google — Redirects user to Google
-
GET /auth/google/callback — Handles the callback, returns
{ accessToken, user }
That's it. User visits /auth/google, signs in, and your callback receives tokens and profile.
The OAuth Flow, Explained
Here's what happens under the hood:
┌─────────────┐ GET /auth/google ┌─────────────┐
│ User │ ───────────────────────► │ Your App │
└─────────────┘ └──────┬──────┘
│
│ 1. Generate state + codeVerifier (for PKCE)
│ 2. Store in httpOnly cookies
│ 3. Build authorization URL
▼
┌─────────────┐ Redirect to Google ┌─────────────┐
│ User │ ◄─────────────────────── │ Google │
└─────────────┘ └──────┬──────┘
│ │
│ User signs in │
▼ │
┌─────────────┐ Redirect with ?code= ┌──────┴─────┐
│ Your App │ ◄─────────────────────── │ Google │
└──────┬──────┘ └────────────┘
│
│ 4. Validate state (CSRF check)
│ 5. Exchange code for tokens
│ 6. Fetch user profile
▼
┌───────────────────────┐
│ { accessToken, user } │
└───────────────────────┘
The package handles steps 1–6. You handle step 7: create/update the user in your DB and issue a JWT (or session cookie) for your app.
PKCE: Why It Matters
PKCE (Proof Key for Code Exchange) is an OAuth 2.0 extension that adds a secret (code_verifier) to the authorization request. The provider binds it to the authorization code. When you exchange the code, you must send the same code_verifier. This prevents authorization code interception attacks.
Google, Microsoft, and Twitter require PKCE for web apps. GitHub and Facebook don't use it (yet).
@hazeljs/oauth handles PKCE automatically:
- For PKCE providers,
getAuthorizationUrl()returns{ url, state, codeVerifier } - The built-in controller stores
codeVerifierin a cookie - On callback, it reads the cookie and passes
codeVerifiertohandleCallback()
If you build a custom flow, you must store and pass codeVerifier yourself.
Custom Flows
Sometimes you need more control—custom redirect logic, different cookie names, or integrating with an existing auth system. Use OAuthService directly:
import { OAuthService } from '@hazeljs/oauth';
@Injectable()
export class CustomAuthController {
constructor(private oauth: OAuthService) {}
@Get('login/:provider')
login(@Param('provider') provider: string, @Res() res: Response): void {
const { url, state, codeVerifier } = this.oauth.getAuthorizationUrl(provider);
// Store state + codeVerifier in your session/cookies
setSessionCookie(res, { state, codeVerifier });
res.redirect(url);
}
@Get('oauth/callback')
async callback(@Query() q: { code: string; state: string }, @Req() req: Request) {
const { state, codeVerifier } = getSessionCookie(req);
if (!this.oauth.validateState(q.state, state)) {
throw new UnauthorizedError('Invalid state');
}
const result = await this.oauth.handleCallback(
'google', q.code, q.state, codeVerifier
);
// result: { accessToken, refreshToken?, expiresAt?, user }
return result;
}
}
Integrating with @hazeljs/auth
OAuth gives you tokens from the provider. For your own API, you typically want a JWT you control. Combine @hazeljs/oauth with @hazeljs/auth:
import { JwtService } from '@hazeljs/auth';
import { OAuthService } from '@hazeljs/oauth';
@Injectable()
export class AuthService {
constructor(
private oauth: OAuthService,
private jwt: JwtService,
private prisma: PrismaService
) {}
async handleOAuthCallback(
provider: string,
code: string,
state: string,
codeVerifier?: string
) {
const { user } = await this.oauth.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 accessToken = this.jwt.sign({
sub: dbUser.id,
email: dbUser.email,
role: dbUser.role,
});
return { user: dbUser, accessToken };
}
}
Provider-Specific Notes
- Uses OpenID Connect. Default scopes:
openid,profile,email - Add
access_type=offlineif you need refresh tokens (handled by Arctic)
Microsoft Entra ID
- Supports
tenantoption: use'common'for multi-tenant, or your Azure AD tenant ID - Same OpenID scopes as Google
GitHub
- No PKCE. Simpler flow.
- Email may require
user:emailscope; the package fetches from/user/emailsif not in profile
- Uses Graph API. Picture is nested:
picture.data.url - Default scopes:
email,public_profile
-
Twitter API v2 does not provide user email. The
user.emailfield will be empty. - Optional
clientSecretfor public clients (PKCE-only) - Add
offline.accessscope for refresh tokens
Security Best Practices
-
Always validate
state— Prevents CSRF. The package does this in the built-in controller; do it yourself in custom flows. - Use HTTPS in production — Redirect URIs must use HTTPS (except localhost for dev).
- Store state/codeVerifier securely — Use httpOnly, SameSite cookies. Don't put them in URLs.
- Don't trust client-side state — Generate and validate server-side only.
- Encrypt stored tokens — If you persist access/refresh tokens, encrypt them. The package returns them; storage is your responsibility.
What's Next
- Full documentation — Configuration, scopes, guards
- @hazeljs/auth — JWT and route protection
- Arctic — 50+ providers; extend the package if you need more
@hazeljs/oauth is available on npm. Add social login to your HazelJS app today:
npm install @hazeljs/oauth
Questions or feedback? Open an issue or join us on Discord.
Top comments (0)