Authentication¶
See the authentication guide for how these fit together.
The seam¶
base
¶
The Auth seam and the non-interactive credential providers.
The client never inspects credentials directly: it only ever asks an
Auth instance to stamp an outgoing request. The OAuth grants live
in datamermaid.auth.oauth, which builds on the same seam.
Classes:
| Name | Description |
|---|---|
Auth |
Base class for credential providers. |
AnonymousAuth |
Send requests without credentials (public endpoints only). |
APIKeyAuth |
Authenticate with a MERMAID API key. |
HTTPXAuthAdapter |
Bridge an |
Functions:
| Name | Description |
|---|---|
resolve_auth |
Pick the credential provider to use. |
Attributes:
| Name | Type | Description |
|---|---|---|
API_KEY_ENV_VAR |
|
Auth
¶
Bases: ABC
Base class for credential providers.
Subclasses implement apply. Implementations
that hold expiring credentials additionally override
refresh and
should_refresh, which together give them a single retry after
an authentication failure.
Methods:
| Name | Description |
|---|---|
apply |
Add credentials to |
refresh |
Obtain fresh credentials. No-op for static credentials. |
should_refresh |
Whether |
AnonymousAuth
¶
Bases: Auth
Send requests without credentials (public endpoints only).
Methods:
| Name | Description |
|---|---|
apply |
|
refresh |
Obtain fresh credentials. No-op for static credentials. |
should_refresh |
Whether |
APIKeyAuth
¶
Bases: Auth
Authenticate with a MERMAID API key.
The key has the form mmd_<key_id>.<secret> and is sent as
Authorization: Bearer <key>.
Methods:
| Name | Description |
|---|---|
apply |
|
refresh |
Obtain fresh credentials. No-op for static credentials. |
should_refresh |
Whether |
Attributes:
| Name | Type | Description |
|---|---|---|
api_key |
|
|
key_id |
str | None
|
The public identifier portion of the key, if it is well formed. |
key_id
property
¶
The public identifier portion of the key, if it is well formed.
resolve_auth
¶
resolve_auth(
auth: Auth | None = None,
api_key: str | None = None,
*,
env: bool = True,
cache: bool = True,
) -> Auth
Pick the credential provider to use.
Precedence: explicit auth, then api_key, then the
MERMAID_API_KEY environment variable, then tokens left behind by
datamermaid.login, then anonymous access.
OAuth¶
oauth
¶
OAuth 2.0 credentials for MermaidClient.
OAuth owns the boring half of logging in - picking a
grant, caching the result, noticing expiry and refreshing - and delegates the
interactive half to the flows in datamermaid.auth.flows.
Classes:
| Name | Description |
|---|---|
OAuth |
Log in to MERMAID through Auth0 and keep the token fresh. |
Functions:
| Name | Description |
|---|---|
is_headless |
Guess whether this terminal can reach a browser on the same machine. |
login |
Log in to MERMAID and cache the tokens. |
logout |
Discard the cached MERMAID tokens. Returns whether any were found. |
Attributes:
| Name | Type | Description |
|---|---|---|
FlowName |
|
|
FLOWS |
dict[str, type[Flow]]
|
|
FLOWS
module-attribute
¶
FLOWS: dict[str, type[Flow]] = {
flow.name: flow
for flow in (
PkceFlow,
ImplicitFlow,
DeviceFlow,
ManualPasteFlow,
)
}
OAuth
¶
OAuth(
*,
flow: FlowName | Flow = "auto",
domain: str | None = None,
client_id: str | None = None,
audience: str | None = None,
scope: str | None = None,
cache: TokenCache | Path | str | bool = True,
redirect_port: int = 0,
redirect_host: str = "localhost",
timeout: float = DEFAULT_FLOW_TIMEOUT,
interactive: bool = True,
env: bool = True,
browser_opener: Callable[[str], bool] | None = None,
prompt: Callable[[str], str] | None = None,
printer: Callable[[str], None] | None = None,
http_client: Client | None = None,
server_factory: Callable[..., LoopbackCallbackServer]
| None = None,
clock: Callable[[], float] | None = None,
sleep: Callable[[float], None] | None = None,
now: Callable[[], float] | None = None,
)
Bases: Auth
Log in to MERMAID through Auth0 and keep the token fresh.
The first request triggers a login unless a usable token is already cached.
Renewal is single-flight: when several threads sharing one client find the
token expired, one of them refreshes it (or logs in) and the others wait
for, then use, the result. Pass interactive=False to raise
AuthFlowError instead of prompting, which
is what an unattended job wants.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flow
|
FlowName | Flow
|
Which grant to run, or a |
'auto'
|
domain
|
str | None
|
Auth0 tenant, e.g. |
None
|
client_id
|
str | None
|
Public Auth0 client id. |
None
|
audience
|
str | None
|
API identifier the access token must be valid for. |
None
|
scope
|
str | None
|
Space-separated OAuth scopes. |
None
|
cache
|
TokenCache | Path | str | bool
|
A |
True
|
redirect_port
|
int
|
Loopback port for the redirect-based grants. |
0
|
redirect_host
|
str
|
Host name used to build the redirect URI; the socket
always listens on |
'localhost'
|
timeout
|
float
|
Seconds to wait for the user to finish the login. |
DEFAULT_FLOW_TIMEOUT
|
interactive
|
bool
|
Whether a login may prompt. |
True
|
env
|
bool
|
Whether the |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Attributes:
| Name | Type | Description |
|---|---|---|
config |
The resolved |
|
cache |
TokenCache | None
|
The token cache in use, or |
Example
Methods:
| Name | Description |
|---|---|
access_token |
A valid access token, refreshing or logging in if need be. |
apply |
|
should_refresh |
Retry a rejected request once, but only if renewal could help. |
refresh |
Renew the credentials, falling back to a fresh login. |
login |
Obtain tokens interactively and cache them. |
logout |
Forget the cached tokens for this tenant. |
config
instance-attribute
¶
config = Auth0Config.resolve(
domain=domain,
client_id=client_id,
audience=audience,
scope=scope,
env=env,
)
tokens
property
¶
tokens: TokenSet | None
The token set in memory or in the cache, without logging in.
should_refresh
¶
Retry a rejected request once, but only if renewal could help.
refresh
¶
Renew the credentials, falling back to a fresh login.
If another thread renewed them after this thread's request was sent, the new token is used as it is, without renewing again.
is_headless
¶
Guess whether this terminal can reach a browser on the same machine.
True for an SSH session, and for a Unix desktop with no display server.
login
¶
login(*, force: bool = False, **kwargs: Any) -> OAuth
Log in to MERMAID and cache the tokens.
Any keyword OAuth accepts may be passed through, e.g.
datamermaid.login(flow="device"). The returned object can be handed
to MermaidClient(auth=...), though a plain MermaidClient() will
also pick the cached tokens up.
logout
¶
Discard the cached MERMAID tokens. Returns whether any were found.
Grants¶
flows
¶
The interactive OAuth grants used to obtain a MERMAID access token.
Each flow is a small object with a single
Flow.authorize method. Everything that would
otherwise touch the outside world - the browser, the terminal, the clock, the HTTP
client, the loopback server - arrives through a
FlowContext, so the tests drive the whole thing
without opening a browser or a socket.
Classes:
| Name | Description |
|---|---|
FlowContext |
The outside world a flow is allowed to touch. |
Flow |
One way of turning a user's consent into a set of tokens. |
PkceFlow |
Authorization code grant with PKCE, redirected to a loopback server. |
ImplicitFlow |
Legacy implicit grant: the token arrives in the redirect fragment. |
DeviceFlow |
Device authorization grant (RFC 8628), for terminals without a browser. |
ManualPasteFlow |
PKCE without a reachable loopback port: the user pastes the redirect. |
Functions:
| Name | Description |
|---|---|
generate_code_verifier |
A fresh PKCE code verifier (unpadded base64url, RFC 7636 section 4.1). |
code_challenge |
The |
open_browser |
Open |
token_set |
Build a token set, reporting a malformed response as a flow error. |
Attributes:
| Name | Type | Description |
|---|---|---|
DEVICE_GRANT_TYPE |
|
DEVICE_GRANT_TYPE
module-attribute
¶
FlowContext
dataclass
¶
FlowContext(
http: Client,
open_browser: Callable[[str], bool] = open_browser,
prompt: Callable[[str], str] = _default_prompt,
printer: Callable[[str], None] = print,
clock: Callable[[], float] = monotonic,
sleep: Callable[[float], None] = sleep,
now: Callable[[], float] = time,
redirect_port: int = 0,
redirect_host: str = "localhost",
timeout: float = 300.0,
server_factory: Callable[
..., LoopbackCallbackServer
] = LoopbackCallbackServer,
)
The outside world a flow is allowed to touch.
Methods:
| Name | Description |
|---|---|
open_server |
|
Attributes:
| Name | Type | Description |
|---|---|---|
http |
Client
|
|
open_browser |
Callable[[str], bool]
|
|
prompt |
Callable[[str], str]
|
|
printer |
Callable[[str], None]
|
|
clock |
Callable[[], float]
|
|
sleep |
Callable[[float], None]
|
|
now |
Callable[[], float]
|
|
redirect_port |
int
|
|
redirect_host |
str
|
|
timeout |
float
|
|
server_factory |
Callable[..., LoopbackCallbackServer]
|
|
open_browser
class-attribute
instance-attribute
¶
open_browser: Callable[[str], bool] = open_browser
server_factory
class-attribute
instance-attribute
¶
server_factory: Callable[..., LoopbackCallbackServer] = (
LoopbackCallbackServer
)
Flow
¶
Bases: ABC
One way of turning a user's consent into a set of tokens.
See TokenSet.
Methods:
| Name | Description |
|---|---|
authorize |
Run the grant to completion and return the issued tokens. |
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
|
authorize
abstractmethod
¶
authorize(
config: Auth0Config, ctx: FlowContext
) -> TokenSet
Run the grant to completion and return the issued tokens.
PkceFlow
¶
ImplicitFlow
¶
DeviceFlow
¶
Bases: Flow
Device authorization grant (RFC 8628), for terminals without a browser.
The user is given a short code and a URL to open on any other device; the terminal polls the token endpoint until they are done.
Methods:
| Name | Description |
|---|---|
request_device_code |
Ask Auth0 for a device and user code. |
authorize |
|
Attributes:
| Name | Type | Description |
|---|---|---|
name |
|
request_device_code
¶
request_device_code(
config: Auth0Config, ctx: FlowContext
) -> dict[str, Any]
Ask Auth0 for a device and user code.
ManualPasteFlow
¶
Bases: Flow
PKCE without a reachable loopback port: the user pastes the redirect.
The browser still lands on http://localhost:<port>/, which fails to
connect when the browser is on a different machine - but the address bar
holds the authorization code, and pasting that URL back is enough.
Methods:
| Name | Description |
|---|---|
authorize |
|
Attributes:
| Name | Type | Description |
|---|---|---|
name |
|
|
default_port |
|
generate_code_verifier
¶
generate_code_verifier(
*, nbytes: int = CODE_VERIFIER_BYTES
) -> str
A fresh PKCE code verifier (unpadded base64url, RFC 7636 section 4.1).
open_browser
¶
Open url in the system browser, reporting whether that worked.
Only the failures a browser launch can legitimately produce are caught:
a broader except would also swallow the test suite's guard against
opening a real browser.
post_form
¶
post_form(
ctx: FlowContext,
url: str,
data: dict[str, str],
*,
expect: str = "token",
) -> dict[str, Any]
POST a form to an OAuth endpoint and return the decoded JSON body.
Errors are raised as AuthFlowError
carrying Auth0's own error/error_description; the caller sees the response
only when it succeeded.
token_set
¶
token_set(
payload: dict[str, Any], ctx: FlowContext
) -> TokenSet
Build a token set, reporting a malformed response as a flow error.
See TokenSet.
Tenant configuration¶
config
¶
Auth0 tenant configuration for the OAuth flows.
The defaults point at MERMAID's production Auth0 tenant and are the same
values the R client (mermaidr) uses. Every one of them can be overridden
per call or through the environment, so the SDK can also be pointed at the
development tenant.
Classes:
| Name | Description |
|---|---|
Auth0Config |
Where to send OAuth requests and on whose behalf. |
Attributes:
| Name | Type | Description |
|---|---|---|
DEFAULT_AUTH0_DOMAIN |
|
|
DEFAULT_CLIENT_ID |
|
|
DEFAULT_AUDIENCE |
|
|
DEFAULT_SCOPE |
|
|
AUTH0_DOMAIN_ENV_VAR |
|
|
CLIENT_ID_ENV_VAR |
|
|
AUDIENCE_ENV_VAR |
|
Auth0Config
dataclass
¶
Auth0Config(
domain: str = DEFAULT_AUTH0_DOMAIN,
client_id: str = DEFAULT_CLIENT_ID,
audience: str = DEFAULT_AUDIENCE,
scope: str = DEFAULT_SCOPE,
)
Where to send OAuth requests and on whose behalf.
Methods:
| Name | Description |
|---|---|
resolve |
Build a config from explicit arguments, then the environment, then defaults. |
scopes |
The scope string, optionally with |
Attributes:
| Name | Type | Description |
|---|---|---|
domain |
str
|
|
client_id |
str
|
|
audience |
str
|
|
scope |
str
|
|
issuer |
str
|
|
authorize_url |
str
|
|
token_url |
str
|
|
device_code_url |
str
|
|
cache_key |
str
|
Identity of the credentials this config produces, for the token cache. |
cache_key
property
¶
Identity of the credentials this config produces, for the token cache.
resolve
classmethod
¶
resolve(
*,
domain: str | None = None,
client_id: str | None = None,
audience: str | None = None,
scope: str | None = None,
env: bool = True,
) -> Auth0Config
Build a config from explicit arguments, then the environment, then defaults.
scopes
¶
The scope string, optionally with offline_access removed.
The implicit grant never issues refresh tokens, and Auth0 rejects the scope outright for it.
Token cache¶
token_cache
¶
On-disk storage for OAuth tokens.
Tokens live in $XDG_CONFIG_HOME/datamermaid/tokens.json (defaulting to
~/.config), readable only by the owner. One file holds one entry per
tenant/client/audience combination, so the production and development Auth0
tenants can be logged into side by side.
Classes:
| Name | Description |
|---|---|
TokenSet |
The credentials returned by a successful OAuth flow. |
TokenCache |
A JSON file of token sets, one entry per Auth0 cache key. |
Functions:
| Name | Description |
|---|---|
default_cache_path |
|
Attributes:
| Name | Type | Description |
|---|---|---|
CACHE_FILE_MODE |
|
|
DEFAULT_LEEWAY |
|
TokenSet
dataclass
¶
TokenSet(
access_token: str,
refresh_token: str | None = None,
id_token: str | None = None,
token_type: str = "Bearer",
scope: str | None = None,
expires_at: float | None = None,
)
The credentials returned by a successful OAuth flow.
Methods:
| Name | Description |
|---|---|
from_response |
Build a token set from an Auth0 token endpoint response. |
from_dict |
|
to_dict |
|
is_expired |
Whether the access token is past (or about to pass) its expiry. |
merged_with |
|
Attributes:
| Name | Type | Description |
|---|---|---|
access_token |
str
|
|
refresh_token |
str | None
|
|
id_token |
str | None
|
|
token_type |
str
|
|
scope |
str | None
|
|
expires_at |
float | None
|
|
from_response
classmethod
¶
from_response(
payload: dict[str, Any], *, now: float | None = None
) -> TokenSet
Build a token set from an Auth0 token endpoint response.
is_expired
¶
is_expired(
*,
leeway: float = DEFAULT_LEEWAY,
now: float | None = None,
) -> bool
Whether the access token is past (or about to pass) its expiry.
A token with no known expiry is assumed valid: the API is the authority, and a 401 triggers a refresh anyway.
TokenCache
¶
A JSON file of token sets, one entry per Auth0 cache key.
The key is
Auth0Config.cache_key.
save and clear each read the file, change it and
write it back while holding a lock on a .lock file beside it, so two
clients or processes updating different entries never lose each other's
changes.
Methods:
| Name | Description |
|---|---|
load |
The cached tokens for |
save |
Store |
clear |
Forget |
Attributes:
| Name | Type | Description |
|---|---|---|
path |
|
|
lock_path |
Path
|
The lock file that serialises updates to |
clear
¶
Forget key (or every entry when it is None).
Returns whether anything was removed.
default_cache_path
¶
$XDG_CONFIG_HOME/datamermaid/tokens.json, falling back to ~/.config.
Redirect server¶
callback_server
¶
A single-shot loopback HTTP server for OAuth redirects.
The browser is sent to http://localhost:<port>/ after the user approves
the login. For the authorization code grant the parameters arrive in the
query string and can be read directly; for the implicit grant they arrive in
the URL fragment, which never reaches the server, so the page served there
bounces them back as a query string.
Classes:
| Name | Description |
|---|---|
LoopbackCallbackServer |
Serve exactly one OAuth redirect on |
Functions:
| Name | Description |
|---|---|
parse_fragment |
Parse the parameters of an OAuth redirect fragment. |
Attributes:
| Name | Type | Description |
|---|---|---|
CallbackMode |
|
|
FRAGMENT_PATH |
|
|
HANDLER_TIMEOUT |
|
LoopbackCallbackServer
¶
LoopbackCallbackServer(
*,
mode: CallbackMode = "query",
port: int = 0,
redirect_host: str = DEFAULT_REDIRECT_HOST,
path: str = "/",
timeout: float = DEFAULT_TIMEOUT,
clock: Callable[[], float] = monotonic,
)
Serve exactly one OAuth redirect on 127.0.0.1.
The socket is bound as soon as the object is created, so the ephemeral port (and
therefore redirect_uri) is known before the authorization URL is
built.
Methods:
| Name | Description |
|---|---|
wait |
Block until the browser hits the callback, or time out. |
close |
|
Attributes:
| Name | Type | Description |
|---|---|---|
timeout |
|
|
redirect_host |
|
|
path |
|
|
port |
int
|
|
redirect_uri |
str
|
|
parse_fragment
¶
Parse the parameters of an OAuth redirect fragment.
Accepts a whole redirect URL, a bare #access_token=... fragment, or
the fragment's contents.
parse_redirect
¶
Parse the parameters of a pasted redirect URL, from either half.
The authorization code grant answers in the query string and the implicit grant in the fragment, so both are considered; the fragment wins when a URL somehow carries both.
Token decoding¶
jwt
¶
Just enough JWT handling to know when an access token has expired.
The SDK never trusts a token it decodes here: the API verifies signatures.
All that is needed locally is the exp claim, so the payload is base64
decoded without any signature check and without a crypto dependency.
Functions:
| Name | Description |
|---|---|
decode_payload |
Return the claims of |
token_expires_at |
The |