npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@flink-app/oauth-plugin

v0.12.1-alpha.45

Published

Flink plugin for OAuth 2.0 authentication with GitHub and Google providers

Readme

OAuth Plugin

A flexible OAuth 2.0 authentication plugin for Flink that supports multiple providers (GitHub, Google) with MongoDB session storage, JWT token generation, and configurable token handling.

Features

  • OAuth 2.0 Authorization Code flow for GitHub and Google
  • Automatic JWT token generation via JWT Auth Plugin integration
  • MongoDB session storage with automatic TTL cleanup
  • Support for linking multiple OAuth providers to a single user account
  • Flexible token storage (store OAuth tokens for API access or auth-only mode)
  • CSRF protection with cryptographically secure state parameters
  • Encrypted OAuth token storage (AES-256-GCM)
  • Built-in HTTP endpoints for OAuth flow
  • TypeScript support with full type safety
  • Configurable response formats (JSON, URL fragment, query parameter)

Installation

npm install @flink-app/oauth-plugin @flink-app/jwt-auth-plugin

Prerequisites

1. JWT Auth Plugin Dependency

This plugin requires @flink-app/jwt-auth-plugin to be installed and configured. The OAuth plugin uses the JWT Auth Plugin to generate authentication tokens after successful OAuth authentication.

2. OAuth Provider Credentials

You need OAuth application credentials from your desired providers:

GitHub OAuth App

  1. Go to GitHub Developer Settings
  2. Create a new OAuth App
  3. Set Authorization callback URL to https://yourdomain.com/oauth/github/callback
  4. Save Client ID and Client Secret

Google OAuth App

  1. Go to Google Cloud Console
  2. Create a new project or select existing
  3. Enable Google+ API
  4. Go to Credentials > Create Credentials > OAuth 2.0 Client ID
  5. Set Authorized redirect URI to https://yourdomain.com/oauth/google/callback
  6. Save Client ID and Client Secret

3. MongoDB Connection

The plugin requires MongoDB to store OAuth sessions.

Quick Start

import { FlinkApp } from "@flink-app/flink";
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
import { oauthPlugin } from "@flink-app/oauth-plugin";
import { Context } from "./Context";

const app = new FlinkApp<Context>({
    name: "My App",

    // JWT Auth Plugin MUST be configured first
    auth: jwtAuthPlugin({
        secret: process.env.JWT_SECRET!,
        getUser: async (tokenData) => {
            return await app.ctx.repos.userRepo.getById(tokenData.userId);
        },
        rolePermissions: {
            user: ["read:own", "write:own"],
            admin: ["read:all", "write:all"],
        },
    }),

    db: {
        uri: process.env.MONGODB_URI!,
    },

    plugins: [
        oauthPlugin({
            providers: {
                github: {
                    clientId: process.env.GITHUB_CLIENT_ID!,
                    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
                    callbackUrl: "https://myapp.com/oauth/github/callback",
                },
                google: {
                    clientId: process.env.GOOGLE_CLIENT_ID!,
                    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
                    callbackUrl: "https://myapp.com/oauth/google/callback",
                },
            },

            // Callback after successful OAuth authentication
            onAuthSuccess: async ({ profile, provider }, ctx) => {
                // Find or create user
                let user = await ctx.repos.userRepo.getOne({ email: profile.email });

                if (!user) {
                    user = await ctx.repos.userRepo.create({
                        email: profile.email,
                        name: profile.name,
                        avatarUrl: profile.avatarUrl,
                        oauthProviders: [{ provider, providerId: profile.id }],
                    });
                } else {
                    // Link provider to existing user
                    await ctx.repos.userRepo.updateOne(user._id, {
                        oauthProviders: [...user.oauthProviders, { provider, providerId: profile.id }],
                    });
                }

                // Generate JWT token using JWT Auth Plugin
                const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id, email: user.email }, ["user"]);

                return {
                    user,
                    token,
                    redirectUrl: "https://myapp.com/dashboard", // Plugin will add: #token=...
                };
            },

            // Optional: Handle OAuth errors
            onAuthError: async ({ error, provider }) => {
                console.error(`OAuth error for ${provider}:`, error);
                return {
                    redirectUrl: `https://myapp.com/login?error=${error.code}`,
                };
            },
        }),
    ],
});

await app.start();

Configuration

OAuthPluginOptions

