Use NuPaaS as an IdP
NuPaaS is an OpenID Provider. Register your internal app as a client, and its users sign in with their NuPaaS account — no second password store, and offboarding in NuPaaS ends access in your app.
What this is
Every organization has its own authorization server: its own issuer, its own discovery document, its own set of registered clients. A client belongs to exactly one organization, so a redirect URI registered in one organization cannot be used against another.
Manage clients at /orgs/<org>/settings/sso, under “Your OAuth / OIDC clients”. Registration is self-service — no support ticket, and no operator involvement.
Register a client
openid is required; email and profile are the usual set.Endpoints
The discovery document is the source of truth. Point your client at it and it will read the rest:
https://authend.<org>.<platform-domain>/o/<organizationId>/.well-known/openid-configurationReplace <organizationId> with your organization's id, shown beside the discovery URL on the settings page — the document you copy there is already the correct one.
| Parameter | Type | Description |
|---|---|---|
| issuer | string | Your organization's issuer identifier. A token's `iss` must equal this. |
| authorization_endpoint | url | Where the browser is sent to sign in. Authorization code flow only. |
| token_endpoint | url | Exchanges an authorization code for tokens. Client authentication is required (a client secret, or PKCE for public clients). |
| jwks_uri | url | Public keys for verifying an id_token's signature. Cache it; do not fetch per request. |
| userinfo_endpoint | url | Returns the same claims as the id_token, for an access token. |
Redirect URIs
A redirect URI is the security boundary of the authorization code flow: a URI that is validated loosely at registration and matched loosely at authorize time delivers a code to an attacker's endpoint. NuPaaS therefore validates at registration and matches byte-for-byte at authorization.
A URI is accepted only when it is:
- absolute — a scheme and a host are both required;
https, with one exception:http://localhostandhttp://127.0.0.1are allowed, because local development genuinely needs them;- free of wildcards;
- free of fragments.
Scopes
| Parameter | Type | Description |
|---|---|---|
| openid | required | Requests an id_token. Without it this is a plain OAuth flow and no identity claims are returned. |
optional | Adds the email address and whether it is verified. | |
| profile | optional | Adds the display name. |
Claims
The id_token and the userinfo endpoint return the same set. Every claim below is emitted only when the value is present — a user with no selected organization carries no organization claims at all, rather than claims with empty strings.
| Parameter | Type | Description |
|---|---|---|
| sub | string | Stable user identifier. Use this as the primary key for the user in your app; it does not change when an email does. |
string | Present when the `email` scope is granted. | |
| email_verified | boolean | Present alongside `email`. |
| name | string | Display name. Present when the `profile` scope is granted. |
| organization_id | string | The organization the user signed in to. Emitted with the other two organization claims, or not at all. |
| org_slug | string | The organization's URL slug. |
| org_role | string | The user's role within that organization: `owner`, `admin`, `member` or `viewer`, or the id of a custom organization role. |
| groups | string[] | The organization slug, as a single-element array. This is a published contract consumed by git host auto-membership — do not treat it as a general-purpose group list. |
| platform_role | string | Present only when the user holds a NuPaaS platform grant. ADVISORY ONLY — see below. |
Rotate a secret
Rotating issues a new secret and leaves the outgoing one working for a bounded window — 24 hours — so you can redeploy without an outage. The window's end is shown in the panel when you rotate; both secrets authenticate at the token endpoint until it closes, and only the new one after.
If the superseded secret is believed compromised rather than merely superseded, use Revoke superseded to close the window immediately. Rotation never deletes a client: disabling is reversible and keeps the audit trail.
Worked example
# Your app sends the browser here:
# https://authend.<org>.<domain>/o/<organizationId>/oauth/authorize
# ?response_type=code
# &client_id=<client id>
# &redirect_uri=https://app.example.com/callback
# &scope=openid%20email%20profile
# &state=<opaque, verified on return>
#
# The user signs in, and the browser comes back to:
# https://app.example.com/callback?code=<code>&state=<state>
# 2 — Exchange the code. The code is single-use; a second exchange fails.
curl -sS -X POST "https://authend.<org>.<domain>/o/<organizationId>/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<code>" \
-d "redirect_uri=https://app.example.com/callback" \
-d "client_id=<client id>" \
-d "client_secret=<client secret>"Verify the id_token against the jwks_uri from discovery, check iss, aud, exp and the nonce you sent, then key the user by sub.