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

  1. 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.
  2. 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.
  3. Register the required scopes and audiences. Omission selects the registered defaults; explicit requests can narrow them, never add unregistered values. Request profile for profile read/update and email for email management. groups requests claims; it does not choose login groups.
  4. 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.
  5. Enable the intended capabilities using the matrix below. Test with a member of this organization and the configured callback, scope and audience.
  6. 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.