Browser-direct tenant authentication
Browser-direct lets an organization's application present its own sign-in and account screens and call SSO from a browser without exposing a client secret. The organization owns the member; the registered OAuth application defines admission and token permissions. An organization's member is distinct from a consumer account, even when that member signs in through a consumer link.
The production tenant base is
https://sso.tenant.m7.org/browser-direct/{CLIENT_ID}. The SSO service also
serves the browser-direct routes on its admitted SSO host. Choose one host
consistently. Use the registered public client UUID, uppercase in the path and
proof target, and no trailing slash. This is a separate public-browser protocol;
existing confidential OAuth flows retain their registered
client authentication. Enabling it does not make the ordinary token endpoint
accept a public client.
Set up an application
- Create an organization-owned OAuth application in User M7. The organization and application must be active and unarchived, the application unexpired, with a nonempty tenant UUID. Administrative applications are ineligible.
- Register exact HTTPS callback URLs, including path, case and query. Callbacks cannot contain fragments. Their normalized origins authorize browser requests; there is no wildcard-origin or independent browser-direct origin registry. Use one exact HTTPS development origin as well as the production registration.
- Register the required scopes and audiences. Omission selects the registered
defaults; explicit requests can narrow them, never add unregistered values.
Request
profilefor profile read/update andemailfor email management.groupsrequests claims; it does not choose login groups. - Configure optional Knobs → Tenant Policy → Groups on the application. Empty login groups add no restriction; otherwise an active membership in any configured login group is required. Registration groups enroll new members and do not grant existing members admission retroactively. Retain applicable IP and certificate/fingerprint policy; browser-direct is not a bypass.
- Enable the intended capabilities using the matrix below. Test with a member of this organization and the configured callback, scope and audience.
- Generate a browser-held P-256 signing key and, for each authorization ceremony, a random state, optional nonce, and a fresh PKCE verifier/challenge. Keep the verifier, private key and pending ceremony in the initiating browser. Never ship a client secret, root token or server configuration to it.
| Capability | Required policy |
|---|---|
| Password sign-in, token/session and signed-in operations | tenant_policy_allow_browser_direct_authentication |
| Signup routes | tenant_policy_allow_browser_direct_registration, global public registration allowance, and effective tenant_policy_hosted_allow_public_registrations; sign-in afterward also needs authentication enabled |
| Provider sign-in/signup/link | Authentication or registration as applicable, global federation allowance and effective tenant_policy_allow_federated_signin |
| Email-code/magic-link sign-in | Authentication plus tenant_policy_allow_browser_direct_email_signin |
| Multiple provider links and tenant 2FA | Separate org-only switches in tenant policy |
The three browser-direct allowances default off and resolve organization then application, without consumer inheritance. An unset application value inherits; an explicit false disables it. General hosted registration/federation policy retains its existing global gates and inheritance. The org-only member-security switches have no application override. See the policy API and full matrix. Current policy and membership are rechecked during pending work and admission.
Protocol conventions
All endpoint paths in the linked references append to the base above. Except
GET /authorize, every operation is POST with a JSON object and
Content-Type: application/json. Only documented fields are accepted. Unless
specified otherwise, text values are strings, limits are bytes, and NUL, CR and
LF are rejected. Numeric-looking codes must remain strings. The request limit
rejects a declared body larger than 65,536 bytes.
Responses are raw JSON, without API User's status/data envelope, and have
Cache-Control: no-store, Pragma: no-cache, Vary: Origin, and
Referrer-Policy: no-referrer. A normal success is HTTP 200; navigation uses
303. Do not cache responses or log request bodies, tokens, proofs or credentials.
CORS and DPoP
Use browser credentials: "omit". These routes do not use or create hosted
SSO cookies. POST requires an exact registered callback origin in Origin.
Allowed CORS request headers are Content-Type and DPoP; Authorization is
not a browser-direct credential. OPTIONS validates application, origin, requested
method and headers, returns 204, and creates no transaction. Unsupported methods
return 405 with Allow; HEAD has no body and creates no transaction. Provider
navigation can omit Origin; its validated callback supplies the binding.
DPoP is mandatory regardless of the ordinary OAuth client's optional DPoP
policy. Use an ES256 proof signed by the same P-256 private key throughout the
ceremony and resulting carrier: typ: dpop+jwt, public jwk, unique jti,
current Unix iat, htm: POST, and the endpoint's absolute URL in htu.
Synchronize clocks; proof verification accepts a bounded five-minute time
window. Retries need a newly signed proof and new jti.
| Operation | Proof transport / target |
|---|---|
/challenge |
No proof JWT yet; commits to dpop_jkt in the authorization context |
GET /authorize |
No proof header; commits to dpop_jkt and PKCE in sealed provider context |
/login |
JWT in JSON dpop, including the returned challenge; htu ends /login |
/token/ack |
DPoP header, htu: https://id.m7.org/api/v2/oauth/token/ack, htm: POST; send the HTTP request to browser-direct /token/ack |
| Every other POST | DPoP header, htu is that browser-direct endpoint |
Do not put carrier handles or secrets in navigation URLs. DPoP for a protected
resource is a separate proof for that resource, including ath when required
by its authorization contract; do not reuse the browser-direct proof there.
Shared request objects
Authorization context is the following set of fields. References saying “context” mean these fields at the top level, not a nested object.
| Field | Contract |
|---|---|
redirect_uri |
Required exact registered HTTPS callback, at most 2,048 bytes, same origin as the initiating POST |
state |
Required nonempty unpredictable string, at most 1,024 bytes; validate it before accepting callbacks |
nonce |
Optional string, at most 1,024 bytes; defaults to empty |
code_challenge, code_challenge_method |
Required 43-character base64url SHA-256 challenge and literal S256 |
dpop_jkt |
Required 43-character base64url JWK thumbprint of the initiating key |
scope |
Optional space-delimited string of exact registered scope names; arrays are rejected; omit for registered defaults |
aud |
Optional exact registered audience string or JSON list of strings; a string is one audience, not a space-delimited list; omit for registered defaults |
Session handles mean top-level bd_carrier, bd_session, bd_key.
Carrier handles omit bd_session. Use the exact returned values. Selected
member operations require an active, selected, activated session and current
member/application admission, with its server-held token successfully checked.
No access token or member UUID in the body substitutes for these handles.
Carrier/session strings are at most 64 bytes and bd_key at most 256 bytes;
use returned values without deriving credentials from these limits.
POST /browser-direct/AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA/profile/get HTTP/1.1
Host: sso.tenant.m7.org
Origin: https://app.example.org
Content-Type: application/json
DPoP: <fresh signed endpoint proof>
{"bd_carrier":"<carrier>","bd_session":"<session>","bd_key":"<carrier secret>"}
Angle-bracket values in examples are placeholders, not usable credentials.
Ownership and custody
flowchart LR
O[Organization] --> M[Tenant member]
O --> A[OAuth application]
A --> C[Browser carrier + signing key]
C --> S1[Remembered member session]
C --> S2[Another member session]
S1 --> T[Server-held refresh and activation state]
T --> B[Browser access token after activation]
H[Hosted SSO cookie session] -. separate .-> O
A carrier is this browser's remembered-account container for one app, origin and DPoP key. Selection identifies one member session within it. Another app or browser gets separate authority. Hosted SSO sessions and consumer sessions are separate. Sharing an organization does not provide cross-application browser session sharing.
Treat codes, transaction/continuation handles, carrier/session IDs, activation
IDs and selection receipts as opaque. bd_key is a secret distinct from the
signing key. Access tokens are credentials; validate them only through the
resource's token contract, not decoded claims in application
UI. The browser never receives refresh tokens, Identity root credentials,
activation secrets or binding-chain internals. Minimize persistence and clear
local ceremony material after completion, cancellation or expiry. Keeping a
carrier requires keeping its secret and signing key safely; losing them requires
fresh sign-in. An XSS attacker with access to the browser's authority remains a
threat; DPoP does not make unsafe script execution harmless.
Reference map
| Guide | Operations |
|---|---|
| Authentication | Password/provider/email sign-in, token pickup, ACK, refresh, me |
| Registration and recovery | Native/provider signup, incomplete signup, forgot password |
| Passwords | Options, change, first setup, removal and recovery receipts |
| Profile and emails | Profile read/update and email ownership/primary management |
| Linked identities | List, explicit link confirmation and unlink |
| Sessions and devices | Remembered accounts, selection, logout, other devices and revocation |
| Tenant two-factor | Enrollment, status, TOTP/backup proof, email preference, removal and login continuation |
| Manager security | User M7 workflows and API User manager authority |
There is no public browser-direct app-policy/provider discovery endpoint and no separate browser-direct discovery document. Existing OIDC discovery describes the hosted OAuth provider; it does not provision this UI. Supply reviewed app configuration yourself. Authenticated identity listing advertises permitted link providers only. Provider sign-in uses full-page or popup navigation; no iframe integration or automatic cross-product account sharing is promised.
Errors and troubleshooting
{"error":"invalid_grant","error_description":"Invalid or expired credential."}
Branch on error and documented status/outcome fields, not description prose.
Operational errors may include a safe trace_id; give that to support without
credentials. A missing response is not proof of failure or success.
| Symptom | Check / recovery |
|---|---|
access_denied (403) |
App/org state, capability gates, exact origin, group membership and current admission; never switch silently to another member |
invalid_request (400), route 404, method 405 |
Exact route/case, JSON fields/types and proof URL; avoid trailing slash |
invalid_scope / invalid_target (400) |
Requested values against current registration |
invalid_dpop_proof / replay rejection |
Correct key, clock, fresh jti, POST target and ACK's exceptional target |
invalid_token (401) on selected-member reads |
Refresh once using held handles, ACK if pending, then repeat with fresh proof; sign in if the session is closed |
reauthentication_required (403) |
Complete an actual fresh sign-in and attach it to the same carrier as described in the session guide |
session_changed (409) |
Reload selected state; discard an obsolete selection receipt |
| Throttle (429) | Honor returned cooldown or documented window; opening a new dialog does not reset member-wide budgets |
| 5xx, timeout, malformed response | Use the operation's status/retry rule; never replay password or consumed login authority blindly |
All limits and policies below describe the implemented service contract. They do not establish a live deployment revision or general production readiness.