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:
User clicks “Login with Provider” - Client redirects to OAuth service
Provider authorization - User authorizes your app on provider’s site
Callback with code - Provider redirects back with authorization code
Token exchange - Server exchanges code for access token
Fetch profile - Server retrieves user profile from provider
Create or link user - Server creates new user or links to existing account
Return JWT - Server creates JWT and redirects with access token
Installation
npm install @feathersjs/authentication-oauth --save
Basic Setup
Install Dependencies
Install required packages: npm install @feathersjs/authentication @feathersjs/authentication-oauth
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"
}
}
}
}
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 ())
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
Option Type Description 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:
Option Type Description 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
Google
GitHub
Facebook
Apple
// 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:
Go to Google Cloud Console
Create project → Enable Google+ API
Create OAuth 2.0 credentials
Add authorized redirect URI: http://localhost:3030/oauth/google/callback
// config/default.json
{
"authentication" : {
"oauth" : {
"redirect" : "http://localhost:3000" ,
"github" : {
"key" : process . env . GITHUB_CLIENT_ID ,
"secret" : process . env . GITHUB_CLIENT_SECRET ,
"scope" : [ "user:email" ]
}
}
}
}
Get credentials:
Go to GitHub Developer Settings
New OAuth App
Add callback URL: http://localhost:3030/oauth/github/callback
// config/default.json
{
"authentication" : {
"oauth" : {
"redirect" : "http://localhost:3000" ,
"facebook" : {
"key" : process . env . FACEBOOK_APP_ID ,
"secret" : process . env . FACEBOOK_APP_SECRET ,
"scope" : [ "email" , "public_profile" ]
}
}
}
}
Get credentials:
Go to Facebook Developers
Create App → Add Facebook Login
Add redirect URI: http://localhost:3030/oauth/facebook/callback
// config/default.json
{
"authentication" : {
"oauth" : {
"redirect" : "http://localhost:3000" ,
"apple" : {
"key" : process . env . APPLE_CLIENT_ID ,
"secret" : {
"key" : process . env . APPLE_PRIVATE_KEY ,
"team" : process . env . APPLE_TEAM_ID
},
"scope" : [ "email" , "name" ]
}
}
}
}
Get credentials:
Go to Apple Developer Portal
Create App ID with Sign In with Apple
Create Service ID and configure domains
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:
Link via JWT
// 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:
Verify the existing JWT token
Get the current user
Link the OAuth account to the user (via updateEntity)
Return a new JWT
Link in Custom Strategy
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 ())
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