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 Case | Auth Method |
|---|---|
| Calling the LLM proxy API | API Key (sk-2389-...) |
| Authenticating users in your app | OAuth 2.1 + PKCE |
| Getting user identity info | OpenID 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-configurationThis 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=S256Parameters
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your registered client ID |
redirect_uri | Yes | Must match a registered redirect URI |
response_type | Yes | Must be code |
scope | Yes | Space-separated scopes (include openid for OIDC) |
state | Recommended | Random value to prevent CSRF |
code_challenge | Yes | PKCE code challenge (S256) |
code_challenge_method | Yes | Must be S256 |
nonce | Optional | Random 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_STATEVerify 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
| Scope | Description |
|---|---|
openid | Required for OIDC. Returns an ID token. |
profile | Access to name and picture |
email | Access to email and email_verified |
offline_access | Returns a refresh token |
llm | Access to user's LLM API keys via Identity API |
preferences | Read/write user preferences via Identity API |
Identity API
The Identity API allows OIDC clients to access user data using access tokens. These endpoints are intended for third-party applications that have user authorization.
Get User Profile
GET /api/v1/profile
Requires openid scope. Returns profile fields based on granted scopes.
curl https://login.2389.ai/api/v1/profile \
-H "Authorization: Bearer ACCESS_TOKEN"Get API Keys
GET /api/v1/keys
Requires llm scope. Returns the user's API key for your application. A unique key is created per (user, client) pair on first access.
curl https://login.2389.ai/api/v1/keys \
-H "Authorization: Bearer ACCESS_TOKEN"Response
{
"keys": [
{
"id": "abc123",
"name": "Created by My App",
"key": "sk-2389-xxxxxxxxxxxx",
"created_at": "2026-01-22T00:00:00.000Z",
"last_used_at": null
}
]
}Note: Each OIDC client receives a unique API key per user. If a user revokes their key in the 2389 dashboard, subsequent calls will return an empty array until they re-authorize your application.
User Preferences
GET /api/v1/preferences - Get all preferences
GET /api/v1/preferences/:key - Get a specific preference
PUT /api/v1/preferences/:key - Set a preference
DELETE /api/v1/preferences/:key - Delete a preference
Requires preferences scope. Store and retrieve user preferences scoped to your application.
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
stateparameter to prevent CSRF attacks - Store tokens securely (encrypted storage for desktop apps)
- Use short-lived access tokens and refresh them as needed
- Validate the
noncein ID tokens if you include one in the request - Only request the scopes your application needs