| Option | Type | Required | Default | Description | | --------------------------- | ---------- | -------- | --------------------- | ---------------------------------------------- | | providers | object | Yes | - | OAuth provider configurations (GitHub, Google) | | storeTokens | boolean | No | false | Store OAuth tokens for future API access | | onAuthSuccess | Function | Yes | - | Callback after successful authentication | | onAuthError | Function | No | - | Callback on OAuth errors | | sessionTTL | number | No | 600 | Session TTL in seconds (default: 10 minutes) | | sessionsCollectionName | string | No | "oauth_sessions" | MongoDB collection for sessions | | connectionsCollectionName | string | No | "oauth_connections" | MongoDB collection for connections |

Provider Configuration

GitHub Provider

{
  github: {
    clientId: string;
    clientSecret: string;
    callbackUrl: string;
    scope?: string[];  // Default: ["user:email"]
  }
}

Google Provider

{
  google: {
    clientId: string;
    clientSecret: string;
    callbackUrl: string;
    scope?: string[];  // Default: ["openid", "email", "profile"]
  }
}

Callback Functions

onAuthSuccess

Called when OAuth authentication succeeds. Must generate and return a JWT token.

onAuthSuccess: async (
    params: {
        profile: OAuthProfile;
        provider: "github" | "google";
        tokens?: OAuthTokens; // Only if storeTokens: true
    },
    ctx: Context
) =>
    Promise<{
        user: any;
        token: string; // JWT token from ctx.plugins.jwtAuth.createToken()
        redirectUrl?: string; // Plugin appends #token=... to this URL
    }>;

Important: The redirectUrl should NOT include the token. The plugin automatically appends #token=... to the URL you return.

OAuth Profile Structure:

interface OAuthProfile {
    id: string; // Provider user ID
    email: string; // User email
    name?: string; // Full name
    avatarUrl?: string; // Profile picture URL
    raw: any; // Raw provider response
}

OAuth Tokens Structure (if storeTokens: true):

interface OAuthTokens {
    accessToken: string;
    refreshToken?: string;
    expiresIn?: number;
    scope?: string;
}

onAuthError

Called when OAuth authentication fails.

onAuthError: async (params: { error: OAuthError; provider: "github" | "google" }) =>
    Promise<{
        redirectUrl?: string;
    }>;

interface OAuthError {
    code: string; // Error code (e.g., "access_denied")
    message: string; // User-friendly error message
    details?: any; // Additional error details
}

Common Error Codes:

  • invalid_state - State parameter mismatch or expired
  • access_denied - User denied OAuth authorization
  • invalid_grant - Authorization code expired or invalid
  • network_error - Provider API unreachable
  • jwt_generation_failed - Failed to generate JWT token

OAuth Flow

Complete Authentication Flow

  1. User clicks "Login with GitHub" or "Login with Google"
  2. Client redirects to /oauth/:provider/initiate
  3. Plugin generates secure state parameter and stores session
  4. Plugin redirects user to OAuth provider (GitHub/Google)
  5. User authorizes app on OAuth provider
  6. OAuth provider redirects to /oauth/:provider/callback with authorization code
  7. Plugin validates state parameter (CSRF protection)
  8. Plugin exchanges authorization code for OAuth access token
  9. Plugin fetches user profile from provider
  10. Plugin calls onAuthSuccess callback with profile and context
  11. App creates/links user account
  12. App generates JWT token via ctx.plugins.jwtAuth.createToken()
  13. Plugin returns JWT token to client via URL fragment
  14. Client stores JWT token and uses it for authenticated requests

How to Extract JWT Token in Frontend

