Authorization (OAuth 2.1)
Authorization (OAuth 2.1)
OAuth 2.1 is how a third-party app asks a Nifty team for permission to call the API on its behalf, without ever handling the user's password. If you are only scripting against your own account, use a Personal Access Token instead — this page is for integrations you ship to other people's teams.
Nifty's authorization server implements the authorization-code grant with PKCE, which OAuth 2.1 requires of every client. The implicit and resource-owner-password grants are not supported.
Scope requirement for the v1.0 REST API. The v1.0 surface only accepts a
token carrying the admin ("Full access") scope tier, and the OAuth 2.1
authorization server does not issue admin — so an OAuth 2.1 access token is
rejected with 403 Forbidden on /api/v1.0/*. Existing legacy Nifty App
integrations are unaffected and continue to work.
Endpoints
| Purpose | URL |
|---|---|
| Discovery (RFC 8414) | https://api.niftypm.com/.well-known/oauth-authorization-server |
| Authorization | https://api.niftypm.com/oauth/authorize |
| Token | https://api.niftypm.com/oauth/token |
Read the discovery document rather than hard-coding those URLs. It is the
authoritative description of this authorization server, and it is what tells
you — without a support ticket — which grant types, scopes, PKCE methods and
client-authentication methods are actually accepted right now. Most OAuth
libraries can consume it directly; point yours at the issuer
https://api.niftypm.com and let it resolve the rest.
Code
The authorization server runs per environment. Substitute the host if you are
building against a non-production environment — QA is
https://awsapi.niftyqa.com and UAT is https://awsapi.niftyuat.com; each
serves its own discovery document at the same path.
Registering your app
Nifty does not support dynamic client registration (RFC 7591) — there is
no registration_endpoint, and one is not advertised in the discovery
document. Register your app by hand: in Nifty, go to Settings → App Center →
"Integrate with API".
Registration gives you a client_id, and — if you register a confidential
client — a client_secret that is displayed once, at creation time. Store
it then; it is never shown again.
| Client type | Credentials | token_endpoint_auth_method |
|---|---|---|
| Public — mobile, desktop, single-page apps | client_id only | none |
| Confidential — server-side apps that can keep a secret | client_id + client_secret | client_secret_post |
Client credentials are sent in the token request body. HTTP Basic
(client_secret_basic) is not among the advertised authentication methods.
Redirect URI rules
You must register at least one redirect URI, and you may register several.
- Matching is an exact, full-string comparison. The
redirect_uriyou send to the authorization endpoint must be byte-identical to one of the registered values — including scheme, host, port, path, and any trailing slash. - There are no wildcards and no prefix matching. A literal
*is rejected at registration time. If your app needs several callback addresses, register each one in full. - A
redirect_urithat does not match a registered value is rejected withinvalid_requestbefore any authorization code exists.
The authorization code flow
1. Create a PKCE code verifier and challenge
The code_verifier is a high-entropy random string you keep private for the
duration of the flow. The code_challenge is its SHA-256 hash, base64url
encoded — S256 is the only challenge method this server accepts.
Code
Keep code_verifier in the user's session — you need it again in step 4, and
it must never travel through the browser redirect.
2. Send the user to the authorization endpoint
Redirect the user's browser (not a background HTTP request) to the authorization endpoint. Nifty responds with a redirect to its consent screen, where the user picks the workspace and approves the access you asked for.
Code
| Parameter | Required | Notes |
|---|---|---|
response_type | yes | code — the only supported response type |
client_id | yes | From registration |
redirect_uri | yes | Must exactly match a registered value |
scope | yes | Space-delimited (see Scopes) |
state | yes | Opaque anti-CSRF value; verify it on the way back |
code_challenge | yes | From step 1 |
code_challenge_method | yes | S256 — the only accepted method |
3. Receive the authorization code
When the user approves, their browser is redirected back to your registered
redirect_uri with the code in the query string (query is the only
supported response mode):
Code
Before doing anything else:
- Check
statematches the value you sent. - Check
issequalshttps://api.niftypm.com. This server setsauthorization_response_iss_parameter_supported, so the issuer is always returned and validating it defends against mix-up attacks.
If the user declines, you get ?error=access_denied&state=… instead.
The authorization code expires after 60 seconds and can be redeemed exactly once. Exchange it immediately; a second exchange of the same code fails.
4. Exchange the code for tokens
POST to the token endpoint as application/x-www-form-urlencoded.
Code
Omit client_secret for a public client; send it for a confidential one.
code_verifier is required for every client, public or confidential.
Code
The scope echoed here is the coarse summary of what was granted; the
token itself carries and enforces the fully expanded fine-grained set.
Use the access token like any bearer credential:
Code
Token lifetimes and refreshing
| Token | Lifetime |
|---|---|
| Authorization code | 60 seconds, single use |
| Access token | 3600 seconds (1 hour) |
| Refresh token | 7 days |
Always trust expires_in on the token response over a hard-coded constant.
To mint a fresh pair before the access token expires, use the refresh_token
grant:
Code
The response has the same shape as step 4 — including a new
refresh_token.
Three things to get right:
- Refresh tokens rotate. Every successful refresh returns a new refresh token and consumes the old one. Persist the new value immediately, replacing the old one.
- Replay revokes everything. Presenting a refresh token that has already been used is treated as a compromise: the entire token family is revoked and the user has to authorize your app again. This is why storing the newest token — and never retrying a refresh with a stale one — matters.
- Scope is re-clamped on every refresh against the granting member's current role.
Public clients refresh with client_id alone; confidential clients must still
present client_secret.
Scopes
Nifty advertises its scopes in scopes_supported, on two levels.
Coarse axes — read, write, delete. Request these when your app wants
broad access and you would rather not enumerate resources. The authorization
server expands them for you:
| Axis | Expands to |
|---|---|
read | every <resource>:read |
write | every <resource>:write, <resource>:read and <resource>:create |
delete | every <resource>:delete |
Fine-grained scopes — <resource>:<action>, where the resource is plural
and the action is one of read, write, create or delete. For example
tasks:read, chats:write, documents:create, projects:delete. Request
these when you want least privilege. The full list is in scopes_supported in
the discovery document.
Scopes are space-delimited in both the authorization request and the token response.
Scope is clamped to the granting member's role
Whatever you register and whatever you request is a ceiling, not a
guarantee. At consent time, and again every time a token is issued or
refreshed, the requested scope is intersected with the permissions the
approving member's role actually grants. A member who cannot delete projects
cannot grant your app projects:delete, however you asked for it.
Practical consequences:
- Always read the
scopefield on the token response; it is the authoritative record of what you got. - If the intersection is empty, authorization fails with
invalid_scope. - Because the clamp is re-applied on refresh, a token's effective access can shrink over the life of a connection.
There is no admin scope obtainable through OAuth 2.1. It is not
advertised in scopes_supported, and it is filtered out before coarse-axis
expansion, so it cannot be requested directly or acquired indirectly.
Migrating from OAuth 2.0
If you have implemented an OAuth 2.0 client before, these are the differences that will actually break your code:
- PKCE is mandatory for every client — not just public ones. A confidential
client with a
client_secretmust still generate acode_verifierand send it on the token exchange.S256is the only accepted challenge method;plainis not supported. - The implicit grant is gone.
response_type=tokenis not supported;codeis the only response type. Tokens are never returned in a redirect. - The resource-owner password grant is gone. Only
authorization_codeandrefresh_tokenare supported. Never ask a user for their Nifty password. - Redirect URIs are matched exactly. Any wildcard or prefix matching your OAuth 2.0 client relied on will fail.
- Refresh tokens are single-use and rotate. A client that stores the original refresh token and reuses it will get its whole token family revoked.
- Client credentials go in the request body (
client_secret_post), not in an HTTP Basic header. - Validate the
issparameter on the authorization response.
Error responses
Errors follow RFC 6749: a JSON body of { "error": …, "error_description": … }
from the token endpoint, and ?error=…&state=… on the redirect back from the
authorization endpoint.
error | Usually means |
|---|---|
invalid_request | A required parameter is missing or malformed — including a redirect_uri that does not exactly match a registered value |
invalid_client | Unknown client_id, or a confidential client sent a missing or wrong client_secret |
invalid_grant | The code or refresh token is expired, already used, or does not belong to this client; also a failed code_verifier check |
invalid_scope | The requested scope is not advertised, or the approving member's role grants none of it |
unsupported_response_type | Anything other than response_type=code |
access_denied | The user declined at the consent screen |
temporarily_unavailable | The authorization server is disabled for this environment; retry later |