Authentication¶
Overview¶
Anubis uses server-side session authentication for runtime access.
- The browser authenticates with the
connect.sidcookie. - Sessions are stored in PostgreSQL through
connect-pg-simple. - The backend serializes a small user snapshot into the session and reloads the full user on each request.
- JWTs are only used for out-of-band flows: email confirmation, new-email confirmation, and password reset.
- Each user has exactly one login provider today, stored directly on
users.auth_providerandusers.provider_subject. - Provider linking is not part of the current auth contract.
Runtime Shape¶
Assuming the default URI versioning setup, auth routes are served under /v1/....
If APP_PREFIX is configured, that prefix is added before the versioned path.
flowchart TD
Browser[Browser or frontend] -->|connect.sid cookie| Nest[NestJS auth endpoints]
Nest --> SessionStore[(PostgreSQL session table)]
Nest --> Users[(users)]
Nest --> Candidates[(candidates)]
Nest --> Mail[Mail service]
Nest --> JWT[JWT hashes for email flows]
Current Endpoints¶
Shared session endpoints¶
GET /v1/auth/mePATCH /v1/auth/meDELETE /v1/auth/mePOST /v1/auth/logoutPOST /v1/auth/onboarding/candidate
Email provider endpoints¶
POST /v1/auth/provider/email/loginPOST /v1/auth/provider/email/registerPOST /v1/auth/provider/email/confirmPOST /v1/auth/provider/email/confirm/newPOST /v1/auth/provider/email/forgot/passwordPOST /v1/auth/provider/email/reset/password
Google provider endpoints¶
POST /v1/auth/provider/google
Operational endpoint¶
POST /system/change-log-levelis not part of user auth; it usesx-system-token.
Data Model¶
users¶
The users table is the auth source of truth.
auth_provideridentifies the login method:emailorgoogleprovider_subjectstores the provider-specific subject, such as the normalized email for email auth or the Google subject for Google authemail,cpf,first_name, andlast_namestore profile datastatus,onboarding_completed, andmust_change_passworddrive session restrictionsconfirm_email_token_versionandforgot_password_token_versioninvalidate old JWT hashesdeleted_atis no longer part of the model; account deletion removes the row
candidates¶
Candidate-specific onboarding data lives in candidates and is keyed by user_id.
Auth Flow Diagrams¶
Session lifecycle¶
flowchart LR
Login[Successful login] --> Regen[Passport login and session regeneration]
Regen --> Snapshot[Store session snapshot]
Snapshot --> Request[Authenticated request]
Request --> Deserialize[Deserialize user from DB]
Deserialize --> Guard[Session guards evaluate access]
Email registration¶
sequenceDiagram
participant C as Client
participant A as AuthEmailController
participant S as AuthEmailService
participant U as UsersService
participant Cand as CandidateService
participant M as MailService
C->>A: POST /v1/auth/provider/email/register
A->>S: register(dto)
S->>U: ensure email and CPF are unique
S->>U: create user(auth_provider=email, status=inactive)
S->>Cand: createProfile(userId, universityOfOrigin)
S->>U: increment confirm_email_token_version
S->>M: send confirmation email
A-->>C: 204 No Content
Email login¶
sequenceDiagram
participant C as Client
participant G as AuthEmailGuard
participant S as AuthEmailService
participant Sess as SessionSerializer
C->>G: POST /v1/auth/provider/email/login
G->>S: validateLogin(email, password)
S-->>G: user if provider=email, password matches, status=active
G->>Sess: logIn(user)
G-->>C: 200 + connect.sid cookie
Google login or signup¶
flowchart TD
Start[POST /v1/auth/provider/google] --> Verify[Verify Google ID token]
Verify --> Valid{Verified email and subject present?}
Valid -- No --> Error422[422 or 401]
Valid -- Yes --> ProviderLookup[Find by auth_provider + provider_subject]
ProviderLookup --> FoundByProvider{User found?}
FoundByProvider -- Yes --> LoginExisting[Return existing user and login]
FoundByProvider -- No --> EmailLookup[Find by normalized email]
EmailLookup --> FoundByEmail{User found by email?}
FoundByEmail -- Yes --> Conflict[409 use your original provider]
FoundByEmail -- No --> Create[Create active candidate with onboardingCompleted=false]
Create --> LoginNew[Return new user and login]
How Each Flow Works¶
Email registration¶
POST /v1/auth/provider/email/register creates a candidate account.
- New users are created with
authProvider=email providerSubjectis the normalized email- The account starts as
inactiveuntil email confirmation succeeds - A candidate profile is created immediately
onboardingCompletedstarts astruefor this flow
Email confirmation¶
POST /v1/auth/provider/email/confirm activates the account.
- The hash is verified with
AUTH_CONFIRM_EMAIL_SECRET - Token version mismatch invalidates old links
- Confirming an already active account is harmless
Email login¶
POST /v1/auth/provider/email/login only works for users whose authProvider is email.
- The email is normalized before lookup
- Password is checked with bcrypt
- Inactive users are rejected
- Bootstrap-password expiration is enforced when
mustChangePasswordis set - A successful login creates or refreshes the session cookie
Google login¶
POST /v1/auth/provider/google handles both first-time signup and later login.
- The Google token must be valid and the Google email must be verified
- Existing users are matched by
authProvider=googleandproviderSubject=<google sub> - If the same email already belongs to an email-auth account, the request is rejected with a provider-conflict message
- First-time Google users are created as active candidates with
onboardingCompleted=false
Candidate onboarding completion¶
POST /v1/auth/onboarding/candidate exists mainly for restricted sessions such as first-time Google signups.
- It requires an authenticated session
- It is allowed when the only restriction is
onboardingIncomplete - It updates both candidate data and the user lifecycle flags
Profile update¶
PATCH /v1/auth/me updates the current authenticated user.
- If
mustChangePassword=true, the user can only perform a password change until that requirement is cleared - Changing email sends a confirmation message to the new address instead of swapping immediately
- Changing password revokes other sessions for the same user
- Certain lifecycle changes can revoke all sessions
Account deletion¶
DELETE /v1/auth/me deletes the user account and destroys the current session.
- User sessions are removed from the session store first
- The user row is then deleted from
users - This is a hard delete
Logout¶
POST /v1/auth/logout destroys only the current session.
Session Snapshot And Guards¶
The session serializer stores a compact snapshot with:
idemailrolestatusonboardingCompletedmustChangePassword
On later requests:
- Passport deserializes the user from the snapshot
id SessionAuthGuardrequires an authenticated sessionSessionLifecycleGuardblocks restricted sessions from unsafe routes
Restriction rules:
mustChangePassword=truerestricts access until the password is changedonboardingCompleted=falserestricts access until onboarding is completedGET /v1/auth/meis allowed for both restricted statesPATCH /v1/auth/meis allowed formustChangePasswordPOST /v1/auth/onboarding/candidateis allowed foronboardingIncomplete
Response Shapes¶
Login response¶
Successful login responses currently return:
{
userId: string;
email: string | null;
firstName: string | null;
lastName: string | null;
role: string;
status: string;
onboardingCompleted: boolean;
mustChangePassword: boolean;
authProvider: 'email' | 'google';
}
Current user response¶
GET /v1/auth/me returns the serialized User entity:
{
id: string;
authProvider: 'email' | 'google';
providerSubject: string | null;
email: string | null;
cpf: string | null;
firstName: string | null;
lastName: string | null;
role: string;
status: string;
onboardingCompleted: boolean;
mustChangePassword: boolean;
bootstrapPasswordExpiresAt: string | null;
confirmEmailTokenVersion: number;
forgotPasswordTokenVersion: number;
createdAt: string;
updatedAt: string;
}
password is excluded from serialized responses.
Error Contract¶
Auth endpoints return message-oriented error payloads.
- The main contract is the top-level
messagefield messagemay be a string or an array- Clients should not depend on field-level validation structures from auth routes