# identity OIDC integration instructions

identity is the central OpenID Connect provider for 1above enterprise apps.
This document is written for humans and coding agents that need enough detail to implement a working integration without guessing.

## issuer
https://identity-dev.1above.io

## discovery
https://identity-dev.1above.io/.well-known/openid-configuration

## integration summary
1. Register an app client in identity.
2. Grant that client to the target organization.
3. Start the browser authorization-code flow with OpenID scope and PKCE.
4. Exchange the returned code server-side at the token endpoint.
5. Validate the ID token against the JWKS endpoint.
6. Create your application's own local session after successful validation.

## automated client setup with device code
Identity supports a provisioning device-code flow for integration tools that need to auto-create or update a confidential OIDC client and fetch its client secret once.
This is not the OIDC device authorization grant for signing users in. It is an admin-approved setup flow for client registration and secret delivery.

Setup flow:
1. The client integration tool starts setup with POST /api/integration/device/start.
2. Identity returns device_code, user_code, verification_uri, verification_uri_complete, expires_in, and interval.
3. The tool shows the user_code and opens verification_uri_complete for a 1above admin.
4. The admin approves the setup in identity.
5. The tool polls POST /api/integration/device/poll with device_code until it receives the client config and client_secret.
6. The client_secret is returned once; store it in the downstream app's secret manager or environment.

Start device setup example:
POST /api/integration/device/start
Content-Type: application/json

{
  "clientSlug": "agentier-web",
  "clientName": "Agentier",
  "baseUrl": "https://agentier.1above.io",
  "redirectUris": ["https://agentier.1above.io/auth/callback"],
  "postLogoutRedirectUris": ["https://agentier.1above.io/logout/complete/"],
  "scopes": ["openid", "email", "profile", "offline_access"],
  "grantOrg": "1above"
}

Poll device setup example:
POST /api/integration/device/poll
Content-Type: application/json

{
  "device_code": "DEVICE_CODE_FROM_START_RESPONSE"
}

Pending poll response:
{
  "error": "authorization_pending",
  "interval": 5
}

Approved poll response includes:
- client_id
- client_secret
- issuer
- authorization_endpoint
- token_endpoint
- userinfo_endpoint
- jwks_uri
- redirect_uris
- scopes
- token_endpoint_auth_method
- pkce_required

Security rules for setup tools:
- device codes expire after 15 minutes
- poll no faster than the returned interval
- do not print client_secret to logs
- write client_secret directly to the app's secret store
- after setup, use normal authorization-code + PKCE login, not the setup device_code

## registration and org access prerequisites
A client must exist in identity before any login flow will work.
A client must also be granted to the organization the user is signing into.
If the org grant is removed later, existing authorization-code and refresh-token exchanges will stop working.

Register or update clients with:
POST /api/admin/app-clients

Important client registration fields:
- slug: used as client_id
- baseUrl: application base URL
- redirectUris: exact allowed callback URLs
- postLogoutRedirectUris: allowed logout return URLs
- tokenEndpointAuthMethod: client_secret_post, client_secret_basic, or none
- grantTypes: usually authorization_code and optionally refresh_token
- responseTypes: currently code
- scopes: choose from openid, email, profile, offline_access
- pkceRequired: usually true
- refreshTokenRotationEnabled: true rotates refresh tokens on use
- clientSecret: required for confidential clients, omit for public clients

Grant a client to an org with:
POST /api/admin/organizations/{orgId}/app-access

Important org app-access fields:
- client: the client slug
- status: active or inactive

## endpoints
authorization endpoint
https://identity-dev.1above.io/oauth/authorize

token endpoint
https://identity-dev.1above.io/oauth/token

end session endpoint
https://identity-dev.1above.io/oauth/logout

userinfo endpoint
https://identity-dev.1above.io/oauth/userinfo

jwks endpoint
https://identity-dev.1above.io/oauth/jwks.json

## authorize request requirements
required query params
client_id
redirect_uri
response_type=code
scope
state
code_challenge
code_challenge_method=S256

optional query params
prompt
login_hint
org
nonce

Rules:
- scope must include openid
- requested scopes must be allowed by the client
- redirect_uri is normalized without a trailing slash before matching a registered redirect URI
- PKCE uses S256 only
- if the client requires PKCE, code_verifier is required at token exchange time
- org is an optional organization hint and may be either org id or org slug
- send org whenever a user may belong to multiple organizations

