Skip to content

Altium.Auth

CI license

Altium 365 OAuth2 / OpenID Connect authentication client for .NET. Supports both client types and both clouds:

  • Public clients (desktop, native) — browser sign-in with PKCE over Altium's ActionWait long-poll: SignInAsync.
  • Confidential clients (web/server backends with a secret) — the standard authorization-code redirect flow via composable steps: CreateAuthorizationUrl
  • ExchangeCodeAsync.
  • Workspace tokens, token refresh / revocation, and first-class Gov Cloud support.

Zero dependencies, net8.0. Validated against the same language-neutral conformance vectors as the TypeScript library — see How it's built.

Documentation

The client implements the flow described in these protocol-level guides (independent of this package):

Quick start

Only ClientId and Scopes are required — endpoints default to the Altium 365 Commercial Cloud. You supply the HttpClient (reuse one / use IHttpClientFactory).

Public apps (desktop / native — ActionWait sign-in)

For apps that can't host a public redirect. SignInAsync invokes your OpenBrowser callback, waits for the callback over ActionWait, and returns tokens.

using Altium.Auth;
using System.Diagnostics;

var http = new HttpClient();
var options = new AltiumAuthOptions
{
    ClientId = "your-client-id",
    Scopes = "openid profile",
    OpenBrowser = url => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }),
};
var client = new AltiumAuthClient(http, options);

// Opens the browser and waits for the sign-in callback.
TokenSet tokens = await client.SignInAsync();

// Persist `tokens` yourself — the client never stores them.

// Exchange the global token for a workspace-scoped token.
TokenSet workspaceToken = await client.SignIntoWorkspaceAsync(tokens.AccessToken, "workspace-id-here");

To prompt the user to select a workspace during sign-in (login-into-workspace mode), pass selectWorkspace:

// WorkspaceSelection.Strict   → user must choose a workspace (token is workspace-scoped after exchange)
// WorkspaceSelection.Optional → selection offered; the user may skip it
TokenSet tokens = await client.SignInAsync(WorkspaceSelection.Optional);

See Login-into-workspace mode.

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

For backends that host their own redirect endpoint. Set ClientSecret to authenticate as a confidential client (HTTP Basic) and drive the flow with two steps.

var options = new AltiumAuthOptions
{
    ClientId = "your-client-id",
    ClientSecret = "your-client-secret", // confidential client → HTTP Basic
    Scopes = "openid profile offline_access",
};
var client = new AltiumAuthClient(http, options);
var redirectUri = "https://my-service.example.com/oauth/callback";

// On your login route: build the URL, stash state + verifier, then redirect.
AuthorizationRequest authz = client.CreateAuthorizationUrl(redirectUri);
// Save authz.State + authz.CodeVerifier (e.g. in the session); redirect to authz.Url.

// On your callback route: verify state matches, then exchange the code.
TokenSet tokens = await client.ExchangeCodeAsync(code, authz.CodeVerifier, redirectUri);

CreateAuthorizationUrl is synchronous (generates PKCE + state); ExchangeCodeAsync performs the token exchange. SignIntoWorkspaceAsync, RefreshTokenAsync, and RevokeRefreshTokenAsync all apply the ClientSecret automatically when it's set.

Gov Cloud

Use the AltiumEndpoints.GovCloud preset — that's it. The client detects the Gov token endpoint and adds the required secure=1 to token requests automatically (the two-token model); no flag to set.

var options = new AltiumAuthOptions
{
    ClientId = "your-gov-client-id",
    Scopes = "openid profile",
    Endpoints = AltiumEndpoints.GovCloud,
    OpenBrowser = url => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }),
};

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, so pointing the token endpoint at the Gov host is all it takes to exchange a Commercial token for a Gov workspace token. See docs/gov-cloud.md.

Refresh & sign-out

// Refresh when the access token has expired (compare TokenSet.ExpiresAt to now).
if (tokens.ExpiresAt is long exp && exp <= DateTimeOffset.UtcNow.ToUnixTimeSeconds()
    && tokens.RefreshToken is not null)
{
    tokens = await client.RefreshTokenAsync(tokens.RefreshToken);
    // Persist again — including a rotated RefreshToken if present.
}

// On sign-out: revoke the refresh token server-side, then discard your local copy.
if (tokens.RefreshToken is not null)
    await client.RevokeRefreshTokenAsync(tokens.RefreshToken);

API reference

Constructor: new AltiumAuthClient(HttpClient http, AltiumAuthOptions options) — implements IAltiumAuthClient.

