Documentation
¶
Overview ¶
Package msauth is a small, pure-Go (cgo-free) helper for Microsoft-identity delegated authentication, built on the Microsoft Authentication Library (MSAL) for Go.
It exists to make CLI and desktop tools sign a user in to a personal Microsoft Account (or a work/school account) and obtain delegated access tokens, without every tool re-implementing the OAuth2 dance. It provides:
- interactive Authorization-Code + PKCE login over a robust, dual-stack loopback redirect (works around MSAL Go binding only one of 127.0.0.1/::1, which breaks the redirect on macOS),
- device-code login (useful for work/school accounts; note that Microsoft rejects device-code for personal accounts),
- silent token refresh using a persisted token cache, and
- a pluggable cache.Store with a macOS Keychain implementation and an encrypted-file fallback.
It is scope-agnostic: callers pass whatever delegated scopes they need (Microsoft Graph scopes, Outlook resource scopes, etc.). It is the shared auth foundation for the carteiro program (outlook-go and friends).
The default client id is the well-known first-party "Microsoft Graph Command Line Tools" public client, which accepts personal Microsoft Accounts with no app registration. Override it with your own registration via Config.
Index ¶
- Constants
- Variables
- type AccessToken
- type AccountInfo
- type AccountRef
- type Authenticator
- func (a *Authenticator) Account(ctx context.Context) (*AccountInfo, error)
- func (a *Authenticator) Accounts(ctx context.Context) ([]AccountInfo, error)
- func (a *Authenticator) Identity(ctx context.Context) (*Identity, error)
- func (a *Authenticator) Login(ctx context.Context, opts ...LoginOption) (*Identity, error)
- func (a *Authenticator) LoginAccount(ctx context.Context, opts ...LoginOption) (*AccountInfo, error)
- func (a *Authenticator) Logout(ctx context.Context) error
- func (a *Authenticator) Token(ctx context.Context, opts ...TokenOption) (string, error)
- func (a *Authenticator) TokenInfo(ctx context.Context, opts ...TokenOption) (AccessToken, error)
- func (a *Authenticator) TokenSource(scopes ...string) *TokenSource
- type Config
- type Identity
- type LoginOption
- type TokenOption
- type TokenSource
Constants ¶
const ( // DefaultAuthority targets personal Microsoft Accounts only. Use // "https://login.microsoftonline.com/common" to also accept work/school // accounts. DefaultAuthority = "https://login.microsoftonline.com/consumers" // GraphCLIClientID is Microsoft's well-known first-party public client, // "Microsoft Graph Command Line Tools". It has http://localhost registered // and grants dynamic delegated Microsoft Graph scopes. It was proven (the // carteiro Gate-0 probe) to accept personal Microsoft Account sign-in with // no app registration, which is why it is the default. Override it with your // own registration for anything distributed. GraphCLIClientID = "14d82eec-204b-4c2f-b7e8-296a70dab67e" // DefaultLoginTimeout bounds an interactive or device-code login. DefaultLoginTimeout = 3 * time.Minute )
Variables ¶
var ErrAccountAmbiguous = errors.New("msauth: exact account reference is ambiguous")
ErrAccountAmbiguous reports multiple cache entries matching an exact selector.
var ErrAccountMismatch = errors.New("msauth: account context mismatch")
ErrAccountMismatch reports a conflicting authority or authenticated identity.
var ErrAccountRemovalUnsafe = errors.New("msauth: exact-bound account removal is unsupported")
ErrAccountRemovalUnsafe reports that MSAL cannot guarantee tenant-scoped removal. Bound authenticators return it before IO. Only an isolated store owner may explicitly use an unbound authenticator to clear that namespace.
var ErrBoundLogin = errors.New("msauth: exact-bound account requires staged login")
ErrBoundLogin reports interactive login on an exact-bound authenticator. Stage login with New and a separate Store, validate LoginAccount's result, then publish that candidate instead of modifying the existing connection.
var ErrInvalidAccountRef = errors.New("msauth: invalid exact account reference")
ErrInvalidAccountRef reports an incomplete or conflicting exact selector.
var ErrLoginRequired = errors.New("msauth: login required")
ErrLoginRequired is returned by Authenticator.Token and Authenticator.Identity when there is no usable cached account (never logged in, cache wiped, or the refresh token has expired/been revoked). Callers should prompt the user to run login. It is joined with the underlying MSAL error when a silent refresh fails, so errors.Is(err, ErrLoginRequired) is true while the cause remains inspectable.
var ErrNoScopes = errors.New("msauth: no scopes requested (set Config.Scopes or use WithScopes/WithLoginScopes)")
ErrNoScopes is returned by Login and Token when no delegated scopes are set (neither Config.Scopes nor a WithScopes/WithLoginScopes option). MSAL rejects empty scopes with an opaque message on login and, worse, silently misses the cache on Token; this turns that into a clear, actionable error.
Functions ¶
This section is empty.
Types ¶
type AccessToken ¶ added in v0.2.0
type AccessToken struct {
// Value is the bearer token to put in an Authorization header.
Value string
// ExpiresOn is when the token stops being accepted, in UTC.
ExpiresOn time.Time
// GrantedScopes are the scopes the service actually issued, which can be a
// subset of those requested when the user declines part of the consent.
GrantedScopes []string
}
AccessToken is a delegated access token together with the metadata a caller needs to reason about it. Authenticator.Token returns only the bearer string, which is enough to send a request but not enough to decide whether a token is about to expire or which scopes consent actually granted.
func (AccessToken) Expired ¶ added in v0.2.0
Expired reports whether the token is expired, treating anything within skew of expiry as already expired. A non-positive skew disables the margin.
func (AccessToken) HasScope ¶ added in v0.2.0
func (t AccessToken) HasScope(scope string) bool
HasScope reports whether scope was granted. Comparison is case-insensitive because Entra echoes scopes back with inconsistent casing.
type AccountInfo ¶ added in v0.4.0
type AccountInfo struct {
Ref AccountRef
Username string
Name string
CacheRealm string
}
AccountInfo is safe account metadata, without tokens or raw MSAL values. CacheRealm records the cache's authority path, which can be a discovery alias such as common rather than the identity's resource tenant. Accounts and unbound Account leave Ref.TenantID empty for such aliases; LoginAccount fills it from the login result's ID-token tenant claim.
type AccountRef ¶ added in v0.4.0
type AccountRef struct {
HomeAccountID string
Environment string
TenantID string
LocalAccountID string
}
AccountRef identifies one account in one resource tenant. Opaque IDs are compared exactly; Environment is a case-insensitive authority hostname. LocalAccountID may be empty when the cache does not provide it. TenantID is the resource tenant, never a tenant inferred from HomeAccountID.
type Authenticator ¶
type Authenticator struct {
// contains filtered or unexported fields
}
Authenticator signs a user in and hands out delegated access tokens, backed by a persistent token cache for silent refresh. Built-in stores coordinate complete operations across cooperating processes. Custom stores must implement cache.TransactionalStore for coordination across Authenticator instances. Older clients and direct Store writes are not coordinated; isolate their storage when they must coexist.
func New ¶
func New(cfg Config, store cache.Store) (*Authenticator, error)
New builds an Authenticator for cfg, persisting the MSAL token cache in store. store must be non-nil (use an in-memory fake in tests, or a cache.Store impl).
func NewForAccount ¶ added in v0.4.0
func NewForAccount(cfg Config, store cache.Store, ref AccountRef) (*Authenticator, error)
NewForAccount creates an authenticator bound to an exact account reference. It preserves New's cache layout but never falls back to another account. Config.Account must be empty. A configured discovery authority is pinned to ref.TenantID; a different environment or specific tenant is rejected. Personal accounts use the consumers authority. Construction does no IO.
A bound authenticator supports silent acquisition and metadata. Login returns ErrBoundLogin and Logout returns ErrAccountRemovalUnsafe before IO. Stage reauthentication using New and a separate Store, call LoginAccount, validate its Ref, then publish the candidate store.
func (*Authenticator) Account ¶ added in v0.4.0
func (a *Authenticator) Account(ctx context.Context) (*AccountInfo, error)
Account returns the selected cached account's safe metadata without acquiring a token. On a bound instance Ref is the exact configured reference; CacheRealm still describes the underlying cache entry.
func (*Authenticator) Accounts ¶ added in v0.4.0
func (a *Authenticator) Accounts(ctx context.Context) ([]AccountInfo, error)
Accounts lists cached account metadata without acquiring tokens or changing the cache. The result is sorted by reference for deterministic discovery. It lists all accounts in this Store/client ID, including on a bound instance.
func (*Authenticator) Identity ¶
func (a *Authenticator) Identity(ctx context.Context) (*Identity, error)
Identity returns the cached user's identity without a network call. Name is empty here (it comes from the login-time id token). Returns ErrLoginRequired when nothing is cached.
func (*Authenticator) Login ¶
func (a *Authenticator) Login(ctx context.Context, opts ...LoginOption) (*Identity, error)
Login signs the user in interactively (or via device code) and returns their identity. The token cache is persisted as a side effect, so later Token calls can refresh silently. The call is bounded by Config.LoginTimeout.
func (*Authenticator) LoginAccount ¶ added in v0.4.0
func (a *Authenticator) LoginAccount(ctx context.Context, opts ...LoginOption) (*AccountInfo, error)
LoginAccount performs Login and returns the full account context. Like Login, it persists credentials before returning; use a separate candidate Store when adding or reauthenticating a managed account. A missing or conflicting tenant claim returns ErrAccountMismatch, leaving the candidate for its owner to discard. A bound instance returns ErrBoundLogin before any login side effect.
func (*Authenticator) Logout ¶
func (a *Authenticator) Logout(ctx context.Context) error
Logout returns ErrAccountRemovalUnsafe before IO on an exact-bound client. For legacy New clients, Logout removes cached accounts. When Config.Account is set, only the matching username or home_account_id is removed; otherwise all accounts under this Authenticator's client id are removed.
func (*Authenticator) Token ¶
func (a *Authenticator) Token(ctx context.Context, opts ...TokenOption) (string, error)
Token returns a valid delegated access token for the cached account, refreshing silently as needed. It returns ErrLoginRequired (joined with the underlying cause) when there is no usable cached account.
func (*Authenticator) TokenInfo ¶ added in v0.2.0
func (a *Authenticator) TokenInfo(ctx context.Context, opts ...TokenOption) (AccessToken, error)
TokenInfo is Authenticator.Token with the token's expiry and granted scopes attached. Use it when the caller has to schedule around expiry or verify that consent actually granted a scope it asked for.
func (*Authenticator) TokenSource ¶ added in v0.2.0
func (a *Authenticator) TokenSource(scopes ...string) *TokenSource
TokenSource returns a token provider bound to scopes. With no scopes it uses Config.Scopes.
type Config ¶
type Config struct {
// ClientID is the OAuth public client (application) id. Defaults to
// [GraphCLIClientID].
ClientID string
// Authority is the OAuth2 authority URL. Defaults to [DefaultAuthority].
Authority string
// Scopes are the default delegated scopes requested by Login and Token when
// no per-call scopes are given. Reserved OIDC scopes (openid, profile,
// offline_access) are added automatically for interactive login.
Scopes []string
// Account is the preferred cached account (matched case-insensitively
// against the account username) when more than one is cached. Empty means
// "the first cached account".
Account string
// LoginTimeout bounds an interactive/device login. Defaults to
// [DefaultLoginTimeout].
LoginTimeout time.Duration
}
Config configures an Authenticator. The zero value is usable: it defaults to the well-known public client, the /consumers authority, and a 3-minute login timeout.
type Identity ¶
type Identity struct {
// Username is the account's preferred username (typically the email).
Username string
// Name is the display name from the id token. It may be empty when the
// identity is read from the cache alone (see [Authenticator.Identity]).
Name string
// HomeAccountID is MSAL's stable account identifier.
HomeAccountID string
}
Identity describes the signed-in user, derived from the cached account and, after an interactive login, the id token.
type LoginOption ¶
type LoginOption func(*loginConfig)
LoginOption customizes a Login call.
func WithBrowserOpener ¶
func WithBrowserOpener(fn func(url string) error) LoginOption
WithBrowserOpener overrides how the interactive flow opens the browser (defaults to pkg/browser). Useful for headless tests.
func WithDevice ¶
func WithDevice() LoginOption
WithDevice selects the device-code flow instead of the interactive browser flow. Note: Microsoft rejects device-code for personal Microsoft Accounts; this is for work/school accounts.
func WithLoginScopes ¶
func WithLoginScopes(scopes ...string) LoginOption
WithLoginScopes overrides the default Config.Scopes for this login.
func WithPrompt ¶
func WithPrompt(fn func(message string)) LoginOption
WithPrompt sets a callback for device-code instructions (defaults to writing the MSAL message to stderr).
type TokenOption ¶
type TokenOption func(*tokenConfig)
TokenOption customizes a Token call.
func WithScopes ¶
func WithScopes(scopes ...string) TokenOption
WithScopes overrides the default Config.Scopes for this token request. MSAL caches tokens per scope set, so requesting different scopes for a resource is fine on one cached account.
type TokenSource ¶ added in v0.2.0
type TokenSource struct {
// contains filtered or unexported fields
}
TokenSource is a scope-bound token provider. Its method set is exactly
Token(ctx context.Context) (string, error)
which is the shape Graph and other Microsoft REST clients expect, so it can be handed to them directly. Authenticator.Token cannot be used for that because its variadic options change its signature.
A TokenSource caches the token it last obtained and reuses it until it is close to expiring, so calling it per request is the intended usage.
The caching is not an optimization of something already cheap. The silent path underneath re-reads and decrypts the token cache on every call, which on a keychain-backed store costs hundreds of milliseconds of CPU; a command making twenty requests spent almost all of its time there rather than talking to the service.
A TokenSource is safe for concurrent use. A refresh is taken under the lock so that concurrent callers wait for one refresh instead of each starting their own, which is the behavior that made the cost visible in the first place.
func (*TokenSource) Invalidate ¶ added in v0.3.0
func (s *TokenSource) Invalidate()
Invalidate discards the cached token so the next call fetches a fresh one.
Expiry is handled without this. It exists for the case expiry cannot predict: a token revoked or a consent withdrawn while it is still within its stated lifetime, which the service reports as a 401 rather than in advance.
func (*TokenSource) Scopes ¶ added in v0.2.0
func (s *TokenSource) Scopes() []string
Scopes returns the scopes this source requests.
func (*TokenSource) Token ¶ added in v0.2.0
func (s *TokenSource) Token(ctx context.Context) (string, error)
Token returns a valid access token, refreshing silently as needed.
func (*TokenSource) TokenInfo ¶ added in v0.2.0
func (s *TokenSource) TokenInfo(ctx context.Context) (AccessToken, error)
TokenInfo returns the access token with its expiry and granted scopes, reusing the last one until it is close to expiring.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package cache provides persistent storage for the MSAL token cache used by msauth.
|
Package cache provides persistent storage for the MSAL token cache used by msauth. |
|
cmd
|
|
|
msauth-live-smoke
command
Command msauth-live-smoke verifies interactive login plus silent token reuse.
|
Command msauth-live-smoke verifies interactive login plus silent token reuse. |
|
Package msauthtest provides test doubles for msauth, so downstream packages can exercise auth-dependent code without real sign-in or a keyring.
|
Package msauthtest provides test doubles for msauth, so downstream packages can exercise auth-dependent code without real sign-in or a keyring. |