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 legacy REST API. The legacy surface —
/api/v1.0/* — admits an OAuth 2.1 token that carries a
scope covering the resource and action the request implies: the resource from
the path segment after the version prefix, the action from the HTTP method. So
a token granted tasks:read can call GET /api/v1.0/tasks.
On the legacy surface, fine-grained scopes cover tasks, projects, docs,
labels and members. Other segments answer 403. Do not refresh the token in
response — a refresh does not change the outcome. Use the v3 API for those
resources.
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 the grant types, scopes, PKCE methods and
client-authentication methods this server accepts. Most OAuth
libraries can consume it directly; point yours at the issuer
https://api.niftypm.com and let it resolve the rest.
Code
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".
You should land on Your apps, with a Create app button that opens Create OAuth app and asks for an app type — Public (PKCE) or Confidential. If you do not see Create app, write to team@niftypm.com.
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. - Only
https://is accepted, plushttp://localhost,http://127.0.0.1andhttp://[::1]for development. Custom schemes (myapp://callback), a URL fragment, and embedded credentials (https://user:pass@host) are all rejected at registration time; native apps should use a loopback redirect.
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 and <resource>:read |
delete | every <resource>:delete |
Fine-grained scopes — <resource>:<action>, where the resource is a
kebab-case plural and the action is one of read, write or delete. For
example tasks:read, chats:write, check-ins:write, 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.
scopes_supported in the discovery document is authoritative.
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:
- The
scopefield on the token response is the coarse summary (read,write,delete), not a per-resource inventory. Treat a403as insufficient scope for that route. - 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.
Migrating from OAuth 2.0
If you have implemented an OAuth 2.0 client before, these are the differences that break an OAuth 2.0 client:
- 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. On refresh, a scope emptied by the role clamp is invalid_grant instead, and it revokes the whole refresh-token family |
unsupported_response_type | Anything other than response_type=code |
access_denied | The user declined at the consent screen |
temporarily_unavailable | The authorization server is temporarily unavailable. Retry later |