Skip to main content
The OAuth strategy enables authentication through third-party providers like Google, Facebook, GitHub, and over 200 others. It’s powered by Grant and supports both OAuth 1.0 and OAuth 2.0.

How OAuth Works

The OAuth authentication flow:
  1. User clicks “Login with Provider” - Client redirects to OAuth service
  2. Provider authorization - User authorizes your app on provider’s site
  3. Callback with code - Provider redirects back with authorization code
  4. Token exchange - Server exchanges code for access token
  5. Fetch profile - Server retrieves user profile from provider
  6. Create or link user - Server creates new user or links to existing account
  7. Return JWT - Server creates JWT and redirects with access token

Installation

npm install @feathersjs/authentication-oauth --save

Basic Setup

1

Install Dependencies

Install required packages:
npm install @feathersjs/authentication @feathersjs/authentication-oauth
2

Configure OAuth

Add OAuth configuration for your providers:
// config/default.json
{
  "authentication": {
    "secret": "your-secret-key",
    "entity": "user",
    "service": "users",
    "authStrategies": ["jwt", "local"],
    "oauth": {
      "redirect": "http://localhost:3000",
      "google": {
        "key": "your-google-client-id",
        "secret": "your-google-client-secret",
        "scope": ["email", "profile"]
      },
      "github": {
        "key": "your-github-client-id",
        "secret": "your-github-client-secret"
      }
    }
  }
}
3

Register Strategy and Service

Set up OAuth strategy and service:
import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication'
import { OAuthStrategy, oauth } from '@feathersjs/authentication-oauth'

// Create authentication service
const authentication = new AuthenticationService(app)
authentication.register('jwt', new JWTStrategy())

// Register OAuth strategies
authentication.register('google', new OAuthStrategy())
authentication.register('github', new OAuthStrategy())

app.use('/authentication', authentication)

// Configure OAuth endpoints
app.configure(oauth())
4

Update User Service

Add provider ID fields to your user schema:
// User schema should include fields for each provider
interface User {
  id: string
  email: string
  googleId?: string
  githubId?: string
  facebookId?: string
  // ... other fields
}

Configuration Options

OAuth Settings

OptionTypeDescription
redirectstringURL to redirect to after authentication
originsstring[]Allowed origin URLs for redirect validation
defaultsobjectDefault settings for all providers

Provider Settings

Each provider (e.g., google, github) can have:
OptionTypeDescription
keystringOAuth client ID / API key
secretstringOAuth client secret
scopestring[]Permissions to request from provider
custom_paramsobjectAdditional provider-specific parameters
redirect_uristringOverride callback URL

Strategy Options

app.configure(oauth({
  linkStrategy: 'jwt',        // Strategy to use for account linking
  authService: 'authentication', // Authentication service path
  expressSession: sessionMiddleware, // Custom session middleware
  koaSession: sessionMiddleware      // Custom Koa session middleware
}))

Supported Providers

Over 200 providers are supported via Grant. Common examples:
  • Google - google
  • Facebook - facebook
  • GitHub - github
  • Twitter - twitter
  • Microsoft - microsoft
  • LinkedIn - linkedin
  • Apple - apple
  • Discord - discord
  • Twitch - twitch
  • Spotify - spotify
View full list of providers →

Provider Examples

// config/default.json
{
  "authentication": {
    "oauth": {
      "redirect": "http://localhost:3000",
      "google": {
        "key": process.env.GOOGLE_CLIENT_ID,
        "secret": process.env.GOOGLE_CLIENT_SECRET,
        "scope": ["email", "profile"],
        "custom_params": {
          "access_type": "offline",
          "prompt": "consent"
        }
      }
    }
  }
}
Get credentials:
  1. Go to Google Cloud Console
  2. Create project → Enable Google+ API
  3. Create OAuth 2.0 credentials
  4. Add authorized redirect URI: http://localhost:3030/oauth/google/callback

Client Integration

Redirect to OAuth Provider

<!-- Simple link approach -->
<a href="http://localhost:3030/oauth/google">
  Login with Google
</a>

<a href="http://localhost:3030/oauth/github">
  Login with GitHub
</a>

With Custom Redirect

