Applications & SSO (OIDC)
Two different directions — don't confuse them
Honeyframe already supports "sign in to Honeyframe with your identity provider" today: an org admin connects Google, Microsoft (Azure AD / Entra ID), or a generic OIDC provider (Okta, Auth0, Keycloak, Ping, Cloudflare Access, or anything OIDC-compliant) from Workspace → SSO providers. That's Honeyframe acting as a relying party — consuming someone else's identity so your own staff can sign in to Honeyframe.
This page is the opposite direction: Honeyframe acting as the identity provider (OP) — issuing tokens to your application so it can offer "Login with Honeyframe" and inherit a person's identity, groups, and site scope, instead of re-implementing auth against Honeyframe's own login endpoints. If you're building a custom application that needs to know who a Honeyframe user is, this page is for you. If you want your organization to sign in to Honeyframe via your company's directory, see Workspace → SSO providers instead.
Registering an application
An admin registers each application ("client") before it can use the flow below — there's no self-service registration. Each client is:
- A display name and a
client_id. - A redirect URI allowlist — exact match only, no wildcards.
- The scopes it's allowed to request.
- Public or confidential:
- Public (no secret) — for anything that can't keep a secret safely: a mobile app, or a single-page app running entirely in the browser. Authenticates with PKCE alone.
- Confidential (secret + PKCE) — for a server-side backend that can hold a client secret safely (for example, your application's own BFF).
Every client — public or confidential — completes PKCE. There is no implicit flow and no client is exempt from PKCE, including confidential ones. The client secret, where one exists, is shown once at creation, the same "shown-once" convention as a Personal Access Token — only its hash is kept afterward.
"Login with Honeyframe"
The standard Authorization Code + PKCE flow: your application redirects the user's browser to Honeyframe's authorize endpoint, the person signs in (or is already signed in) with their normal Honeyframe credentials, Honeyframe redirects back to your application with a short-lived code, and your application's backend exchanges that code for tokens.
Endpoints
| Endpoint | Method | Reachable by | Purpose |
|---|---|---|---|
/.well-known/openid-configuration | GET | Anyone | Provider metadata document |
/oauth/authorize | GET | Browser redirect | Hosted page that starts the flow (frontend route, not /api) |
/api/oidc/token | POST | Your backend | Exchanges the code (+ PKCE verifier) for tokens |
/api/oidc/userinfo | GET / POST | Bearer access token | Fetches the same claims as the ID token |
/api/oidc/jwks.json | GET | Anyone | The RSA public keys used to verify a token's signature |
/oauth/authorize is served by the frontend SPA, not the /api backend —
it renders the sign-in/consent step, then the browser lands back on your
redirect_uri. Every other endpoint above is a normal /api JSON endpoint.
Discovery
{
"issuer": "https://platform.your-domain.com",
"authorization_endpoint": "https://platform.your-domain.com/oauth/authorize",
"token_endpoint": "https://platform.your-domain.com/api/oidc/token",
"userinfo_endpoint": "https://platform.your-domain.com/api/oidc/userinfo",
"jwks_uri": "https://platform.your-domain.com/api/oidc/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "profile", "email", "groups"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"code_challenge_methods_supported": ["S256"],
"claims_supported": [
"iss", "sub", "aud", "exp", "iat", "nonce", "email", "name", "groups",
"https://honeyframe.io/claims/principal",
"https://honeyframe.io/claims/site_keys"
]
}
issuer is one stable, deployment-wide URL — your application pins one
issuer, not one per organization. There is no per-organization issuer or
org_id claim (see Token claims below) — scope your
application's data access through groups instead. Don't hardcode any of
the endpoint paths above — resolve them from the discovery document
(GET /.well-known/openid-configuration) like a standard OIDC client would.
Starting the flow
GET /oauth/authorize — browser-redirected, requires the user to be
authenticated:
| Param | Required | Notes |
|---|---|---|
response_type | yes | Must be code — no implicit flow. |
client_id | yes | Your registered client. |
redirect_uri | yes | Exact match against your client's registered redirect URIs. |
scope | yes | Space-delimited, must include openid. |
state | yes | Opaque, echoed back — your CSRF defense. |
code_challenge | yes | PKCE is mandatory for every client. base64url(SHA256(code_verifier)). |
code_challenge_method | yes | Must be S256 — plain is rejected. |
nonce | recommended | Echoed into the ID token as replay defense. |
Exchanging the code
POST /api/oidc/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<opaque code>
&redirect_uri=<must match the one used at /oauth/authorize>
&client_id=<...>
&client_secret=<...> # confidential clients only
&code_verifier=<...> # PKCE — the server recomputes SHA256 and compares
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJ...",
"scope": "openid profile email groups"
}
Getting user info
GET /api/oidc/userinfo, bearer-gated with the access_token from the
exchange above. Returns the same claim set as the ID token, minus its
timing-specific claims (aud, nonce, exp) — always a subset, never a
superset.
Verifying tokens
GET /api/oidc/jwks.json returns the RSA public keys (RS256) used to verify a
token's signature — standard JWKS shape, {"keys": [...]}. When the
signing key rotates, the previous key stays published in keys[] alongside
the new one for one full token lifetime, so a token signed moments before
rotation still verifies.
Token claims
| Claim | Notes |
|---|---|
sub | Stable, opaque, never an email — shaped user:<id> or site_user:<id> depending on the principal kind below. Never reused. |
email | |
name | |
groups | An array of group names, not ids — your application shouldn't need to resolve ids back to Honeyframe. |
https://honeyframe.io/claims/principal | "user" (a platform user with a Honeyframe seat) or "site_user" (a webapp-scoped identity with no seat). Always present, regardless of requested scope — this is how your application tells the two subject kinds apart. |
https://honeyframe.io/claims/site_keys | A custom, namespaced claim carrying the same site-scope convention described on Site Users — absent or null means unrestricted, an array means a site allowlist. Present only for site_user principals. |
There is no org_id or org_slug claim in v1 — tenancy isn't carried on the
token. If your application needs to scope data by organization, resolve it
the same way Site Users and the
Webapp Data API already do, not from this token.
Scopes
openid is required on every request. profile and email add the
matching claims. groups adds the groups claim. There is no separate
site_keys scope — the https://honeyframe.io/claims/site_keys and
https://honeyframe.io/claims/principal claims above are always included
for a site_user principal, unconditional on requested scope.
Using the identity against your data
Completing "Login with Honeyframe" establishes who the person is. If your application also needs to call the dashboard/webapp data endpoints as that specific person, it exchanges the OIDC identity for a site-user token via a small bridge step — not part of the first release; details will be announced in the release notes once it ships. Meanwhile the data endpoints themselves (config fetch, card execute, execute-batch, block execute, filter-options preview) are unaffected: they keep working exactly as documented on Site Users and the Webapp Data API. Adding "Login with Honeyframe" in front of them doesn't change that contract.
Not in the first release
- No self-service application registration — an admin creates each client.
- No refresh-token grant, logout endpoint, or revocation endpoint.
- No consent screen. First-party applications (the ones you build in-house) get silent consent, the way a platform's own first-party apps typically skip a permission-grant screen. A consent screen for third-party applications is planned for later.
- No encrypted ID tokens (JWE). Tokens are signed only, not encrypted — every client is expected to talk to the token endpoint directly over TLS, not through an untrusted intermediary.