IMPORTANT: The plugin returns the JWT token as a URL fragment (#token=...), NOT as a query parameter (?token=...).

URL fragments are more secure because they are:

  • NOT sent to the server in HTTP requests
  • NOT logged in server access logs
  • Only accessible to client-side JavaScript

Correct way to extract the token:

// ✅ CORRECT - Read from URL fragment (hash)
const hash = window.location.hash.slice(1); // Remove leading #
const params = new URLSearchParams(hash);
const token = params.get("token");

// ❌ WRONG - Reading from query parameters won't work
const params = new URLSearchParams(window.location.search);
const token = params.get("token"); // This returns null!

In your onAuthSuccess callback:

// ✅ CORRECT - Just return the redirectUrl, plugin adds #token=...
return {
    user,
    token,
    redirectUrl: "https://myapp.com/dashboard",
};
// Result: https://myapp.com/dashboard#token=eyJ...

// ❌ WRONG - Don't manually add the token
return {
    user,
    token,
    redirectUrl: `https://myapp.com/dashboard?token=${token}`,
};
// Plugin will add token again: ...?token=eyJ...#token=eyJ...

See Client Integration Examples for complete frontend code.

Initiate OAuth Flow

GET /oauth/:provider/initiate?redirect_uri={optional_redirect}

Example:

GET /oauth/github/initiate
GET /oauth/google/initiate?redirect_uri=https://myapp.com/welcome

Response:

  • 302 redirect to OAuth provider authorization URL

OAuth Callback

GET /oauth/:provider/callback?code={auth_code}&state={state}&response_type={json|fragment|query}

Query Parameters:

  • code - Authorization code from provider
  • state - CSRF protection token
  • response_type - Optional response format: json for JSON response, otherwise redirect

Response Formats:

  1. URL Fragment Redirect (default):

The plugin redirects to your redirectUrl with the JWT token appended as a URL fragment for enhanced security:

https://myapp.com/dashboard#token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Why fragments? URL fragments (#token=...) are NOT sent to the server in HTTP requests, making them more secure than query parameters. They're only accessible to client-side JavaScript.

Important: Do NOT manually add the token to your redirectUrl. The plugin automatically appends it:

// ❌ WRONG - Don't do this
return {
    user,
    token,
    redirectUrl: `${frontendUrl}/auth/callback?token=${token}`, // Plugin will add token again!
};

// ✅ CORRECT - Let plugin append the token
return {
    user,
    token,
    redirectUrl: `${frontendUrl}/auth/callback`, // Plugin adds: #token=...
};
  1. JSON Response (when response_type=json):

Use response_type=json for API clients that need direct JSON response instead of redirect:

GET /oauth/github/callback?code=xxx&state=yyy&response_type=json
{
    "user": {
        "_id": "...",
        "email": "[email protected]",
        "name": "John Doe"
    },
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Context API

The plugin exposes methods via ctx.plugins.oauth:

getConnection

Get stored OAuth connection for a user and provider.

const connection = await ctx.plugins.oauth.getConnection(userId, "github");

// Returns OAuthConnection or null
interface OAuthConnection {
    _id: string;
    userId: string;
    provider: "github" | "google";
    providerId: string;
    accessToken: string; // Encrypted
    refreshToken?: string; // Encrypted
    scope: string;
    expiresAt?: Date;
    createdAt: Date;
    updatedAt: Date;
}

getConnections

Get all OAuth connections for a user.

const connections = await ctx.plugins.oauth.getConnections(userId);
// Returns OAuthConnection[]

deleteConnection

Delete/unlink an OAuth connection.

await ctx.plugins.oauth.deleteConnection(userId, "github");

Token Storage

Auth-Only Mode (Default)

By default, storeTokens: false, meaning OAuth tokens are NOT stored. OAuth is used only for authentication.

oauthPlugin({
  providers: { github: {...}, google: {...} },
  storeTokens: false,  // OAuth tokens discarded after auth
  onAuthSuccess: async ({ profile }, ctx) => {
    // Create user and generate JWT token
    // OAuth tokens are NOT available here
  }
})

Use when:

  • You only need OAuth for user authentication
  • You don't need to call provider APIs on behalf of users
  • You want to minimize stored credentials

Token Storage Mode

Set storeTokens: true to store encrypted OAuth tokens for future API access.

oauthPlugin({
  providers: { github: {...}, google: {...} },
  storeTokens: true,  // Store encrypted OAuth tokens
  onAuthSuccess: async ({ profile, tokens }, ctx) => {
    // tokens.accessToken and tokens.refreshToken are available
    // Tokens are automatically encrypted and stored
  }
})

Use when:

  • You need to call GitHub/Google APIs on behalf of users
  • You want to access user's GitHub repos or Google Drive
  • You need long-term API access

Note: OAuth tokens are encrypted using AES-256-GCM before storage.

JWT vs OAuth Tokens

It's important to understand the difference between OAuth tokens and JWT tokens:

OAuth Tokens

  • Purpose: Access provider APIs (GitHub, Google) on behalf of the user
  • Issued by: OAuth provider (GitHub, Google)
  • Used for: Calling GitHub API, Google API, etc.
  • Storage: Optional (only if storeTokens: true)
  • Lifetime: Varies by provider (hours to months)

JWT Tokens

  • Purpose: Authenticate requests to YOUR app
  • Issued by: Your app (via JWT Auth Plugin)
  • Used for: Accessing protected endpoints in your app
  • Storage: Client-side (localStorage, sessionStorage)
  • Lifetime: Configured in JWT Auth Plugin

Example Flow:

User OAuth Login (GitHub)
  -> OAuth access token (to call GitHub API)
  -> Your app generates JWT token
  -> Client uses JWT token for app authentication

Security

CSRF Protection

The plugin uses cryptographically secure state parameters to prevent CSRF attacks:

  1. Generate 32-byte random state using crypto.randomBytes()
  2. Store state in MongoDB session with 10-minute expiration
  3. Validate state on callback using constant-time comparison
  4. Clear session after successful validation

Token Encryption

When storeTokens: true, OAuth tokens are encrypted before storage:

  • Algorithm: AES-256-GCM
  • Encryption key: Derived from client secret
  • Storage: Encrypted tokens in MongoDB
  • Decryption: Automatic when retrieved via context methods

HTTPS Requirement

IMPORTANT: OAuth callback URLs MUST use HTTPS in production. OAuth providers reject HTTP callback URLs for security reasons.

Secrets Management

Never commit secrets to version control:

# .env
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
JWT_SECRET=your_jwt_secret

JWT Token Security

  • Store JWT tokens in secure storage (httpOnly cookies or secure localStorage)
  • Never expose JWT tokens in URLs for long-term storage
  • Use short token expiration times
  • Implement token refresh mechanism
  • Validate tokens on every request

API Client Integration

For API clients (mobile apps, SPAs), use response_type=json:

// Mobile app OAuth flow
const initiateUrl = "https://api.myapp.com/oauth/github/initiate";

// Open browser for OAuth
openBrowser(initiateUrl);

// After OAuth, catch callback
const callbackUrl = "https://api.myapp.com/oauth/github/callback?code=xxx&state=yyy&response_type=json";

// Fetch JSON response
const response = await fetch(callbackUrl);
const { user, token } = await response.json();

// Store JWT token in secure storage
await secureStorage.setItem("jwt_token", token);

// Use JWT token for authenticated requests
const headers = {
    Authorization: `Bearer ${token}`,
};

Client Integration Examples

React Web App

import React from "react";

function LoginPage() {
    const handleGitHubLogin = () => {
        // Redirect to OAuth initiation
        window.location.href = "/oauth/github/initiate?redirect_uri=https://myapp.com/dashboard";
    };

    const handleGoogleLogin = () => {
        window.location.href = "/oauth/google/initiate?redirect_uri=https://myapp.com/dashboard";
    };

    React.useEffect(() => {
        // IMPORTANT: Token is in URL FRAGMENT (#token=...), not query parameter (?token=...)
        const hash = window.location.hash.slice(1); // Remove the leading #
        const params = new URLSearchParams(hash);
        const token = params.get("token");

        if (token) {
            // Store JWT token
            localStorage.setItem("jwt_token", token);

            // Clean URL (remove fragment)
            window.history.replaceState({}, document.title, "/dashboard");

            // Redirect to dashboard
            window.location.href = "/dashboard";
        }
    }, []);

    return (
        <div>
            <h1>Login</h1>
            <button onClick={handleGitHubLogin}>Login with GitHub</button>
            <button onClick={handleGoogleLogin}>Login with Google</button>
        </div>
    );
}

React Native App

import { openAuthSessionAsync } from "expo-auth-session";

async function loginWithGitHub() {
    const result = await openAuthSessionAsync("https://api.myapp.com/oauth/github/initiate", "myapp://oauth/callback");

    if (result.type === "success") {
        const url = result.url;

        // IMPORTANT: Token is in URL FRAGMENT (#token=...), not query parameter (?token=...)
        const urlObj = new URL(url);
        const token = new URLSearchParams(urlObj.hash.slice(1)).get("token");

        if (token) {
            await AsyncStorage.setItem("jwt_token", token);
            // Navigate to home screen
        }
    }
}

Multiple Provider Linking

Allow users to link multiple OAuth providers to their account:

onAuthSuccess: async ({ profile, provider }, ctx) => {
    // Find user by email (regardless of provider)
    let user = await ctx.repos.userRepo.getOne({ email: profile.email });

    if (user) {
        // User exists - link new provider
        const existingProviders = user.oauthProviders || [];
        const isAlreadyLinked = existingProviders.some((p) => p.provider === provider && p.providerId === profile.id);

        if (!isAlreadyLinked) {
            await ctx.repos.userRepo.updateOne(user._id, {
                oauthProviders: [...existingProviders, { provider, providerId: profile.id }],
            });
        }
    } else {
        // New user - create account
        user = await ctx.repos.userRepo.create({
            email: profile.email,
            name: profile.name,
            avatarUrl: profile.avatarUrl,
            oauthProviders: [{ provider, providerId: profile.id }],
        });
    }

    // Generate JWT token
    const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id }, ["user"]);

    return { user, token, redirectUrl: "/dashboard" };
};

