masks-client
masks-client is the Python SDK. It imports as masks and needs Python 3.10 or later. For a
walkthrough, read Python apps.
| Module | |
|---|---|
masks |
The client: discovery, the code flow, tokens, DPoP, introspection, token exchange, delegations, enrollment, and checking tokens. |
masks.web |
The parts every framework shares: Config, the actor, sessions, and credentials. |
masks.fastapi |
The FastAPI integration. Install the fastapi extra. |
masks.django |
The Django integration. Install the django extra. |
masks.flask |
The Flask integration. Install the flask extra. |
masks.asgi |
Middleware that checks tokens in any ASGI app. |
Config
Section titled “Config”masks.web.Config holds the settings for every framework. Each framework module also exports it.
A setting marked request also takes a function. The function gets the framework’s request and returns the value. Derive the value from something the app trusts, such as a tenant it looked up by host.
| Setting | Default | |
|---|---|---|
issuer |
Required | The masks issuer, such as https://acme.auth.example.com. Request. |
resource |
"" |
The app’s own absolute https URL. Sign-ins name it as their resource, and tokens for the app’s API must name it as their audience. The handshake needs it. Request. |
secret |
"" |
At least 32 bytes that seal the cookie session. A list rotates secrets, newest first. Not needed with sessions. Without either, Config raises ValueError. Django and Flask fill it in from the app’s secret key only when their settings leave it out. |
name |
"An app" |
The name the handshake gives the app. Request. |
scope |
openid profile email offline_access |
What every sign-in asks for. |
privileges |
{} |
Names for what a route needs. See privileges. |
namespace |
"" |
A scope prefix, such as catalog:. The handshake registers the namespace in place of each scope under it. |
organization |
"" |
The organization every sign-in names. Request. |
resource_scopes |
() |
The scopes the API accepts. A dict of scope to description also gives masks the sentence its consent screen shows. |
credentials |
FileCredentials() |
Where the handshake keeps the app’s credentials. Needed when issuer is a function. See credentials. Request. |
sessions |
Cookie sessions | Where sessions live. See sessions. |
session_lifetime |
14 days | How long a session lasts, in seconds. |
store |
MemoryStore() |
Where refreshes, logouts, and DPoP proofs are remembered. See stores. |
base_url |
"" |
The app’s public origin, when it differs from the origin of resource. Request. |
prefix |
"/auth" |
Where the routes answer. |
after_sign_in |
"/" |
Where a person goes after signing in. |
after_sign_out |
"/" |
Where a person goes after signing out. |
sign_out_of_issuer |
False |
Signs the person out of masks too whenever they sign out of the app. |
manages |
None |
Who may connect the app again or disconnect it. A privilege name, or a function that takes an actor and returns True or False. Nobody when None. |
logged_out |
None |
A function that takes a masks.Logout, called on each back-channel logout. |
delegation_redirect_uri |
"" |
The path or full address a provider’s answer comes back to. The handshake then asks for masks:delegate: and registers this address. Request. |
allow_plain_http |
False |
Allows a plain http issuer on another machine. An issuer on this machine is always allowed. |
cookie_secure |
True |
False drops the Secure flag and the __Host- prefix, for development on plain http. |
http, async_http |
Built-in | The httpx.Client and httpx.AsyncClient the app talks to masks with. |
Routes
Section titled “Routes”These answer under prefix.
| Route | |
|---|---|
GET /auth |
Starts a sign-in. Takes return_to, organization, and privilege. |
GET /auth/callback |
Finishes a sign-in. |
GET /auth/session |
The signed-in actor as JSON, or 401. |
GET /auth/avatar |
The person’s photo, fetched from masks with their token. Takes size. |
GET, POST, or DELETE /auth/logout |
Signs out. Takes everywhere to sign out of masks as well. A browser that names no site gets a page first. |
GET /auth/logout/return |
Where masks sends the browser after signing out of masks. |
POST /auth/logout/backchannel |
Takes a logout token from masks. Refuses a body over 64 KB. |
GET /auth/handshake |
Starts the handshake, or shows the connection. |
POST /auth/handshake |
Connects the app again. |
GET /auth/handshake/callback |
Finishes the handshake. |
POST /auth/handshake/disconnect or DELETE /auth/handshake |
Disconnects the app. |
GET /.well-known/oauth-protected-resource |
The app’s resource metadata. This route ignores prefix. |
Refused requests
Section titled “Refused requests”A browser is redirected. A JSON request gets one of these:
| Status | error |
Other fields |
|---|---|---|
401 |
login_required |
login_url |
401 |
handshake_required |
handshake_url |
403 |
privilege_required |
privileges, login_url |
403 |
insufficient_scope, insufficient_role, or insufficient_organization |
error_description |
A guard that cannot reach masks answers 503 with an HTML page, to a JSON request as well.
A request counts as JSON when it sends X-Requested-With: XMLHttpRequest, a JSON Accept or
Content-Type, or an Accept with neither text/html nor */*.
A guard that checks a token answers in JSON. See accepting tokens.
The actor
Section titled “The actor”masks.web.Actor is the actor for a request.
signed_in |
Whether there is a sign-in and its token has not expired. |
subject, name, nickname, email, email_verified |
Who signed in. |
picture |
The app’s /auth/avatar route when masks holds a photo, since the photo at masks loads only for someone signed in there. Otherwise the picture claim, then the identicon. |
avatars |
Each avatar URL, by kind. |
tenant |
The tenant’s uuid, subdomain, and name. |
organization |
The organization signed in to, or None. |
organizations |
Every organization the person has joined. |
has_role(*roles) |
Whether the person holds one of these roles in the organization. |
member(*roles, organization="") |
The organization, or raises masks.errors.Forbidden, a masks.Error. |
scopes |
The scopes the token holds. |
permits(scope) |
Whether the token holds the scope. |
privileges |
The names of the privileges the person holds. |
has_privilege(*names) |
Whether the person holds every one of them. |
access_token, expires_at |
The access token and when it expires. |
by_token |
Whether the actor came from an access token in place of a session. |
account_url |
The person’s account page at masks. |
actor["claim"], actor.get("claim") |
Any other claim. |
to_dict() |
What GET /auth/session returns. It leaves out access_token. |
An organization has id, key, name, role, owner, and has_role(*roles).
Credentials
Section titled “Credentials”FileCredentials(path="masks.json") |
A file, written with mode 600 in a folder it creates with mode 700. It is writable only when the app can create files in that folder. |
MemoryCredentials(client_id="", client_secret="", issuer="") |
This process’s memory, for tests. |
StaticCredentials(client_id, client_secret="", issuer="") |
Credentials from configuration. The app cannot disconnect with these. |
A store of your own has a writable attribute, false when it cannot save, and these methods:
| Method | |
|---|---|
read() |
Returns a dict with client_id, and with issuer and client_secret when it has them. |
write(issuer, registration) |
Saves a masks.Registration and the issuer that made it. |
clear() |
Forgets the credentials. |
The app uses credentials only with the issuer they name, and answers 500 for credentials from another
issuer. Credentials that name no issuer work only when issuer is a fixed string.
Sessions
Section titled “Sessions”CookieSessions(secret, name="__Host-masks", lifetime=...) |
The default. The cookie holds the sealed session. |
ServerSessions(store, name="__Host-masks", lifetime=...) |
The cookie holds a random id, and the store holds the session under a hash of it. |
MemorySessionStore() |
A store in this process’s memory. |
A store of your own has these methods. In FastAPI they can be async def, as
masks.web.AsyncSessionStore describes. Django raises ImproperlyConfigured and Flask raises
TypeError for an async store.
| Method | |
|---|---|
read(key) |
Returns the session, or None. |
write(key, data, expires_in, tags) |
Saves the session. tags holds the issuer as iss, the person’s sub, and the session’s sid. |
delete(key) |
Removes the session. |
end(issuer=..., subject=..., sid=...) |
Removes every session from that issuer with that sid, or every session of that subject. Returns how many it removed. Back-channel logout calls it. |
Stores
Section titled “Stores”store remembers what running several processes
lists.
MemoryStore() |
The default. Keeps them in one process. |
RedisStore(client) |
Keeps them in Redis, through a redis.Redis client. |
AsyncRedisStore(client) |
Keeps them in Redis, through a redis.asyncio.Redis client. FastAPI only. |
A store of your own has these methods. In a FastAPI app, they can be async def.
| Method | |
|---|---|
add(key, value, expires_in) |
Saves the value only when the key is not there. Returns whether it saved it. |
get(key) |
Returns the value, or None. |
set(key, value, expires_in) |
Saves the value. |
delete(key) |
Removes the key. |
Each key is a hash, and each refresh result is sealed with a key made from the old refresh token, so the store holds no tokens anyone can read.
The client
Section titled “The client”masks.discover(issuer) reads the issuer’s metadata and returns a masks.Issuer.
masks.adiscover is the async form. Both take http and allow_plain_http.
discover_resource and adiscover_resource read an API’s resource metadata.
masks.Client(issuer, client_id, ...) takes secret, method, key, dpop, and http.
masks.AsyncClient takes the same and has an async form of each call.
Client.register(issuer, metadata, initial_access_token) |
Registers a client with an initial access token. |
Client.from_registration(issuer, registration, ...) |
A client for a saved registration. |
start(redirect_uri, scope=..., resource=..., organization=..., max_age=...) |
Starts a sign-in. Returns an AuthorizationRequest with its url. |
complete(started, params) |
Finishes a sign-in. Returns a SignIn with token and claims. When start asked for max_age, an ID token whose auth_time is missing or older than that many seconds, plus 30 seconds of leeway, raises masks.InvalidToken. |
userinfo(token) |
The person, from userinfo. |
verify_id_token(id_token, nonce=..., access_token=...) |
Checks an ID token. |
verify_logout_token(logout_token) |
Checks a logout token, which must have an expiry. Returns a Logout. |
end_session_url(post_logout_redirect_uri=..., state=...) |
A link that signs the person out of masks. |
client_credentials(scope=..., resource=...) |
A token for the client itself. |
refresh(refresh_token, scope=..., resource=...) |
A new token from a refresh token. |
device_authorization(scope=...), await_device(device) |
The device flow. |
revoke(token, hint="") |
Ends a token. |
introspect(token, hint="") |
Asks masks whether a token is active. |
exchange(subject_token, ...) |
Trades one token for another under RFC 8693. Takes subject_token_type, actor_token, audience, scope, resource, and requested_lifetime. |
start_delegation(provider, redirect_uri) |
Starts connecting a provider for the signed-in person. |
finish_delegation(started, params) |
Finishes it. Returns a HeldConnection with connection and secret. |
delegated_token(secret, connection=...) |
A provider token. Returns an Upstream with the new secret. |
enroll(scope=..., client_name=..., expires_in=...) |
An initial access token for one of the app’s machines. expires_in is 60 to 3600 seconds, and 15 minutes by default. |
unenroll(client_id) |
Removes a machine the app enrolled. |
read_registration, update_registration, delete_registration |
Manage a registration. |
credentials_source(...), refresh_source(token, ..., keep=...) |
Token sources that renew a token before it expires. keep gets each rotated token to save. |
auth(source, *origins) |
An httpx.Auth that sends the token, and a DPoP proof with a dpop key, to those origins. |
http_client(source, *origins) |
An httpx client with that auth. |
The client authenticates with client_secret_basic when secret is set, and with
private_key_jwt when key is set. Pass method to choose another. With a dpop key, the client
refuses a token that comes back unbound. auth raises masks.ForeignOrigin for a request to any
origin it was not given.
masks.Handshake(issuer, name=..., resource=..., redirect_uris=..., return_to=...) runs the
handshake in an app without a framework integration. start()
returns the url to send the browser to and the state to keep in its session.
complete(params, state) returns the masks.Registration.
Machines
Section titled “Machines”A machine keeps two P-256 keys. One signs its client assertions, and the other holds its tokens with DPoP.
import masks
key = masks.Key.generate()dpop = masks.Key.generate()
issuer = masks.discover("https://acme.auth.example.com")registration = masks.Client.register( issuer, masks.ClientMetadata(client_name="nas", token_endpoint_auth_method=masks.PRIVATE_KEY_JWT, jwks=key.jwks()), initial_access_token,)client = masks.Client.from_registration(issuer, registration, key=key, dpop=dpop)
source = client.credentials_source(scope=["files:read"], resource=["https://files.example.com"])
with client.http_client(source, "https://files.example.com") as api: api.get("https://files.example.com/files")Key.from_jwk(jwk) loads a key, and to_jwk() writes it out with its private half. Keep that file
readable only by the machine’s service user. jwks() is the public half to register.
A machine with no browser can use the device flow. device_authorization() returns the
verification_uri and user_code to show. await_device(device) waits until a person approves the
code on another device. The synchronous form takes a timeout in seconds.
Checking tokens
Section titled “Checking tokens”masks.Verifier(issuer, audience, public_url=None, http=None, replay=None) checks access tokens.
masks.AsyncVerifier takes the same and is async.
authenticate(method, url, headers) |
Checks the Authorization header and any DPoP proof. Returns Claims. |
verify(token) |
Checks the token alone and skips any DPoP proof. A request goes through authenticate. |
Claims has issuer, subject, audience, expiry, issued_at, id, client_id, scope,
scopes, tenant, act, thumbprint, bound, has_scope(scope), and raw.
A verifier fetches the issuer’s keys again every five minutes. While masks cannot be reached, the
keys it last fetched keep checking tokens, a token signed with a key it has never seen raises
masks.Unavailable, and it tries masks again after 5 seconds, then 10, doubling up to once a
minute. The web layer keeps the issuer’s discovery document the same way: it reads it again after
an hour, and serves the last good one with the same backoff while masks is down.
Pass the URL the client used. Set public_url when a proxy sits in front of the API, since a DPoP
proof names that address.
ASGI middleware
Section titled “ASGI middleware”masks.asgi.Middleware(app, verifier) checks every request with an AsyncVerifier. The claims are
at request.state.masks_claims. A failed check gets 401 with challenges, and a verifier that
cannot check gets 503.
Replay stores
Section titled “Replay stores”A replay store remembers DPoP proofs. Its first(key, expires_in) returns True only the first
time it sees a key. MemoryReplayStore is the default, and keeps them in one process. An
AsyncVerifier also takes a store whose first is async, and Verifier raises TypeError for
one.
Errors
Section titled “Errors”masks.MasksError |
Every error the SDK raises. |
masks.Error |
masks answered with an OAuth error. Has code, description, and status. |
masks.InvalidToken |
A token or proof failed a check. |
masks.MissingToken |
A request carried no token. A kind of masks.InvalidToken. |
masks.Unavailable |
masks, its keys, or a replay store could not be reached. |
masks.DelegationRefused |
masks refused a delegation call. sign_in_again says whether the person must sign in again. secret holds a replacement secret. |
masks.DelegationUnavailable |
masks or the provider did not answer. secret holds a replacement secret. |
masks.InsecureURL |
A plain http URL off this machine, without allow_plain_http. |
masks.ForeignOrigin |
A URL on another origin than the one expected. |
masks.web.Refusal |
A guard refused the request. answer holds the response. |