Typical authorize URL example:
https://identity-dev.1above.io/oauth/authorize?client_id=agentier-web&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&response_type=code&scope=openid%20email%20profile%20offline_access&state=opaque-csrf-value&nonce=opaque-nonce-value&code_challenge=BASE64URL_SHA256_OF_CODE_VERIFIER&code_challenge_method=S256&org=acme

## token endpoint behavior
supported grants
authorization_code
refresh_token
urn:ietf:params:oauth:grant-type:api-key

token endpoint auth methods
client_secret_post
client_secret_basic
none

Rules:
- token requests may be sent as application/x-www-form-urlencoded or JSON
- confidential clients must authenticate according to their registered tokenEndpointAuthMethod
- public clients with tokenEndpointAuthMethod=none must not send client_secret or HTTP Basic auth
- authorization_code exchange requires code and redirect_uri
- authorization_code exchange requires code_verifier when the client has pkceRequired=true
- refresh_token exchange is allowed only when the client allows the refresh_token grant
- refresh tokens are issued only when offline_access is granted and the client allows refresh_token
- if refreshTokenRotationEnabled=true, the refresh token value changes every refresh
- if refreshTokenRotationEnabled=false, the same refresh token value is returned again

Authorization code exchange example for client_secret_post:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&client_id=agentier-web&client_secret=YOUR_CLIENT_SECRET&code=AUTHORIZATION_CODE&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&code_verifier=ORIGINAL_CODE_VERIFIER

Authorization code exchange example for client_secret_basic:
POST /oauth/token
Authorization: Basic BASE64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&client_id=agentier-web&code=AUTHORIZATION_CODE&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&code_verifier=ORIGINAL_CODE_VERIFIER

Authorization code exchange example for public clients:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&client_id=agentier-web-public&code=AUTHORIZATION_CODE&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&code_verifier=ORIGINAL_CODE_VERIFIER

Refresh token exchange example:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&client_id=agentier-web&client_secret=YOUR_CLIENT_SECRET&refresh_token=REFRESH_TOKEN

Successful token response fields:
- token_type: Bearer
- expires_in: access token lifetime in seconds
- access_token: JWT
- id_token: JWT
- refresh_token: opaque string when applicable
- scope: granted scope string

## logout
Use the end session endpoint to clear the identity session.
Supported query params:
- id_token_hint: optional but required when sending post_logout_redirect_uri
- post_logout_redirect_uri: optional, normalized with a trailing slash before matching one of the client's registered postLogoutRedirectUris
- state: optional round-trip value returned only when redirecting to a validated post_logout_redirect_uri

Logout example:
https://identity-dev.1above.io/oauth/logout?id_token_hint=ID_TOKEN&post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2Flogout%2Fcomplete%2F&state=opaque-logout-state

## supported scopes
openid
email
profile
offline_access

Scope behavior:
- openid is mandatory for OIDC login
- email adds email and email_verified claims
- profile adds name and picture claims
- offline_access allows refresh token issuance when the client also allows refresh_token

## claims
Always present in ID token and access token:
- iss
- aud
- sub
- exp
- iat
- auth_time
- https://identity.1above.io/claims/org_id
- https://identity.1above.io/claims/org_slug
- https://identity.1above.io/claims/org_name
- https://identity.1above.io/claims/membership_role

Present only when email scope was granted:
- email
- email_verified

Present only when profile scope was granted:
- name
- picture

Example userinfo response for scope openid email profile:
{
  "sub": "usr_123",
  "email": "user@example.com",
  "email_verified": true,
  "name": "Alex Roe",
  "picture": "https://example.com/avatar.png",
  "https://identity.1above.io/claims/org_id": "org_123",
  "https://identity.1above.io/claims/org_slug": "acme",
  "https://identity.1above.io/claims/org_name": "Acme",
  "https://identity.1above.io/claims/membership_role": "member"
}

## validation requirements for downstream apps
When receiving an ID token, validate all of the following server-side:
- signature against the JWKS endpoint
- iss equals the issuer above
- aud equals your client_id
- exp has not passed
- nonce matches the value you sent, if you sent one
- type should be treated as an ID token only when returned from the token response

