This document provides a detailed overview of AuthGate's architecture, design patterns, and OAuth 2.0 Device Authorization flow implementation.
- Project Structure
- Technology Stack
- Device Flow Sequence
- Key Endpoints
- Refresh Token Architecture
- Client Credentials Architecture
authgate/
├── config/ # Configuration management (environment variables, defaults)
├── handlers/ # HTTP request handlers
│ ├── auth.go # User login/logout endpoints
│ ├── device.go # Device authorization flow (/device, /device/verify)
│ ├── token.go # Token issuance (/oauth/token), verification (/oauth/tokeninfo), and revocation (/oauth/revoke)
│ ├── oidc.go # OIDC Discovery (/.well-known/openid-configuration) and UserInfo (/oauth/userinfo)
│ ├── jwks.go # JWKS endpoint (/.well-known/jwks.json) for RS256/ES256 public keys
│ ├── session.go # Session management (/account/sessions)
│ ├── client.go # Admin client management
│ ├── oauth_handler.go # OAuth third-party login handlers
│ └── audit.go # Audit log viewing and export (/admin/audit)
├── middleware/ # HTTP middleware
│ ├── auth.go # Session authentication (RequireAuth, RequireAdmin)
│ ├── csrf.go # CSRF protection middleware
│ └── ratelimit.go # Rate limiting middleware (memory/Redis store)
├── models/ # Data models
│ ├── user.go # User accounts (includes IsActive flag for admin disable/enable)
│ ├── client.go # OAuth clients (OAuthClient)
│ ├── device.go # Device codes (DeviceCode)
│ ├── token.go # Access tokens (AccessToken)
│ ├── oauth_connection.go # OAuth provider connections
│ └── audit_log.go # Audit log entries (AuditLog)
├── auth/ # Authentication providers (pluggable design)
│ ├── local.go # Local authentication (database)
│ ├── http_api.go # External HTTP API authentication
│ └── oauth_provider.go # OAuth 2.0 provider implementations (GitHub, Gitea)
├── token/ # Token provider
│ ├── types.go # Shared data structures (TokenResult, TokenValidationResult)
│ ├── errors.go # Provider-level error definitions
│ ├── local.go # Local JWT provider (HS256/RS256/ES256)
│ └── key.go # Asymmetric key loading (PEM) and key ID derivation
├── services/ # Business logic layer (depends on store and providers)
│ ├── user.go # User management (integrates auth providers)
│ ├── device.go # Device code generation and validation
│ ├── token.go # Token service (integrates token providers)
│ ├── client.go # OAuth client management
│ └── audit.go # Audit logging service (async batch writing, sensitive data masking)
├── store/ # Database layer (GORM)
│ ├── driver.go # Database driver factory (SQLite, PostgreSQL)
│ ├── sqlite.go # Database initialization, migrations, seed data, batch queries
│ └── audit_filters.go # Audit log filtering and pagination
├── templates/ # Type-safe templ templates (compiled to Go code)
│ ├── *.templ # Templ template files (generate *_templ.go files)
│ ├── props.go # Type-safe template props structures
│ ├── render.go # Templ rendering helper for Gin
│ └── generate.go # Go generate directive for templ
├── docker/ # Docker configuration
│ └── Dockerfile # Alpine-based multi-arch image
├── docs/ # Documentation
│ ├── ARCHITECTURE.md # System architecture and design patterns
│ ├── AUTHORIZATION_CODE_FLOW.md # Auth Code Flow with PKCE guide
│ ├── CLIENT_CREDENTIALS_FLOW.md # Client Credentials Grant (M2M) guide
│ ├── CONFIGURATION.md # Environment variables and configuration
│ ├── DEPLOYMENT.md # Production deployment guide
│ ├── DEVELOPMENT.md # Developer guide and extension points
│ ├── METRICS.md # Prometheus metrics documentation
│ ├── MONITORING.md # Monitoring, logging, and alerting
│ ├── OAUTH_SETUP.md # OAuth provider setup guide
│ ├── PERFORMANCE.md # Scalability and optimization
│ ├── RATE_LIMITING.md # Rate limiting configuration
│ ├── SECURITY.md # Security best practices
│ ├── TROUBLESHOOTING.md # Common issues and FAQ
│ └── USE_CASES.md # Real-world examples
├── _example/ # Example CLI client implementations
│ ├── (Device Code Flow CLI → github.com/go-authgate/device-cli)
│ └── (Authorization Code Flow CLI → github.com/go-authgate/oauth-cli)
├── version/ # Version information (embedded at build time)
├── Makefile # Build automation and targets
├── main.go # Application entry point and router setup
├── .env.example # Environment configuration template
└── CLAUDE.md # AI assistant guidance (optional)- Web Framework: Gin - Fast HTTP router
- Templates: templ - Type-safe HTML templating with compile-time validation
- ORM: GORM - Database abstraction
- Database: SQLite (default) / PostgreSQL - Embedded or network database
- Sessions: gin-contrib/sessions - Cookie sessions
- JWT: golang-jwt/jwt - Token generation
- Config: joho/godotenv - Environment management
sequenceDiagram
participant CLI as CLI Tool
participant AuthGate as AuthGate Server
participant User as User (Browser)
Note over CLI,User: Phase 1: Device Code Request
CLI->>+AuthGate: POST /oauth/device/code<br/>(client_id)
AuthGate-->>-CLI: device_code, user_code<br/>verification_uri
Note over CLI: Display to user:<br/>"Visit http://..../device"<br/>"Enter code: 12345678"
Note over CLI,User: Phase 2: User Authorization
User->>+AuthGate: GET /device
AuthGate-->>-User: Login page (if not authenticated)
User->>+AuthGate: POST /login<br/>(username, password)
AuthGate-->>-User: Redirect to /device<br/>(session created)
User->>+AuthGate: GET /device<br/>(show code entry form)
AuthGate-->>-User: Code entry page
User->>+AuthGate: POST /device/verify<br/>(user_code: 12345678)
AuthGate-->>-User: Success page
Note over CLI,User: Phase 3: Token Polling
CLI->>+AuthGate: POST /oauth/token<br/>(device_code, polling)
AuthGate-->>-CLI: {"error": "authorization_pending"}
Note over CLI: Wait 5 seconds
CLI->>+AuthGate: POST /oauth/token<br/>(device_code, polling)
AuthGate-->>-CLI: {"access_token": "eyJ...",<br/>"token_type": "Bearer",<br/>"expires_in": 36000}
Note over CLI: Authentication complete!<br/>Store and use access token
| Endpoint | Method | Auth Required | Purpose |
|---|---|---|---|
/health |
GET | No | Health check with database connection test |
/.well-known/openid-configuration |
GET | No | OIDC Discovery metadata (RFC 8414 / OIDC Discovery 1.0) |
/.well-known/jwks.json |
GET | No | JWKS public keys for RS256/ES256 JWT verification (RFC 7517) |
/oauth/device/code |
POST | No | Request device and user codes (CLI/device) |
/oauth/authorize |
GET | Yes (Session) | Authorization Code Flow consent page (web apps) |
/oauth/authorize |
POST | Yes (Session) | Submit consent decision (allow/deny) |
/oauth/token |
POST | No | Token endpoint (grant_type=device_code, authorization_code, refresh_token, or client_credentials) |
/oauth/tokeninfo |
GET | No (Bearer) | Verify token validity from Authorization: Bearer access token |
/oauth/userinfo |
GET/POST | No (Bearer) | OIDC UserInfo — returns profile claims for authenticated user (OIDC Core §5.3) |
/oauth/revoke |
POST | No | Revoke access token (RFC 7009) |
/oauth/register |
POST | No | Dynamic client registration (RFC 7591); optional Bearer token |
/oauth/introspect |
POST | No | Token introspection (RFC 7662); client auth via Basic or form |
/device |
GET | Yes (Session) | User authorization page (browser) |
/device/verify |
POST | Yes (Session) | Complete authorization (submit user_code) |
/account/sessions |
GET | Yes (Session) | View all active sessions |
/account/sessions/:id/revoke |
POST | Yes (Session) | Revoke specific session |
/account/sessions/revoke-all |
POST | Yes (Session) | Revoke all user sessions |
/account/authorizations |
GET | Yes (Session) | View apps authorized via Authorization Code Flow |
/login |
GET/POST | No | User login (creates session) |
/logout |
GET | Yes (Session) | User logout (destroys session) |
/auth/login/:provider |
GET | No | Initiate OAuth login (provider: github, gitea, microsoft) |
/auth/callback/:provider |
GET | No | OAuth callback endpoint |
/admin/audit |
GET | Yes (Admin) | View audit logs (HTML interface) |
/admin/audit/export |
GET | Yes (Admin) | Export audit logs as CSV |
/admin/audit/api |
GET | Yes (Admin) | List audit logs (JSON API) |
/admin/audit/api/stats |
GET | Yes (Admin) | Get audit log statistics |
/admin/clients/:id/authorizations |
GET | Yes (Admin) | View all users who consented to a client |
/admin/clients/:id/revoke-all |
POST | Yes (Admin) | Revoke all tokens and consents for a client |
/admin/users |
GET/POST | Yes (Admin) | List users / create a user (auto-generates password if none supplied; auth_source=local) |
/admin/users/:id |
GET/POST | Yes (Admin) | View / update user (cannot change own role) |
/admin/users/:id/reset-password |
POST | Yes (Admin) | Generate a new random password (local-auth users only) |
/admin/users/:id/delete |
POST | Yes (Admin) | Delete user (blocked for self and last active admin) |
/admin/users/:id/disable |
POST | Yes (Admin) | Disable user — revokes all tokens, clears any live session on next request |
/admin/users/:id/enable |
POST | Yes (Admin) | Re-enable a disabled user |
/admin/users/:id/connections |
GET | Yes (Admin) | List the user's third-party OAuth connections (GitHub, Gitea, Microsoft) |
/admin/users/:id/connections/:conn_id/delete |
POST | Yes (Admin) | Unlink a specific third-party OAuth connection |
/admin/users/:id/authorizations |
GET | Yes (Admin) | List apps the user has authorized |
/admin/users/:id/authorizations/:uuid/revoke |
POST | Yes (Admin) | Revoke a single user authorization |
POST /oauth/device/code- Returnsdevice_code,user_code,verification_uri,interval(5s)POST /oauth/token- Token endpoint supporting multiple grant types:- Device Code Grant:
grant_type=urn:ietf:params:oauth:grant-type:device_code- Poll with
device_codeandclient_id - Returns
access_token,refresh_token,token_type,expires_in,scope - Returns
authorization_pendingerror while waiting for user
- Poll with
- Refresh Token Grant:
grant_type=refresh_token- Request with
refresh_token,client_id, and optionalscope - Returns new
access_token(fixed mode) or newaccess_token+refresh_token(rotation mode) - Returns
invalid_granterror if refresh token is invalid/expired
- Request with
- Client Credentials Grant:
grant_type=client_credentials- Authenticate via HTTP Basic Auth (
Authorization: Basic base64(client_id:client_secret)) or form body - Returns
access_token,token_type,expires_in,scope— norefresh_token - Requires confidential client with
EnableClientCredentialsFlow = true - Returns
401 UnauthorizedwithWWW-Authenticate: Basic realm="authgate"for auth failures
- Authenticate via HTTP Basic Auth (
- Device Code Grant:
GET /device- Shows code entry form (redirects to/loginif not authenticated)POST /device/verify- Validates and approves user code (requires valid session)
GET /oauth/tokeninfowithAuthorization: Bearer <JWT>header — returns token details or error. Pass the token in the header, not as a query string, to prevent token leakage via server logs andRefererheaders.
-
GET /.well-known/openid-configuration- Returns OIDC Provider Metadata (RFC 8414):- Issuer, authorization/token/userinfo/revocation endpoint URLs
- Supported response types, grant types, scopes, claims, and code challenge methods
-
GET /oauth/userinfoorPOST /oauth/userinfo- Returns claims for the token owner (OIDC Core 1.0 §5.3):- Requires
Authorization: Bearer <access_token>header - Returns
401withWWW-Authenticate: Bearer error="invalid_token"on failure - Claims returned depend on the scopes granted when the token was issued:
- Requires
| Scope | Claims returned |
|---|---|
| (any) | sub (always present — user UUID) |
profile |
name, preferred_username, picture (if set), updated_at |
email |
email, email_verified (always false — no email verification) |
Example request for openid profile email scopes:
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"preferred_username": "johndoe",
"picture": "https://example.com/avatar.jpg",
"updated_at": 1708646400,
"email": "john@example.com",
"email_verified": false
}POST /oauth/revoke- Revoke access token (CLI)- Parameters:
token(required) - The JWT token to revoke - Parameters:
token_type_hint(optional) - Set to "access_token" - Returns: HTTP 200 on success (even if token doesn't exist, per RFC 7009)
- Note: Prevents token scanning attacks by always returning success
- Parameters:
-
GET /account/sessions- View all active sessions for current user- Displays: Client name, Client ID, scopes, creation/expiration times, status
- Requires: Valid user session (login required)
-
POST /account/sessions/:id/revoke- Revoke specific session- Parameters:
:id- Token ID to revoke - Requires: Valid user session, token must belong to current user
- Returns: Redirect to sessions page
- Parameters:
-
POST /account/sessions/revoke-all- Sign out all devices- Revokes all access tokens for the current user
- Useful for security incidents or password changes
- Returns: Redirect to sessions page
Security Note: Session management endpoints use CSRF protection and verify token ownership before revocation.
POST /oauth/register- Register a new OAuth client programmatically- Request body (JSON):
client_name(required),redirect_uris,grant_types,token_endpoint_auth_method,scope - Gated by
ENABLE_DYNAMIC_CLIENT_REGISTRATION=true(disabled by default) - Optional Bearer token protection via
DYNAMIC_CLIENT_REGISTRATION_TOKEN - Registered clients default to "pending" status (admin approval required before use)
- Rate limited (default: 5 req/min)
- Returns:
client_id,client_secret(for confidential clients),registration_access_token, and echoed metadata
- Request body (JSON):
POST /oauth/introspect- Determine the active state and metadata of a token- Client authentication via HTTP Basic Auth or form-body
client_id/client_secret - Parameters:
token(required),token_type_hint(optional:access_tokenorrefresh_token) - Active response includes:
active,scope,client_id,username,token_type,exp,iat,sub,iss,jti - Inactive/invalid tokens return:
{"active": false} - Rate limited (default: 20 req/min)
- Client authentication via HTTP Basic Auth or form-body
AuthGate supports refresh tokens following RFC 6749 with configurable rotation modes for different security requirements.
- Dual Modes: Fixed (reusable) vs Rotation (one-time use) refresh tokens
- Unified Storage: Both access and refresh tokens stored in
AccessTokentable withtoken_categoryfield - Token Family Tracking:
parent_token_idlinks tokens for audit trails and revocation - Status Management: Tokens can be
active,disabled, orrevoked - Configurable Expiration:
REFRESH_TOKEN_EXPIRATIONenv var (default: 720h = 30 days) - Provider Support: LocalTokenProvider supports refresh operations
- Device code exchange returns
access_token+refresh_token - When access token expires, client POSTs to
/oauth/tokenwithgrant_type=refresh_token - Server returns new
access_tokenonly (refresh token remains unchanged and reusable) - Process repeats until refresh token expires or is manually disabled/revoked
- Each device/application gets its own refresh token that doesn't affect others
- Users can manage all tokens (disable/enable/revoke) via backend UI
- LastUsedAt field tracks activity for identifying inactive sessions
- Same as fixed mode, but step 3 returns both new
access_token+ newrefresh_token - Old refresh token is automatically revoked (status set to 'revoked') after each use
- Prevents token replay attacks
- Requires clients to update stored refresh token after each use
- Enable via
ENABLE_TOKEN_ROTATION=true
- Status Field:
active(usable) /disabled(temporarily blocked, can re-enable) /revoked(permanently blocked) - Independent Revocation: Revoking refresh token doesn't affect existing access tokens
- Family Tracking: ParentTokenID enables audit trails and selective revocation
- Scope Validation: Refresh requests cannot escalate privileges beyond original grant
REFRESH_TOKEN_EXPIRATION=720h- Refresh token lifetime (default: 30 days)ENABLE_REFRESH_TOKENS=true- Feature flag (default: enabled)ENABLE_TOKEN_ROTATION=false- Enable rotation mode (default: disabled, uses fixed mode)
urn:ietf:params:oauth:grant-type:device_code- Device authorization flow (returns access + refresh)authorization_code- Authorization Code Flow (returns access + refresh)refresh_token- RFC 6749 refresh token grant (returns new tokens)client_credentials- Machine-to-machine grant (returns access token only, no refresh token)
- Refresh tokens validated by type claim (
"type": "refresh"in JWT) - Refresh tokens cannot be used as access tokens (separate validation logic)
- Client ID verification prevents cross-client token usage
- Token family tracking enables detection of suspicious patterns
- Optional rotation mode for high-security scenarios
- Device codes expire after 30min (configurable via Config.DeviceCodeExpiration)
- User codes are 8-char uppercase alphanumeric (generated by generateUserCode in services/device.go)
- User codes normalized: uppercase + dashes removed before lookup
- JWTs signed with configurable algorithm (HS256/RS256/ES256), configurable expiry via
JWT_EXPIRATION(default: 10 hours) with optional jitter (JWT_EXPIRATION_JITTER, default: 30 minutes) - Sessions stored in encrypted cookies (gin-contrib/sessions), 7-day expiry
- Polling interval is 5 seconds (Config.PollingInterval)
- Templates and static files embedded via go:embed in main.go
AuthGate supports the Client Credentials Grant (RFC 6749 §4.4) for machine-to-machine (M2M) authentication where no user context is involved.
- Confidential clients only — public clients cannot securely store a secret; enabling the flow for a public client is silently ignored at the service layer
- No refresh token — per RFC 6749 §4.4.3; the client simply requests a new token when the current one expires
- Machine UserID — tokens are stored with
user_id = "client:<clientID>"to clearly distinguish M2M tokens from user-delegated tokens throughout the system subject_typefield —/oauth/tokeninforeturns"subject_type": "client"whenuser_idstarts with"client:", and"subject_type": "user"otherwise- Independent TTL —
CLIENT_CREDENTIALS_TOKEN_EXPIRATION(default: 1h) is separate from the user access token TTL; keep short per RFC 9700 recommendations
| Request scope | Effective scope |
|---|---|
Empty ("") |
All scopes registered on the client |
Subset (e.g., "read") |
Only the requested scopes (validated against client's registered scopes) |
| Superset or unknown scope | invalid_scope error |
openid or offline_access |
invalid_scope error (user-centric OIDC scopes are not permitted) |
Per RFC 6749 §2.3.1, HTTP Basic Authentication is preferred:
Authorization: Basic base64(client_id + ":" + client_secret)
Form body is also accepted as a fallback:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=<id>&client_secret=<secret>&scope=read
| Condition | HTTP Status | OAuth Error |
|---|---|---|
| Missing or invalid credentials | 401 + WWW-Authenticate: Basic realm="authgate" |
invalid_client |
| Client is public | 400 | unauthorized_client |
| Flow not enabled for client | 400 | unauthorized_client |
| Scope exceeds client's registered scopes | 400 | invalid_scope |
openid / offline_access requested |
400 | invalid_scope |
CLIENT_CREDENTIALS_TOKEN_EXPIRATION=1h— Access token lifetime (default: 1 hour)
- Create or edit an OAuth client in Admin → OAuth Clients
- Set Client Type to
Confidential - Check Client Credentials Flow (RFC 6749 §4.4)
- Save — the client can now authenticate with
grant_type=client_credentials
The application automatically creates these tables:
users- User accounts (includes OAuth-linked users)oauth_clients- Registered client applicationsdevice_codes- Active device authorization requestsaccess_tokens- Issued JWT tokens (both access and refresh tokens)oauth_connections- OAuth provider connections (GitHub, Gitea, etc.)audit_logs- Comprehensive audit trail of all operations (authentication, tokens, admin actions, security events)
Next Steps:
- Configuration Guide - Configure environment variables
- Development Guide - Build and extend AuthGate
- Deployment Guide - Deploy to production