Files
ResolveSpec/pkg/security/OAUTH2_REFRESH_QUICK_REFERENCE.md
Hein e11e6a8bf7 feat(security): Add OAuth2 authentication examples and methods
* 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.
2026-01-31 22:35:40 +02:00

7.6 KiB

OAuth2 Refresh Token - Quick Reference

Quick Setup (3 Steps)

1. Initialize Authenticator

auth := security.NewGoogleAuthenticator(
    "client-id",
    "client-secret", 
    "http://localhost:8080/auth/google/callback",
    db,
)

2. OAuth2 Login Flow

// Login - Redirect to Google
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)
})

// Callback - Store tokens
router.HandleFunc("/auth/google/callback", func(w http.ResponseWriter, r *http.Request) {
    loginResp, _ := auth.OAuth2HandleCallback(
        r.Context(),
        "google",
        r.URL.Query().Get("code"),
        r.URL.Query().Get("state"),
    )
    
    // Save refresh_token on client
    // loginResp.RefreshToken - Store this securely!
    // loginResp.Token - Session token for API calls
})

3. Refresh Endpoint

router.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
    var req struct {
        RefreshToken string `json:"refresh_token"`
    }
    json.NewDecoder(r.Body).Decode(&req)
    
    // Refresh token
    loginResp, err := auth.OAuth2RefreshToken(r.Context(), req.RefreshToken, "google")
    if err != nil {
        http.Error(w, err.Error(), 401)
        return
    }
    
    json.NewEncoder(w).Encode(loginResp)
})

Multi-Provider Example

// Configure multiple providers
auth := security.NewDatabaseAuthenticator(db).
    WithOAuth2(security.OAuth2Config{
        ProviderName: "google",
        ClientID:     "google-client-id",
        ClientSecret: "google-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",
    }).
    WithOAuth2(security.OAuth2Config{
        ProviderName: "github",
        ClientID:     "github-client-id",
        ClientSecret: "github-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",
    })

// Refresh with provider selection
router.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
    var req struct {
        RefreshToken string `json:"refresh_token"`
        Provider     string `json:"provider"` // "google" or "github"
    }
    json.NewDecoder(r.Body).Decode(&req)
    
    loginResp, err := auth.OAuth2RefreshToken(r.Context(), req.RefreshToken, req.Provider)
    if err != nil {
        http.Error(w, err.Error(), 401)
        return
    }
    
    json.NewEncoder(w).Encode(loginResp)
})

Client-Side JavaScript

// Automatic token refresh on 401
async function apiCall(url) {
    let response = await fetch(url, {
        headers: {
            'Authorization': 'Bearer ' + localStorage.getItem('access_token')
        }
    });
    
    // Token expired - refresh it
    if (response.status === 401) {
        await refreshToken();
        
        // Retry request with new token
        response = await fetch(url, {
            headers: {
                'Authorization': 'Bearer ' + localStorage.getItem('access_token')
            }
        });
    }
    
    return response.json();
}

async function refreshToken() {
    const response = await fetch('/auth/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            refresh_token: localStorage.getItem('refresh_token'),
            provider: localStorage.getItem('provider')
        })
    });
    
    if (response.ok) {
        const data = await response.json();
        localStorage.setItem('access_token', data.token);
        localStorage.setItem('refresh_token', data.refresh_token);
    } else {
        // Refresh failed - redirect to login
        window.location.href = '/login';
    }
}

API Methods

Method Parameters Returns
OAuth2RefreshToken ctx, refreshToken, provider *LoginResponse, error
OAuth2HandleCallback ctx, provider, code, state *LoginResponse, error
OAuth2GetAuthURL provider, state string, error
OAuth2GenerateState none string, error
OAuth2GetProviders none []string

LoginResponse Structure

type LoginResponse struct {
    Token        string       // New session token for API calls
    RefreshToken string       // Refresh token (store securely)
    User         *UserContext // User information
    ExpiresIn    int64        // Seconds until token expires
}

Database Stored Procedures

  • resolvespec_oauth_getrefreshtoken(refresh_token) - Get session by refresh token
  • resolvespec_oauth_updaterefreshtoken(update_data) - Update tokens after refresh
  • resolvespec_oauth_getuser(user_id) - Get user data

All procedures return: {p_success bool, p_error text, p_data jsonb}


Common Errors

Error Cause Solution
invalid or expired refresh token Token revoked/expired Re-authenticate user
OAuth2 provider 'xxx' not found Provider not configured Add with WithOAuth2()
failed to refresh token with provider Provider rejected request Check credentials, re-auth user

Security Checklist

  • Use HTTPS for all OAuth2 endpoints
  • Store refresh tokens securely (HttpOnly cookies or encrypted storage)
  • Set cookie flags: HttpOnly, Secure, SameSite=Strict
  • Implement rate limiting on refresh endpoint
  • Log refresh attempts for audit
  • Rotate tokens on refresh
  • Revoke old sessions after successful refresh

Testing

# 1. Login and get refresh token
curl http://localhost:8080/auth/google/login
# Follow OAuth2 flow, get refresh_token from callback response

# 2. Refresh token
curl -X POST http://localhost:8080/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token":"ya29.xxx","provider":"google"}'

# 3. Use new token
curl http://localhost:8080/api/protected \
  -H "Authorization: Bearer sess_abc123..."

Pre-configured Providers

// Google
auth := security.NewGoogleAuthenticator(clientID, secret, redirectURL, db)

// GitHub  
auth := security.NewGitHubAuthenticator(clientID, secret, redirectURL, db)

// Microsoft
auth := security.NewMicrosoftAuthenticator(clientID, secret, redirectURL, db)

// Facebook
auth := security.NewFacebookAuthenticator(clientID, secret, redirectURL, db)

// All providers at once
auth := security.NewMultiProviderAuthenticator(db, map[string]security.OAuth2Config{
    "google": {...},
    "github": {...},
})

Provider-Specific Notes

Google

  • Add access_type=offline to get refresh token
  • Add prompt=consent to force consent screen
authURL += "&access_type=offline&prompt=consent"

GitHub

  • Refresh tokens not always provided
  • May need to request offline_access scope

Microsoft

  • Use offline_access scope for refresh token

Facebook

  • Tokens expire after 60 days by default
  • Check app settings for token expiration policy

Complete Example

See /pkg/security/oauth2_examples.go line 250 for full working example.

For detailed documentation see /pkg/security/OAUTH2_REFRESH_TOKEN_IMPLEMENTATION.md.