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 |
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