oauth

package
v1.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2026 License: MIT Imports: 42 Imported by: 0

Documentation

Overview

Package oauth provides database migration management for the OAuth plugin.

This file handles parsing and loading migration files from the embedded filesystem, supporting multiple SQL dialects (PostgreSQL, MySQL, SQLite).

Migration Versioning:

  • Version 001+: Migrations from migrations/<dialect>/<version>_<description>.<up|down>.sql

File Naming Convention:

  • Up migrations: 001_initial.up.sql
  • Down migrations: 001_initial.down.sql

Package oauth provides OAuth 2.0 / OpenID Connect authentication for Aegis.

This plugin enables "Login with Google", "Login with GitHub", and other OAuth-based authentication flows. It uses the Goth library as the provider implementation while maintaining abstraction for potential alternatives.

OAuth Flow:

  1. User clicks "Login with Google" → GET /auth/oauth/google
  2. Plugin generates CSRF state token and stores it in signed cookie
  3. Plugin redirects to Google's authorization page
  4. User approves → Google redirects to /auth/oauth/google/callback?code=xxx&state=xxx
  5. Plugin validates state token (CSRF protection)
  6. Plugin exchanges authorization code for access token
  7. Plugin fetches user profile from Google
  8. Plugin creates/links Aegis user account and session
  9. User is authenticated with session cookie

Supported Providers (via Goth):

  • google: Google OAuth 2.0
  • github: GitHub OAuth 2.0
  • line: LINE OAuth 2.0 (Japan, Taiwan, Thailand)
  • microsoft: Microsoft Azure AD / Office 365
  • apple: Apple Sign In
  • discord, slack, gitlab, bitbucket, twitter, linkedin, spotify, twitch, amazon
  • generic: Custom OAuth 2.0 / OIDC providers (requires manual configuration)

Multi-Provider Setup: Configure multiple providers to offer users a choice:

cfg := &oauth.Config{
    CallbackURL: "https://example.com/auth",
    Providers: []oauth.ProviderConfig{
        {ProviderID: "google", ProviderType: "google", ClientID: "...", ClientSecret: "..."},
        {ProviderID: "github", ProviderType: "github", ClientID: "...", ClientSecret: "..."},
    },
}
oauthPlugin := oauth.New(cfg, nil)

Security Features:

  • CSRF Protection: State tokens with HMAC signing prevent cross-site request forgery
  • Secure Cookies: HTTPOnly, Secure, SameSite settings via CookieManager
  • State Expiration: OAuth states expire after 15 minutes
  • Token Storage: Access/refresh tokens stored in database (not cookies)

Account Linking: Users can link multiple OAuth providers to a single Aegis account:

  • Same email: Automatically linked on first sign-in
  • Different emails: Manual linking via LinkAccount API
  • Multiple providers: One user can have Google + GitHub + Apple linked

Database Schema:

  • oauth_connections table: Stores provider links (user_id, provider, provider_user_id, tokens)
  • Foreign key to auth.users: Ensures referential integrity

Index

Constants

View Source
const (
	// SchemaLinkAccountRequest is the OpenAPI schema name for account linking.
	// Request Body:
	//   {
	//     "provider": "google"
	//   }
	SchemaLinkAccountRequest = "LinkAccountRequest"
)

Schema names for OpenAPI specification generation.

These constants define the OpenAPI schema names for OAuth request types. They are used in route metadata to generate accurate API documentation.

View Source
const SecretPurposeOAuthState = "aegis:oauth-state"

SecretPurposeOAuthState is the purpose string for deriving OAuth state secrets. Aegis's secret derivation system uses this to generate a unique secret for signing OAuth state cookies, // SecretPurposeOAuthState is the purpose for OAuth state tokens #nosec G101

View Source
const StateCookieName = "_aegis_oauth_state"

StateCookieName is the cookie name used for OAuth state storage. This cookie is separate from the session cookie and is only used during the OAuth flow (created on BeginAuth, validated on callback, then deleted).

Variables

This section is empty.

Functions

func Apple

