Documentation2389 Docs

Authentication

2389 ID implements OAuth 2.1 with PKCE and OpenID Connect for secure authentication in desktop and CLI applications.

Overview

For most use cases, you only need an API key to call the LLM proxy. OAuth/OIDC is for applications that need to authenticate users and access their 2389 identity.

When to Use What

Use CaseAuth Method
Calling the LLM proxy APIAPI Key (sk-2389-...)
Authenticating users in your appOAuth 2.1 + PKCE
Getting user identity infoOpenID Connect

OIDC Discovery

The OpenID Connect discovery document is available at the standard well-known endpoint:

GET https://login.2389.ai/api/.well-known/openid-configuration

This returns the full configuration including all supported endpoints and capabilities.

Client Registration

Before using OAuth, register your application using dynamic client registration (RFC 7591):

curl -X POST https://login.2389.ai/api/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My CLI App",
    "redirect_uris": ["http://localhost:8080/callback"],
    "token_endpoint_auth_method": "none"
  }'

Response

{
  "client_id": "abc123...",
  "client_secret": "xyz789...",
  "client_name": "My CLI App",
  "redirect_uris": ["http://localhost:8080/callback"],
  "token_endpoint_auth_method": "none"
}

Note: For desktop/CLI apps (public clients), set token_endpoint_auth_method to "none" and use PKCE for security.

OAuth 2.1 + PKCE Flow

2389 ID requires PKCE (Proof Key for Code Exchange) for all OAuth flows. This is mandatory per OAuth 2.1 specification.

Step 1: Generate PKCE Values

Generate a code_verifier (random string) and derive the code_challenge:

// Generate code_verifier (43-128 characters)
const codeVerifier = generateRandomString(64);

// Derive code_challenge using S256
const codeChallenge = base64url(sha256(codeVerifier));

Step 2: Authorization Request

Redirect the user to the authorization endpoint:

https://login.2389.ai/api/authorize?
  client_id=YOUR_CLIENT_ID&
  redirect_uri=http://localhost:8080/callback&
  response_type=code&
  scope=openid%20profile%20email&
  state=RANDOM_STATE&
  code_challenge=CODE_CHALLENGE&
  code_challenge_method=S256

Parameters

ParameterRequiredDescription
client_idYesYour registered client ID
redirect_uriYesMust match a registered redirect URI
response_typeYesMust be code
scopeYesSpace-separated scopes (include openid for OIDC)
stateRecommendedRandom value to prevent CSRF
code_challengeYesPKCE code challenge (S256)
code_challenge_methodYesMust be S256
nonceOptionalRandom value for ID token replay protection

Step 3: Handle Callback

After the user authenticates, they're redirected to your callback URL with an authorization code:

http://localhost:8080/callback?code=AUTH_CODE&state=RANDOM_STATE

Verify the state matches what you sent, then exchange the code for tokens.

Step 4: Token Exchange

Exchange the authorization code for tokens using the code_verifier:

curl -X POST https://login.2389.ai/api/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "code=AUTH_CODE" \
  -d "redirect_uri=http://localhost:8080/callback" \
  -d "code_verifier=CODE_VERIFIER"

Response

{
  "access_token": "eyJhbG...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBp...",
  "id_token": "eyJhbG...",
  "scope": "openid profile email"
}

Token Refresh

Use the refresh token to get new access tokens without user interaction:

curl -X POST https://login.2389.ai/api/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "refresh_token=REFRESH_TOKEN"

Note: Refresh tokens are rotated on each use. Always store and use the new refresh token from the response.

UserInfo Endpoint

Get user profile information using an access token:

curl https://login.2389.ai/api/userinfo \
  -H "Authorization: Bearer ACCESS_TOKEN"

Response

{
  "sub": "user123",
  "email": "user@example.com",
  "email_verified": true,
  "name": "Jane Doe",
  "picture": "https://..."
}

Supported Scopes

ScopeDescription
openidRequired for OIDC. Returns an ID token.
profileAccess to name and picture
emailAccess to email and email_verified
offline_accessReturns a refresh token

Example: CLI Application

Here's a complete example flow for a CLI application:

import crypto from "crypto";
import http from "http";
import open from "open";

// 1. Generate PKCE values
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
  .createHash("sha256")
  .update(codeVerifier)
  .digest("base64url");

const state = crypto.randomBytes(16).toString("base64url");

// 2. Start local server for callback
const server = http.createServer(async (req, res) => {
  const url = new URL(req.url!, "http://localhost:8080");

  if (url.pathname === "/callback") {
    const code = url.searchParams.get("code");
    const returnedState = url.searchParams.get("state");

    if (returnedState !== state) {
      res.end("State mismatch!");
      return;
    }

    // 3. Exchange code for tokens
    const tokenResponse = await fetch(
      "https://login.2389.ai/api/token",
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        body: new URLSearchParams({
          grant_type: "authorization_code",
          client_id: "YOUR_CLIENT_ID",
          code: code!,
          redirect_uri: "http://localhost:8080/callback",
          code_verifier: codeVerifier,
        }),
      }
    );

    const tokens = await tokenResponse.json();
    console.log("Authenticated!", tokens);

    res.end("Success! You can close this window.");
    server.close();
  }
});

server.listen(8080);

// 4. Open browser for authorization
const authUrl = new URL("https://login.2389.ai/api/authorize");
authUrl.searchParams.set("client_id", "YOUR_CLIENT_ID");
authUrl.searchParams.set("redirect_uri", "http://localhost:8080/callback");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("scope", "openid profile email");
authUrl.searchParams.set("state", state);
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");

open(authUrl.toString());

Security Best Practices

  • Always use PKCE with the S256 method
  • Validate the state parameter to prevent CSRF attacks
  • Store tokens securely (encrypted storage for desktop apps)
  • Use short-lived access tokens and refresh them as needed
  • Validate the nonce in ID tokens if you include one in the request
  • Only request the scopes your application needs