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:
- User clicks "Login with Google" → GET /auth/oauth/google
- Plugin generates CSRF state token and stores it in signed cookie
- Plugin redirects to Google's authorization page
- User approves → Google redirects to /auth/oauth/google/callback?code=xxx&state=xxx
- Plugin validates state token (CSRF protection)
- Plugin exchanges authorization code for access token
- Plugin fetches user profile from Google
- Plugin creates/links Aegis user account and session
- 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
- func Apple(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func Bitbucket(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func CreateGothProvider(cfg oauthtypes.ProviderConfig, callbackURL string) (goth.Provider, error)
- func Discord(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func Generic(providerID, clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func GetMigrations(dialect plugins.Dialect) ([]plugins.Migration, error)
- func GitHub(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func GitLab(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func Google(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func GothUserToUser(gothUser goth.User) *oauthtypes.User
- func LINE(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func LinkedIn(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func Microsoft(clientID, clientSecret, tenantID string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func Slack(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func Spotify(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func Twitch(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func Twitter(clientID, clientSecret string, opts ...oauthtypes.ProviderOption) oauthtypes.ProviderConfig
- func UserToGothUser(oauthUser *oauthtypes.User, provider string) goth.User
- func WithAccessType(accessType string) oauthtypes.ProviderOption
- func WithDisableImplicitSignUp() oauthtypes.ProviderOption
- func WithDisableSignUp() oauthtypes.ProviderOption
- func WithDiscoveryURL(url string) oauthtypes.ProviderOption
- func WithOverrideUserInfo() oauthtypes.ProviderOption
- func WithPKCE() oauthtypes.ProviderOption
- func WithProfileMapper(fn func(map[string]any) (*oauthtypes.User, error)) oauthtypes.ProviderOption
- func WithPrompt(prompt string) oauthtypes.ProviderOption
- func WithProviderID(id string) oauthtypes.ProviderOption
- func WithRedirectURI(uri string) oauthtypes.ProviderOption
- func WithScopes(scopes ...string) oauthtypes.ProviderOption
- func WithUserInfoFetcher(fn func(*oauthtypes.Tokens) (*oauthtypes.User, error)) oauthtypes.ProviderOption
- type Config
- type GothAdapter
- type Handlers
- type Plugin
- func (p *Plugin) BeginAuth(w http.ResponseWriter, r *http.Request, providerName string) error
- func (p *Plugin) CompleteAuth(ctx context.Context, w http.ResponseWriter, r *http.Request) (*oauthtypes.User, *auth.Session, error)
- func (p *Plugin) Dependencies() []plugins.Dependency
- func (p *Plugin) Description() string
- func (p *Plugin) GetMigrations() []plugins.Migration
- func (p *Plugin) GetStateStore() *StateStore
- func (p *Plugin) GetUserConnections(ctx context.Context, userID string) ([]*oauthtypes.Connection, error)
- func (p *Plugin) Init(_ context.Context, a plugins.Aegis) error
- func (p *Plugin) LinkAccount(ctx context.Context, userID string, oauthUser *oauthtypes.User, ...) error
- func (p *Plugin) MountRoutes(r router.Router, prefix string)
- func (p *Plugin) Name() string
- func (p *Plugin) ProvidesAuthMethods() []string
- func (p *Plugin) RefreshConnection(ctx context.Context, userID, provider string) (*oauthtypes.Connection, error)
- func (p *Plugin) RequiresTables() []string
- func (p *Plugin) UnlinkAccount(ctx context.Context, userID, provider string) error
- func (p *Plugin) Version() string
- type StateData
- type StateStore
- func (s *StateStore) ClearState(w http.ResponseWriter)
- func (s *StateStore) GenerateState() (string, error)
- func (s *StateStore) GetMaxAge() time.Duration
- func (s *StateStore) GetState(r *http.Request) (*StateData, error)
- func (s *StateStore) SetMaxAge(age time.Duration)
- func (s *StateStore) StoreState(w http.ResponseWriter, data *StateData) error
- func (s *StateStore) ValidateState(r *http.Request, callbackState string) (*StateData, error)
- type StateStoreConfig
Constants ¶
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.
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
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 ¶
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 ¶
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 ¶
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 ¶
BeginAuth starts the OAuth authentication flow with CSRF protection.
This method initiates the OAuth flow by:
- Generating a cryptographically secure CSRF state token
- Starting the OAuth session with the provider
- Obtaining the provider's authorization URL
- Storing state and session data in a signed cookie
- Redirecting the user to the provider's authorization page
OAuth Flow (Step 1-3):
- User → GET /auth/oauth/google
- Plugin → Generate state="abc123", store in cookie
- 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:
- Validating the CSRF state token from the callback
- Exchanging the authorization code for an access token
- Fetching the user's profile from the provider
- Creating or linking an Aegis user account
- Creating an Aegis session for the authenticated user
OAuth Flow (Step 4-9):
- Provider → Redirect to /auth/oauth/google/callback?code=xyz&state=abc123
- Plugin → Validate state=abc123 matches cookie
- Plugin → Exchange code=xyz for access token
- Plugin → Fetch user profile from provider
- Plugin → Create/link user account in Aegis
- 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 ¶
Description returns a human-readable description
func (*Plugin) GetMigrations ¶
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 ¶
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:
- Get user, session, and account services from Aegis
- Initialize OAuth store if not provided
- Derive state secret from master secret ("aegis:oauth-state" purpose)
- 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 ¶
MountRoutes registers HTTP routes for the OAuth plugin
func (*Plugin) ProvidesAuthMethods ¶
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 ¶
RequiresTables returns core tables this plugin depends on RequiresTables returns the core tables this plugin reads from.
func (*Plugin) UnlinkAccount ¶
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")
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:
- Attacker initiates OAuth flow → gets callback URL with auth code
- Attacker tricks victim into visiting callback URL
- Victim's browser exchanges code → victim's account linked to attacker's provider
- Attacker can now access victim's account via OAuth
CSRF Protection With State:
- Plugin generates random state token before redirect
- State stored in signed cookie (attacker can't forge)
- Provider includes state in callback URL
- Plugin validates callback state matches cookie
- 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:
- Read cookie value
- Base64-decode payload
- Verify HMAC signature (if secret is set)
- Parse: state|provider|compressedSession
- 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:
- Compress SessionData with gzip (can be large ~1-2KB)
- Concatenate: state|provider|compressedSession
- Sign with HMAC-SHA256 if secret is set
- Base64-encode payload
- 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 ¶
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:
- Retrieve state data from cookie
- Compare stored.State with callbackState from query parameter
- 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)
Source Files
¶
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. |