func Apple(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Apple creates an Apple Sign In provider configuration.

Apple requires special setup:

  • Client Secret: Not a simple string, but a JWT signed with your private key
  • Team ID: Your Apple Developer Team ID

Default Scopes: ["name", "email"] Discovery: https://appleid.apple.com/.well-known/openid-configuration

Note: Apple's "client secret" is actually a JWT that you must generate and sign with your private key. See Apple's documentation for details.

Parameters:

  • clientID: Apple Service ID
  • clientSecret: JWT signed with your private key
  • teamID: Apple Developer Team ID (currently unused)
  • opts: Optional customization

Returns:

  • ProviderConfig: Apple provider configuration

func Bitbucket

func Bitbucket(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Bitbucket creates a Bitbucket OAuth provider configuration.

Default Scopes: ["account", "email"]

func CreateGothProvider

func CreateGothProvider(cfg oauthtypes.ProviderConfig, callbackURL string) (goth.Provider, error)

CreateGothProvider creates a goth.Provider from ProviderConfig.

This factory function instantiates the correct Goth provider based on the ProviderType, applying scopes and callback URL. It handles provider-specific initialization quirks (e.g., Apple's JWT client secret).

Supported Providers:

  • google, github, line, microsoft, apple
  • discord, slack, gitlab, bitbucket, twitter
  • linkedin, spotify, twitch, amazon
  • generic (manual endpoint configuration)

Parameters:

  • cfg: Provider configuration with type, credentials, and options
  • callbackURL: OAuth callback URL for this provider

Returns:

  • goth.Provider: Initialized Goth provider
  • error: Unsupported provider type or missing configuration

Example:

cfg := oauth.ProviderConfig{
    ProviderType: "google",
    ClientID:     "...",
    ClientSecret: "...",
    Scopes:       []string{"email", "profile"},
}
provider, _ := oauth.CreateGothProvider(cfg, "https://example.com/auth/oauth/google/callback")

func Discord

func Discord(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Discord creates a Discord OAuth provider configuration.

Default Scopes: ["identify", "email"]

Parameters:

  • clientID: Discord Application client ID
  • clientSecret: Discord Application client secret
  • opts: Optional customization

func Generic

func Generic(providerID, clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Generic creates a custom OAuth provider configuration.

Use this for OAuth providers not included in the pre-configured helpers. You must provide either:

  • Discovery URL (OIDC providers): Automatic endpoint discovery
  • Manual endpoints: AuthURL, TokenURL, UserInfoURL

Parameters:

  • providerID: Unique provider identifier (used in URLs)
  • clientID: OAuth client ID from provider
  • clientSecret: OAuth client secret from provider
  • opts: Required configuration (scopes, endpoints, etc.)

Returns:

  • ProviderConfig: Generic provider configuration

Example (OIDC Discovery):

custom := oauth.Generic("keycloak", clientID, clientSecret,
    oauth.WithDiscoveryURL("https://auth.example.com/realms/master/.well-known/openid-configuration"),
    oauth.WithScopes("openid", "email", "profile"),
)

Example (Manual Endpoints):

custom := oauth.Generic("custom", clientID, clientSecret,
    oauth.WithScopes("email"),
)
custom.AuthURL = "https://provider.com/oauth/authorize"
custom.TokenURL = "https://provider.com/oauth/token"
custom.UserInfoURL = "https://provider.com/oauth/userinfo"

func GetMigrations

func GetMigrations(dialect plugins.Dialect) ([]plugins.Migration, error)

GetMigrations returns all database migrations for the specified SQL dialect.

This function combines the initial schema (version 001) with any additional migrations (version 002+) to produce a complete, ordered list of migrations.

Parameters:

  • dialect: SQL dialect (DialectPostgres, DialectMySQL, DialectSQLite)

Returns:

  • []plugins.Migration: Ordered list of migrations (version ASC)
  • error: File read error or unsupported dialect

func GitHub

func GitHub(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

GitHub creates a GitHub OAuth provider configuration.

Default Scopes: ["user:email"] No discovery URL (GitHub uses fixed endpoints)

Parameters:

  • clientID: GitHub OAuth App client ID
  • clientSecret: GitHub OAuth App client secret
  • opts: Optional customization

Returns:

  • ProviderConfig: GitHub provider configuration

func GitLab

func GitLab(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

GitLab creates a GitLab OAuth provider configuration.

Default Scopes: ["read_user", "openid", "profile", "email"]

Supports both GitLab.com and self-hosted instances.

func Google

func Google(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Google creates a Google OAuth provider configuration.

Default Scopes: ["openid", "email", "profile"] Discovery: https://accounts.google.com/.well-known/openid-configuration

Parameters:

  • clientID: Google OAuth client ID (from Google Cloud Console)
  • clientSecret: Google OAuth client secret
  • opts: Optional customization (scopes, prompt, etc.)

Returns:

  • ProviderConfig: Google provider configuration

Example:

googleCfg := oauth.Google("123456.apps.googleusercontent.com", "secret",
    oauth.WithScopes("email", "profile", "calendar.readonly"),
    oauth.WithAccessType("offline"), // Request refresh token
    oauth.WithPrompt("consent"),      // Force consent to get refresh token
)

func GothUserToUser

func GothUserToUser(gothUser goth.User) *oauthtypes.User

GothUserToUser converts goth.User to Aegis's User model.

This helper function transforms Goth's OAuth user representation into Aegis's User struct, preserving OAuth tokens and provider-specific data.

Field Mapping:

  • goth.UserID → User.ID (provider's user ID)
  • goth.Email → User.Email
  • goth.Name → User.Name
  • goth.AvatarURL → User.Avatar
  • goth.AccessToken, RefreshToken, ExpiresAt preserved

Parameters:

  • gothUser: User data from Goth provider

Returns:

  • *User: Aegis user model

func LINE

func LINE(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

LINE creates a LINE OAuth provider configuration.

LINE supports multiple channels for different countries:

  • Japan: Regular LINE Login
  • Thailand, Taiwan, Indonesia: Country-specific configurations

Default Scopes: ["profile", "openid", "email"] Discovery: https://access.line.me/.well-known/openid-configuration

For multiple LINE channels:

lineJP := oauth.LINE(clientID_JP, clientSecret_JP,
    oauth.WithProviderID("line-jp"),
)
lineTW := oauth.LINE(clientID_TW, clientSecret_TW,
    oauth.WithProviderID("line-tw"),
)

Parameters:

  • clientID: LINE Channel ID
  • clientSecret: LINE Channel Secret
  • opts: Optional customization

Returns:

  • ProviderConfig: LINE provider configuration

func LinkedIn

func LinkedIn(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

LinkedIn creates a LinkedIn OAuth provider configuration.

Default Scopes: ["r_liteprofile", "r_emailaddress"]

Note: LinkedIn's API and scopes change frequently. Verify current scopes in LinkedIn's docs.

func Microsoft

func Microsoft(clientID, clientSecret, tenantID string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Microsoft creates a Microsoft/Azure AD OAuth provider configuration.

Default Scopes: ["openid", "email", "profile"] Discovery: Uses tenant-specific discovery URL

Parameters:

  • clientID: Azure AD App client ID
  • clientSecret: Azure AD App client secret
  • tenantID: Azure AD tenant ID (or "common" for multi-tenant)
  • opts: Optional customization

Returns:

  • ProviderConfig: Microsoft provider configuration

func Slack

func Slack(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Slack creates a Slack OAuth provider configuration.

Default Scopes: ["openid", "profile", "email"] Discovery: https://slack.com/.well-known/openid-configuration

func Spotify

func Spotify(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Spotify creates a Spotify OAuth provider configuration.

Default Scopes: ["user-read-email", "user-read-private"]

func Twitch

func Twitch(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Twitch creates a Twitch OAuth provider configuration.

Default Scopes: ["user:read:email"] Discovery: https://id.twitch.tv/oauth2/.well-known/openid-configuration

func Twitter

func Twitter(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig

Twitter (X) creates a Twitter/X OAuth provider configuration.

Default Scopes: ["tweet.read", "users.read"]

Note: Twitter's OAuth implementation has changed over time. This uses OAuth 2.0.

func UserToGothUser

func UserToGothUser(oauthUser *oauthtypes.User, provider string) goth.User

UserToGothUser converts Aegis User to goth.User.

This helper function transforms Aegis's User struct back into Goth's representation, useful for interacting with Goth's provider APIs.

Parameters:

  • oauthUser: Aegis OAuth user
  • provider: Provider name ("google", "github", etc.)

Returns:

  • goth.User: Goth user model

func WithAccessType

func WithAccessType(accessType string) oauthtypes.ProviderOption

WithAccessType sets the access type (e.g., "offline" for refresh tokens).

Setting access_type="offline" requests a refresh token from the provider, allowing token renewal without re-authentication. This is provider-specific:

  • Google: access_type=offline
  • Microsoft: access_type=offline (or prompt=consent)

Example:

oauth.Google(clientID, clientSecret,
    oauth.WithAccessType("offline"), // Request refresh token
    oauth.WithPrompt("consent"),     // Force consent to get refresh token
)

func WithDisableImplicitSignUp

func WithDisableImplicitSignUp() oauthtypes.ProviderOption

WithDisableImplicitSignUp disables automatic sign-up for new OAuth users.

When enabled, users must be explicitly invited or pre-created before they can sign in via OAuth. Useful for enterprise applications with controlled user provisioning.

Behavior:

  • Existing user + OAuth: Link OAuth to existing account (allowed)
  • New OAuth user: Return error "Sign-up not allowed" (blocked)

Example:

oauth.Google(clientID, clientSecret,
    oauth.WithDisableImplicitSignUp(), // Require pre-existing accounts
)

func WithDisableSignUp

func WithDisableSignUp() oauthtypes.ProviderOption

WithDisableSignUp disables sign-up entirely (only existing users can sign in).

This is stricter than WithDisableImplicitSignUp - it prevents both new user creation and OAuth linking to existing accounts.

Behavior:

  • Existing user with OAuth already linked: Sign-in allowed
  • Existing user without OAuth: Linking blocked
  • New user: Sign-up blocked

Use Case: Read-only OAuth for authentication of pre-provisioned users only.

func WithDiscoveryURL

func WithDiscoveryURL(url string) oauthtypes.ProviderOption

WithDiscoveryURL sets the OIDC discovery URL for automatic endpoint configuration.

For OpenID Connect providers, the discovery URL provides all OAuth endpoints (authorization, token, userinfo, JWKS) automatically via a JSON document.

Example:

oauth.Generic("custom", clientID, clientSecret,
    oauth.WithDiscoveryURL("https://auth.example.com/.well-known/openid-configuration"),
)

func WithOverrideUserInfo

func WithOverrideUserInfo() oauthtypes.ProviderOption

WithOverrideUserInfo enables updating user info on each sign-in.

By default, user profile data (name, email, avatar) is only saved on first sign-up. Enabling this option updates the user profile every time they sign in via OAuth, keeping data synchronized with the provider.

Use Cases:

  • Keep user names/avatars up-to-date from provider
  • Sync email changes from provider
  • Corporate directory synchronization

Caution:

  • May overwrite user-edited profile data
  • Consider allowing users to opt out of sync

func WithPKCE

func WithPKCE() oauthtypes.ProviderOption

WithPKCE enables PKCE (Proof Key for Code Exchange) for enhanced security.

PKCE protects against authorization code interception attacks, especially important for mobile apps and public clients that can't securely store secrets.

Recommended for:

  • Mobile apps (iOS, Android)
  • Single-page applications (SPAs)
  • Any public OAuth client

func WithProfileMapper

func WithProfileMapper(fn func(map[string]any) (*oauthtypes.User, error)) oauthtypes.ProviderOption

WithProfileMapper sets a custom profile mapper function.

Use this to transform provider-specific profile data into Aegis User format. Useful for extracting custom fields or handling non-standard profile structures.

Example:

mapProfile := func(profile map[string]any) (*oauth.User, error) {
    return &oauth.User{
        User: auth.User{
            ID:     profile["sub"].(string),
            Email:  profile["email"].(string),
            Name:   profile["display_name"].(string),
            Avatar: profile["picture_url"].(string),
        },
        ProviderData: profile,
    }, nil
}

oauth.Generic("custom", clientID, clientSecret,
    oauth.WithProfileMapper(mapProfile),
)

func WithPrompt

func WithPrompt(prompt string) oauthtypes.ProviderOption

WithPrompt sets the OAuth prompt parameter.

The prompt parameter controls how the provider asks for user consent:

  • "none": No UI shown (silent auth, may fail if interaction required)
  • "login": Always show login screen (even if logged in)
  • "consent": Always show consent screen
  • "select_account": Show account picker

Example:

oauth.Google(clientID, clientSecret,
    oauth.WithPrompt("select_account"), // Always show account picker
)

func WithProviderID

func WithProviderID(id string) oauthtypes.ProviderOption

WithProviderID sets a custom provider ID.

Useful for having multiple instances of the same provider with different configurations (e.g., LINE for Japan vs Taiwan, Google for different tenants).

Example:

// LINE for Japan
lineJP := oauth.LINE(clientID_JP, clientSecret_JP,
    oauth.WithProviderID("line-jp"),
)

// LINE for Taiwan
lineTW := oauth.LINE(clientID_TW, clientSecret_TW,
    oauth.WithProviderID("line-tw"),
)

func WithRedirectURI

func WithRedirectURI(uri string) oauthtypes.ProviderOption

WithRedirectURI sets a custom redirect URI for the provider.

By default, the plugin constructs redirect URIs as:

{CallbackURL}/oauth/{provider}/callback

Use this to override with a provider-specific redirect URI, for example if you've registered a different callback URL in the provider's console.

Example:

oauth.Google(clientID, clientSecret,
    oauth.WithRedirectURI("https://example.com/custom/google/callback"),
)

func WithScopes

func WithScopes(scopes ...string) oauthtypes.ProviderOption

WithScopes sets custom OAuth scopes for the provider.

Scopes determine what user data the provider will share. Each provider has different scope names and defaults.

Example:

oauth.Google(clientID, clientSecret,
    oauth.WithScopes("email", "profile", "calendar.readonly"),
)

func WithUserInfoFetcher

func WithUserInfoFetcher(fn func(*oauthtypes.Tokens) (*oauthtypes.User, error)) oauthtypes.ProviderOption

WithUserInfoFetcher sets a custom user info fetcher function.

Use this to customize how user data is retrieved from the provider, for example to call additional API endpoints or parse non-standard responses.

Example:

fetchUser := func(tokens *oauth.Tokens) (*oauth.User, error) {
    // Call custom API with access token
    resp, _ := http.Get("https://api.example.com/user?access_token=" + tokens.AccessToken)
    // Parse custom response format
    var data map[string]any
    json.NewDecoder(resp.Body).Decode(&data)
    return &oauth.User{...}, nil
}

oauth.Generic("custom", clientID, clientSecret,
    oauth.WithUserInfoFetcher(fetchUser),
)

Types

type Config

type Config struct {
	// Providers configures which OAuth providers to enable.
	// Each provider needs a client ID, client secret from the provider's developer console.
	Providers []oauthtypes.ProviderConfig

	// CallbackURL is the base URL for OAuth callbacks (e.g., "https://example.com/auth").
	// The plugin appends "/oauth/:provider/callback" to this base.
	// Example: CallbackURL="https://example.com/auth" → callback at "https://example.com/auth/oauth/google/callback"
	CallbackURL string

	// StateSecret is deprecated - Aegis now derives state secrets from master secret.
	// This field is kept for backward compatibility but is ignored.
	StateSecret []byte
}

Config holds OAuth plugin configuration.

This structure defines all OAuth providers to enable and their settings. Multiple providers can be configured to offer users authentication choices.

Example:

cfg := &oauth.Config{
    CallbackURL: "https://example.com/auth",
    Providers: []oauth.ProviderConfig{
        {
            ProviderID:   "google",
            ProviderType: "google",
            ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
            ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
            Scopes:       []string{"email", "profile"},
        },
    },
}

type GothAdapter

type GothAdapter struct {
	// contains filtered or unexported fields
}

GothAdapter adapts goth.Provider to Aegis's Provider interface.

This adapter makes Goth the default/recommended OAuth provider implementation while keeping it technically optional through abstraction. If you want to use a different OAuth library, you can implement the Provider interface without Goth.

Goth Benefits:

  • 50+ pre-configured OAuth providers (Google, GitHub, Apple, etc.)
  • Battle-tested OAuth 2.0 / OIDC implementation
  • Active maintenance and security updates
  • Provider-specific quirks handled (Apple's JWT client secret, etc.)

Abstraction Benefits:

  • Aegis core doesn't depend on Goth directly
  • Easier testing with mock providers
  • Future flexibility if Goth is discontinued

func NewGothAdapter

func NewGothAdapter(provider goth.Provider) *GothAdapter

NewGothAdapter creates a new Goth adapter wrapping a Goth provider.

This function wraps any Goth provider to work with Aegis's OAuth plugin. Most users won't call this directly - the plugin creates adapters automatically from ProviderConfig using CreateGothProvider.

Parameters:

  • provider: Goth provider instance (google.New, github.New, etc.)

Returns:

  • *GothAdapter: Adapter implementing Provider interface

Example:

googleProvider := google.New(clientID, clientSecret, callbackURL)
adapter := oauth.NewGothAdapter(googleProvider)

func (*GothAdapter) Exchange

func (g *GothAdapter) Exchange(_ string) (*oauthtypes.User, error)

Exchange exchanges authorization code for user information.

Note: This is a simplified interface. In practice, Aegis uses the full gothic.CompleteUserAuth flow which handles the complete OAuth callback processing (code exchange, token retrieval, user info fetch).

This method is currently not used - instead, the plugin uses Goth's session-based flow directly for more flexibility.

func (*GothAdapter) GetAuthURL

func (g *GothAdapter) GetAuthURL(state string) (string, error)

GetAuthURL returns the provider's authorization URL with CSRF state.

This method starts the OAuth session and returns the URL to redirect the user to for authorization (e.g., https://accounts.google.com/o/oauth2/auth).

Parameters:

  • state: CSRF state token (random string for security)

Returns:

  • string: Authorization URL to redirect user to
  • error: OAuth session creation error

func (*GothAdapter) Name

func (g *GothAdapter) Name() string

Name returns the provider identifier (e.g., "google", "github").

type Handlers

type Handlers struct {
	// contains filtered or unexported fields
}

Handlers provides HTTP endpoint handlers for OAuth authentication.

All handlers have been made private (lowercase) to encourage programmatic use of the underlying Plugin methods. This struct serves as a mounting point for the router.

func NewHandlers

func NewHandlers(plugin *Plugin) *Handlers

NewHandlers creates OAuth plugin HTTP handlers.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin provides OAuth 2.0 authentication integration for Aegis.

This plugin manages multiple OAuth providers simultaneously, allowing users to authenticate with Google, GitHub, Apple, and other services. It integrates with Aegis's user and session management to create unified accounts.

Components:

  • providerConfigs: Plugin configuration for each provider (client ID, secret, scopes)
  • gothProviders: Goth provider instances for OAuth protocol handling
  • stateStore: CSRF protection via signed cookies (state parameter)
  • store: Database persistence for OAuth connections
  • sessionService: Creates Aegis sessions after successful OAuth

Thread Safety: Plugin is safe for concurrent use after initialization (Init called).

func New

func New(cfg *Config, store oauthtypes.Store, dialect ...plugins.Dialect) *Plugin

New creates a new OAuth plugin with configured providers.

This function initializes the plugin with provider configurations and creates Goth provider instances for each configured provider. Providers that fail to initialize are logged but don't prevent other providers from working.

Provider Configuration: Each provider needs:

  • ProviderID: Unique identifier (used in URLs like /oauth/google)
  • ProviderType: Provider implementation type ("google", "github", etc.)
  • ClientID: OAuth client ID from provider's developer console
  • ClientSecret: OAuth client secret from provider's developer console

Parameters:

  • cfg: Plugin configuration with providers and callback URL
  • store: OAuth connection storage (nil = use DefaultOAuthStore)
  • dialect: Database dialect (optional, defaults to PostgreSQL)

Returns:

  • *Plugin: Initialized plugin ready for Init() call

Example:

cfg := &oauth.Config{
    CallbackURL: "https://example.com/auth",
    Providers: []oauth.ProviderConfig{
        {ProviderID: "google", ProviderType: "google", ClientID: "...", ClientSecret: "..."},
        {ProviderID: "github", ProviderType: "github", ClientID: "...", ClientSecret: "..."},
    },
}
plugin := oauth.New(cfg, nil, plugins.DialectPostgres)

func (*Plugin) BeginAuth

func (p *Plugin) BeginAuth(w http.ResponseWriter, r *http.Request, providerName string) error

BeginAuth starts the OAuth authentication flow with CSRF protection.

This method initiates the OAuth flow by:

  1. Generating a cryptographically secure CSRF state token
  2. Starting the OAuth session with the provider
  3. Obtaining the provider's authorization URL
  4. Storing state and session data in a signed cookie
  5. Redirecting the user to the provider's authorization page

OAuth Flow (Step 1-3):

  1. User → GET /auth/oauth/google
  2. Plugin → Generate state="abc123", store in cookie
  3. Plugin → Redirect to https://accounts.google.com/authorize?state=abc123&...

State Cookie: The state cookie contains:

  • CSRF state token (random 32 bytes, base64-encoded)
  • Provider name ("google", "github", etc.)
  • Marshaled OAuth session data (for completing the flow)
  • HMAC signature (prevents tampering)
  • Expiration: 15 minutes (short-lived for security)

Parameters:

  • w: HTTP response writer for setting cookies and redirecting
  • r: HTTP request (currently unused)
  • providerName: Provider identifier ("google", "github", etc.)

Returns:

  • error: Provider not found, state generation failed, or redirect failed

Security:

  • State token is cryptographically random (32 bytes from crypto/rand)
  • State cookie is HMAC-signed to prevent tampering
  • Cookie uses Secure, HTTPOnly, SameSite settings from SessionConfig

func (*Plugin) CompleteAuth

func (p *Plugin) CompleteAuth(ctx context.Context, w http.ResponseWriter, r *http.Request) (*oauthtypes.User, *auth.Session, error)

CompleteAuth completes the OAuth authentication flow after provider callback.

This method handles the OAuth callback by:

  1. Validating the CSRF state token from the callback
  2. Exchanging the authorization code for an access token
  3. Fetching the user's profile from the provider
  4. Creating or linking an Aegis user account
  5. Creating an Aegis session for the authenticated user

OAuth Flow (Step 4-9):

  1. Provider → Redirect to /auth/oauth/google/callback?code=xyz&state=abc123
  2. Plugin → Validate state=abc123 matches cookie
  3. Plugin → Exchange code=xyz for access token
  4. Plugin → Fetch user profile from provider
  5. Plugin → Create/link user account in Aegis
  6. Plugin → Create session and set cookie

User Account Matching:

  • Existing OAuth connection: Retrieve linked user
  • New OAuth connection with known email: Link to existing user
  • New OAuth connection with unknown email: Create new user
  • OAuth without email: Create user without email (uses provider name)

Parameters:

  • ctx: Request context for database operations
  • w: HTTP response writer for clearing state cookie
  • r: HTTP request with OAuth callback parameters (code, state)

Returns:

  • *User: Authenticated user with OAuth data
  • *auth.Session: New Aegis session for the user
  • error: State validation, token exchange, or user creation error

Security:

  • State validation prevents CSRF attacks
  • State cookie is cleared after validation (one-time use)
  • Access tokens stored in database (not cookies)

func (*Plugin) Dependencies

func (p *Plugin) Dependencies() []plugins.Dependency

Dependencies returns external package dependencies

func (*Plugin) Description

func (p *Plugin) Description() string

Description returns a human-readable description

func (*Plugin) GetMigrations

func (p *Plugin) GetMigrations() []plugins.Migration

GetMigrations returns the plugin migrations

func (*Plugin) GetStateStore

func (p *Plugin) GetStateStore() *StateStore

GetStateStore returns the OAuth state store

func (*Plugin) GetUserConnections

func (p *Plugin) GetUserConnections(ctx context.Context, userID string) ([]*oauthtypes.Connection, error)

GetUserConnections retrieves all OAuth provider connections for a user.

This method returns all linked OAuth providers, including access tokens, refresh tokens, and provider-specific data. Useful for displaying linked accounts in user settings or managing provider connections.

Parameters:

  • ctx: Request context
  • userID: Aegis user ID

Returns:

  • []*Connection: List of OAuth connections (may be empty)
  • error: Database query error

Example:

connections, _ := plugin.GetUserConnections(ctx, user.ID)
for _, conn := range connections {
    fmt.Printf("Linked: %s (%s)\n", conn.Provider, conn.Email)
}

func (*Plugin) Init

func (p *Plugin) Init(_ context.Context, a plugins.Aegis) error

Init initializes the OAuth plugin with Aegis services.

This method is called during Aegis startup to inject dependencies and set up the state store. It retrieves services from the Aegis interface and derives the OAuth state secret from the master secret.

Initialization Steps:

  1. Get user, session, and account services from Aegis
  2. Initialize OAuth store if not provided
  3. Derive state secret from master secret ("aegis:oauth-state" purpose)
  4. Create StateStore with derived secret for CSRF protection

Parameters:

  • ctx: Initialization context (currently unused)
  • a: Aegis interface providing services and configuration

Returns:

  • error: Initialization error (currently always nil)

func (*Plugin) LinkAccount

func (p *Plugin) LinkAccount(ctx context.Context, userID string, oauthUser *oauthtypes.User, provider string) error

LinkAccount links an OAuth provider to an existing authenticated user account.

This method allows users to add additional OAuth providers to their account. For example, a user who signed up with email/password can later link their Google account for easier login.

Use Cases:

  • Link Google to existing email/password account
  • Link multiple providers to one account (Google + GitHub + Apple)
  • Re-link provider after unlinking

Parameters:

  • ctx: Request context
  • userID: Aegis user ID to link provider to
  • oauthUser: OAuth user data from provider
  • provider: Provider name ("google", "github", etc.)

Returns:

  • error: Database error or duplicate connection error

Example:

// User already authenticated via session
user, _ := core.GetUser(r.Context())
err := plugin.LinkAccount(ctx, user.ID, oauthUser, "google")

func (*Plugin) MountRoutes

func (p *Plugin) MountRoutes(r router.Router, prefix string)

MountRoutes registers HTTP routes for the OAuth plugin

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns the plugin identifier

func (*Plugin) ProvidesAuthMethods

func (p *Plugin) ProvidesAuthMethods() []string

ProvidesAuthMethods returns authentication methods provided

func (*Plugin) RefreshConnection added in v1.5.0

func (p *Plugin) RefreshConnection(ctx context.Context, userID, provider string) (*oauthtypes.Connection, error)

RefreshConnection uses the stored refresh token

Not all providers issue refresh tokens (e.g., GitHub does not by default). The method returns an error when:

  • No connection exists for the given user + provider pair
  • The stored refresh token is empty
  • The Goth provider does not implement token refresh (goth.TokenRefresher)
  • The provider rejects the refresh request

Call this proactively when conn.ExpiresAt is approaching to avoid making API calls to the provider with a stale access token.

Parameters:

  • ctx: Request context
  • userID: Aegis user ID
  • provider: Provider name ("google", "github", etc.)

Returns:

  • *Connection: Updated connection with fresh tokens
  • error: Refresh failure or unsupported provider

func (*Plugin) RequiresTables

func (p *Plugin) RequiresTables() []string

RequiresTables returns core tables this plugin depends on RequiresTables returns the core tables this plugin reads from.

func (*Plugin) UnlinkAccount

func (p *Plugin) UnlinkAccount(ctx context.Context, userID, provider string) error

UnlinkAccount removes an OAuth provider link from a user account.

This method allows users to disconnect OAuth providers from their account. The user's Aegis account remains active, but they can no longer sign in using the unlinked provider.

Safety: This method does NOT check if the user has other authentication methods. You should verify the user has email/password or other OAuth providers before allowing unlinking to prevent account lockout.

Parameters:

  • ctx: Request context
  • userID: Aegis user ID
  • provider: Provider name to unlink ("google", "github", etc.)

Returns:

  • error: Database error or connection not found

Example:

// Unlink Google from user's account
err := plugin.UnlinkAccount(ctx, user.ID, "google")

func (*Plugin) Version

func (p *Plugin) Version() string

Version returns the plugin version

type StateData

type StateData struct {
	State       string // CSRF state token (32 random bytes, base64)
	Provider    string // OAuth provider name ("google", "github", etc.)
	SessionData string // Marshaled goth.Session (provider-specific OAuth state)
}

StateData holds the OAuth state data stored during the OAuth flow.

This data is serialized, compressed, signed, and stored in a cookie during BeginAuth, then retrieved and validated during the callback.

Cookie Format (with signature):

<hmac-signature>.<base64(state|provider|base64(gzip(sessionData)))>

Example:

"a8f3d..." (HMAC) + "." + "YWJjMTIzfGdvb2dsZXxINHNJQUFBLi4u" (payload)

type StateStore

type StateStore struct {
	// contains filtered or unexported fields
}

StateStore manages OAuth state cookies for CSRF protection during OAuth flows.

OAuth CSRF Attack Without State:

  1. Attacker initiates OAuth flow → gets callback URL with auth code
  2. Attacker tricks victim into visiting callback URL
  3. Victim's browser exchanges code → victim's account linked to attacker's provider
  4. Attacker can now access victim's account via OAuth

CSRF Protection With State:

  1. Plugin generates random state token before redirect
  2. State stored in signed cookie (attacker can't forge)
  3. Provider includes state in callback URL
  4. Plugin validates callback state matches cookie
  5. If mismatch → reject (CSRF attempt)

State Cookie Contents:

  • CSRF state token (32 random bytes, base64-encoded)
  • Provider name ("google", "github", etc.)
  • Marshaled OAuth session (for resuming flow)
  • HMAC signature (prevents tampering)

Security Features:

  • HMAC-SHA256 signing with derived secret (prevents cookie tampering)
  • Gzip compression (session data can be large)
  • Short expiration (15 minutes default)
  • HTTPOnly, Secure, SameSite settings from CookieManager

Integration: This store integrates with Aegis's core CookieManager for consistent cookie settings (domain, secure flag, SameSite policy) across all cookies.

func NewStateStore

func NewStateStore(cfg *StateStoreConfig) *StateStore

NewStateStore creates a new StateStore for managing OAuth state with CSRF protection.

The secret should be at least 32 bytes for cryptographic security. Aegis derives this from the master secret using the purpose "aegis:oauth-state".

Parameters:

  • cfg: Configuration with session settings, secret, and max age

Returns:

  • *StateStore: Initialized store ready for use

Example:

secret := aegis.DeriveSecret("aegis:oauth-state")
store := oauth.NewStateStore(&oauth.StateStoreConfig{
    SessionConfig: aegis.GetSessionConfig(),
    Secret: secret,
    MaxAge: 15 * 60, // 15 minutes
})

func (*StateStore) ClearState

func (s *StateStore) ClearState(w http.ResponseWriter)

ClearState clears the OAuth state cookie.

This method is called after successful callback validation to delete the one-time-use state cookie. It sets MaxAge=-1 to instruct the browser to delete the cookie immediately.

Parameters:

  • w: HTTP response writer for clearing cookie

func (*StateStore) GenerateState

func (s *StateStore) GenerateState() (string, error)

GenerateState generates a cryptographically secure random state string.

The state is a 32-byte random value base64-encoded for use as a CSRF token. This is the value included in the OAuth authorization URL and validated in the callback.

Returns:

  • string: Base64-encoded random state (44 characters)
  • error: Crypto random generation error (extremely rare)

func (*StateStore) GetMaxAge

func (s *StateStore) GetMaxAge() time.Duration

GetMaxAge returns the max age for state cookies

func (*StateStore) GetState

func (s *StateStore) GetState(r *http.Request) (*StateData, error)

GetState retrieves and validates the OAuth state data from the cookie.

This method is called during the OAuth callback to retrieve the stored state data for validation. It verifies the HMAC signature to ensure the cookie wasn't tampered with.

Processing:

  1. Read cookie value
  2. Base64-decode payload
  3. Verify HMAC signature (if secret is set)
  4. Parse: state|provider|compressedSession
  5. Decompress SessionData with gzip

Parameters:

  • r: HTTP request with state cookie

Returns:

  • *StateData: Decoded state data
  • error: Cookie not found, invalid signature, or decoding error

func (*StateStore) SetMaxAge

func (s *StateStore) SetMaxAge(age time.Duration)

SetMaxAge sets the max age for state cookies

func (*StateStore) StoreState

func (s *StateStore) StoreState(w http.ResponseWriter, data *StateData) error

StoreState stores OAuth state data in a signed, compressed cookie.

This method is called at the start of the OAuth flow (BeginAuth) to save the state data for validation during the callback. The cookie is HTTPOnly and Secure (if configured) to prevent JavaScript access and MITM attacks.

Processing:

  1. Compress SessionData with gzip (can be large ~1-2KB)
  2. Concatenate: state|provider|compressedSession
  3. Sign with HMAC-SHA256 if secret is set
  4. Base64-encode payload
  5. Store in cookie with configured expiration (15 minutes default)

Parameters:

  • w: HTTP response writer for setting cookie
  • data: State data to store

Returns:

  • error: Compression or encoding error

func (*StateStore) ValidateState

func (s *StateStore) ValidateState(r *http.Request, callbackState string) (*StateData, error)

ValidateState checks if the callback state matches the stored state.

This is the critical CSRF protection check. It ensures the OAuth callback originated from a legitimate authorization flow initiated by this server.

Validation:

  1. Retrieve state data from cookie
  2. Compare stored.State with callbackState from query parameter
  3. If mismatch → return error (possible CSRF attack)

Parameters:

  • r: HTTP request with state cookie
  • callbackState: State parameter from OAuth callback URL

Returns:

  • *StateData: Validated state data (if state matches)
  • error: State mismatch (CSRF) or cookie retrieval error

type StateStoreConfig

type StateStoreConfig struct {
	// SessionConfig contains cookie settings (Domain, Secure, HTTPOnly, SameSite)
	SessionConfig *core.SessionConfig
	// Secret is the key used for signing cookies (should be at least 32 bytes)
	Secret []byte
	// MaxAge overrides the default OAuth state cookie max age in seconds (default: 15 minutes)
	MaxAge int
}

StateStoreConfig holds configuration for creating a StateStore.

Example:

cfg := &oauth.StateStoreConfig{
    SessionConfig: aegis.GetSessionConfig(), // Domain, Secure, HTTPOnly, SameSite
    Secret: aegis.DeriveSecret("aegis:oauth-state"), // 32+ bytes
    MaxAge: 15 * 60, // 15 minutes
}
store := oauth.NewStateStore(cfg)

Directories

Path Synopsis
Package defaultstore implements the SQL-backed default store for the oauth plugin.
Package defaultstore implements the SQL-backed default store for the oauth plugin.
internal
Package types defines the domain models and types used by the oauth plugin.
Package types defines the domain models and types used by the oauth plugin.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL