Skip to content

Authentication

SLOzy implements a comprehensive authentication system with JWT tokens, password management, OAuth2, and audit logging.

JWT Access and Refresh Tokens

Authentication is handled by internal/auth/service.go using HS256 JWT tokens.

Access Token

Generated with user claims and configurable expiration:

go
func (s *AuthenticationService) GenerateToken(userID int, organizationID int, email, name string, expiration int) (string, error) {
    claims := &Claims{
        UserID:         userID,
        OrganizationID: organizationID,
        Email:          email,
        Name:           name,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiration) * time.Second)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "slozy",
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(s.secretKey)
}

Default expiration: 8 hours (configurable via JWT_EXPIRATION).

Refresh Token

Refresh tokens allow obtaining new access tokens without re-authentication:

POST /api/v1/auth/refresh
json
{
  "refresh_token": "your-refresh-token"
}

Response includes a new access token and refresh token pair. Refresh tokens are stored in the user_sessions table and validated server-side.

Login Request

POST /api/v1/auth/login
json
{
  "email": "user@example.com",
  "password": "your-password"
}

Success response:

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "dGhpcyBpcyBhIHJlZnJl...",
  "token_type": "Bearer",
  "expires_at": "2026-06-11T18:00:00Z",
  "user": {
    "id": 1,
    "email": "user@example.com",
    "name": "John Doe",
    "organization_id": 1
  }
}

Password Reset Flow

Implemented in internal/handlers/auth.go with dedicated database migrations (000014_password_reset):

  1. Request reset: POST /api/v1/auth/password-reset/request

    • User submits their email
    • System generates a time-limited reset token
    • Token is stored in the database
  2. Reset password: POST /api/v1/auth/password-reset/confirm

    • User submits the reset token and new password
    • Token is validated and consumed
    • Password hash is updated

Password change is also available for authenticated users:

POST /api/v1/auth/change-password
json
{
  "current_password": "old-password",
  "new_password": "new-password"
}

Password hashing uses bcrypt with default cost (internal/auth/service.go:49):

go
func (s *AuthenticationService) HashPassword(password string) (string, error) {
    hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(hash), nil
}

OAuth2 Persistence

OAuth2 support is implemented via migration 000013_oauth2_persistence with four tables:

  • oauth_clients — Registered OAuth applications (client_id, client_secret, redirect_uris, grant_types)
  • oauth_authorization_codes — Temporary authorization codes (with PKCE support via code_challenge)
  • oauth_access_tokens — Issued access tokens with expiration and scopes
  • oauth_refresh_tokens — Refresh tokens linked to access tokens

An automatic cleanup function removes expired tokens:

sql
CREATE OR REPLACE FUNCTION cleanup_expired_oauth_tokens()
RETURNS void AS $$
BEGIN
  DELETE FROM oauth_authorization_codes WHERE expires_at < NOW();
  DELETE FROM oauth_access_tokens WHERE expires_at < NOW();
  DELETE FROM oauth_refresh_tokens WHERE expires_at < NOW();
END;
$$ LANGUAGE plpgsql;

Login Attempt Audit Logging

All authentication attempts are logged to the audit log table (migrations/000006_slo_audit_log). This includes:

  • Successful and failed login attempts
  • Password change requests
  • Token refresh events
  • Account creation

The audit log provides a complete trail for security investigation and compliance requirements. Access audit logs via GET /api/v1/audit-log (admin role required).