Authenticate a web or server application¶
This guide walks through the standard authentication flow for applications that can host an HTTPS redirect endpoint. See Key terms for the token vocabulary used here.
Prerequisites: a registered application with a client_id, client_secret, and redirect URL. See Register your application.
Flow at a glance¶
sequenceDiagram
participant User
participant App
participant Identity as Altium Identity
participant API as Altium 365 API
User->>App: Start sign-in
App->>Identity: 1️⃣ Authorize (openid profile) + PKCE challenge
Identity->>User: Sign-in + consent
Identity-->>App: Redirect with authorization code
App->>Identity: 2️⃣ Exchange code + PKCE verifier
Identity-->>App: Global access token
App->>API: 3️⃣ desWorkspaceInfos (global token)
API-->>App: Workspaces
User->>App: Choose a workspace
App->>Identity: 4️⃣ Token exchange (a365:workspace:{id})
Identity-->>App: Workspace access token + (optional) refresh token
App->>API: API calls (workspace token)
Step 1 — Request authorization (with PKCE)¶
Generate a PKCE code_verifier (a high-entropy random string) and derive the code_challenge as the base64url-encoded SHA-256 of the verifier. Redirect the user to:
GET https://auth.altium.com/connect/authorize
?client_id=<client_id>
&response_type=code
&scope=openid+profile
&redirect_uri=https%3A%2F%2Fyour-app.example.com%2Foauth%2Fcallback
&code_challenge=<code_challenge>
&code_challenge_method=S256
&state=<opaque_state>
redirect_uriis the callback URL you registered with your application.stateis an opaque value you generate and later verify to protect against CSRF.
Login-into-workspace mode (selectWorkspace)¶
Add the optional selectWorkspace parameter to the authorize request to have the code exchange return a workspace-scoped token directly — skipping the discover-and-exchange steps below. See Login-into-workspace mode for the values (none / strict / optional) and behavior.
GET https://auth.altium.com/connect/authorize
?client_id=<client_id>
&response_type=code
&scope=openid+profile+offline_access
&redirect_uri=https%3A%2F%2Fyour-app.example.com%2Foauth%2Fcallback
&code_challenge=<code_challenge>
&code_challenge_method=S256
&state=<opaque_state>
&selectWorkspace=optional
After sign-in and consent, Altium Identity redirects to your callback:
If the user declines consent, the callback carries an error instead:
GET https://your-app.example.com/oauth/callback?error=consent_required&state=<opaque_state>&session_state=<session_state>
Step 2 — Exchange the code for a global access token¶
This guide authenticates as a confidential client using Authorization: Basic <base64(client_id:client_secret)>. Public clients (desktop/native/SPA) have no secret — they omit the Basic header and authenticate with client_id plus the PKCE code_verifier instead. See Authenticate a desktop or on-prem application.
POST https://auth.altium.com/connect/token
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<code>
&code_verifier=<code_verifier>
&redirect_uri=https%3A%2F%2Fyour-app.example.com%2Foauth%2Fcallback
cURL:
curl -X POST https://auth.altium.com/connect/token \
-u "<client_id>:<client_secret>" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<code>" \
-d "code_verifier=<code_verifier>" \
--data-urlencode "redirect_uri=https://your-app.example.com/oauth/callback"
Response:
{
"id_token": "...",
"access_token": "...",
"expires_in": 14400,
"token_type": "Bearer",
"scope": "openid profile"
}
The access_token is your global access token.
Read the user's profile (optional)¶
The id_token already carries identity claims. For up-to-date claims, call the userinfo endpoint with the access token:
cURL:
Response (a JSON object of claims; the exact set depends on the granted scopes and the user's profile):
{
"sub": "00000000-0000-0000-0000-000000000000",
"username": "jane.engineer@example.com",
"email": "jane.engineer@example.com",
"given_name": "Jane",
"family_name": "Engineer",
"organization_id": "11111111-1111-1111-1111-111111111111",
"picture": "https://ids.api.altium.com/UserPic/?picName=cus_<sub>&size=px128x128"
}
sub is the stable user identifier — the same value that appears in the id_token.
Step 3 — Discover the user's workspaces¶
Query the Altium 365 API with the global access token to list workspaces, then let the user choose one. Include location.name — it tells you whether a workspace lives in Gov Cloud, which decides where you exchange the token in Step 4.
A Gov Cloud workspace reports location.name = "US GovCloud":
{
"authId": "110ee681-0f50-48e4-a1d1-0e7466ab8682",
"name": "OnShape GOV",
"url": "https://altium-inc-8685.365-gov.altium.com/",
"location": {
"name": "US GovCloud"
}
}
Use the chosen workspace's authId as {workspaceId} in the next step, and its location.name to pick the exchange endpoint. Checking for a GovCloud location.name is the current recommended way to tell Gov workspaces apart.
See the API examples.
Step 4 — Exchange for a workspace access token¶
Exchange the global access token for a token scoped to the chosen workspace, at the endpoint that matches the workspace's location (Step 3). Requesting offline_access also returns a refresh token. In both variants subject_token is the same global access token from Step 2.
Commercial workspace — exchange on auth.altium.com:
POST https://auth.altium.com/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&scope=openid offline_access a365:workspace:<workspaceId>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&subject_token=<global access token>
&client_id=<client_id>
&client_secret=<client_secret>
Gov Cloud workspace (location.name = "US GovCloud") — exchange on auth.365-gov.altium.com and add secure=1:
POST https://auth.365-gov.altium.com/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&scope=openid offline_access a365:workspace:<workspaceId>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&subject_token=<global access token>
&secure=1
&client_id=<client_id>
&client_secret=<client_secret>
Presenting your Commercial global token to the Gov /token endpoint with secure=1 mints a Gov workspace token — one whose iss is https://auth.365-gov.altium.com and which carries a secure claim. Omitting secure=1 on the Gov endpoint — or exchanging a Gov workspace on auth.altium.com — returns access_denied. See Gov Cloud considerations.
cURL (Commercial workspace; for a Gov workspace, target https://auth.365-gov.altium.com and add --data-urlencode "secure=1"):
curl -X POST https://auth.altium.com/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
--data-urlencode "scope=openid offline_access a365:workspace:<workspaceId>" \
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
--data-urlencode "subject_token=<global access token>" \
--data-urlencode "client_id=<client_id>" \
--data-urlencode "client_secret=<client_secret>"
Response (both variants):
{
"access_token": "...",
"expires_in": 14400,
"token_type": "Bearer",
"refresh_token": "...",
"scope": "a365:workspace:<workspaceId> offline_access openid"
}
The access_token is your workspace access token. Use it to call the Altium 365 API for that workspace:
Shortcut when you already know the workspace. Request the workspace scope directly in Step 1 — set
scope=openid profile offline_access a365:workspace:<workspaceId>in the authorize request — and the code exchange in Step 2 returns the workspace token (and refresh token) directly, skipping Steps 3–4. Run the whole flow on the host that matches the workspace:auth.altium.comfor a Commercial workspace, orauth.365-gov.altium.comfor a Gov Cloud workspace (sendsecure=1on the/connect/tokenrequests only). Use the discover-then-exchange flow (Steps 3–4) when the user chooses a workspace at runtime.Alternatively, use
selectWorkspace=strict(oroptional) in Step 1 to have the server present workspace selection during sign-in — the user picks from their own workspaces without your app needing to enumerate them first. See Login-into-workspace mode.
Refresh the workspace access token¶
When the workspace access token expires, use the refresh token to get a new one. Refresh at the endpoint that issued the token: a Commercial workspace token refreshes on auth.altium.com; a Gov Cloud workspace token refreshes on auth.365-gov.altium.com with secure=1.
POST https://auth.altium.com/connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=<refresh_token>
&client_id=<client_id>
&client_secret=<client_secret>
Response:
{
"id_token": "...",
"access_token": "...",
"expires_in": 14400,
"token_type": "Bearer",
"refresh_token": "...",
"scope": "a365:workspace:<workspaceId> offline_access openid"
}
Revoke a refresh token¶
Only refresh tokens can be revoked. Access tokens are self-contained (JWT) and are validated by signature rather than looked up per request, so they cannot be revoked — an issued access token stays valid until it expires. To cut a user off, revoke their refresh token (which stops it minting new access tokens) and discard the current access token; keep access-token lifetimes short.
POST the refresh token to the revocation endpoint, authenticating with your client credentials:
POST https://auth.altium.com/connect/revocation
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded
token=<refresh_token>
&token_type_hint=refresh_token
cURL:
curl -X POST https://auth.altium.com/connect/revocation \
-u "<client_id>:<client_secret>" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=<refresh_token>" \
-d "token_type_hint=refresh_token"
token— the refresh token to revoke.token_type_hint—refresh_token.
A successful request returns 200 OK with an empty body. Per the revocation standard, the endpoint also returns 200 for an unknown or already-invalid token, so a success response does not confirm the token previously existed.