Altium.Auth 0.2.0

dotnet add package Altium.Auth --version 0.2.0
                    
NuGet\Install-Package Altium.Auth -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Altium.Auth" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Altium.Auth" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Altium.Auth" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Altium.Auth --version 0.2.0
                    
#r "nuget: Altium.Auth, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Altium.Auth@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Altium.Auth&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Altium.Auth&version=0.2.0
                    
Install as a Cake Tool

Altium.Auth

CI license

Altium 365 OAuth2 / OpenID Connect authentication client for .NET. Supports both client types as well as the different deployment types: Commercial Cloud, GovCloud, and AES (on-prem).

  • Public clients (desktop) — 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 and ExchangeCodeAsync.
  • Workspace tokens, token refresh / revocation, and first-class GovCloud and AES (on-prem) support.

Zero dependencies, net8.0. Validated against the same language-neutral conformance vectors as the TypeScript library — see <a href="#how-its-built">How it's built</a>.

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

GovCloud

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 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 AltiumEndpoints.Aes(origin) 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.

var clientId = "your-aes-client-id";
var endpoints = AltiumEndpoints.Aes("https://aes.server.example:9785");

// AES hosts a single workspace — introspect the exact scope to request for it.
var scopesArray = await AltiumAuthClient.GetClientScopesAsync(http, endpoints.ScopeEndpoint!, clientId);
var scopes = string.Join(" ", scopesArray);

var options = new AltiumAuthOptions
{
    ClientId = clientId,
    Scopes = scopes,
    Endpoints = endpoints,
    OpenBrowser = url => Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }),
};

See the AES (on-prem) guide.

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 endpoints (AuthorizeEndpoint, TokenEndpoint, ActionWaitEndpoint, RedirectUri, ScopeEndpoint) with two fixed presets — AltiumEndpoints.CommercialCloud (default) and AltiumEndpoints.GovCloud — plus AltiumEndpoints.Aes(origin), a factory that derives the endpoint set for an AES (on-prem) installation from its server origin, including ScopeEndpoint for scope introspection — use AltiumAuthClient.GetClientScopesAsync (GET with a clientId query parameter) to discover the exact a365:workspace:{id} scope for the installation's single workspace. ScopeEndpoint is null in the Cloud presets: the endpoint exists there too ({base}/api/ClientScopes), but a Cloud response carries no a365:workspace:{id} scope — a Cloud client may reach many workspaces, none implied by its client ID — so set it yourself if you want the client's static scopes. Construct your own record for other 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

# AES (on-prem) — verifies the origin-derived endpoints, no secure=1
dotnet run --project tools/SignInTest -- --env aes --aes-origin https://aes.server.example:9785 YOUR_AES_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|aes, --aes-origin (required for --env aes), --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

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 135 9/4/2026