StackLoop must provide a secure, modern authentication experience for developers while supporting GitHub OAuth, session-based web access, API access, and future client integrations.
- Follow OAuth 2.1 best practices
- Prevent CSRF and token theft
- Support short-lived access tokens and rotation-friendly refresh tokens
- Provide least-privilege authorization
- Protect both browser and API workflows
- Support future growth into organizations, teams, and admins
- GitHub OAuth 2.1 for user sign-in
- Short-lived access tokens for API requests
- Refresh tokens for renewing access tokens securely
- HttpOnly, Secure, SameSite cookies for browser sessions
- Optional bearer tokens for non-browser clients
- User clicks Sign in with GitHub
- StackLoop redirects to GitHub authorization endpoint
- GitHub authenticates the user and returns an authorization code
- StackLoop exchanges that code for an access token and user profile
- StackLoop creates or updates the user account and issues a session
- The app stores a secure session cookie and returns the user to the requested page
sequenceDiagram
participant User
participant Web as StackLoop Web App
participant GitHub as GitHub OAuth
participant API as StackLoop API
participant DB as PostgreSQL
User->>Web: Click Sign in with GitHub
Web->>GitHub: Redirect with PKCE parameters
GitHub-->>User: User authorizes app
GitHub-->>Web: Authorization code
Web->>API: Exchange code for token
API->>GitHub: Token exchange
GitHub-->>API: Access token + user profile
API->>DB: Create/update user and account link
API-->>Web: Session created
Web-->>User: Authenticated session established
- Use authorization code flow with PKCE
- Do not use implicit flow
- Do not exchange tokens in query strings without protection
- Use state parameter to prevent CSRF during redirect
- Use nonce or state to correlate request initiation and callback
- client_id
- redirect_uri
- response_type=code
- scope=read:user,user:email
- state=random opaque value
- code_challenge and code_challenge_method=S256
- code_verifier in the token exchange step
- PKCE prevents interception and code injection attacks
- State ensures the callback belongs to the current login attempt
- GitHub handles the user identity source, reducing custom credential handling
- The callback endpoint receives the authorization code and state
- It validates the state value against the server-side session or signed cookie
- It exchanges the code for a token at GitHub
- It fetches the GitHub user profile and email data
- It creates or links the account to the existing StackLoop user
- It creates the app session and redirects the user to the post-login route
GET /auth/github/callback
- Validate state parameter before exchanging the code
- Validate redirect URI matches the registered URI
- Use HTTPS only
- Reject reused authorization codes
- Store transient OAuth state in server-side store or signed cookie
StackLoop should use server-managed browser sessions with secure cookies for the web app and short-lived access tokens for APIs.
- Create a server-side session record with:
- session id
- user id
- issued at
- expires at
- last activity at
- revoked at
- user agent and IP fingerprint
- Set cookies as HttpOnly, Secure, SameSite=Lax or Strict
- Use a dedicated session cookie such as:
- stackloop_session
- Do not store access tokens directly in browser-localStorage
Set-Cookie: stackloop_session=<session_id>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400
- Protects tokens from XSS-based theft
- Prevents accidental exposure to scripts
- Fits the browser-based session model well
- Session records should be stored in PostgreSQL or a dedicated session store
- Redis can be used for fast session lookup and expiry management
- The session record should support revocation and rotation
- Access tokens: short-lived, e.g. 15 minutes
- Refresh tokens: long-lived, e.g. 30 days
- Rotate refresh tokens on each use
- Revoke refresh tokens when a session is logged out or suspicious activity occurs
- JWT or opaque token format depending on service needs
- If JWT is used, keep claims minimal and signed with asymmetric keys
- If opaque tokens are used, store token metadata in the database and use a random token ID
If JWTs are used, the recommended approach is:
- Use asymmetric signing (RS256 or ES256)
- Keep claims minimal:
- sub: user id
- role: user role
- scope: delegated permissions
- exp: expiry time
- iat: issued at
- jti: unique token id
- Do not embed sensitive information in claims
- Rotate signing keys over time
- Verify signature and expiration
- Reject tokens with missing or invalid issuer/audience
- Use server-side revocation support if the token is part of a session model
- Browser-based web sessions: store only the session cookie; do not expose access tokens to JavaScript
- API clients: use secure storage such as OS keychain or encrypted secret storage
- Do not store bearer tokens in localStorage for web apps
- Store refresh token hashes in the database, not in plaintext
- Store only the hashed value and metadata such as expiry and revocation status
sequenceDiagram
participant Client
participant API as StackLoop API
participant DB as PostgreSQL
Client->>API: POST /auth/refresh with refresh token
API->>DB: Validate refresh token hash and session
alt valid and not revoked
API->>DB: Issue new refresh token and rotate old one
API-->>Client: New access token + new refresh token
else invalid or expired
API-->>Client: 401 Unauthorized
end
- Rotate on every use
- Revoke the old token immediately after replacement
- Expire refresh tokens after inactivity or after a maximum lifetime
- Limit refresh token reuse to prevent replay attacks
- If a refresh token is reused, revoke the entire session chain and force re-login
sequenceDiagram
participant User
participant Web as StackLoop Web App
participant API as StackLoop API
participant DB as PostgreSQL
User->>Web: Click Logout
Web->>API: POST /auth/logout
API->>DB: Mark session revoked
API-->>Web: Success
Web-->>User: Clear secure session cookie
- Invalidate the current session record
- Revoke refresh tokens linked to that session
- Clear the session cookie from the browser
- If using JWTs, make sure token revocation is supported or use short expiry and a revocation list
- user: default role for authenticated users
- maintainer: user with repository ownership or verification responsibilities
- admin: platform administrator with elevated capabilities
- moderator: optional future role for content moderation
user < maintainer < admin
- Users receive the user role by default after GitHub account verification
- Maintainer role is granted when the user claims or verifies a repository
- Admin role is granted only by existing admins or secure provisioning workflows
- Roles should be stored in the users table or a dedicated roles table
- For flexibility, a dedicated user_roles table is recommended for future expansion
CREATE TABLE user_roles (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL,
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
granted_by UUID REFERENCES users(id) ON DELETE SET NULL,
PRIMARY KEY (user_id, role)
);Use a role-based access control model with resource-specific permissions.
- repository.read
- repository.write
- repository.manage
- collection.create
- collection.manage
- recommendation.read
- notification.read
- admin.user.manage
- admin.repo.verify
- admin.search.reindex
- Authorization should be checked at the route or service layer
- Permission checks should be explicit and centralized
- Every protected route should require a permission or role check
admin -> all admin permissions
maintainer -> repository.manage for owned repositories
user -> repository.read, collection.create, notification.read
- Least privilege is enforced by design
- Permission logic is easier to audit and evolve over time
- Authentication middleware
- Verifies access token or session cookie
- Loads the current user into request context
- Authorization middleware
- Verifies required role or permission
- CSRF middleware
- Protects state-changing requests for browser sessions
- Rate limiting middleware
- Prevents brute-force and abuse
- Audit middleware
- Logs security-sensitive actions
flowchart TD
Request[Incoming Request] --> Auth[Auth Middleware]
Auth --> Session[Session Validation]
Session --> Authz[Authorization Middleware]
Authz --> CSRF[CSRF Protection]
CSRF --> Rate[Rate Limiting]
Rate --> Route[Protected Route Handler]
- /repositories/{id} PATCH
- /collections POST
- /saved-repositories POST
- /notifications PATCH
- /admin/* routes
- Reject missing or invalid credentials
- Enforce role and permission checks
- Abort on suspicious or malformed requests
- Add user context to the request for downstream services
A browser-based attacker could trick a logged-in user into submitting a state-changing request to StackLoop without the user intending it.
- Use SameSite=Lax or Strict on session cookies
- Use CSRF tokens for state-changing non-GET requests where the site uses cookie-based auth
- For APIs consumed by browsers, include an X-CSRF-Token header or double-submit cookie pattern
- For browser-based forms and mutations, issue a CSRF token in a cookie and require it in a header for POST, PATCH, DELETE requests
- Validate the token server-side on every state-changing request
POST /collections
X-CSRF-Token: <token>
Cookie: stackloop_session=<session>; csrf_token=<token>
- Prevents cross-site form submission attacks
- Keeps browser-based auth flows consistent with security best practices
- Do not store access tokens in localStorage or sessionStorage
- Store session cookies only
- Use HttpOnly to reduce XSS risk
- Use Secure to ensure transport security
- Use SameSite=Lax/Strict to reduce CSRF exposure
- For server-to-server clients, store tokens in secure secret storage
- For desktop/mobile apps, use OS secure storage if possible
- Never log tokens or leave them in environment variables in source code
- Access tokens expire quickly
- Refresh tokens can be rotated and revoked
- Session cookies should also expire after inactivity or absolute max age
- Session cookie lifetime: 24 hours or configurable
- Idle timeout: 8 hours or 12 hours
- Access token lifetime: 15 minutes
- Refresh token lifetime: 30 days
- Extend the session only on activity if the user remains active
- If a user is inactive for too long, require re-authentication
- Refresh tokens may be rotated on use and invalidated on suspicious behavior
- Sessions should be invalidated on logout or password/account change events
- Short-lived sessions reduce the blast radius of token leakage
- Inactivity timeouts reduce long-lived unauthorized access exposure
StackLoop should support linking GitHub accounts to a single StackLoop account.
- If the user signs in with GitHub and no existing StackLoop account exists, create the account
- If the user already has a StackLoop account, link the GitHub identity to that account and merge profile data where appropriate
- If the GitHub account is already linked to another user, require a secure confirmation or admin intervention
CREATE TABLE account_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
provider_user_id VARCHAR(255) NOT NULL,
email VARCHAR(320),
linked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (provider, provider_user_id)
);- Prevents identity conflicts
- Makes multi-provider login manageable and auditable
sequenceDiagram
participant User
participant Web as StackLoop Web
participant OAuth as GitHub OAuth
participant API as StackLoop API
participant DB as PostgreSQL
User->>Web: Start login
Web->>OAuth: Redirect with PKCE and state
OAuth-->>User: Consent screen
User->>OAuth: Approve access
OAuth-->>Web: Authorization code
Web->>API: Exchange code
API->>OAuth: Token exchange
OAuth-->>API: Access token and profile
API->>DB: Create/update user + session
API-->>Web: Session cookie set
Web-->>User: Redirect to dashboard
sequenceDiagram
participant Client
participant API as StackLoop API
participant DB as PostgreSQL
Client->>API: Refresh token request
API->>DB: Validate and rotate refresh token
DB-->>API: Valid session data
API-->>Client: New access + refresh tokens
sequenceDiagram
participant User
participant Web as StackLoop Web
participant API as StackLoop API
participant DB as PostgreSQL
User->>Web: Logout
Web->>API: POST /auth/logout
API->>DB: Revoke session and refresh token
API-->>Web: Success
Web-->>User: Clear secure cookie
- HTTPS only in production
- HSTS enabled
- Secure cookie flags
- CSRF tokens for cookie-based browser mutations
- Rate limiting for login and token endpoints
- Audit logging for auth events
- Secure storage of refresh tokens and OAuth state values
- Monitoring for suspicious login patterns
- Re-authentication for sensitive admin actions
- IP-based anomaly detection for unusual login locations
- Device or session fingerprinting for suspicious activity
- User notification on new login location or device
Log the following:
- sign-in success/failure
- logout events
- token refresh events
- session revocations
- failed permission checks
- suspicious or repeated auth failures
- Trust proxy / HTTPS enforcement
- CSRF protection
- Authentication middleware
- Authorization middleware
- Rate limiting
- Audit logging
- Session table in PostgreSQL for durable session state
- Redis for fast session lookup and caching if required
- Refresh token hashes in PostgreSQL
- Access token TTL: 15 minutes
- Refresh token TTL: 30 days
- Session idle timeout: 8 hours
- Max concurrent sessions per user: configurable, default 5
StackLoop’s authentication system should be built around GitHub OAuth 2.1, secure session cookies, short-lived access tokens, rotated refresh tokens, clear role-based authorization, and strong CSRF and audit protections. This model is appropriate for a secure, modern, developer-first platform and can scale to an enterprise-style product without exposing the user to unnecessary complexity.