When calling userinfo:
- send the access token as Authorization: Bearer <token>
- do not send the ID token to /oauth/userinfo
- /oauth/userinfo also accepts access_token as a query parameter, but Authorization header is preferred

Do not trust browser-provided token contents without server-side verification.

## refresh token semantics
- refresh tokens are opaque strings, not JWTs
- refresh preserves the original auth_time
- refresh may fail if the org membership or org app grant has been revoked since the original login
- refresh may fail if the client no longer allows refresh_token

## common error cases
- invalid_scope: openid missing or one of the requested scopes is not allowed by the client
- invalid_request: missing required authorize or token parameters
- invalid_request: public clients sent client authentication
- invalid_client: client is inactive or client authentication failed
- invalid_grant: authorization code or refresh token is invalid, expired, revoked, or already used
- unauthorized_client: client is not allowed to use refresh_token grant
- login_required: prompt=none was used but no active identity session exists

## research before implementation
Before generating design or integration code, inspect the live provider first instead of assuming endpoint details.
Recommended research sequence for coding agents:
1. Fetch the discovery document and record issuer, endpoints, scopes, auth methods, and supported grants.
2. Fetch the JWKS and record the active kid values and key algorithms.
3. Confirm how the target app client is registered: client_id, redirect URI, token auth method, PKCE requirement, scopes, and whether refresh_token is enabled.
4. Confirm the target org has an active app-access grant for that client.
5. Only then write an implementation plan or code.

Node.js snippet to inspect discovery and JWKS:
```ts
const issuer = "https://identity-dev.1above.io";
const discovery = await fetch(`${issuer}/.well-known/openid-configuration`).then((r) => r.json());
console.log(discovery);
const jwks = await fetch(discovery.jwks_uri).then((r) => r.json());
console.log(jwks);
```

Node.js snippet to inspect userinfo with an access token:
```ts
const issuer = "https://identity-dev.1above.io";
const accessToken = process.env.ACCESS_TOKEN!;
const userinfo = await fetch(`${issuer}/oauth/userinfo`, {
  headers: { Authorization: `Bearer ${accessToken}` },
}).then((r) => r.json());
console.log(userinfo);
```

Node.js snippet for token exchange research using client_secret_post:
```ts
const issuer = "https://identity-dev.1above.io";
const params = new URLSearchParams({
  grant_type: "authorization_code",
  client_id: "YOUR_CLIENT_ID",
  client_secret: "YOUR_CLIENT_SECRET",
  code: "AUTHORIZATION_CODE",
  redirect_uri: "https://app.example.com/auth/callback",
  code_verifier: "ORIGINAL_CODE_VERIFIER",
});
const tokenResponse = await fetch(`${issuer}/oauth/token`, {
  method: "POST",
  headers: { "content-type": "application/x-www-form-urlencoded" },
  body: params,
});
console.log(await tokenResponse.json());
```

Debugging checklist for coding agents:
- If authorize fails immediately, verify redirect_uri exact match and scope contents.
- If token exchange fails, verify the client's tokenEndpointAuthMethod and whether code_verifier is required.
- If refresh fails, verify the org app-access grant still exists and the client still allows refresh_token.
- If claims look incomplete, compare granted scope against expected email/profile fields.
- If signature validation fails, re-fetch discovery and JWKS and verify issuer and kid alignment.

## recommended application flow
1. Generate state, nonce, and a PKCE code_verifier in your app.
2. Redirect the browser to the authorize endpoint.
3. On callback, verify returned state before doing anything else.
4. Exchange the authorization code from your server, not from browser-only code.
5. Validate the ID token and extract the user and org claims.
6. Create your app-local session using sub as the stable user identifier.
7. Store refresh_token only if your app actually needs long-lived sessions.
8. Use the custom org and membership claims to choose tenant context and authorization defaults.

## custom org-aware claims
https://identity.1above.io/claims/org_id
https://identity.1above.io/claims/org_slug
https://identity.1above.io/claims/org_name
https://identity.1above.io/claims/membership_role

## api keys for non-interactive agents
Identity issues two key types so agents and browser SDKs can authenticate without a human in the loop.

Secret keys (sk_live_*, sk_test_*) are confidential credentials bound to one org, optional calling service, and one or more resource services.
Publishable keys (pk_live_*, pk_test_*) are public identifiers for an app client; security comes from an Origin allowlist, not from secrecy.

