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:
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{
"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{
"email": "user@example.com",
"password": "your-password"
}Success response:
{
"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):
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
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{
"current_password": "old-password",
"new_password": "new-password"
}Password hashing uses bcrypt with default cost (internal/auth/service.go:49):
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:
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).