@mvp-factory/holy-oauth
v1.0.0
Published
OAuth 2.0 authentication system extracted from Holy Habit project with Google OAuth integration
Maintainers
Readme
@mvp-factory/holy-oauth
OAuth 2.0 authentication system extracted from Holy Habit project with Google OAuth integration.
Features
- 🔐 Google OAuth 2.0 Integration - Complete OAuth flow implementation
- 🛡️ CSRF Protection - State parameter validation
- 🔄 Token Refresh - Automatic token refresh handling
- 📱 Session Management - Secure session handling
- 🔧 Environment Configuration - Easy setup with environment variables
- 📦 TypeScript Support - Full TypeScript definitions included
Installation
npm install @mvp-factory/holy-oauthQuick Start
1. Environment Configuration
Create a .env file with your Google OAuth credentials:
# Google OAuth Configuration
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Application URLs
BASE_URL=http://localhost:3000
OAUTH_CALLBACK_PATH=/api/auth/callback
# Redirect URLs
OAUTH_SUCCESS_URL=/dashboard
OAUTH_ERROR_URL=/login?error=oauth_failed
OAUTH_LINK_SUCCESS_URL=/profile?linked=true2. Express.js Integration
import express from 'express';
import session from 'express-session';
import { OAuthFactory, OAuthConfig, OAuthHandler, StateManager, UserManager } from '@mvp-factory/holy-oauth';
const app = express();
// Session middleware
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: false,
cookie: { secure: false } // Set to true in production with HTTPS
}));
// OAuth login route
app.get('/api/auth/login', async (req, res) => {
try {
const provider = 'google';
// Load configuration
const config = OAuthConfig.load(provider);
// Create OAuth provider
const oauthProvider = OAuthFactory.create(provider, config);
// Create managers (implement these based on your database)
const stateManager = new StateManager(/* your database connection */);
const userManager = new UserManager(/* your database connection */);
// Create handler
const handler = new OAuthHandler(provider, oauthProvider, stateManager, userManager);
// Initiate login
const result = await handler.initiateLogin({
returnUrl: req.query.return as string || '/'
});
// Store state in session
req.session.oauth2state = result.state;
// Redirect to OAuth provider
res.redirect(result.url);
} catch (error) {
console.error('OAuth login error:', error);
res.redirect('/login?error=oauth_setup');
}
});
// OAuth callback route
app.get('/api/auth/callback', async (req, res) => {
try {
const { code, state, error } = req.query;
if (error) {
return res.redirect(`/login?error=${error}`);
}
// Validate state
if (!state || req.session.oauth2state !== state) {
return res.redirect('/login?error=invalid_state');
}
// Clear state from session
delete req.session.oauth2state;
// Process callback
const provider = 'google';
const config = OAuthConfig.load(provider);
const oauthProvider = OAuthFactory.create(provider, config);
const stateManager = new StateManager(/* your database connection */);
const userManager = new UserManager(/* your database connection */);
const handler = new OAuthHandler(provider, oauthProvider, stateManager, userManager);
const result = await handler.handleCallback(code as string, state as string);
if (result.success && result.user) {
// Store user in session
req.session.userId = result.user.id;
req.session.userEmail = result.user.email;
req.session.userName = result.user.name;
req.session.loggedIn = true;
// Redirect to success page
res.redirect('/dashboard');
} else {
res.redirect('/login?error=oauth_failed');
}
} catch (error) {
console.error('OAuth callback error:', error);
res.redirect('/login?error=oauth_callback');
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});3. Database Integration
Implement the required managers for your database:
import { StateManager, UserManager } from '@mvp-factory/holy-oauth';
// Example with SQLite
class MySQLiteStateManager extends StateManager {
constructor(private db: Database) {
super();
}
async store(state: string, data: any): Promise<void> {
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
await this.db.run(
'INSERT INTO oauth_states (state, data, expires_at) VALUES (?, ?, ?)',
[state, JSON.stringify(data), expiresAt.toISOString()]
);
}
async retrieve(state: string): Promise<any> {
const row = await this.db.get(
'SELECT data FROM oauth_states WHERE state = ? AND expires_at > datetime("now")',
[state]
);
if (row) {
// Clean up used state
await this.db.run('DELETE FROM oauth_states WHERE state = ?', [state]);
return JSON.parse(row.data);
}
return null;
}
}Configuration Options
Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| GOOGLE_CLIENT_ID | Yes | - | Google OAuth Client ID |
| GOOGLE_CLIENT_SECRET | Yes | - | Google OAuth Client Secret |
| BASE_URL | No | http://localhost:3000 | Your application base URL |
| OAUTH_CALLBACK_PATH | No | /api/auth/callback | OAuth callback path |
| OAUTH_SUCCESS_URL | No | / | Redirect URL after successful login |
| OAUTH_ERROR_URL | No | /login?error=oauth_failed | Redirect URL on error |
| OAUTH_LINK_SUCCESS_URL | No | /profile?linked=true | Redirect URL after account linking |
Google OAuth Setup
- Go to Google Cloud Console
- Create a new project or select existing one
- Enable Google+ API
- Create OAuth 2.0 credentials
- Add your callback URL:
http://localhost:3000/api/auth/callback
API Reference
OAuthConfig
// Load configuration for a provider
const config = OAuthConfig.load('google');
// Set custom configuration
OAuthConfig.set('google', {
provider: 'google',
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
redirectUri: 'http://localhost:3000/api/auth/callback',
scopes: ['openid', 'email', 'profile'],
successUrl: '/dashboard',
errorUrl: '/login?error=oauth_failed',
linkSuccessUrl: '/profile?linked=true',
options: {
state: true,
pkce: false,
prompt: 'consent',
accessType: 'offline'
}
});OAuthFactory
// Create OAuth provider
const provider = OAuthFactory.create('google', config);
// Check if provider is supported
const isSupported = OAuthFactory.isSupported('google'); // true
// Get supported providers
const providers = OAuthFactory.getSupportedProviders(); // ['google']OAuthHandler
const handler = new OAuthHandler(provider, oauthProvider, stateManager, userManager);
// Initiate login
const result = await handler.initiateLogin({
returnUrl: '/dashboard',
userId: 'existing-user-id', // For account linking
action: 'link' // 'login' or 'link'
});
// Handle callback
const result = await handler.handleCallback(code, state);Examples
Account Linking
// Link additional OAuth provider to existing user
app.get('/api/auth/link/:provider', requireAuth, async (req, res) => {
const { provider } = req.params;
const userId = req.session.userId;
const config = OAuthConfig.load(provider);
const oauthProvider = OAuthFactory.create(provider, config);
const handler = new OAuthHandler(provider, oauthProvider, stateManager, userManager);
const result = await handler.initiateLogin({
userId: userId,
action: 'link',
returnUrl: '/profile'
});
req.session.oauth2state = result.state;
res.redirect(result.url);
});Token Refresh
// Refresh expired tokens
const googleProvider = OAuthFactory.create('google', config);
const newTokens = await googleProvider.refreshToken(user.refreshToken);
// Update user tokens in database
await userManager.updateTokens(user.id, newTokens);Error Handling
The library throws descriptive errors that you can catch and handle:
try {
const result = await handler.handleCallback(code, state);
} catch (error) {
if (error.message.includes('Invalid state')) {
// Handle CSRF attack attempt
console.warn('Possible CSRF attack detected');
res.redirect('/login?error=security');
} else if (error.message.includes('Failed to exchange code')) {
// Handle OAuth provider errors
console.error('OAuth provider error:', error);
res.redirect('/login?error=provider');
} else {
// Handle other errors
console.error('OAuth error:', error);
res.redirect('/login?error=unknown');
}
}Testing
# Run tests
npm test
# Run with coverage
npm run test:coverageContributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT © MVP Factory
Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 📖 Documentation: Full Documentation
Extracted from Holy Habit project - Battle-tested OAuth implementation used in production.
