Delegated portal login
Let a trusted application provision Supermail users and hand them short-lived, one-time links into the portal.
Delegated login lets your application hand one of its authenticated users into
the Supermail portal without asking them to enter a Supermail password first.
Your server proves its own identity with the application's existing
client_id and client_secret, identifies the user by a stable app-local
subject, and receives a short-lived confirmation URL.
This is a portal login capability, not an OAuth Resource API scope. Its
durable source of truth is a separate per-user delegated grant carrying
delegated_login; that grant is projected into the user's list of login
methods.
Only use delegated login when your server has already authenticated the user.
Never expose the client secret or call AppDelegatedLogin/* from browser code.
Before you start
- Register a confidential OIDC application and store its one-time
client_secret. - Ask the deployment administrator to enable Delegated login for that app in Admin → Apps. App ownership alone cannot enable this trust.
- Keep a stable, opaque
external_subjectfor each of your users. It must be 1–255 bytes and must not contain@.
All AppDelegatedLogin/* JSON-RPC calls use HTTP Basic authentication:
Authorization: Basic base64(client_id:client_secret)The current client secret and a secret still inside its rotation grace window both work.
Pick the right onboarding path
| User state | Start with | What happens |
|---|---|---|
| No Supermail account yet | AppDelegatedLogin/provision | Atomically creates the user and your delegated grant |
| Already has a Supermail account | AppDelegatedLogin/create_link_ticket | The signed-in user explicitly confirms linking your app |
| Already provisioned or linked to your app | AppDelegatedLogin/create_ticket | Creates a one-time portal login URL |
Do not use an email address or username as external_subject, and do not try to
match it to an existing Supermail user. Generate or reuse an opaque identifier
from your own identity system instead.
1. Provision a new user
AppDelegatedLogin/provision creates a Supermail user and its delegated grant in one
transaction. It is idempotent on (client_id, external_subject), so retrying
the same request returns the same user and grant with created: false.
curl --user "$CLIENT_ID:$CLIENT_SECRET" \
--header "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "AppDelegatedLogin/provision",
"params": {
"external_subject": "usr_7f3d9b1c",
"username": "ada"
}
}' \
https://supermail.example.com/rpc{
"user": {
"id": "user_01H...",
"username": "ada",
"allow_app_create": false,
"created": "2026-07-19T08:14:03+00:00"
},
"grant": {
"id": "dgrant_01H...",
"app_id": "app_01H...",
"user_id": "user_01H...",
"external_subject": "usr_7f3d9b1c",
"origin": "provisioned",
"scopes": ["delegated_login"],
"created": "2026-07-19T08:14:03+00:00"
},
"created": true
}The username is used only when the user is first created. It must be an
available Supermail username; an idempotent replay resolves by
external_subject, not by username.
2. Create a login ticket
Once your app has a live delegated grant for the subject, request a login URL:
curl --user "$CLIENT_ID:$CLIENT_SECRET" \
--header "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": 2,
"method": "AppDelegatedLogin/create_ticket",
"params": {
"external_subject": "usr_7f3d9b1c",
"return_to": "/accounts"
}
}' \
https://supermail.example.com/rpc{
"login_url": "https://supermail.example.com/delegated-login/start?ticket=...",
"expires": "2026-07-19T08:16:03+00:00"
}Redirect the user's browser to login_url. Supermail shows the application
name and requires an explicit confirmation before it signs the user in. A
newly provisioned user is then sent through a one-time security setup to add a
password before the rest of the portal is available. That password is their
independent backup if your application later disappears.
return_to is optional and defaults to /accounts. It must be a local absolute
portal path beginning with one /; external URLs, protocol-relative paths, and
backslashes are rejected.
Link an existing Supermail account
If the person already has a Supermail account, create a link ticket instead of provisioning a duplicate:
curl --user "$CLIENT_ID:$CLIENT_SECRET" \
--header "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": 3,
"method": "AppDelegatedLogin/create_link_ticket",
"params": {
"external_subject": "usr_7f3d9b1c",
"return_to": "/profile"
}
}' \
https://supermail.example.com/rpcThe response contains link_url and expires. Send the browser there. The
user must sign into their existing Supermail account and explicitly confirm
the link; only then does Supermail create the delegated grant. Future
AppDelegatedLogin/create_ticket calls for that subject sign into the linked account.
The link call returns a conflict if that external subject already has a grant for your application.
Ticket security and lifetime
- Login and link URLs expire after about two minutes and can be confirmed only once.
- Opening the URL with
GETdoes not consume it. Supermail moves the token into anHttpOnly, path-scoped cookie and removes it from the visible URL; the confirmationPOSTconsumes it. This keeps mail and chat link scanners from burning a ticket. - Ticket values are random bearer secrets and are stored only as SHA-256 hashes. Treat a returned URL like a password: do not log it, persist it, or send it through analytics.
- Ticket creation is limited to 60 tickets per rolling minute per app.
- Disabling delegated login, revoking the user's delegated method, or deleting the app invalidates outstanding access.
Grants, removal, and app deletion
The user sees the delegated grant as a login method named after your app. They may remove it from their profile once they have an independent password; removal revokes the grant and burns its outstanding tickets.
Deleting an app requires an explicit acknowledgement when delegated grants would be revoked. Supermail refuses deletion while any user still depends on that app as their only usable login path. Those users must first add a password or another live delegated path.
Delegated login establishes a portal session only. To call the Resource API, your app still uses the normal authorization-code + PKCE flow and obtains a user-approved OAuth grant.