Migration from BankID Plugin

If you're migrating from the BankID plugin, the OAuth plugin follows similar patterns:

Similarities

  • Callback-based architecture with onAuthSuccess
  • JWT token generation via JWT Auth Plugin
  • MongoDB session storage with TTL
  • Context-based dependency injection

Improvements

  1. Provider abstraction - Easy to add new OAuth providers
  2. Flexible token storage - Choose whether to store OAuth tokens
  3. Better error handling - Dedicated onAuthError callback
  4. Multiple provider support - Built-in linking of GitHub + Google
  5. Cleaner separation - Plugin handles OAuth, app handles user logic

Migration Example

BankID Plugin:

bankIdPlugin({
    onAuthSuccess: async (userData, ip, payload) => {
        const user = await findOrCreateUser(userData);
        const token = await ctx.auth.createToken({ userId: user._id }, ["user"]);
        return { user, token };
    },
});

OAuth Plugin:

oauthPlugin({
    onAuthSuccess: async ({ profile, provider }, ctx) => {
        const user = await findOrCreateUser(profile, provider);
        const token = await ctx.plugins.jwtAuth.createToken({ userId: user._id }, ["user"]);
        return { user, token, redirectUrl: "/dashboard" };
    },
});

Troubleshooting

OAuth Error: Invalid Redirect URI

