Documentation
¶
Overview ¶
Package altiumauth is the Altium 365 OAuth2 / OpenID Connect authentication library for Go.
It supports both client types, and Commercial Cloud, GovCloud, and AES (on-prem) deployments:
- Public clients (desktop, CLI, native) sign in through the browser with PKCE over Altium's ActionWait long poll: Config.SignIn.
- Confidential clients (web/server backends with a secret) use the standard authorization-code redirect flow: Config.CreateAuthorizationURL followed by Config.ExchangeCode.
- Workspace tokens (Config.SignIntoWorkspace), refresh (Config.RefreshToken), revocation (Config.RevokeRefreshToken), and scope introspection (GetClientScopes).
Only Config.ClientID and Config.Scopes are required; the endpoints default to CommercialCloudEndpoints. Use GovCloudEndpoints for GovCloud — the library detects the Gov token endpoint and sends secure=1 automatically — or AESEndpoints for an AES installation.
Storage is the caller's concern: the functions return a TokenSet and never persist it. The package has no dependencies outside the standard library.
The behavior is specified by the language-neutral spec at https://github.com/AltiumDeveloper/altium-auth/blob/main/spec/SPEC.md and verified by the shared conformance vectors.
Index ¶
- Constants
- Variables
- func GetClientScopes(ctx context.Context, httpClient *http.Client, scopeEndpoint, clientID string) ([]string, error)
- type AuthorizationError
- type AuthorizationRequest
- type AuthorizationURLOptions
- type Config
- func (c Config) CreateAuthorizationURL(opts AuthorizationURLOptions) (AuthorizationRequest, error)
- func (c Config) ExchangeCode(ctx context.Context, p ExchangeCodeParams) (*TokenSet, error)
- func (c Config) RefreshToken(ctx context.Context, refreshToken string) (*TokenSet, error)
- func (c Config) RevokeRefreshToken(ctx context.Context, refreshToken string) error
- func (c Config) SignIn(ctx context.Context, opts SignInOptions) (*TokenSet, error)
- func (c Config) SignIntoWorkspace(ctx context.Context, baseAccessToken, workspaceAuthID string) (*TokenSet, error)
- type Endpoints
- type ExchangeCodeParams
- type SignInOptions
- type TokenError
- type TokenSet
- type WorkspaceSelection
Examples ¶
Constants ¶
const DefaultSignInTimeout = 3 * time.Minute
DefaultSignInTimeout bounds the whole ActionWait reconnect loop when SignInOptions.Timeout is zero.
Variables ¶
var CommercialCloudEndpoints = Endpoints{
AuthEndpoint: "https://auth.altium.com/connect/authorize",
TokenEndpoint: "https://auth.altium.com/connect/token",
ActionWaitEndpoint: "https://actionwait.altium.com/await",
RedirectURI: "https://auth.altium.com/api/AuthComplete",
}
CommercialCloudEndpoints are the Altium 365 Commercial Cloud (non-Gov) endpoints. They are the defaults for any endpoint a Config omits.
var ErrSignInCancelled = errors.New("altiumauth: Sign-in cancelled")
ErrSignInCancelled is returned by SignIn when the ActionWait wait token is no longer valid (410: consumed or superseded) or the caller's context is cancelled. Restart sign-in with a new token.
var ErrStateMismatch = errors.New("altiumauth: State mismatch during sign-in (possible CSRF attack)")
ErrStateMismatch is returned by SignIn when the state delivered with the authorization code does not match the one it generated (CSRF guard). The code is not exchanged.
var GovCloudEndpoints = Endpoints{
AuthEndpoint: "https://auth.365-gov.altium.com/connect/authorize",
TokenEndpoint: "https://auth.365-gov.altium.com/connect/token",
ActionWaitEndpoint: "https://actionwait.altium.com/await",
RedirectURI: "https://auth.altium.com/api/AuthComplete",
}
GovCloudEndpoints are the Altium 365 GovCloud (Production) endpoints. The library detects the Gov token endpoint and adds secure=1 to token requests automatically.
Only AuthEndpoint and TokenEndpoint are Gov-specific. ActionWait has no Gov DNS: both its poll endpoint and its AuthComplete callback are the Commercial Cloud hosts for the tier (a Gov-host callback is not registered and won't authorize).
var UserAgent = "altium-auth-go/" + moduleVersion()
UserAgent is the User-Agent header sent on every request, e.g. "altium-auth-go/v0.2.0", so Altium services can identify the client.
Functions ¶
func GetClientScopes ¶
func GetClientScopes(ctx context.Context, httpClient *http.Client, scopeEndpoint, clientID string) ([]string, error)
GetClientScopes fetches the scopes registered for a client from the ClientScopes endpoint ({base}/api/ClientScopes). The endpoint itself answers an empty list for an unknown client ID. Anything else unexpected — a non-200 status, or a body that is not a JSON array of strings — is an error: an empty list is a meaningful answer, so a failed lookup must not look like one.
On Commercial/GovCloud this returns only the client's static scopes (e.g. openid, profile). On an AES installation, which hosts exactly one workspace, it also includes that workspace's a365:workspace:{id} scope.
httpClient may be nil to use http.DefaultClient.
Types ¶
type AuthorizationError ¶
AuthorizationError is an OAuth error delivered through the ActionWait callback instead of an authorization code (e.g. "access_denied" when a workspace scope is requested at sign-in on the wrong partition).
func (*AuthorizationError) Error ¶
func (e *AuthorizationError) Error() string
type AuthorizationRequest ¶
type AuthorizationRequest struct {
// URL is the authorization URL to send the user's browser to.
URL string
// State must be persisted and compared with the callback's state (CSRF).
State string
// CodeVerifier must be persisted and passed to ExchangeCode.
CodeVerifier string
}
AuthorizationRequest is a prepared authorization request: where to send the user, and what to keep for the callback.
type AuthorizationURLOptions ¶
type AuthorizationURLOptions struct {
// RedirectURI overrides Config.RedirectURI (e.g. your own callback route).
RedirectURI string
// State overrides the generated state (default: a random UUID).
State string
// CodeVerifier overrides the generated PKCE code verifier.
CodeVerifier string
// Scopes overrides Config.Scopes for this request.
Scopes string
// SelectWorkspace enables login-into-workspace mode. Empty or
// SelectWorkspaceNone omits the parameter.
SelectWorkspace WorkspaceSelection
}
AuthorizationURLOptions customizes Config.CreateAuthorizationURL. The zero value uses the configured redirect URI and scopes, and a fresh state and PKCE verifier.
type Config ¶
type Config struct {
// ClientID is the OAuth2 client ID registered with the Altium identity
// provider.
ClientID string
// Scopes is the space-delimited list of scopes to request (must include
// "openid profile").
Scopes string
// ClientSecret makes this a confidential client (e.g. a backend): token and
// revocation requests then authenticate with HTTP Basic. Leave it empty for
// public clients (desktop/native), which rely on PKCE. Never embed a secret
// in a public client.
ClientSecret string
// Secure overrides GovCloud detection. By default secure=1 is sent on token
// requests when TokenEndpoint is a Gov host and omitted otherwise, so leave
// this nil. Set it only to force behavior for a non-standard host.
Secure *bool
// Endpoints holds the endpoint URLs; empty fields default to
// CommercialCloudEndpoints.
Endpoints
// HTTPClient is used for every request. Defaults to http.DefaultClient.
// Bound request lifetimes with the context passed to each call rather than
// Client.Timeout, which would cut ActionWait long polls short.
HTTPClient *http.Client
}
Config is the OAuth2 client configuration. Only ClientID and Scopes are required; every endpoint defaults to CommercialCloudEndpoints. Set Endpoints to GovCloudEndpoints or AESEndpoints for those deployments, or override individual fields for a custom one. Any endpoint that is set must be an absolute URL.
func (Config) CreateAuthorizationURL ¶
func (c Config) CreateAuthorizationURL(opts AuthorizationURLOptions) (AuthorizationRequest, error)
CreateAuthorizationURL builds an OAuth2 authorization URL with PKCE for the redirect-based (authorization code) flow used by web/server apps that host their own redirect endpoint. It does no network I/O.
Redirect the user to the returned URL and persist State and CodeVerifier (e.g. in the session). On the callback, verify the returned state matches, then call Config.ExchangeCode with the code and the stored verifier.
Example ¶
A web/server app (confidential client) uses the redirect flow.
package main
import (
"net/http"
altiumauth "github.com/AltiumDeveloper/altium-auth/libs/go"
)
func main() {
cfg := altiumauth.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret", // confidential client → HTTP Basic
Scopes: "openid profile offline_access",
}
redirectURI := "https://my-service.example.com/oauth/callback"
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
authz, err := cfg.CreateAuthorizationURL(altiumauth.AuthorizationURLOptions{RedirectURI: redirectURI})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Stash authz.State and authz.CodeVerifier in the user's session, then:
http.Redirect(w, r, authz.URL, http.StatusFound)
})
http.HandleFunc("/oauth/callback", func(w http.ResponseWriter, r *http.Request) {
state, verifier := "from-session", "from-session"
if r.URL.Query().Get("state") != state {
http.Error(w, "state mismatch", http.StatusBadRequest)
return
}
tokens, err := cfg.ExchangeCode(r.Context(), altiumauth.ExchangeCodeParams{
Code: r.URL.Query().Get("code"),
CodeVerifier: verifier,
RedirectURI: redirectURI,
})
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
_ = tokens // persist, then discover/exchange workspaces as usual
})
}
Output:
func (Config) ExchangeCode ¶
ExchangeCode exchanges an authorization code (from your redirect callback) for tokens via the authorization_code grant. Pair it with Config.CreateAuthorizationURL for the redirect-based flow.
func (Config) RefreshToken ¶
RefreshToken exchanges a refresh token for a fresh TokenSet. No scope is sent, so the grant keeps its original scope (a global token stays global, a workspace token stays workspace-scoped). If the response carries a new refresh token (rotation), persist it and discard the old one.
Example ¶
Refresh an expired access token, and handle a revoked or expired refresh token by signing in again.
package main
import (
"context"
"errors"
"log"
altiumauth "github.com/AltiumDeveloper/altium-auth/libs/go"
)
func main() {
cfg := altiumauth.Config{ClientID: "your-client-id", Scopes: "openid profile offline_access"}
tokens := &altiumauth.TokenSet{ /* loaded from storage */ }
if tokens.Expired() && tokens.RefreshToken != "" {
refreshed, err := cfg.RefreshToken(context.Background(), tokens.RefreshToken)
var te *altiumauth.TokenError
if errors.As(err, &te) && te.Code == "invalid_grant" {
// Refresh token expired or revoked: sign in again.
} else if err != nil {
log.Fatal(err)
}
_ = refreshed // persist, including a rotated refresh token
}
}
Output:
func (Config) RevokeRefreshToken ¶
RevokeRefreshToken revokes a refresh token at the revocation endpoint (RFC 7009), derived from the token endpoint (/connect/token → /connect/revocation). Only refresh tokens can be revoked. The call is idempotent: the endpoint answers 200 for known and unknown tokens alike. After revoking, RefreshToken with the same token fails with invalid_grant.
func (Config) SignIn ¶
SignIn performs an OAuth2 PKCE sign-in over Altium's ActionWait long poll — the flow for public clients (desktop/CLI) that cannot host a redirect:
- Generate a PKCE verifier and a connection token, used as the OAuth state.
- Start the ActionWait long poll, then open the browser to the authorize URL.
- Wait for the code, verify the state (CSRF), and exchange it for tokens.
Cancel ctx to abandon sign-in early (ErrSignInCancelled). Persisting the returned TokenSet is the caller's concern. For redirect-based apps, use Config.CreateAuthorizationURL and Config.ExchangeCode instead.
Example ¶
A desktop or CLI app (public client) signs in through the browser over ActionWait, then exchanges the global token for a workspace token.
package main
import (
"context"
"fmt"
"log"
altiumauth "github.com/AltiumDeveloper/altium-auth/libs/go"
)
func main() {
cfg := altiumauth.Config{ClientID: "your-client-id", Scopes: "openid profile offline_access"}
ctx := context.Background()
tokens, err := cfg.SignIn(ctx, altiumauth.SignInOptions{})
if err != nil {
log.Fatal(err)
}
// Persist tokens yourself (keychain, file, …).
ws, err := cfg.SignIntoWorkspace(ctx, tokens.AccessToken, "workspace-auth-id")
if err != nil {
log.Fatal(err)
}
fmt.Println(ws.AccessToken != "")
}
Output:
func (Config) SignIntoWorkspace ¶
func (c Config) SignIntoWorkspace(ctx context.Context, baseAccessToken, workspaceAuthID string) (*TokenSet, error)
SignIntoWorkspace exchanges a global access token (from SignIn or ExchangeCode) for a workspace-scoped token via the token-exchange grant (RFC 8693). Point TokenEndpoint at the Gov host to exchange a Commercial token for a Gov workspace token (secure=1 is added automatically).
type Endpoints ¶
type Endpoints struct {
// AuthEndpoint is the authorization endpoint (/connect/authorize).
AuthEndpoint string
// TokenEndpoint is the token endpoint (/connect/token). A Gov host here
// switches on secure=1 for every token request.
TokenEndpoint string
// ActionWaitEndpoint is the ActionWait long-poll endpoint used by SignIn.
ActionWaitEndpoint string
// RedirectURI is the fixed redirect URI registered with the auth client.
RedirectURI string
// ScopeEndpoint is the client scope introspection endpoint
// ({base}/api/ClientScopes). Optional; see GetClientScopes.
ScopeEndpoint string
}
Endpoints is the set of URLs a client talks to. Any field left empty in a Config falls back to CommercialCloudEndpoints.
func AESEndpoints ¶
AESEndpoints derives the endpoints of an AES (on-prem) installation from its server origin (scheme + host, plus port if non-default), e.g. AESEndpoints("https://aes.example.com:9785").
AES hosts its own ActionWait and AuthComplete callback (unlike Gov, which shares Commercial's) and never uses secure=1 (same rule as Commercial).
Example ¶
AES (on-prem): derive the endpoints from the server origin, discover the installation's workspace scope, and request it at sign-in (one trip).
package main
import (
"context"
"fmt"
"log"
altiumauth "github.com/AltiumDeveloper/altium-auth/libs/go"
)
func main() {
ctx := context.Background()
endpoints := altiumauth.AESEndpoints("https://aes.server.example:9785")
scopes, err := altiumauth.GetClientScopes(ctx, nil, endpoints.ScopeEndpoint, "your-aes-client-id")
if err != nil {
log.Fatal(err)
}
fmt.Println(scopes)
}
Output:
type ExchangeCodeParams ¶
type ExchangeCodeParams struct {
// Code is the authorization code delivered to your redirect callback.
Code string
// CodeVerifier is the PKCE verifier from CreateAuthorizationURL
// (recommended).
CodeVerifier string
// RedirectURI must match the one used in the authorize request. Defaults to
// Config.RedirectURI.
RedirectURI string
}
ExchangeCodeParams are the inputs to Config.ExchangeCode.
type SignInOptions ¶
type SignInOptions struct {
// Timeout bounds the whole ActionWait wait (the user's time to sign in),
// across reconnects. Default: DefaultSignInTimeout.
Timeout time.Duration
// SelectWorkspace enables login-into-workspace mode (SPEC §3.1). See
// AuthorizationURLOptions.SelectWorkspace.
SelectWorkspace WorkspaceSelection
// OpenBrowser overrides how the authorization URL is opened (e.g. a host
// application's own API). By default the library launches the OS browser
// and prints the URL to stderr for manual copy-and-paste. An error aborts
// sign-in.
OpenBrowser func(url string) error
}
SignInOptions customizes Config.SignIn.
type TokenError ¶
type TokenError struct {
StatusCode int
Code string // OAuth "error"; empty if the body was not an OAuth error
Description string // OAuth "error_description"; may be empty
Body string // raw response body (truncated)
}
TokenError is a non-success response from the token endpoint. Code carries the OAuth error (e.g. "access_denied" for a cross-partition workspace exchange, "invalid_grant" for an expired or revoked refresh token).
func (*TokenError) Error ¶
func (e *TokenError) Error() string
type TokenSet ¶
type TokenSet struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
IDToken string `json:"id_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
// ExpiresAt is the epoch second at which the access token expires,
// computed by the library as now + expires_in - 30 (clock-skew buffer).
ExpiresAt int64 `json:"expires_at,omitempty"`
Scope string `json:"scope,omitempty"`
}
TokenSet is the token endpoint response (RFC 6749 / OIDC).
type WorkspaceSelection ¶
type WorkspaceSelection string
WorkspaceSelection controls whether the user is prompted to select a workspace during authorization (login-into-workspace mode, SPEC §3.1).
const ( // SelectWorkspaceNone skips workspace selection; the flow issues a global // access token. Equivalent to leaving the option empty. SelectWorkspaceNone WorkspaceSelection = "none" // SelectWorkspaceStrict makes workspace selection mandatory; the issued // token is workspace-scoped, so no separate exchange is needed. SelectWorkspaceStrict WorkspaceSelection = "strict" // SelectWorkspaceOptional offers workspace selection; the user may skip it. SelectWorkspaceOptional WorkspaceSelection = "optional" )
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
tools
|
|
|
signintest
command
Command signintest verifies sign-in end to end against a live Altium environment.
|
Command signintest verifies sign-in end to end against a live Altium environment. |