mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-02-01 15:34:25 +00:00
* Introduce OAuth2 authentication examples for Google, GitHub, and custom providers. * Implement OAuth2 methods for handling authentication, token refresh, and logout. * Create a flexible structure for supporting multiple OAuth2 providers. * Enhance DatabaseAuthenticator to manage OAuth2 sessions and user creation. * Add database schema setup for OAuth2 user and session management.
528 lines
17 KiB
Markdown
528 lines
17 KiB
Markdown
# OAuth2 Authentication Guide
|
|
|
|
## Overview
|
|
|
|
The security package provides OAuth2 authentication support for any OAuth2-compliant provider including Google, GitHub, Microsoft, Facebook, and custom providers.
|
|
|
|
## Features
|
|
|
|
- **Universal OAuth2 Support**: Works with any OAuth2 provider
|
|
- **Pre-configured Providers**: Google, GitHub, Microsoft, Facebook
|
|
- **Multi-Provider Support**: Use all OAuth2 providers simultaneously
|
|
- **Custom Providers**: Easy configuration for any OAuth2 service
|
|
- **Session Management**: Database-backed session storage
|
|
- **Token Refresh**: Automatic token refresh support
|
|
- **State Validation**: Built-in CSRF protection
|
|
- **User Auto-Creation**: Automatically creates users on first login
|
|
- **Unified Authentication**: OAuth2 and traditional auth share same session storage
|
|
|
|
## Quick Start
|
|
|
|
### 1. Database Setup
|
|
|
|
```sql
|
|
-- Run the schema from database_schema.sql
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(255) NOT NULL UNIQUE,
|
|
email VARCHAR(255) NOT NULL UNIQUE,
|
|
password VARCHAR(255),
|
|
user_level INTEGER DEFAULT 0,
|
|
roles VARCHAR(500),
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
last_login_at TIMESTAMP,
|
|
remote_id VARCHAR(255),
|
|
auth_provider VARCHAR(50)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
id SERIAL PRIMARY KEY,
|
|
session_token VARCHAR(500) NOT NULL UNIQUE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
expires_at TIMESTAMP NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
last_activity_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
ip_address VARCHAR(45),
|
|
user_agent TEXT,
|
|
access_token TEXT,
|
|
refresh_token TEXT,
|
|
token_type VARCHAR(50) DEFAULT 'Bearer',
|
|
auth_provider VARCHAR(50)
|
|
);
|
|
|
|
-- OAuth2 stored procedures (7 functions)
|
|
-- See database_schema.sql for full implementation
|
|
```
|
|
|
|
### 2. Google OAuth2
|
|
|
|
```go
|
|
import "github.com/bitechdev/ResolveSpec/pkg/security"
|
|
|
|
// Create authenticator
|
|
oauth2Auth := security.NewGoogleAuthenticator(
|
|
"your-google-client-id",
|
|
"your-google-client-secret",
|
|
"http://localhost:8080/auth/google/callback",
|
|
db,
|
|
)
|
|
|
|
// Login route - redirects to Google
|
|
router.HandleFunc("/auth/google/login", func(w http.ResponseWriter, r *http.Request) {
|
|
state, _ := oauth2Auth.OAuth2GenerateState()
|
|
authURL, _ := oauth2Auth.OAuth2GetAuthURL(state)
|
|
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
|
|
})
|
|
|
|
// Callback route - handles Google response
|
|
router.HandleFunc("/auth/google/callback", func(w http.ResponseWriter, r *http.Request) {
|
|
code := r.URL.Query().Get("code")
|
|
state := r.URL.Query().Get("state")
|
|
|
|
loginResp, err := oauth2Auth.OAuth2HandleCallback(r.Context(), code, state)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Set session cookie
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "session_token",
|
|
Value: loginResp.Token,
|
|
Path: "/",
|
|
MaxAge: int(loginResp.ExpiresIn),
|
|
HttpOnly: true,
|
|
Secure: true,
|
|
})
|
|
|
|
http.Redirect(w, r, "/dashboard", http.StatusTemporaryRedirect)
|
|
})
|
|
```
|
|
|
|
### 3. GitHub OAuth2
|
|
|
|
```go
|
|
oauth2Auth := security.NewGitHubAuthenticator(
|
|
"your-github-client-id",
|
|
"your-github-client-secret",
|
|
"http://localhost:8080/auth/github/callback",
|
|
db,
|
|
)
|
|
|
|
// Same routes pattern as Google
|
|
router.HandleFunc("/auth/github/login", ...)
|
|
router.HandleFunc("/auth/github/callback", ...)
|
|
```
|
|
|
|
### 4. Microsoft OAuth2
|
|
|
|
```go
|
|
oauth2Auth := security.NewMicrosoftAuthenticator(
|
|
"your-microsoft-client-id",
|
|
"your-microsoft-client-secret",
|
|
"http://localhost:8080/auth/microsoft/callback",
|
|
db,
|
|
)
|
|
```
|
|
|
|
### 5. Facebook OAuth2
|
|
|
|
```go
|
|
oauth2Auth := security.NewFacebookAuthenticator(
|
|
"your-facebook-client-id",
|
|
"your-facebook-client-secret",
|
|
"http://localhost:8080/auth/facebook/callback",
|
|
db,
|
|
)
|
|
```
|
|
|
|
## Custom OAuth2 Provider
|
|
|
|
```go
|
|
oauth2Auth := security.NewDatabaseAuthenticator(db).WithOAuth2(security.OAuth2Config{
|
|
ClientID: "your-client-id",
|
|
ClientSecret: "your-client-secret",
|
|
RedirectURL: "http://localhost:8080/auth/callback",
|
|
Scopes: []string{"openid", "profile", "email"},
|
|
AuthURL: "https://your-provider.com/oauth/authorize",
|
|
TokenURL: "https://your-provider.com/oauth/token",
|
|
UserInfoURL: "https://your-provider.com/oauth/userinfo",
|
|
DB: db,
|
|
ProviderName: "custom",
|
|
|
|
// Optional: Custom user info parser
|
|
UserInfoParser: func(userInfo map[string]any) (*security.UserContext, error) {
|
|
return &security.UserContext{
|
|
UserName: userInfo["username"].(string),
|
|
Email: userInfo["email"].(string),
|
|
RemoteID: userInfo["id"].(string),
|
|
UserLevel: 1,
|
|
Roles: []string{"user"},
|
|
Claims: userInfo,
|
|
}, nil
|
|
},
|
|
})
|
|
```
|
|
|
|
## Protected Routes
|
|
|
|
```go
|
|
// Create security provider
|
|
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
|
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
|
provider, _ := security.NewCompositeSecurityProvider(oauth2Auth, colSec, rowSec)
|
|
securityList, _ := security.NewSecurityList(provider)
|
|
|
|
// Apply middleware to protected routes
|
|
protectedRouter := router.PathPrefix("/api").Subrouter()
|
|
protectedRouter.Use(security.NewAuthMiddleware(securityList))
|
|
protectedRouter.Use(security.SetSecurityMiddleware(securityList))
|
|
|
|
protectedRouter.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
|
|
userCtx, _ := security.GetUserContext(r.Context())
|
|
json.NewEncoder(w).Encode(userCtx)
|
|
})
|
|
```
|
|
|
|
## Token Refresh
|
|
|
|
OAuth2 access tokens expire after a period of time. Use the refresh token to obtain a new access token without requiring the user to log in again.
|
|
|
|
```go
|
|
router.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
RefreshToken string `json:"refresh_token"`
|
|
Provider string `json:"provider"` // "google", "github", etc.
|
|
}
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
// Default to google if not specified
|
|
if req.Provider == "" {
|
|
req.Provider = "google"
|
|
}
|
|
|
|
// Use OAuth2-specific refresh method
|
|
loginResp, err := oauth2Auth.OAuth2RefreshToken(r.Context(), req.RefreshToken, req.Provider)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Set new session cookie
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "session_token",
|
|
Value: loginResp.Token,
|
|
Path: "/",
|
|
MaxAge: int(loginResp.ExpiresIn),
|
|
HttpOnly: true,
|
|
Secure: true,
|
|
})
|
|
|
|
json.NewEncoder(w).Encode(loginResp)
|
|
})
|
|
```
|
|
|
|
**Important Notes:**
|
|
- The refresh token is returned in the `LoginResponse.RefreshToken` field after successful OAuth2 callback
|
|
- Store the refresh token securely on the client side
|
|
- Each provider must be configured with the appropriate scopes to receive a refresh token (e.g., `access_type=offline` for Google)
|
|
- The `OAuth2RefreshToken` method requires the provider name to identify which OAuth2 provider to use for refreshing
|
|
|
|
## Logout
|
|
|
|
```go
|
|
router.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) {
|
|
userCtx, _ := security.GetUserContext(r.Context())
|
|
|
|
oauth2Auth.Logout(r.Context(), security.LogoutRequest{
|
|
Token: userCtx.SessionID,
|
|
UserID: userCtx.UserID,
|
|
})
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: "session_token",
|
|
Value: "",
|
|
MaxAge: -1,
|
|
})
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
```
|
|
|
|
## Multi-Provider Setup
|
|
|
|
```go
|
|
// Single DatabaseAuthenticator with ALL OAuth2 providers
|
|
auth := security.NewDatabaseAuthenticator(db).
|
|
WithOAuth2(security.OAuth2Config{
|
|
ClientID: "google-client-id",
|
|
ClientSecret: "google-client-secret",
|
|
RedirectURL: "http://localhost:8080/auth/google/callback",
|
|
Scopes: []string{"openid", "profile", "email"},
|
|
AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
|
TokenURL: "https://oauth2.googleapis.com/token",
|
|
UserInfoURL: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
ProviderName: "google",
|
|
}).
|
|
WithOAuth2(security.OAuth2Config{
|
|
ClientID: "github-client-id",
|
|
ClientSecret: "github-client-secret",
|
|
RedirectURL: "http://localhost:8080/auth/github/callback",
|
|
Scopes: []string{"user:email"},
|
|
AuthURL: "https://github.com/login/oauth/authorize",
|
|
TokenURL: "https://github.com/login/oauth/access_token",
|
|
UserInfoURL: "https://api.github.com/user",
|
|
ProviderName: "github",
|
|
})
|
|
|
|
// Get list of configured providers
|
|
providers := auth.OAuth2GetProviders() // ["google", "github"]
|
|
|
|
// Google routes
|
|
router.HandleFunc("/auth/google/login", func(w http.ResponseWriter, r *http.Request) {
|
|
state, _ := auth.OAuth2GenerateState()
|
|
authURL, _ := auth.OAuth2GetAuthURL("google", state)
|
|
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
|
|
})
|
|
|
|
router.HandleFunc("/auth/google/callback", func(w http.ResponseWriter, r *http.Request) {
|
|
loginResp, err := auth.OAuth2HandleCallback(r.Context(), "google",
|
|
r.URL.Query().Get("code"), r.URL.Query().Get("state"))
|
|
// ... handle response
|
|
})
|
|
|
|
// GitHub routes
|
|
router.HandleFunc("/auth/github/login", func(w http.ResponseWriter, r *http.Request) {
|
|
state, _ := auth.OAuth2GenerateState()
|
|
authURL, _ := auth.OAuth2GetAuthURL("github", state)
|
|
http.Redirect(w, r, authURL, http.StatusTemporaryRedirect)
|
|
})
|
|
|
|
router.HandleFunc("/auth/github/callback", func(w http.ResponseWriter, r *http.Request) {
|
|
loginResp, err := auth.OAuth2HandleCallback(r.Context(), "github",
|
|
r.URL.Query().Get("code"), r.URL.Query().Get("state"))
|
|
// ... handle response
|
|
})
|
|
|
|
// Use same authenticator for protected routes - works for ALL providers
|
|
provider, _ := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
securityList, _ := security.NewSecurityList(provider)
|
|
```
|
|
|
|
## Configuration Options
|
|
|
|
### OAuth2Config Fields
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| ClientID | string | OAuth2 client ID from provider |
|
|
| ClientSecret | string | OAuth2 client secret |
|
|
| RedirectURL | string | Callback URL registered with provider |
|
|
| Scopes | []string | OAuth2 scopes to request |
|
|
| AuthURL | string | Provider's authorization endpoint |
|
|
| TokenURL | string | Provider's token endpoint |
|
|
| UserInfoURL | string | Provider's user info endpoint |
|
|
| DB | *sql.DB | Database connection for sessions |
|
|
| UserInfoParser | func | Custom parser for user info (optional) |
|
|
| StateValidator | func | Custom state validator (optional) |
|
|
| ProviderName | string | Provider name for logging (optional) |
|
|
|
|
## User Info Parsing
|
|
|
|
The default parser extracts these standard fields:
|
|
- `sub` → RemoteID
|
|
- `email` → Email, UserName
|
|
- `name` → UserName
|
|
- `login` → UserName (GitHub)
|
|
|
|
Custom parser example:
|
|
|
|
```go
|
|
UserInfoParser: func(userInfo map[string]any) (*security.UserContext, error) {
|
|
// Extract custom fields
|
|
ctx := &security.UserContext{
|
|
UserName: userInfo["preferred_username"].(string),
|
|
Email: userInfo["email"].(string),
|
|
RemoteID: userInfo["sub"].(string),
|
|
UserLevel: 1,
|
|
Roles: []string{"user"},
|
|
Claims: userInfo, // Store all claims
|
|
}
|
|
|
|
// Add custom roles based on provider data
|
|
if groups, ok := userInfo["groups"].([]interface{}); ok {
|
|
for _, g := range groups {
|
|
ctx.Roles = append(ctx.Roles, g.(string))
|
|
}
|
|
}
|
|
|
|
return ctx, nil
|
|
}
|
|
```
|
|
|
|
## Security Best Practices
|
|
|
|
1. **Always use HTTPS in production**
|
|
```go
|
|
http.SetCookie(w, &http.Cookie{
|
|
Secure: true, // Only send over HTTPS
|
|
HttpOnly: true, // Prevent XSS access
|
|
SameSite: http.SameSiteLaxMode, // CSRF protection
|
|
})
|
|
```
|
|
|
|
2. **Store secrets securely**
|
|
```go
|
|
clientID := os.Getenv("GOOGLE_CLIENT_ID")
|
|
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
|
```
|
|
|
|
3. **Validate redirect URLs**
|
|
- Only register trusted redirect URLs with OAuth2 providers
|
|
- Never accept redirect URL from request parameters
|
|
|
|
5. **Session expiration**
|
|
- OAuth2 sessions automatically expire based on token expiry
|
|
- Clean up expired sessions periodically:
|
|
```sql
|
|
DELETE FROM user_sessions WHERE expires_at < NOW();
|
|
```
|
|
|
|
4. **State parameter**
|
|
- Automatically generated with cryptographic randomness
|
|
- One-time use and expires after 10 minutes
|
|
- Prevents CSRF attacks
|
|
|
|
## Implementation Details
|
|
|
|
All database operations use stored procedures for consistency and security:
|
|
- `resolvespec_oauth_getorcreateuser` - Find or create OAuth2 user
|
|
- `resolvespec_oauth_createsession` - Create OAuth2 session
|
|
- `resolvespec_oauth_getsession` - Validate and retrieve session
|
|
- `resolvespec_oauth_deletesession` - Logout/delete session
|
|
- `resolvespec_oauth_getrefreshtoken` - Get session by refresh token
|
|
- `resolvespec_oauth_updaterefreshtoken` - Update tokens after refresh
|
|
- `resolvespec_oauth_getuser` - Get user data by ID
|
|
|
|
## Provider Setup Guides
|
|
|
|
### Google
|
|
|
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
2. Create a new project or select existing
|
|
3. Enable Google+ API
|
|
4. Create OAuth 2.0 credentials
|
|
5. Add authorized redirect URI: `http://localhost:8080/auth/google/callback`
|
|
6. Copy Client ID and Client Secret
|
|
|
|
### GitHub
|
|
|
|
1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
|
|
2. Click "New OAuth App"
|
|
3. Set Homepage URL: `http://localhost:8080`
|
|
4. Set Authorization callback URL: `http://localhost:8080/auth/github/callback`
|
|
5. Copy Client ID and Client Secret
|
|
|
|
### Microsoft
|
|
|
|
1. Go to [Azure Portal](https://portal.azure.com/)
|
|
2. Register new application in Azure AD
|
|
3. Add redirect URI: `http://localhost:8080/auth/microsoft/callback`
|
|
4. Create client secret
|
|
5. Copy Application (client) ID and secret value
|
|
|
|
### Facebook
|
|
|
|
1. Go to [Facebook Developers](https://developers.facebook.com/)
|
|
2. Create new app
|
|
3. Add Facebook Login product
|
|
4. Set Valid OAuth Redirect URIs: `http://localhost:8080/auth/facebook/callback`
|
|
5. Copy App ID and App Secret
|
|
|
|
## Troubleshooting
|
|
|
|
### "redirect_uri_mismatch" error
|
|
- Ensure the redirect URL in code matches exactly with provider configuration
|
|
- Include protocol (http/https), domain, port, and path
|
|
|
|
### "invalid_client" error
|
|
- Verify Client ID and Client Secret are correct
|
|
- Check if credentials are for the correct environment (dev/prod)
|
|
|
|
### "invalid_grant" error during token exchange
|
|
- State parameter validation failed
|
|
- Token might have expired
|
|
- Check server time synchronization
|
|
|
|
### User not created after successful OAuth2 login
|
|
- Check database constraints (username/email unique)
|
|
- Verify UserInfoParser is extracting required fields
|
|
- Check database logs for constraint violations
|
|
|
|
## Testing
|
|
|
|
```go
|
|
func TestOAuth2Flow(t *testing.T) {
|
|
// Mock database
|
|
db, mock, _ := sqlmock.New()
|
|
|
|
oauth2Auth := security.NewGoogleAuthenticator(
|
|
"test-client-id",
|
|
"test-client-secret",
|
|
"http://localhost/callback",
|
|
db,
|
|
)
|
|
|
|
// Test state generation
|
|
state, err := oauth2Auth.GenerateState()
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, state)
|
|
|
|
// Test auth URL generation
|
|
authURL := oauth2Auth.GetAuthURL(state)
|
|
assert.Contains(t, authURL, "accounts.google.com")
|
|
assert.Contains(t, authURL, state)
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### DatabaseAuthenticator with OAuth2
|
|
|
|
| Method | Description |
|
|
|--------|-------------|
|
|
| WithOAuth2(cfg) | Adds OAuth2 provider (can be called multiple times, returns *DatabaseAuthenticator) |
|
|
| OAuth2GetAuthURL(provider, state) | Returns OAuth2 authorization URL for specified provider |
|
|
| OAuth2GenerateState() | Generates random state for CSRF protection |
|
|
| OAuth2HandleCallback(ctx, provider, code, state) | Exchanges code for token and creates session |
|
|
| OAuth2RefreshToken(ctx, refreshToken, provider) | Refreshes expired access token using refresh token |
|
|
| OAuth2GetProviders() | Returns list of configured OAuth2 provider names |
|
|
| Login(ctx, req) | Standard username/password login |
|
|
| Logout(ctx, req) | Invalidates session (works for both OAuth2 and regular sessions) |
|
|
| Authenticate(r) | Validates session token from request (works for both OAuth2 and regular sessions) |
|
|
|
|
### Pre-configured Constructors
|
|
|
|
- `NewGoogleAuthenticator(clientID, secret, redirectURL, db)` - Single provider
|
|
- `NewGitHubAuthenticator(clientID, secret, redirectURL, db)` - Single provider
|
|
- `NewMicrosoftAuthenticator(clientID, secret, redirectURL, db)` - Single provider
|
|
- `NewFacebookAuthenticator(clientID, secret, redirectURL, db)` - Single provider
|
|
- `NewMultiProviderAuthenticator(db, configs)` - Multiple providers at once
|
|
|
|
All return `*DatabaseAuthenticator` with OAuth2 pre-configured.
|
|
|
|
For multiple providers, use `WithOAuth2()` multiple times or `NewMultiProviderAuthenticator()`.
|
|
|
|
## Examples
|
|
|
|
Complete working examples available in `oauth2_examples.go`:
|
|
- Basic Google OAuth2
|
|
- GitHub OAuth2
|
|
- Custom provider
|
|
- Multi-provider setup
|
|
- Token refresh
|
|
- Logout flow
|
|
- Complete integration with security middleware
|