Skip to main content
View as Markdown

@altium-developer/altium-auth

npm version npm downloads node CI license

Altium 365 OAuth2 / OpenID Connect authentication library. Supports both client types, and Commercial Cloud, GovCloud, and AES (on-prem) deployments:

  • Public clients (desktop, on-prem, native) — browser sign-in with PKCE over Altium's ActionWait long-poll: signIn.
  • Confidential clients (web/server backends with a secret) — the standard authorization-code redirect flow via composable steps: createAuthorizationUrl + exchangeCode.
  • Workspace tokens, token refresh, and first-class GovCloud and AES (on-prem) support.

Zero runtime dependencies. Runs on Node ≥22, Bun, and Deno (and bundled apps that polyfill Node's crypto).

Documentation​

The library implements the flow described in these guides (protocol-level, independent of this package) — start here if you're new to Altium Identity:

Installation​

npm install @altium-developer/altium-auth

Quick Start​

Only clientId and scopes are required — the endpoints default to the Altium 365 Commercial Cloud. Pick the flow that matches your app.

Public apps (desktop — ActionWait sign-in)​

For desktop clients that can't host a public redirect. signIn opens the browser, waits for the callback over Altium's ActionWait long-poll, and returns tokens. See Desktop apps.

import { signIn, signIntoWorkspace } from "@altium-developer/altium-auth";

const config = {
clientId: "your-client-id",
scopes: "openid profile",
};

// Opens a browser login page and waits for the callback
const tokens = await signIn(config);

// Persist `tokens` yourself — e.g. VS Code SecretStorage, a keychain, or a file.

// Exchange the global token for a workspace-scoped token
const workspaceToken = await signIntoWorkspace(config, tokens.access_token, "workspace-id-here");

The library returns tokens and never stores them — persistence, refresh, and sign-out are entirely yours to manage.

If your host runtime needs to open URLs through a host API, keep using signIn and provide a browser opener. The library still owns PKCE, ActionWait polling, state correlation, CSRF validation, and token exchange:

const tokens = await signIn(config, {
openBrowser: async (url) => {
// Example: VS Code extension host, Electron shell, native app bridge, etc.
await openExternalUrl(url);
},
});

Confidential apps (web / server — authorization-code redirect)​

For web/server backends that host their own redirect endpoint. Set clientSecret on the config to authenticate as a confidential client (HTTP Basic), and drive the flow with two composable steps. See Web / server apps.

import { createAuthorizationUrl, exchangeCode } from "@altium-developer/altium-auth";

const config = {
clientId: "your-client-id",
clientSecret: "your-client-secret", // confidential client → HTTP Basic auth
scopes: "openid profile offline_access",
};
const redirectUri = "https://my-service.example.com/oauth/callback";

// On your login route: build the URL, stash state + verifier, then redirect.
app.get("/login", (req, res) => {
const { url, state, codeVerifier } = createAuthorizationUrl(config, { redirectUri });
req.session.oauth = { state, codeVerifier };
res.redirect(url);
});

// On your callback route: verify state, then exchange the code for tokens.
app.get("/oauth/callback", async (req, res) => {
const { state, codeVerifier } = req.session.oauth;
if (req.query.state !== state) throw new Error("state mismatch");

const tokens = await exchangeCode(config, {
code: String(req.query.code),
codeVerifier,
redirectUri,
});
// Persist `tokens`; then discover/exchange workspaces and refresh as usual.
});

createAuthorizationUrl is synchronous (it generates PKCE + state and returns the URL to redirect to); exchangeCode performs the token exchange. Both signIntoWorkspace and refreshToken also send the clientSecret automatically when it's set.

GovCloud​

Altium GovCloud is an isolated environment for ITAR/regulated workspaces. Use the exported GOV_CLOUD_ENDPOINTS — that's it. The library detects the Gov token endpoint and adds the required secure=1 to token requests automatically (the two-token model); no secure flag to set.

import { signIn, GOV_CLOUD_ENDPOINTS } from "@altium-developer/altium-auth";

const tokens = await signIn({
clientId: "your-gov-client-id",
scopes: "openid profile",
...GOV_CLOUD_ENDPOINTS,
});

Commercial and Gov are kept strictly separate: a global token can only be exchanged for a workspace of the matching kind. secure=1 is driven by which token endpoint you use — Gov endpoint → sent, Commercial endpoint → omitted — so pointing tokenEndpoint at the Gov host is all it takes to exchange a Commercial token for a Gov workspace token.

Gov tokens must never be used against Commercial services, and vice versa. See the GovCloud guide.

AES (on-prem)​

Altium Enterprise Server (AES) is a customer-hosted, on-prem installation — unlike Commercial/GovCloud (fixed Altium-hosted domains), there's no fixed host, so use createAesEndpoints() to derive the endpoint set from your AES server's origin. AES does not use secure=1 (same rule as Commercial), and there's no cross-cloud bridging to/from Commercial or Gov.

import { signIn, createAesEndpoints, getClientScopes } from "@altium-developer/altium-auth";

const clientId = "your-aes-client-id";
const endpoints = createAesEndpoints("https://aes.server.example:9785");
const scopes = await getClientScopes(endpoints.scopeEndpoint, clientId);

const tokens = await signIn({
clientId,
scopes: scopes.join(" "),
...endpoints,
});

See the AES (on-prem) guide.

Custom Deployments​

For other custom deployments, override any of the four endpoints. Anything you omit still falls back to the Commercial Cloud:

import { COMMERCIAL_CLOUD_ENDPOINTS } from "@altium-developer/altium-auth";

const config = {
clientId: "your-client-id",
scopes: "openid profile",
// Point auth + token at a custom deployment; keep the rest on the Commercial Cloud:
authEndpoint: "https://auth.my-custom-deployment.example/connect/authorize",
tokenEndpoint: "https://auth.my-custom-deployment.example/connect/token",
};

// Endpoint presets are exported to inspect or spread:
// COMMERCIAL_CLOUD_ENDPOINTS and GOV_CLOUD_ENDPOINTS.
console.log(COMMERCIAL_CLOUD_ENDPOINTS.tokenEndpoint);

API Reference​

signIn(config, options?)​

Performs OAuth2 PKCE sign-in. Opens the browser to the authorization URL, long-polls for the code callback via ActionWait, exchanges it for tokens, and returns the result.

Does NOT persist tokens or manage auth state — those are your responsibility. You receive a TokenSet back and store it yourself.

Parameters:

ParameterTypeDescription
configOAuthConfigOAuth2 configuration (clientId + scopes required; endpoints default to the Commercial Cloud)
optionsSignInOptionsOptional: timeoutMs (default 180s), AbortSignal for cancellation, selectWorkspace for login-into-workspace mode, and openBrowser for host-specific URL opening

Returns: Promise<TokenSet> — the full token response from the IdP.

createAuthorizationUrl(config, options?)​

Builds an OAuth2 authorization URL with PKCE for the redirect-based (authorization-code) flow. Synchronous — no network I/O.

Parameters:

ParameterTypeDescription
configOAuthConfigSame as signIn
optionsAuthorizationUrlOptionsOptional: redirectUri, state, codeVerifier, scopes, selectWorkspace overrides

Returns: AuthorizationRequest — { url, state, codeVerifier }. Redirect the user to url; persist state and codeVerifier (e.g. in the session) for the callback.

exchangeCode(config, params)​

Exchanges an authorization code (from your redirect callback) for tokens via the authorization_code grant. Sends the clientSecret (HTTP Basic) for confidential clients, or client_id + PKCE codeVerifier for public clients.

Parameters:

ParameterTypeDescription
configOAuthConfigSame as signIn
paramsExchangeCodeParams{ code, codeVerifier?, redirectUri? }

Returns: Promise<TokenSet>.

Throws: Error if code is empty.

signIntoWorkspace(config, baseAccessToken, workspaceAuthId)​

Exchanges a base access token (the access_token from signIn) for a workspace-scoped token using OAuth2's urn:ietf:params:oauth:grant-type:token-exchange grant.

Does NOT cache tokens internally — call it each time you need a fresh token.

Parameters:

ParameterTypeDescription
configOAuthConfigSame as signIn
baseAccessTokenstringThe access_token returned by signIn
workspaceAuthIdstringThe workspace's auth ID from Altium 365

Returns: Promise<TokenSet> — the workspace-scoped token.

Throws: Error if baseAccessToken is empty.

refreshToken(config, refreshToken)​

Exchanges a refresh token for a fresh TokenSet using the OAuth2 refresh_token grant. Use it when an access token has expired (compare TokenSet.expires_at against the current epoch seconds) to avoid a full interactive sign-in.

Works the same for global and workspace refresh tokens — no scope is sent, so the refreshed token keeps whatever grant the refresh token was issued for (a global token stays global; a workspace token stays workspace-scoped).

Parameters:

ParameterTypeDescription
configOAuthConfigSame as signIn
refreshTokenstringA refresh_token from a prior signIn or signIntoWorkspace TokenSet

Returns: Promise<TokenSet> — the refreshed tokens. If the IdP rotates refresh tokens, the response includes a new refresh_token; persist it and discard the old one.

Throws: Error if refreshToken is empty, or if the grant is rejected (e.g. the refresh token expired or was revoked).

Tip: request the offline_access scope at sign-in to receive a refresh_token.

import { refreshToken } from "@altium-developer/altium-auth";

const nowSeconds = Math.floor(Date.now() / 1000);
if (tokens.expires_at && tokens.expires_at <= nowSeconds && tokens.refresh_token) {
tokens = await refreshToken(config, tokens.refresh_token);
// Persist `tokens` again — including a rotated refresh_token if present.
}

revokeRefreshToken(config, refreshToken)​

Revokes a refresh token via the OAuth2 Token Revocation endpoint (RFC 7009) — call it on sign-out to invalidate the grant server-side. The revocation endpoint is derived from the token endpoint (/connect/token → /connect/revocation); only refresh tokens are revoked. Client authentication follows the client type (confidential → HTTP Basic; public → client_id in body).

Parameters:

ParameterTypeDescription
configOAuthConfigSame as signIn
refreshTokenstringThe refresh_token to revoke

Returns: Promise<void>. Per RFC 7009 the call is idempotent (the endpoint returns 200 for both known and unknown tokens).

Throws: Error if refreshToken is empty, or the endpoint returns a non-2xx status. After a successful revoke, a subsequent refreshToken with the same token fails with invalid_grant.

import { revokeRefreshToken } from "@altium-developer/altium-auth";

// On sign-out:
if (tokens.refresh_token) {
await revokeRefreshToken(config, tokens.refresh_token);
}
// ...then discard your local copy of the tokens.

Types​

OAuthConfig​

interface OAuthConfig {
clientId: string; // OAuth2 client ID registered with Altium
scopes: string; // Space-delimited, must include "openid profile"

clientSecret?: string; // Confidential clients only → HTTP Basic auth
secure?: boolean; // Override Gov auto-detection (normally unset)

// Optional — default to COMMERCIAL_CLOUD_ENDPOINTS. Override individually for
// custom installations, spread GOV_CLOUD_ENDPOINTS for GovCloud, or spread
// createAesEndpoints(origin) for an AES (on-prem) installation.
authEndpoint?: string; // default: https://auth.altium.com/connect/authorize
tokenEndpoint?: string; // default: https://auth.altium.com/connect/token
actionWaitEndpoint?: string;// default: https://actionwait.altium.com/await
scopeEndpoint?: string;
redirectUri?: string; // default: https://auth.altium.com/api/AuthComplete
}

TokenSet​

interface TokenSet {
access_token: string;
refresh_token?: string;
id_token?: string;
token_type?: string;
expires_in?: number;
expires_at?: number; // Epoch seconds (computed by library)
scope?: string;
}

access_token is a signed JWT — decode it to read iss, workspaceId, secure, and scopes. See Access token claims.

AuthorizationUrlOptions / SignInOptions​

Both createAuthorizationUrl and signIn accept a selectWorkspace option for login-into-workspace mode:

// In AuthorizationUrlOptions (createAuthorizationUrl) and SignInOptions (signIn):
selectWorkspace?: "none" | "strict" | "optional";
// "strict" — workspace selection mandatory; returned token is workspace-scoped.
// "optional" — workspace selection offered; user may skip.
// "none" or omitted (default) — no workspace prompt; issues a global access token.

// SignInOptions only:
openBrowser?: (url: string) => void | Promise<void>;

Use openBrowser when embedding the library into a host with its own browser API such as VS Code, Electron, or a native app bridge. ActionWait polling remains owned by the library.

Error Handling​

The library throws descriptive Error objects in these scenarios:

ScenarioError message pattern
Missing clientId/scopesOAuthConfig.{field} is required and must be non-empty.
Invalid endpoint URLOAuthConfig.{field} is not a valid URL:
ActionWait timeoutActionWait poll timed out after {n}ms.
Sign-in cancelledSign-in cancelled.
CSRF detectedState mismatch during sign-in (possible CSRF attack).
Empty base token for workspace exchangebaseAccessToken is required — pass the access_token from signIn().
Empty refresh tokenrefreshToken is required — pass the refresh_token from a prior TokenSet.
Empty authorization codecode is required — pass the authorization code from the redirect callback.
Token endpoint returns OAuth errorToken endpoint {status} {error_code} — {description}
ClientScopes lookup failsClientScopes endpoint {status}: / ClientScopes endpoint returned an unexpected body (expected a JSON array of strings):

Compatibility​

  • Node.js ≥22 (engines in package.json) — uses the global fetch, the global Web crypto (randomUUID), and the crypto module (randomBytes, createHash). CI tests Node 22, 24 and 26; Node 20 and older are not supported.
  • Bun / Deno — supported through their Node-compatible fetch and crypto. CI runs the unit tests and the conformance vectors inside the latest Bun and Deno 2.x, and checks that the built package loads in each one.
  • Browsers / bundlers — works where your bundler polyfills Node's crypto and Buffer.

Development​

npm install # install dev dependencies
npm test # run unit tests
npm run test:conformance # run the shared conformance vectors
npm run test:bun # both suites inside Bun (needs bun on PATH)
npm run test:deno # both suites inside Deno (needs deno on PATH)
npm run lint # lint src/
npm run build # compile to dist/

E2E sign-in test​

Run the sign-in flow against a live Altium environment. It opens your browser, waits for the ActionWait callback, and prints the resulting tokens (and optionally exercises workspace exchange and refresh). It drives the built package in dist/, so npm run test:e2e builds first. On Bun or Deno, build, then run the script with the same options: bun scripts/test-signin.ts … or deno run -A scripts/test-signin.ts ….

# Commercial Cloud, public client
npm run test:e2e -- YOUR_CLIENT_ID

# GovCloud (Dev) — verifies the secure=1 two-token model
npm run test:e2e -- --env dev-gov YOUR_GOV_CLIENT_ID

# AES (on-prem) — verifies the origin-derived endpoints, no secure=1
npm run test:e2e -- --env aes --aes-origin https://aes.server.example:9785 YOUR_AES_CLIENT_ID

# Also exchange a workspace token and exercise refresh
npm run test:e2e -- --workspace <authId> --refresh YOUR_CLIENT_ID

# Confidential client (HTTP Basic) — pass the secret via env, never on the CLI
A365_CLIENT_SECRET=... npm run test:e2e -- YOUR_CLIENT_ID

Options include --env prod|dev|gov|dev-gov|aes, --aes-origin (required for --env aes), --workspace-env, --secure/--no-secure, --scopes, --workspace, --refresh, --userinfo, --revoke, plus --authorize-url/--exchange-code/--redirect-uri for confidential (custom-callback) clients — see the header of scripts/test-signin.ts. The client secret is read from A365_CLIENT_SECRET so it never appears in shell history or the process list.

See CONTRIBUTING.md and AGENTS.md for the full contributor guide.

Security​

Please report vulnerabilities privately — see SECURITY.md.

License​

MIT © Altium Limited


Comments​