altium-auth (Python)¶
Altium 365 OAuth2 / OpenID Connect authentication for Python. Supports both client types across Commercial Cloud, GovCloud, and AES (on-prem):
- Public clients (desktop, native) — browser sign-in with PKCE over Altium's ActionWait long-poll:
AltiumAuthClient.sign_in. - Confidential clients (web/server backends) — the standard authorization-code redirect flow:
create_authorization_url+exchange_code. - Workspace tokens, refresh, revocation, and first-class GovCloud + AES support.
Zero runtime dependencies (stdlib only). Python 3.10+.
Documentation¶
The library implements the protocol described in the language-neutral guides (start here if you're new to Altium Identity):
- Authentication overview
- Register your application
- Web / server apps
- Desktop apps
- GovCloud
- AES (on-prem)
- Access token claims
Installation¶
Quick start¶
Construct an AltiumAuthClient once with an AltiumAuthConfig. Only client_id and scopes are required — endpoints default to the Commercial Cloud.
Public apps (desktop — ActionWait sign-in)¶
from altium_auth import AltiumAuthClient, AltiumAuthConfig
client = AltiumAuthClient(AltiumAuthConfig(client_id="your-client-id", scopes="openid profile"))
# Opens a browser login page and waits for the callback.
tokens = client.sign_in()
# Persist `tokens` yourself — a keyring, an OS credential store, or a file.
workspace = client.sign_into_workspace(tokens.access_token, "workspace-id-here")
Provide a custom browser opener (e.g. an IDE/host bridge) via config; the library still owns PKCE, ActionWait polling, state correlation, CSRF validation, and the token exchange:
config = AltiumAuthConfig(
client_id="your-client-id",
scopes="openid profile",
open_browser=lambda url: my_host.open_external(url),
)
Confidential apps (web / server — authorization-code redirect)¶
from altium_auth import AltiumAuthClient, AltiumAuthConfig
client = AltiumAuthClient(
AltiumAuthConfig(
client_id="your-client-id",
client_secret="your-client-secret", # confidential client → HTTP Basic
scopes="openid profile offline_access",
)
)
redirect_uri = "https://my-service.example.com/oauth/callback"
# On your login route: build the URL, stash state + verifier, then redirect.
req = client.create_authorization_url(redirect_uri=redirect_uri)
session["oauth"] = {"state": req.state, "code_verifier": req.code_verifier}
# redirect(req.url)
# On your callback route: verify state, then exchange the code.
if request.args["state"] != session["oauth"]["state"]:
raise ValueError("state mismatch")
tokens = client.exchange_code(
request.args["code"],
code_verifier=session["oauth"]["code_verifier"],
redirect_uri=redirect_uri,
)
GovCloud¶
from altium_auth import AltiumAuthClient, AltiumAuthConfig, GOV_CLOUD_ENDPOINTS
client = AltiumAuthClient(
AltiumAuthConfig(
client_id="your-gov-client-id",
scopes="openid profile",
endpoints=GOV_CLOUD_ENDPOINTS,
)
)
tokens = client.sign_in() # secure=1 is added to token requests automatically
AES (on-prem)¶
from altium_auth import AltiumAuthClient, AltiumAuthConfig, aes_endpoints
endpoints = aes_endpoints("https://aes.server.example:9785")
scopes = AltiumAuthClient.get_client_scopes(endpoints.scope_endpoint, "your-aes-client-id")
client = AltiumAuthClient(
AltiumAuthConfig(
client_id="your-aes-client-id",
scopes=" ".join(scopes),
endpoints=endpoints,
)
)
tokens = client.sign_in()
Using from async code¶
The library is synchronous. From an event loop, bridge with the stdlib:
API reference¶
| Method | Description |
|---|---|
create_authorization_url(*, redirect_uri=None, state=None, code_verifier=None, scopes=None, select_workspace=WorkspaceSelection.NONE) |
Build a PKCE authorization URL (no I/O). Returns AuthorizationRequest(url, state, code_verifier). |
exchange_code(code, *, code_verifier=None, redirect_uri=None, timeout=30.0) |
Authorization-code grant → TokenSet. |
sign_in(*, select_workspace=WorkspaceSelection.NONE, timeout=180.0) |
Full ActionWait sign-in → TokenSet. |
sign_into_workspace(base_access_token, workspace_auth_id, *, timeout=30.0) |
RFC 8693 workspace exchange → TokenSet. |
refresh_token(refresh_token, *, timeout=30.0) |
Refresh grant (no scope resent) → TokenSet. |
revoke_refresh_token(refresh_token, *, timeout=30.0) |
RFC 7009 revocation (idempotent). |
AltiumAuthClient.get_client_scopes(scope_endpoint, client_id, *, timeout=30.0) |
Static; scope introspection → list[str]. |
Types¶
AltiumAuthConfig(client_id, scopes, client_secret=None, endpoints=COMMERCIAL_CLOUD_ENDPOINTS, secure=None, open_browser=None)— propertiesis_confidential,use_secure.AltiumEndpoints(authorize_endpoint, token_endpoint, action_wait_endpoint, redirect_uri, scope_endpoint=None)— constantsCOMMERCIAL_CLOUD_ENDPOINTS,GOV_CLOUD_ENDPOINTS; factoryaes_endpoints(origin).TokenSet(access_token, token_type, expires_in, expires_at, refresh_token, id_token, scope)—expires_atcomputed with a 30 s clock-skew buffer.WorkspaceSelection—NONE/STRICT/OPTIONAL.
access_tokenis a signed JWT — decode it to readiss,workspaceId,secure, and scopes. See Access token claims.
Error handling¶
All errors subclass AltiumAuthError:
| Class | When |
|---|---|
ConfigurationError |
Missing client_id/scopes, invalid endpoint URL, or empty required argument. |
OAuthError |
Token/revocation/scope endpoint returned a non-success status (carries .status, .error, .error_description). |
ActionWaitError |
ActionWait timed out, was cancelled (410), or returned an unusable body. |
StateMismatchError |
Returned state ≠ wait token (CSRF guard). Subclass of ActionWaitError. |
TransportError |
Network-level failure. |
Development¶
Live E2E sign-in¶
uv run python tools/signin_test.py YOUR_CLIENT_ID
uv run python tools/signin_test.py --env gov YOUR_GOV_CLIENT_ID
uv run python tools/signin_test.py --env aes --aes-origin https://aes.server.example:9785 YOUR_AES_CLIENT_ID
Security¶
Report vulnerabilities privately — see SECURITY.md.
License¶
MIT © Altium Limited