Documentation
¶
Index ¶
- Constants
- Variables
- func BrowserLogin(ctx context.Context, loginURL string) (string, error)
- func CleanAllExpiredTokens() (int, error)
- func CleanExpiredTokens(serverURL string) (int, error)
- func DeleteExchangedToken(serverURL string, req *exchange.ExchangeRequest) error
- func DeleteIdentity(serverURL string) error
- func DeleteSession(serverURL string) error
- func GetConfigDir() (string, error)
- func GetDefaultIdentityPath() (string, error)
- func GetSessionDir(serverURL string) (string, error)
- func GetSessionIdentityPath(serverURL string) (string, error)
- func GetToken(ctx context.Context, audience ...string) (string, error)
- func GetTokenForServer(ctx context.Context, serverURL string, audience ...string) (string, error)
- func GetTokenForServerWithOptions(ctx context.Context, serverURL string, req *exchange.ExchangeRequest) (string, error)
- func GetTokenWithOptions(ctx context.Context, req *exchange.ExchangeRequest) (string, error)
- func GetTokensDir(serverURL string) (string, error)
- func HashExchangeRequest(req *exchange.ExchangeRequest) string
- func HeadlessLogin(ctx context.Context, serverURL, idpToken string) (string, error)
- func ListSessions() (sessions map[string]*SessionInfo, defaultServer string, err error)
- func LoadDefaultIdentity() (string, time.Time, error)
- func LoadExchangedToken(serverURL string, req *exchange.ExchangeRequest) (string, time.Time, error)
- func LoadExchangedTokenWithRenewal(ctx context.Context, serverURL string, req *exchange.ExchangeRequest) (token string, expiresAt time.Time, renewed bool, err error)
- func LoadIdentity(serverURL string) (string, time.Time, error)
- func LoadIdentityWithRenewal(ctx context.Context, serverURL string) (token string, expiresAt time.Time, renewed bool, err error)
- func Login(ctx context.Context, serverURL, loginURL string) (string, error)
- func ResolveExchangeServer(override string) string
- func SaveExchangedToken(serverURL string, req *exchange.ExchangeRequest, token string, ...) error
- func SaveIdentity(serverURL, token string) error
- func SetDefaultSession(serverURL string) error
- type CachedToken
- type ChainedTokenSource
- type EnvTokenSource
- type FileTokenSource
- type LoginOptions
- type LoginOptionsOption
- type Manager
- type Option
- type ServerOptions
- func (o *ServerOptions) AddFlags(cmd *cobra.Command)
- func (o *ServerOptions) Config() *command.OptionsSetConfig
- func (o *ServerOptions) Manager(ctx context.Context) (*Manager, error)
- func (o *ServerOptions) TokenSource(ctx context.Context, audience ...string) (TokenSource, error)
- func (o *ServerOptions) TokenSourceWithRequest(ctx context.Context, req *exchange.ExchangeRequest) (TokenSource, error)
- func (o *ServerOptions) Validate() error
- type ServerOptionsOption
- type ServiceTokenSource
- type ServiceTokenSourceOption
- type SessionInfo
- type SessionsConfig
- type StaticTokenSource
- type TokenSource
Constants ¶
const ( // DefaultCredentialsEnvVar is the environment variable checked for credentials. DefaultCredentialsEnvVar = "CARABINER_CREDENTIALS" //nolint:gosec // env var name, not a credential // DefaultCredentialsFile is the filename for the default credentials file. DefaultCredentialsFile = "identity.json" // DefaultConfigDir is the subdirectory under os.UserConfigDir for carabiner config. DefaultConfigDir = "carabiner" // DefaultExchangeServer is the default deadrop exchange server URL. DefaultExchangeServer = "https://auth.carabiner.dev" // DefaultIdentityAudience is the audience of a Carabiner identity token: // the public API. It is what the login service requests when it mints the // identity token for a browser login, and what the deadrop server's // identity exchangers (the GitHub Actions one included) accept and // re-exchange from. It is the same in every environment; only the exchange // server differs. DefaultIdentityAudience = "https://api.carabiner.dev" )
const BrowserLoginTimeout = 5 * time.Minute
BrowserLoginTimeout is the maximum time BrowserLogin waits for the user to complete the authentication flow in the browser.
const DefaultLoginURL = "https://login.carabiner.dev"
DefaultLoginURL is the default login URL for the Carabiner login service.
const DefaultServer = "https://auth.carabiner.dev"
DefaultServer is the default deadrop exchange server URL.
const RenewalThreshold = 10 * time.Minute
RenewalThreshold is the time before expiry when a token should be renewed. If a token expires within this duration, it will be automatically renewed.
Variables ¶
var ErrTokenExpired = fmt.Errorf("token is expired and cannot be renewed, please log in again")
ErrTokenExpired is returned when the token is expired and cannot be renewed.
Functions ¶
func BrowserLogin ¶
BrowserLogin runs the interactive browser flow against the Carabiner login service at loginURL. It starts a callback server on a random loopback port and drives the OAuth authorization-code flow with PKCE: it sends the user's browser to the login service, receives a one-time code on the loopback callback, and redeems it over a back-channel for the Carabiner identity token — so the token never appears in a browser-visible URL.
Progress messages are written to stderr; the token is returned, not persisted.
func CleanAllExpiredTokens ¶
CleanAllExpiredTokens removes expired tokens from all server sessions. Returns the total number of tokens removed.
func CleanExpiredTokens ¶
CleanExpiredTokens removes all expired tokens from a server's token cache. Returns the number of tokens removed.
func DeleteExchangedToken ¶
func DeleteExchangedToken(serverURL string, req *exchange.ExchangeRequest) error
DeleteExchangedToken removes a specific cached token.
func DeleteIdentity ¶
DeleteIdentity removes the identity file for a specific server.
func DeleteSession ¶
DeleteSession removes the entire session for a server, including its directory.
func GetConfigDir ¶
GetConfigDir returns the carabiner config directory path
func GetDefaultIdentityPath ¶
GetDefaultIdentityPath returns the path to the default session's identity file
func GetSessionDir ¶
GetSessionDir returns the directory path for a server's session
func GetSessionIdentityPath ¶
GetSessionIdentityPath returns the path to the identity file for a server's session
func GetToken ¶
GetToken is a convenience function for apps to get a token for a specific audience. It uses the default session's server and automatically handles caching and renewal.
Example usage:
token, err := credentials.GetToken(ctx, "https://api.example.com")
if err != nil {
log.Fatal(err)
}
// Use token...
func GetTokenForServer ¶
GetTokenForServer is like GetToken but allows specifying a specific server.
func GetTokenForServerWithOptions ¶
func GetTokenForServerWithOptions(ctx context.Context, serverURL string, req *exchange.ExchangeRequest) (string, error)
GetTokenForServerWithOptions is like GetTokenWithOptions but allows specifying a specific server.
func GetTokenWithOptions ¶
GetTokenWithOptions is like GetToken but allows specifying full exchange options.
Example usage:
token, err := credentials.GetTokenWithOptions(ctx, &exchange.ExchangeRequest{
Audience: []string{"https://api.example.com"},
Scope: []string{"read", "write"},
})
func GetTokensDir ¶
GetTokensDir returns the tokens directory path for a server's session.
func HashExchangeRequest ¶
func HashExchangeRequest(req *exchange.ExchangeRequest) string
HashExchangeRequest generates a deterministic hash for an exchange request. This is used to create unique filenames for cached tokens. Only the Audience, Scope, and Resource fields are hashed.
func HeadlessLogin ¶
HeadlessLogin exchanges an identity provider token (for example a GitHub Actions OIDC token) for a Carabiner identity token at the deadrop server. The IdP token must have been minted with the deadrop server as audience.
The exchange requests DefaultIdentityAudience (the public API), the same audience the login service requests for a browser login, so the headless identity is interchangeable with an interactive one: the server's identity exchangers only accept that audience, and it is what they re-exchange from when the identity is later traded for service tokens.
func ListSessions ¶
func ListSessions() (sessions map[string]*SessionInfo, defaultServer string, err error)
ListSessions returns all configured sessions
func LoadDefaultIdentity ¶
LoadDefaultIdentity loads the identity token from the default session.
func LoadExchangedToken ¶
LoadExchangedToken loads a cached exchanged token for a specific server and request. Returns the token and its expiry time if found and not expired.
func LoadExchangedTokenWithRenewal ¶
func LoadExchangedTokenWithRenewal(ctx context.Context, serverURL string, req *exchange.ExchangeRequest) (token string, expiresAt time.Time, renewed bool, err error)
LoadExchangedTokenWithRenewal loads a cached exchanged token, automatically re-exchanging it if it's about to expire (within RenewalThreshold). Returns the token, expiry time, whether renewal was performed, and any error. This function opportunistically cleans up expired tokens with ~10% probability.
func LoadIdentity ¶
LoadIdentity loads the identity token for a specific server. Returns the token and its expiry time if valid. This function does NOT perform auto-renewal. Use LoadIdentityWithRenewal for that.
func LoadIdentityWithRenewal ¶
func LoadIdentityWithRenewal(ctx context.Context, serverURL string) (token string, expiresAt time.Time, renewed bool, err error)
LoadIdentityWithRenewal loads the identity token for a specific server, automatically renewing it if it's about to expire (within RenewalThreshold). Returns the token, expiry time, and whether renewal was performed.
func Login ¶
Login obtains a Carabiner identity token from the deadrop server at serverURL.
It first probes the registered STS providers (sts.DefaultProviders from carabiner-dev/signer) for ambient identity provider credentials, such as the GitHub Actions or GitLab CI OIDC tokens. The first available IdP token is exchanged at the deadrop server for a Carabiner identity (headless login).
If no ambient credentials are found and the process is attached to a terminal, Login falls back to the interactive browser flow against the login service at loginURL. In non-interactive environments without ambient credentials, Login fails fast instead of waiting for a browser that will never open.
Login returns the Carabiner identity token. It does not persist it; use SaveIdentity to store it in the server's session.
func ResolveExchangeServer ¶
ResolveExchangeServer returns the exchange server URL to use. If override is non-empty it is returned directly. Otherwise the server URL from the default session is used. If no session exists, DefaultExchangeServer is returned.
func SaveExchangedToken ¶
func SaveExchangedToken(serverURL string, req *exchange.ExchangeRequest, token string, expiresAt time.Time) error
SaveExchangedToken saves an exchanged token to the server's token cache.
func SaveIdentity ¶
SaveIdentity saves the identity token to the session-specific identity file for a server. This creates the session if it doesn't exist.
func SetDefaultSession ¶
SetDefaultSession sets the default session to the specified server
Types ¶
type CachedToken ¶
type CachedToken struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
Spec *exchange.ExchangeRequest `json:"spec"`
}
CachedToken represents an exchanged token stored on disk.
type ChainedTokenSource ¶
type ChainedTokenSource struct {
// contains filtered or unexported fields
}
ChainedTokenSource tries multiple TokenSources in order until one succeeds.
func NewChainedTokenSource ¶
func NewChainedTokenSource(sources ...TokenSource) *ChainedTokenSource
NewChainedTokenSource creates a TokenSource that tries each source in order. The first source that returns a valid token is used.
type EnvTokenSource ¶
type EnvTokenSource struct {
// contains filtered or unexported fields
}
EnvTokenSource reads a token from an environment variable.
func DefaultEnvTokenSource ¶
func DefaultEnvTokenSource() *EnvTokenSource
DefaultEnvTokenSource returns a TokenSource that reads from the CARABINER_CREDENTIALS environment variable.
func NewEnvTokenSource ¶
func NewEnvTokenSource(envVar string) *EnvTokenSource
NewEnvTokenSource creates a TokenSource that reads from an environment variable.
type FileTokenSource ¶
type FileTokenSource struct {
// contains filtered or unexported fields
}
FileTokenSource reads a token from a file. The file is read on each call to Token(), allowing for external token rotation.
func DefaultFileTokenSource ¶
func DefaultFileTokenSource() (*FileTokenSource, error)
DefaultFileTokenSource returns a TokenSource that reads from the default session's identity file. If no session exists, falls back to the legacy path at os.UserConfigDir()/carabiner/identity.json.
func NewFileTokenSource ¶
func NewFileTokenSource(path string) *FileTokenSource
NewFileTokenSource creates a TokenSource that reads from a file. The file should contain a JWT token (whitespace is trimmed).
func ServerFileTokenSource ¶
func ServerFileTokenSource(serverURL string) (*FileTokenSource, error)
ServerFileTokenSource returns a TokenSource that reads from a specific server's session identity file.
type LoginOptions ¶
type LoginOptions struct {
// LoginURL is the login service URL for interactive authentication.
LoginURL string
// PrintToken prints the token to stdout after successful login.
PrintToken bool
// Force forces a new login even if a valid cached token exists.
Force bool
// Prefix is an optional prefix for flag names.
Prefix string
}
LoginOptions is an OptionsSet for configuring login behavior in CLI tools. It provides flags for the login URL, print token, and force login.
Example usage:
type MyOptions struct {
credentials.Options
credentials.LoginOptions
}
func (o *MyOptions) AddFlags(cmd *cobra.Command) {
o.Options.AddFlags(cmd)
o.LoginOptions.AddFlags(cmd)
}
func NewLoginOptions ¶
func NewLoginOptions(opts ...LoginOptionsOption) *LoginOptions
NewLoginOptions creates a new LoginOptions with default values.
func (*LoginOptions) AddFlags ¶
func (o *LoginOptions) AddFlags(cmd *cobra.Command)
AddFlags adds the login flags to the command.
func (*LoginOptions) Config ¶
func (o *LoginOptions) Config() *command.OptionsSetConfig
Config returns the OptionsSetConfig for this options set.
func (*LoginOptions) Validate ¶
func (o *LoginOptions) Validate() error
Validate validates the login options.
type LoginOptionsOption ¶
type LoginOptionsOption func(*LoginOptions)
LoginOptionsOption is a functional option for configuring LoginOptions.
func WithLoginPrefix ¶
func WithLoginPrefix(prefix string) LoginOptionsOption
WithLoginPrefix sets the flag prefix for the login options.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager manages credential sessions by handling token lifecycle. It uses a central identity token source to exchange for short lived audience-specific tokens.
func NewManager ¶
NewManager creates a new credential manager.
Use WithTokenSource to configure the central identity token source(s). If no sources are provided, the manager uses the default sources:
- CARABINER_CREDENTIALS environment variable
- os.UserConfigDir()/carabiner/identity.json
Use WithServer to configure the deadrop exchange server URL.
func (*Manager) Register ¶
Register adds a new exchange request and immediately exchanges a token.
The id is used to retrieve the token later via Token() or TokenSource().
func (*Manager) Token ¶
Token returns the token for the given id. If the token is expired or about to expire, it will be refreshed.
func (*Manager) TokenSource ¶
func (m *Manager) TokenSource(id string) (TokenSource, error)
TokenSource returns a TokenSource for the given id.
type Option ¶
type Option func(*Manager)
Option is a functional option for configuring the Manager.
func WithHTTPClient ¶
WithHTTPClient sets a custom HTTP client for the exchange client.
func WithMaxRetries ¶
WithMaxRetries sets the maximum number of retry attempts for token exchange. Default is 5.
func WithRefreshBuffer ¶
WithRefreshBuffer sets the refresh buffer as a fraction of token lifetime. For example, 0.2 means refresh when 20% of the token lifetime remains (i.e., at 80% of the way through the token's validity period). Default is 0.2.
func WithRetryInterval ¶
WithRetryInterval sets the initial retry interval for exponential backoff. Default is 1 second.
func WithServer ¶
WithServer sets the deadrop exchange server URL.
func WithTokenSource ¶
func WithTokenSource(sources ...TokenSource) Option
WithTokenSource adds one or more TokenSources for the central identity token. Multiple sources are tried in order until one succeeds. If no sources are provided via this option, the Manager uses the default sources: 1. CARABINER_CREDENTIALS environment variable 2. os.UserConfigDir()/carabiner/identity.json
type ServerOptions ¶
type ServerOptions struct {
// Server is the deadrop exchange server URL.
Server string
// Prefix is an optional prefix for flag names (e.g., "deadrop" -> "--deadrop-auth-server").
Prefix string
// Audience is the default audience for token exchange.
Audience []string
// DisablePersistence disables token caching to disk.
DisablePersistence bool
}
ServerOptions is an ServerOptionsSet for configuring deadrop credentials in CLI tools. It provides the --auth-server flag and methods to obtain TokenSource instances for specific audiences.
Example usage in a CLI tool:
type MyServerOptions struct {
credentials.ServerOptions
// other options...
}
func (o *MyServerOptions) AddFlags(cmd *cobra.Command) {
o.ServerOptions.AddFlags(cmd)
// add other flags...
}
// In command handler:
source, err := opts.TokenSource(ctx)
if err != nil {
return err
}
token, err := source.Token(ctx)
Example with prefix and audience:
opts := credentials.NewServerOptions(
credentials.WithPrefix("deadrop"),
credentials.WithAudience("https://api.example.com"),
)
func NewServerOptions ¶
func NewServerOptions(opts ...ServerOptionsOption) *ServerOptions
NewServerOptions creates a new ServerOptions with default values. Use functional options to customize the configuration.
Examples:
// Default options
opts := credentials.NewServerOptions()
// With prefix
opts := credentials.NewServerOptions(credentials.WithPrefix("deadrop"))
// With prefix and audience
opts := credentials.NewServerOptions(
credentials.WithPrefix("deadrop"),
credentials.WithAudience("https://api.example.com"),
)
func (*ServerOptions) AddFlags ¶
func (o *ServerOptions) AddFlags(cmd *cobra.Command)
AddFlags adds the deadrop credential flags to the command.
func (*ServerOptions) Config ¶
func (o *ServerOptions) Config() *command.OptionsSetConfig
Config returns the OptionsSetConfig for this options set.
func (*ServerOptions) Manager ¶
func (o *ServerOptions) Manager(ctx context.Context) (*Manager, error)
Manager returns a new credentials Manager configured with these options. The Manager provides more control over token lifecycle than TokenSource.
Example:
mgr, err := opts.Manager(ctx)
if err != nil {
return err
}
defer mgr.Close()
err = mgr.Register(ctx, "api", &exchange.ExchangeRequest{
Audience: []string{"https://api.example.com"},
})
token, err := mgr.Token(ctx, "api")
func (*ServerOptions) TokenSource ¶
func (o *ServerOptions) TokenSource(ctx context.Context, audience ...string) (TokenSource, error)
TokenSource returns a TokenSource for the given audience. If no audience is provided, uses the audience configured via WithAudience(). The returned TokenSource handles token exchange and automatic renewal.
Example:
// With audience specified at call time
source, err := opts.TokenSource(ctx, "https://api.example.com")
// Using preloaded audience from WithAudience()
opts := credentials.NewServerOptions(credentials.WithAudience("https://api.example.com"))
source, err := opts.TokenSource(ctx)
func (*ServerOptions) TokenSourceWithRequest ¶
func (o *ServerOptions) TokenSourceWithRequest(ctx context.Context, req *exchange.ExchangeRequest) (TokenSource, error)
TokenSourceWithRequest returns a TokenSource for the given exchange request. This allows specifying additional exchange parameters like scope and resource.
Example:
source, err := opts.TokenSourceWithRequest(ctx, &exchange.ExchangeRequest{
Audience: []string{"https://api.example.com"},
Scope: []string{"read", "write"},
})
func (*ServerOptions) Validate ¶
func (o *ServerOptions) Validate() error
Validate validates the options.
type ServerOptionsOption ¶
type ServerOptionsOption func(*ServerOptions)
ServerOptionsOption is a functional option for configuring ServerOptions.
func WithAudience ¶
func WithAudience(audience ...string) ServerOptionsOption
WithAudience sets the default audience for token exchange. This can be overridden when calling TokenSource().
func WithDisablePersistence ¶
func WithDisablePersistence() ServerOptionsOption
WithDisablePersistence disables token caching to disk.
func WithPrefix ¶
func WithPrefix(prefix string) ServerOptionsOption
WithPrefix sets the flag prefix for the options. For example, WithPrefix("deadrop") results in flags like --deadrop-auth-server.
type ServiceTokenSource ¶
type ServiceTokenSource struct {
// contains filtered or unexported fields
}
ServiceTokenSource is a TokenSource that exchanges the main Carabiner identity token for a service-specific token based on an ExchangeRequest. It manages the lifecycle of the exchanged token, automatically renewing it when less than 20% of its lifetime remains (configurable via WithServiceRefreshBuffer).
The ExchangeRequest serves as a template: its Audience, Scope, Resource, and token type fields are preserved, while SubjectToken is set automatically from the identity source on each exchange.
Example usage:
source, err := credentials.NewServiceTokenSource(
&exchange.ExchangeRequest{
Audience: []string{"https://api.example.com"},
},
"https://auth.carabiner.dev",
credentials.WithServicePersistence(),
)
if err != nil {
log.Fatal(err)
}
token, err := source.Token(ctx)
func NewServiceTokenSource ¶
func NewServiceTokenSource(req *exchange.ExchangeRequest, serverURL string, opts ...ServiceTokenSourceOption) (*ServiceTokenSource, error)
NewServiceTokenSource creates a ServiceTokenSource that exchanges the main Carabiner identity for a service-specific token matching the given ExchangeRequest.
The request serves as a template: SubjectToken is ignored and set automatically from the identity source on each exchange. All other fields (Audience, Scope, Resource, SubjectTokenType, RequestedTokenType) are forwarded as-is.
The serverURL is the deadrop exchange server URL. By default the subject identity is read from serverURL's own session (see ServerTokenSource), so it matches the environment being exchanged at; override with WithServiceIdentitySource. Use options to configure persistence and refresh behavior.
func (*ServiceTokenSource) Token ¶
func (s *ServiceTokenSource) Token(ctx context.Context) (string, error)
Token returns a valid exchanged service token. If the current token has less than 20% of its lifetime remaining, it is automatically renewed in the background. If no valid token is available, a new exchange is performed synchronously.
type ServiceTokenSourceOption ¶
type ServiceTokenSourceOption func(*ServiceTokenSource)
ServiceTokenSourceOption configures a ServiceTokenSource.
func WithServiceIdentitySource ¶
func WithServiceIdentitySource(source TokenSource) ServiceTokenSourceOption
WithServiceIdentitySource overrides the default identity token source used to obtain the subject token for exchange.
func WithServicePersistence ¶
func WithServicePersistence() ServiceTokenSourceOption
WithServicePersistence enables token persistence in the server's session directory. When enabled, exchanged tokens are cached to disk and loaded on subsequent calls, surviving process restarts.
func WithServiceRefreshBuffer ¶
func WithServiceRefreshBuffer(buffer float64) ServiceTokenSourceOption
WithServiceRefreshBuffer sets the fraction of token lifetime remaining that triggers a renewal. Default is 0.2 (renew when 20% of lifetime remains).
type SessionInfo ¶
type SessionInfo struct {
Dir string `json:"dir"`
CreatedAt time.Time `json:"created_at"`
ServerURL string `json:"server_url,omitempty"` // Stored for reference
}
SessionInfo holds information about a session for a specific server
func GetDefaultSession ¶
func GetDefaultSession() (*SessionInfo, string, error)
GetDefaultSession returns the default session info, if any
func GetOrCreateSession ¶
func GetOrCreateSession(serverURL string) (*SessionInfo, error)
GetOrCreateSession gets an existing session for a server or creates a new one
type SessionsConfig ¶
type SessionsConfig struct {
Sessions map[string]*SessionInfo `json:"sessions"`
Default string `json:"default,omitempty"`
}
SessionsConfig holds the mapping of servers to session directories
func LoadSessionsConfig ¶
func LoadSessionsConfig() (*SessionsConfig, error)
LoadSessionsConfig loads the sessions configuration from disk
type StaticTokenSource ¶
type StaticTokenSource struct {
// contains filtered or unexported fields
}
StaticTokenSource returns a fixed token. Useful for testing.
func NewStaticTokenSource ¶
func NewStaticTokenSource(token string) *StaticTokenSource
NewStaticTokenSource creates a TokenSource that always returns the same token.
type TokenSource ¶
type TokenSource interface {
// Token returns a valid token or an error.
Token(ctx context.Context) (string, error)
}
TokenSource is an interface for retrieving tokens, similar to oauth2.TokenSource.
func DefaultTokenSource ¶
func DefaultTokenSource() (TokenSource, error)
DefaultTokenSource returns a ChainedTokenSource that tries: 1. CARABINER_CREDENTIALS environment variable 2. os.UserConfigDir()/carabiner/identity.json
func ServerTokenSource ¶
func ServerTokenSource(serverURL string) TokenSource
ServerTokenSource returns a TokenSource for the identity to exchange at serverURL. It tries, in order:
- the CARABINER_CREDENTIALS environment variable
- the identity file for serverURL's session
Unlike DefaultTokenSource, it binds the identity to a specific server's session rather than the global default session, so the subject identity always matches the exchange server — avoiding exchanging one environment's identity at another environment's deadrop. When serverURL has no session, only the environment variable is consulted (construction stays non-fatal; a missing identity surfaces when Token is called).