Secret key format:
sk_<mode>_<keyId>.<secret>  where <keyId> is 16 chars and <secret> is 32 chars

Publishable key format:
pk_<mode>_<random32>

Authenticating with a secret key:
Exchange the key for a 15-minute Bearer JWT at the token endpoint, then call downstream services with that JWT.
Do not send raw sk_* values to resource services. Resource services should reject them and accept only JWTs.
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:api-key&api_key=sk_live_KEYID.SECRET&resource=agentier-orchestration&scope=optional%20space%20separated

Successful api-key grant response:
- access_token: JWT, 15-minute lifetime
- token_type: Bearer
- expires_in: 900
- scope: granted scope, space separated

JWT claims for the api-key grant:
- sub: the user id, or svc:<azp> for service keys
- org: the org id
- aud: exactly one resource-service slug
- azp: registered calling service slug when known, otherwise the API key stable id
- scope: granted scopes joined by space
- mode: live or test
- typ: api-key
- standard iss, exp, iat, jti

Rules:
- no refresh token is issued; re-exchange the api key when the JWT is near expiry (clients should cache the JWT and refresh ~30s before exp)
- resource is required on every exchange and maps to the JWT aud value
- omitted scope grants all approved scopes for the requested resource
- requested scope downscopes only within the approved scopes for the requested resource
- scopes are checked against the current resource-service vocabulary, so deleted scopes stop working without revoking the whole key
- a key is rejected if revoked, expired, mode mismatched, or if the matching OrganizationAppAccess is no longer active
- treat sk_* values as secrets equivalent to a long-lived password

Agent setup APIs:
- MCP agents should prefer POST /mcp with tools whoami, list_targets, request_service_access, request_api_key, and get_request
- MCP tool calls authenticate with a user access token, a browser session, or a requests:create bootstrap sk
- org admins and owners can discover accessible and requestable resource services with GET /api/orgs/{orgId}/resource-services
- when service access is missing, POST /api/orgs/{orgId}/access-requests with {kind:"service_access", resourceServiceId}; requestable services auto-approve and create OrganizationAppAccess
- API-key setup is a separate POST /api/orgs/{orgId}/access-requests with {kind:"api_key", keyName, resourceGrants, mode, expiresAt?, confirmNoExpiry?}
- missing service access fails with code missing_service_access and a request_service_access hint
- approved API-key requests are claimed once with POST /api/orgs/{orgId}/access-requests/{requestId}/claim
- renewals use POST /api/orgs/{orgId}/api-keys/{id}/renewal-request and follow the same approval and claim rules

Browser flows with publishable keys:
GET /api/public/app-client
Authorization: Bearer pk_live_RANDOM

Or, equivalently:
GET /api/public/app-client
x-publishable-key: pk_live_RANDOM

Rules:
- the request must include an Origin (or Referer) header that matches the pk's allowlist
- exact hostnames and a single leading *. wildcard are supported
- on success, the endpoint returns the AppClient public configuration (slug, name, kind, baseUrl, oidcEnabled, oidcRedirectUris, oidcScopes, availableScopes) plus mode and publishableKeyId
- CORS echoes the validated origin; do not deploy a pk without an origins allowlist that covers the calling site

Managing keys:
- mint sk directly: POST /api/orgs/{orgId}/api-keys with body {appClientIds, name, scopes, mode, expiresAt?}; the raw key is returned once
- request sk through approval: POST /api/orgs/{orgId}/access-requests with kind=api_key, approve, then claim once
- list sk: GET /api/orgs/{orgId}/api-keys
- revoke sk: DELETE /api/orgs/{orgId}/api-keys/{id}
- mint pk: POST /api/orgs/{orgId}/publishable-keys with body {appClientId, mode, name, origins}
- list pk: GET /api/orgs/{orgId}/publishable-keys
- revoke pk: DELETE /api/orgs/{orgId}/publishable-keys/{id}

## agent brief
A concise project brief for coding agents is served at /agents.md.
It summarizes the stack, auth flows, key endpoints, and where to look in the source tree.

## notes
Identity remains the system of record for authentication, organization membership, and app access authorization.
If a user belongs to multiple orgs, send org as an org id or slug hint when starting authorize.
If you are writing integration code automatically, prefer reading the discovery document first, then use the constraints in this file to drive your implementation.