DEV Community

Cover image for Introducing @hazeljs/oauth: One Package for Google, Microsoft, GitHub, Facebook & Twitter Login
Muhammad Arslan
Muhammad Arslan

Posted on

Introducing @hazeljs/oauth: One Package for Google, Microsoft, GitHub, Facebook & Twitter Login

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:

  1. Provider-specific flows — Google and Microsoft require PKCE; GitHub and Facebook don't. Twitter uses OAuth 2.0 with different scopes.
  2. State management — You need to generate, store, and validate a cryptographically secure state to prevent CSRF. For PKCE providers, you also store a code_verifier.
  3. Token exchange — Exchange the authorization code for tokens. Handle errors. Parse responses.
  4. User profile — Each provider has a different API: Google's userinfo, Microsoft Graph /me, GitHub /user, Facebook Graph /me, Twitter API v2 /users/me.
  5. Scopes — Different scope formats: openid profile email vs user:email vs email,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
Google Yes openid, profile, email
Microsoft Entra ID Yes openid, profile, email
GitHub No user:email
Facebook No email, public_profile
Twitter 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:

  • OAuthServicegetAuthorizationUrl(), handleCallback(), validateState(), generateState()
  • OAuthController — Ready-made routes: GET /auth/:provider and GET /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
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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 } │
└───────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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 codeVerifier in a cookie
  • On callback, it reads the cookie and passes codeVerifier to handleCallback()

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 };
  }
}
Enter fullscreen mode Exit fullscreen mode

Provider-Specific Notes

Google

  • Uses OpenID Connect. Default scopes: openid, profile, email
  • Add access_type=offline if you need refresh tokens (handled by Arctic)

Microsoft Entra ID

  • Supports tenant option: 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:email scope; the package fetches from /user/emails if not in profile

Facebook

  • Uses Graph API. Picture is nested: picture.data.url
  • Default scopes: email, public_profile

Twitter

  • Twitter API v2 does not provide user email. The user.email field will be empty.
  • Optional clientSecret for public clients (PKCE-only)
  • Add offline.access scope for refresh tokens

Security Best Practices

  1. Always validate state — Prevents CSRF. The package does this in the built-in controller; do it yourself in custom flows.
  2. Use HTTPS in production — Redirect URIs must use HTTPS (except localhost for dev).
  3. Store state/codeVerifier securely — Use httpOnly, SameSite cookies. Don't put them in URLs.
  4. Don't trust client-side state — Generate and validate server-side only.
  5. Encrypt stored tokens — If you persist access/refresh tokens, encrypt them. The package returns them; storage is your responsibility.

What's Next


@hazeljs/oauth is available on npm. Add social login to your HazelJS app today:

npm install @hazeljs/oauth
Enter fullscreen mode Exit fullscreen mode

Questions or feedback? Open an issue or join us on Discord.

Top comments (0)