Skip to content

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 (UserClaims payload) 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, and email_change purposes
  • 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>) via ApiKeyRepository

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, APIKeyRepository
  • RedisService — Apple JWKS cache, refresh-token cache, device fingerprint counters, WebAuthn session blobs, email-change verification
  • EmailService — welcome email, OTP email, deactivation email
  • storage.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