Skip to main content
rs-tunnel uses a secure authentication flow combining Slack OAuth 2.0 with PKCE (Proof Key for Code Exchange) to authenticate users via their Slack workspace credentials.

Authentication Flow

The complete authentication sequence involves four steps:

1. Start OAuth Flow

The CLI generates a PKCE code verifier and challenge, then calls POST /v1/auth/slack/start with:
  • User’s email address
  • PKCE code challenge (SHA-256 hash of code verifier)
  • CLI callback URL (localhost listener)
The API validates the email domain, creates an OAuth session, and returns a Slack authorization URL.

2. Slack Authorization

The CLI opens the authorization URL in the user’s browser. The user authenticates with Slack and grants the requested OpenID scopes (openid, profile, email).

3. OAuth Callback

Slack redirects back to the API’s callback endpoint with an authorization code. The API:
  • Exchanges the code for a Slack access token
  • Fetches the user’s Slack profile (email, team ID, user ID)
  • Validates the email matches the requested email
  • Validates the Slack team ID matches ALLOWED_SLACK_TEAM_ID
  • Generates a short-lived login code
  • Redirects to the CLI’s callback URL with the login code

4. Token Exchange

The CLI receives the login code and calls POST /v1/auth/exchange with:
  • Login code from the callback
  • PKCE code verifier (original random string)
The API verifies the PKCE challenge matches and issues a token pair.

Token Types

JWT Access Token

  • Type: JSON Web Token (JWT)
  • Algorithm: HS256
  • Default TTL: 15 minutes (configurable via JWT_ACCESS_TTL_MINUTES)
  • Usage: Include in Authorization: Bearer <token> header for all authenticated API requests
  • Payload:
    {
      "sub": "user-uuid",
      "email": "[email protected]",
      "slackUserId": "U0123456789",
      "slackTeamId": "T0123456789",
      "iat": 1234567890,
      "exp": 1234568790
    }
    

Refresh Token

  • Type: Opaque token (48-byte random base64url string)
  • Default TTL: 30 days (configurable via REFRESH_TTL_DAYS)
  • Storage: SHA-256 hash stored in database
  • Usage: Call POST /v1/auth/refresh to obtain a new token pair
  • Security: Single-use only - each refresh revokes the old token and issues a new pair
Refresh tokens are revoked and replaced on each use. Always store the new refresh token returned from the /auth/refresh endpoint.

Security Constraints

Email Domain Restriction

Only email addresses ending with the configured ALLOWED_EMAIL_DOMAIN are permitted. The API enforces this during both the start flow and Slack callback validation.

Slack Workspace Restriction

Only users from the Slack workspace matching ALLOWED_SLACK_TEAM_ID can authenticate. This is validated after fetching the Slack profile.

PKCE Protection

PKCE prevents authorization code interception attacks:
  • The CLI generates a random code_verifier (43-128 characters)
  • Computes code_challenge = SHA256(code_verifier) in base64url
  • Sends only the challenge during /auth/start
  • Proves possession of the verifier during /auth/exchange
  • The API rejects exchanges where SHA256(code_verifier) != stored_code_challenge

Session Expiration

OAuth sessions expire 10 minutes after creation. Login codes must be exchanged before the session expires.

Token Refresh Strategy

To maintain continuous access:
  1. Monitor the expiresInSec value returned with each token pair
  2. Refresh tokens proactively before the access token expires (e.g., at 50-80% of TTL)
  3. Handle 401 INVALID_TOKEN errors by attempting a refresh
  4. If refresh fails, re-authenticate via the full OAuth flow
interface TokenPair {
  accessToken: string;
  refreshToken: string;
  expiresInSec: number;
}

class TokenManager {
  private tokens: TokenPair | null = null;
  private expiresAt: number = 0;

  async getAccessToken(): Promise<string> {
    // Check if token needs refresh (with 2-minute buffer)
    if (Date.now() >= this.expiresAt - 120_000) {
      await this.refresh();
    }
    return this.tokens!.accessToken;
  }

  private async refresh(): Promise<void> {
    const response = await fetch('https://api.example.com/v1/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken: this.tokens!.refreshToken }),
    });

    if (!response.ok) {
      throw new Error('Token refresh failed');
    }

    this.tokens = await response.json();
    this.expiresAt = Date.now() + this.tokens.expiresInSec * 1000;
  }
}

Error Codes

Common authentication error codes:
CodeDescription
EMAIL_NOT_ALLOWEDEmail domain not in ALLOWED_EMAIL_DOMAIN
WORKSPACE_NOT_ALLOWEDSlack team ID does not match ALLOWED_SLACK_TEAM_ID
INVALID_STATEOAuth state parameter is invalid or expired
OAUTH_EXPIREDOAuth session expired (>10 minutes)
EMAIL_MISMATCHAuthenticated email does not match requested email
INVALID_CODE_VERIFIERPKCE code verifier does not match challenge
LOGIN_CODE_EXPIREDLogin code not exchanged within session TTL
INVALID_REFRESH_TOKENRefresh token is invalid, revoked, or expired

Next Steps

Start Flow

Begin the OAuth authentication flow

Token Exchange

Exchange login code for access tokens

Refresh Token

Refresh expired access tokens

Build docs developers (and LLMs) love