Member Description
SignInAsync(selectWorkspace, ct) Public-client PKCE sign-in via ActionWait. Uses OpenBrowser to launch the authorization URL. selectWorkspace is a WorkspaceSelection (None default, Strict, Optional). Returns TokenSet.
CreateAuthorizationUrl(redirectUri?, state?, codeVerifier?, selectWorkspace) Build the authorize URL for the redirect flow. Returns AuthorizationRequest. Synchronous. selectWorkspace is a WorkspaceSelection (None default, Strict, Optional).
ExchangeCodeAsync(code, codeVerifier?, redirectUri?, ct) Exchange an authorization code for tokens.
SignIntoWorkspaceAsync(baseAccessToken, workspaceAuthId, ct) Workspace-scoped token via RFC 8693 token-exchange.
RefreshTokenAsync(refreshToken, ct) Refresh via the refresh_token grant (sends no scope — retains the original grant).
RevokeRefreshTokenAsync(refreshToken, ct) Revoke a refresh token (RFC 7009); idempotent.

AltiumAuthOptions

public sealed class AltiumAuthOptions
{
    public required string ClientId { get; init; }
    public required string Scopes { get; init; }          // must include "openid profile"
    public string? ClientSecret { get; init; }            // confidential clients → HTTP Basic
    public AltiumEndpoints Endpoints { get; init; }        // default: AltiumEndpoints.CommercialCloud
    public bool? Secure { get; init; }                     // override Gov auto-detection (normally null)
    public Action<string>? OpenBrowser { get; init; }      // invoked by SignInAsync to open the URL
}

AltiumEndpoints

A record of the four endpoints (AuthorizeEndpoint, TokenEndpoint, ActionWaitEndpoint, RedirectUri) with two presets: AltiumEndpoints.CommercialCloud (default) and AltiumEndpoints.GovCloud. Construct your own for on-prem or custom hosts.

TokenSet

public sealed class TokenSet
{
    public string AccessToken { get; set; }
    public string? TokenType { get; set; }
    public int? ExpiresIn { get; set; }
    public long? ExpiresAt { get; set; }   // epoch seconds (computed by the client)
    public string? RefreshToken { get; set; }
    public string? IdToken { get; set; }
    public string? Scope { get; set; }
}

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

Error handling

Methods throw on empty required arguments and on non-success responses. The message includes the HTTP status and the OAuth error/error_description when present — e.g. a Gov workspace exchange on a Commercial endpoint surfaces access_denied; a refresh with a revoked/expired token surfaces invalid_grant. ActionWait failures surface a descriptive message (timeout, cancellation, or a CSRF state mismatch).

Compatibility

  • .NET 8.0+ (net8.0). Dependency-free.
  • You provide the HttpClient; the client sets headers/bodies but does not own the transport.

How it's built

Altium.Auth is dependency-free by design: it acquires tokens and never validates JWTs, so it needs no OIDC/JWT library. Its behavior is pinned by the shared, language-neutral vectors in spec/conformance/vectors.json — the exact contract the TypeScript library passes. This proves the two implementations are behavior-identical. Spec: spec/SPEC.md.

Development

Open Altium.Auth.sln in your IDE (library, tests, and tools grouped into src/tests/tools folders), or from the CLI:

# Build / test the whole solution
dotnet build Altium.Auth.sln -c Release
dotnet test Altium.Auth.sln -c Release      # runs the xUnit conformance suite

# Or target a single project:
dotnet test tests/Altium.Auth.Tests -c Release

The shipped library (src/Altium.Auth) builds with analyzers + TreatWarningsAsErrors (see .editorconfig), so dotnet build is the style/quality gate.

Live E2E against a real environment

tools/SignInTest runs the real flow (ActionWait sign-in, workspace exchange, refresh, revoke) against a live environment — needs network + a browser:

# Commercial, public client
dotnet run --project tools/SignInTest -- YOUR_CLIENT_ID

# Dev Gov — verifies the secure=1 two-token model
dotnet run --project tools/SignInTest -- --env dev-gov YOUR_GOV_CLIENT_ID

# Confidential client (HTTP Basic) — secret via env, never on the CLI
A365_CLIENT_SECRET=... dotnet run --project tools/SignInTest -- --workspace <authId> --revoke YOUR_CLIENT_ID

Options mirror the TS harness: --env prod|dev|gov|dev-gov, --workspace-env, --secure/--no-secure, --scopes, --workspace, --refresh, --revoke, --userinfo, and --authorize-url/--exchange-code/--code-verifier/--redirect-uri for confidential/custom-callback clients.

See CONTRIBUTING.md and AGENTS.md for the repo-wide, spec-first contribution model.

Security

Please report vulnerabilities privately — see SECURITY.md.

License

MIT © Altium Limited

Comments