// Specify where to redirect after authentication
const redirectUrl = encodeURIComponent('/dashboard')
window.location.href = `http://localhost:3030/oauth/google?redirect=${redirectUrl}`
function loginWithProvider(provider) {
  const width = 600
  const height = 700
  const left = (window.innerWidth - width) / 2
  const top = (window.innerHeight - height) / 2
  
  const popup = window.open(
    `http://localhost:3030/oauth/${provider}`,
    'OAuth Login',
    `width=${width},height=${height},left=${left},top=${top}`
  )
  
  // Listen for redirect with token
  window.addEventListener('message', (event) => {
    if (event.origin === window.location.origin) {
      const { accessToken } = event.data
      if (accessToken) {
        popup.close()
        // Store token and redirect
        localStorage.setItem('accessToken', accessToken)
        window.location.href = '/dashboard'
      }
    }
  })
}

Receive Token

After successful authentication, the user is redirected with the token:
http://localhost:3000#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Extract the token:
// Parse token from URL hash
const hash = window.location.hash.substring(1)
const params = new URLSearchParams(hash)
const accessToken = params.get('access_token')

if (accessToken) {
  // Store token
  localStorage.setItem('accessToken', accessToken)
  
  // Clean URL
  window.history.replaceState(null, '', window.location.pathname)
  
  // Redirect to app
  window.location.href = '/dashboard'
}

// Check for error
const error = params.get('error')
if (error) {
  console.error('Authentication failed:', error)
}

Account Linking

Link OAuth accounts to existing users:
// User is already logged in with JWT
const currentToken = localStorage.getItem('accessToken')

// Link OAuth account
window.location.href = 
  `http://localhost:3030/oauth/google?feathers_token=${currentToken}`
The OAuth strategy will:
  1. Verify the existing JWT token
  2. Get the current user
  3. Link the OAuth account to the user (via updateEntity)
  4. Return a new JWT
import { OAuthStrategy } from '@feathersjs/authentication-oauth'

class CustomOAuthStrategy extends OAuthStrategy {
  async getEntityData(profile, existingEntity, params) {
    const baseData = await super.getEntityData(profile, existingEntity, params)
    
    return {
      ...baseData,
      [`${this.name}Id`]: profile.id,
      [`${this.name}AccessToken`]: profile.accessToken,
      [`${this.name}RefreshToken`]: profile.refreshToken,
      lastLogin: new Date()
    }
  }
}

authentication.register('google', new CustomOAuthStrategy())

Customization

Custom Profile Data

Extract additional data from OAuth profile:
import { OAuthStrategy } from '@feathersjs/authentication-oauth'

class GoogleStrategy extends OAuthStrategy {
  async getEntityData(profile, existingEntity, params) {
    const baseData = await super.getEntityData(profile, existingEntity, params)
    
    // Extract Google-specific data
    return {
      ...baseData,
      googleId: profile.sub || profile.id,
      email: profile.email,
      firstName: profile.given_name,
      lastName: profile.family_name,
      avatar: profile.picture,
      emailVerified: profile.email_verified
    }
  }
}

authentication.register('google', new GoogleStrategy())

Custom Entity Query

Customize how users are found:
class CustomOAuthStrategy extends OAuthStrategy {
  async getEntityQuery(profile, params) {
    // Default queries by `${strategy}Id`
    // Override to search by email first
    if (profile.email) {
      return {
        $or: [
          { [`${this.name}Id`]: profile.id },
          { email: profile.email }
        ]
      }
    }
    
    return super.getEntityQuery(profile, params)
  }
}

Create vs Update Logic

class CustomOAuthStrategy extends OAuthStrategy {
  async getEntityData(profile, existingEntity, params) {
    const baseData = {
      [`${this.name}Id`]: profile.id,
      email: profile.email
    }
    
    // Different logic for create vs update
    if (existingEntity) {
      // Only update OAuth-specific fields
      return {
        ...baseData,
        lastLogin: new Date()
      }
    } else {
      // Set all fields for new user
      return {
        ...baseData,
        firstName: profile.given_name,
        lastName: profile.family_name,
        avatar: profile.picture,
        createdAt: new Date()
      }
    }
  }
}

Prevent User Creation

import { NotAuthenticated } from '@feathersjs/errors'

