npm package: @jager-ai/holy-oauth
Description: OAuth 2.0 authentication system extracted from Holy Habit project with Google OAuth integration
Author: MVP Factory
Latest version: 1.0.0

# @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

```bash
npm install @mvp-factory/holy-oauth
```

## Quick Start

### 1. Environment Configuration

Create a `.env` file with your Google OAuth credentials:

```env
# 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=true
```

### 2. Express.js Integration

```typescript
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:

```typescript
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

1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select existing one
3. Enable Google+ API
4. Create OAuth 2.0 credentials
5. Add your callback URL: `http://localhost:3000/api/auth/callback`

## API Reference

### OAuthConfig

```typescript
// 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

```typescript
// 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

```typescript
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

```typescript
// 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

```typescript
// 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:

```typescript
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

```bash
# Run tests
npm test

# Run with coverage
npm run test:coverage
```

## Contributing

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## License

MIT © MVP Factory

## Support

- 📧 Email: support@mvp-factory.dev
- 🐛 Issues: [GitHub Issues](https://github.com/mvp-factory/modules/issues)
- 📖 Documentation: [Full Documentation](https://docs.mvp-factory.dev/modules/holy-oauth)

---

**Extracted from Holy Habit project** - Battle-tested OAuth implementation used in production.