Issue: redirect_uri_mismatch error from OAuth provider

Solution:

  • Verify callback URL in provider settings matches exactly
  • Ensure callback URL uses HTTPS in production
  • Check for trailing slashes (they matter!)

State Parameter Mismatch

Issue: invalid_state error

Solution:

  • Ensure cookies are enabled (sessions use MongoDB, but state validation may use cookies)
  • Check session TTL hasn't expired (default: 10 minutes)
  • Verify clock synchronization between servers

JWT Token Not Generated

Issue: jwt_generation_failed error

Solution:

  • Ensure JWT Auth Plugin is configured
  • Verify ctx.plugins.jwtAuth is available in onAuthSuccess
  • Check JWT secret is set in environment variables

User Denied Access

Issue: User cancels OAuth authorization

Solution:

onAuthError: async ({ error, provider }) => {
    if (error.code === "access_denied") {
        return {
            redirectUrl: "/login?message=You must authorize the app to continue",
        };
    }
    return { redirectUrl: "/login?error=oauth_failed" };
};

Tokens Not Stored

Issue: getConnection() returns null

Solution:

  • Set storeTokens: true in plugin configuration
  • Verify onAuthSuccess completes successfully
  • Check MongoDB connection is active

TypeScript Types

import { OAuthPluginOptions, OAuthProfile, OAuthTokens, OAuthConnection, OAuthError, OAuthPluginContext } from "@flink-app/oauth-plugin";

// OAuth profile from provider
interface OAuthProfile {
    id: string;
    email: string;
    name?: string;
    avatarUrl?: string;
    raw: any;
}

// OAuth tokens (if storeTokens: true)
interface OAuthTokens {
    accessToken: string;
    refreshToken?: string;
    expiresIn?: number;
    scope?: string;
}

// Stored connection
interface OAuthConnection {
    _id: string;
    userId: string;
    provider: "github" | "google";
    providerId: string;
    accessToken: string;
    refreshToken?: string;
    scope: string;
    expiresAt?: Date;
    createdAt: Date;
    updatedAt: Date;
}

Production Checklist

  • [ ] Configure HTTPS for all OAuth callback URLs
  • [ ] Set OAuth credentials in secure environment variables
  • [ ] Configure JWT Auth Plugin with secure secret
  • [ ] Set appropriate JWT token expiration
  • [ ] Implement rate limiting on OAuth endpoints
  • [ ] Set up monitoring and error alerting
  • [ ] Test OAuth flow for all providers
  • [ ] Implement proper error handling in callbacks
  • [ ] Configure CORS for OAuth endpoints
  • [ ] Set up session cleanup and monitoring
  • [ ] Document OAuth provider setup for team
  • [ ] Test token refresh mechanism (if using stored tokens)

Examples

See the examples/ directory for complete working examples:

  • basic-auth.ts - Basic OAuth authentication with JWT
  • multi-provider.ts - Multiple provider linking
  • token-storage.ts - Storing OAuth tokens for API access
  • api-client-auth.ts - API client integration with response_type=json

License

MIT