class NoCreateOAuthStrategy extends OAuthStrategy {
  async authenticate(authentication, originalParams) {
    const { provider, ...params } = originalParams
    const profile = await this.getProfile(authentication, params)
    const existingEntity = await this.findEntity(profile, params)
    
    // Don't create new users, only link existing ones
    if (!existingEntity) {
      throw new NotAuthenticated(
        'No account found. Please sign up first.'
      )
    }
    
    const entity = await this.updateEntity(existingEntity, profile, params)
    
    return {
      authentication: { strategy: this.name },
      [this.configuration.entity]: await this.getEntity(entity, originalParams)
    }
  }
}

Security Configuration

Allowed Origins

Always validate redirect origins to prevent open redirect vulnerabilities.
// config/production.json
{
  "authentication": {
    "oauth": {
      "origins": [
        "https://yourdomain.com",
        "https://app.yourdomain.com",
        "https://mobile.yourdomain.com"
      ]
    }
  }
}
The OAuth strategy validates:
  • Referer header matches allowed origin
  • Redirect parameter doesn’t contain URL injection characters
  • Final redirect URL starts with allowed origin

Redirect Validation

// strategy.ts:70-123
// The strategy validates redirect parameters:
// 1. Checks referer against allowed origins
// 2. Rejects @ \ // characters that could change URL authority
// 3. Ensures redirect doesn't contain malicious paths

if (queryRedirect && /[@\\]|^\/\/|\/\//.test(queryRedirect)) {
  throw new NotAuthenticated('Invalid redirect path.')
}

Session Security

Configure secure session handling:
import session from 'express-session'

app.configure(oauth({
  expressSession: session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      secure: process.env.NODE_ENV === 'production', // HTTPS only
      httpOnly: true,                                 // No client JS access
      maxAge: 10 * 60 * 1000,                        // 10 minutes
      sameSite: 'lax'                                 // CSRF protection
    }
  })
}))

Environment Variables

Never commit OAuth secrets to version control!
# .env
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret
// config/default.js
module.exports = {
  authentication: {
    oauth: {
      google: {
        key: process.env.GOOGLE_CLIENT_ID,
        secret: process.env.GOOGLE_CLIENT_SECRET
      },
      github: {
        key: process.env.GITHUB_CLIENT_ID,
        secret: process.env.GITHUB_CLIENT_SECRET
      }
    }
  }
}

Multi-Tenancy

Scope OAuth authentication to tenants:
class TenantOAuthStrategy extends OAuthStrategy {
  async getEntityQuery(profile, params) {
    const baseQuery = await super.getEntityQuery(profile, params)
    
    // Add tenant scope
    const tenantId = params.route?.query?.tenant || params.query?.tenant
    
    if (!tenantId) {
      throw new NotAuthenticated('Tenant ID required')
    }
    
    return {
      ...baseQuery,
      tenantId
    }
  }
  
  async getEntityData(profile, existingEntity, params) {
    const baseData = await super.getEntityData(profile, existingEntity, params)
    const tenantId = params.route?.query?.tenant || params.query?.tenant
    
    return {
      ...baseData,
      tenantId
    }
  }
}

// Login with tenant context
window.location.href = 
  'http://localhost:3030/oauth/google?tenant=tenant-123'

Troubleshooting

Redirect URI Mismatch

Error: redirect_uri_mismatch
Solution: Ensure callback URL matches exactly in provider settings:
  • Development: http://localhost:3030/oauth/google/callback
  • Production: https://api.yourdomain.com/oauth/google/callback

Invalid Origin

Error: Referer "http://example.com" is not allowed
Solution: Add origin to allowed origins:
{
  "authentication": {
    "oauth": {
      "origins": [
        "http://localhost:3000",
        "https://yourdomain.com"
      ]
    }
  }
}

Session Not Persisting

Error: Session data lost between redirect
Solution: Configure session middleware properly:
// For Express
import session from 'express-session'
import RedisStore from 'connect-redis'

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false
}))

app.configure(oauth())

Provider Not Configured

Error: No oauth configuration found
Solution: Ensure oauth section exists in authentication config:
{
  "authentication": {
    "oauth": {  // Must have this section
      "redirect": "http://localhost:3000",
      "google": { ... }
    }
  }
}

Next Steps

JWT Strategy

Understand JWT token authentication

Local Strategy

Add username/password authentication

Build docs developers (and LLMs) love