Auth¶
Purpose¶
The auth layer owns every code path that produces a JWT or refresh token: registration, password login, social login (Google, Apple, LINE), WebAuthn (passkeys), OTP email verification, password reset, session listing, and revocation. It is split across three services that compose together — AuthService does identity, SessionService does refresh tokens / session listing, OTPService does one-time codes for sensitive flows.
Responsibilities¶
- Issue HS256-signed JWTs (
UserClaimspayload) and verify them on every protected request - Generate, validate, and revoke refresh tokens (web TTL is shorter than native)
- Verify Google ID tokens via
google.golang.org/api/idtoken - Verify Apple ID tokens by fetching and caching Apple's JWKS keys in Redis
- Handle Apple server-to-server consent-revoked / account-delete notifications
- Run WebAuthn registration and login ceremonies (
github.com/go-webauthn/webauthn) - Generate and verify 6-digit OTP codes for
signup,password_reset, andemail_changepurposes - Enforce device-fingerprint registration caps (max 3 accounts/device, 30-day Redis window)
- Reject disposable email domains and accounts under 18
- Record login attempts and expose login history
- List, revoke single, and revoke-all sessions
- Issue and validate long-lived API keys (
sk_<uuid>) viaApiKeyRepository
HTTP endpoints¶
Public¶
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/auth/register |
Email/password registration (rate-limited: 5/hr/IP) |
| POST | /api/v1/auth/login |
Email-or-username + password (rate-limited: 10/15min/IP) |
| POST | /api/v1/auth/google-login |
Google ID token exchange |
| POST | /api/v1/auth/line-login |
LINE access token exchange |
| POST | /api/v1/auth/apple-login |
Apple ID token exchange (verifies via Apple JWKS) |
| POST | /api/v1/auth/refresh |
Exchange refresh token for new access token |
| POST | /api/v1/auth/logout |
Revoke a refresh token |
| POST | /api/v1/auth/reset-password |
Reset password (requires otpToken from OTP verify) |
| POST | /api/v1/auth/otp/send |
Send 6-digit code to email |
| POST | /api/v1/auth/otp/verify |
Verify code, return short-lived otpToken JWT |
| POST | /api/v1/auth/webauthn/login/begin |
Start passkey login ceremony |
| POST | /api/v1/auth/webauthn/login/finish |
Finish passkey login |
| POST | /api/v1/auth/apple/notifications |
Apple S2S notifications (ES256-signed) |
Authenticated¶
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/auth/profile |
JWT | Current user profile |
| PATCH | /api/v1/auth/profile |
JWT | Update profile fields |
| DELETE | /api/v1/auth/profile |
JWT | Soft-deactivate account (30-day grace) |
| POST | /api/v1/auth/phone |
JWT | Update verified phone number |
| POST | /api/v1/auth/email/change |
JWT | Begin email-change flow (sends OTP to new email) |
| POST | /api/v1/auth/email/change/confirm |
JWT | Confirm email change with OTP |
| PUT | /api/v1/auth/profile/password |
JWT | Change password (requires current) |
| PUT | /api/v1/auth/profile/name |
JWT | Change display name (14-day cooldown) |
| PUT | /api/v1/auth/profile/username |
JWT | Change username (uniqueness checked) |
| POST | /api/v1/auth/profile/avatar |
JWT | Upload avatar (max 5 MB, jpeg/png/gif/webp) |
| POST | /api/v1/auth/webauthn/register/begin |
JWT | Begin passkey registration |
| POST | /api/v1/auth/webauthn/register/finish |
JWT | Finish passkey registration |
| POST | /api/v1/auth/user/webhook/test |
JWT | Fire a test webhook to the user's configured URL |
| POST | /api/v1/auth/api-keys |
JWT | Mint an API key (sk_<uuid>) |
| GET | /api/v1/auth/api-keys |
JWT | List API keys |
| DELETE | /api/v1/auth/api-keys/:id |
JWT | Revoke an API key |
| GET | /api/v1/auth/sessions |
JWT | List active sessions |
| DELETE | /api/v1/auth/sessions/:id |
JWT | Revoke a single session |
| DELETE | /api/v1/auth/sessions/all |
JWT | Revoke all sessions for the user |
| GET | /api/v1/auth/login-history |
JWT | Recent 20 login attempts (success + failures) |
| GET | /.well-known/webauthn |
none | RPID-allowed origins for related-origin requests |
Key types¶
type UserClaims struct {
jwt.RegisteredClaims
UserID uuid.UUID
Email string
Role models.UserRole // "user" | "admin"
EmailVerified bool // always true post-OTP; kept for old-token compat
}
type AuthService struct {
userRepo repository.UserRepository
userSvc UserService
apiKeyRepo repository.APIKeyRepository
jwtConfig *config.JWTConfig
googleConfig *config.GoogleConfig
lineConfig *config.LineConfig
appleConfig *config.AppleConfig
webAuthn *webauthn.WebAuthn
sessionSvc *SessionService
redisService RedisService
storage storage.FileStorage
emailService *EmailService
frontendURL string
rpOrigins []string
}
SessionService exposes GenerateRefreshToken, ValidateRefreshToken, RevokeRefreshToken, RevokeAllUserTokens, GetUserSessions, RevokeSession, RecordLoginAttempt, GetLoginHistory, CheckLoginRateLimit, and CleanupExpiredTokens.
OTPService exposes SendOTP(email, purpose), VerifyOTP(email, code, purpose) (otpToken, err), and ValidateOTPToken(token) (email, err).
Data model¶
| Table | Used for |
|---|---|
users |
Identity, OAuth IDs (google_id, apple_id, line_id), password hash, role, deactivated_at |
refresh_tokens |
Long-lived tokens, indexed by token string |
sessions |
One row per active device, denormalises device_info + ip + last_active_at |
login_history |
Audit trail of every login attempt (method, success, fail_reason, ip) |
web_authn_credentials |
Passkey credentials per user |
api_keys |
Long-lived sk_<uuid> keys with scopes |
otps |
Short-lived 6-digit codes with purpose discriminator and used flag |
Dependencies¶
repository.UserRepository,SessionRepository,APIKeyRepositoryRedisService— Apple JWKS cache, refresh-token cache, device fingerprint counters, WebAuthn session blobs, email-change verificationEmailService— welcome email, OTP email, deactivation emailstorage.FileStorage— avatar uploads (with old-file cleanup)UserService— deactivation / reactivation hook
Notable behavior¶
OTP-first signup
Email verification is no longer a separate token flow. Registration sets email_verified = true because callers must complete OTP verification before posting /auth/register. The legacy email_verify_token columns are dropped on startup migration (see cmd/server/main.go).
Account reactivation on login
finalizeLogin detects DeactivatedAt != nil and reactivates the account within the 30-day grace window before issuing tokens. Beyond 30 days the user is hard-deleted by the scheduled deactivated_acct_purge job.
Refresh-token TTL by client
SessionService.GenerateRefreshToken sniffs the User-Agent. Browsers (mozilla, chrome, safari, etc.) get 7 days; native clients get 30 days. The same value is mirrored into Redis (refresh_token:<token>) for fast lookup.
Password reset (OTP flow)¶
sequenceDiagram
participant U as User
participant API as AuthHandler
participant OTP as OTPService
participant Auth as AuthService
U->>API: POST /auth/otp/send {email, purpose:"password_reset"}
API->>OTP: SendOTP
OTP-->>U: 6-digit code (via EmailService)
U->>API: POST /auth/otp/verify {email, code}
API->>OTP: VerifyOTP → otpToken (15-min JWT)
API-->>U: otpToken
U->>API: POST /auth/reset-password {otpToken, newPassword}
API->>Auth: ResetPassword(otpToken, newPassword, OTPService)
Auth->>OTP: ValidateOTPToken → email
Auth->>Auth: bcrypt + UserRepo.Update
Apple S2S notifications¶
HandleAppleNotification accepts ES256-signed payloads from Apple, verifies the kid against GetApplePublicKeyEC, and on consent-revoked / account-delete invokes DeactivateUserByAppleID — which then triggers the standard 30-day purge path.
Where to look¶
backend/internal/services/auth_service.gobackend/internal/services/auth_service_ext.gobackend/internal/services/session_service.gobackend/internal/services/otp_service.gobackend/internal/handlers/auth_handler.gobackend/internal/handlers/session_handler.gobackend/internal/middleware/auth.go— JWT validation middlewarebackend/internal/middleware/rate_limiter.go—RegisterLimit,LoginLimit,ResetPasswordLimit