Skip to main content

Authentication

The Kadu API uses a Kobana Account access token to authenticate every request, in the Authorization: Bearer header, like Kobana's other APIs.

curl -H "Authorization: Bearer $KOBANA_TOKEN" \
-H "Content-Type: application/json" \
https://api.kadu.ia.br/v1/me

The token​

The token is an access token issued by Kobana's Authorization Server (account.kobana.com.br). Kadu only verifies it: it keeps no signing key and cannot issue any token.

  • Algorithm: RS256, and only RS256.
  • Type: the token header must have typ: at+jwt (RFC 9068). Another JWT from the same issuer, such as an ID token, is rejected.
  • Key: public, and discovered, not configured. The issuer identifier is a URL, and the keys endpoint (jwks_uri) is read from the RFC 8414 metadata document published under it (/.well-known/oauth-authorization-server). When the Authorization Server rotates the key, Kadu follows on its own.
  • When the Authorization Server is called: never to verify a token, because signature checking is local. What comes from it is the key set, which is cached. A fetch happens only with an empty cache, with the cached set older than an hour, or when a token arrives with a kid that is not in the cache (a rotation; at most one new fetch every 30 seconds). Each fetch has a 5-second timeout.

Claims​

ClaimDescription
issThe Authorization Server's identifier, as a URL: https://account[.{env}].kobana.com.br.
audThis API, as a URL: https://api.kadu.ia.br (staging: https://api.staging.kadu.ia.br). Can be a list: the rule is contains, not equality, so a token can address more than one Kobana API.
expRequired. A token with no expiration is rejected.
orgThe organization on whose behalf the token acts, and it is required. It scopes every response. A token without org is rejected, instead of falling back to sub.
subThe person, never the organization. Goes to auditing.
emailWhen present, ties the token to a person in the organization, and their role applies too. An integration token, with no person, is governed by scopes only.
jtiUnique token id, used for revocation and for the rate limit.
scopeSpace-separated scopes (below).

:::info Check the environment Production's aud is not valid in staging, and vice versa. Do not reuse the token across environments. :::

Test the token​

GET /v1/me tells you who is behind the token. It is the only route without a scope check, precisely so you can test a new token.

Scopes​

Scopes have two orthogonal axes: the resource, always prefixed by the app (kadu/tasks), and the access level, a global scope with no prefix: read, write, delete, import, export or all. A resource without a verb means read. Everything else requires both: the resource scope and the level.

A route's resource is the first segment after /v1: /v1/executions/{id}/logs belongs to the executions resource.

ScopeGrants read access to
kadu/organizationsthe token's organization
kadu/workspacesworkspaces
kadu/activitiesactivities
kadu/taskstasks, the run pre-check and the run endpoint (the run itself is write)
kadu/executionsexecutions, logs, events, pending items and search (cancel and continue are write)
kadu/todospending items (resolving is write)
kadu/filesinput and output files
kadu/environmentsenvironments
kadu/secretssecrets: key, notes and metadata. The value is never returned by any endpoint
kadu/schedulesschedules
kadu/memoriesmemories
kadu/webhookswebhooks (rotating the signing secret is write)
kadu/webhook_deliverieswebhook deliveries (resending is write)
kadu/repositorythe Playbook (syncing is write)
kadu/usageusage and cost
kadu/usersorganization members
kadu/membershipslinks between people and the organization
kadu/rolesroles
kadu/system_eventssystem events
kadu/allwildcard of the resource axis: reads all the resources above and grants no level
Access levelGrants
readRead. The resource scope already implies it; on a token with no kadu/… scope, reads all resources
writeEvery POST, PUT and PATCH on the resources the token carries: create, update, run, cancel, sync, rotate secret, resend delivery. Does not delete
deleteEvery DELETE on the resources the token carries, and nothing else
importBulk import. No endpoint in this API imports in bulk today
exportBulk export. No endpoint in this API exports in bulk today
allAll the levels above

Which level each endpoint requires​

One level per request, determined by the HTTP method. No level substitutes for another: write does not delete, and delete does not write.

RequestRequires
GETThe resource scope (kadu/tasks)
POST, PUT, PATCHThe resource scope + write (kadu/tasks write)
DELETEThe resource scope + delete (kadu/tasks delete); write is not enough
GET /v1/meAny valid token, with no scope check

Every deletion is a DELETE (the resource gets deleted_at) and every DELETE is a deletion.

Three examples: kadu/executions reads executions and does not cancel; kadu/tasks write also creates, updates and runs tasks, but does not delete them; kadu/tasks write delete does all three. None of them reaches secrets.

The granular form kadu/tasks:write (or kadu/tasks:delete) is an exception to the grammar, not the default format: it satisfies both axes of that resource on its own and exists for "reads one resource, writes another". kadu/* is a registration wildcard and is never issued in a token: the comparison here is always literal.

A token that carries kadu/… scopes is limited to the resources they name, and the levels act only within them. A token with no kadu/… scope is limited by level: it reads with read, write or all; writes with write or all; deletes with delete or all. A token with neither reaches nothing beyond GET /v1/me.

Insufficient scope responds 403 INSUFFICIENT_SCOPE, with the levels that would suffice in details.required and the token's scopes in details.granted.

Validation pipeline​

In this order:

  1. Extracts Authorization: Bearer <jwt>.
  2. Verifies the signature with the Authorization Server's public key, accepting only RS256 and typ: at+jwt.
  3. Validates iss, that aud contains this API, and that exp is present and in the future.
  4. Checks whether the jti has been revoked.
  5. Resolves the organization from org.
  6. Applies the token's rate limit.
  7. Checks the scope against the endpoint.
  8. With a person in the token, checks their role in the organization (Permissions).

Isolation between organizations​

Every query is filtered by the org organization. There is no way to point to another organization through the body or the query. A resource from another organization responds 404 NOT_FOUND, not 403, so as not to reveal that it exists.

Failure modes​

HTTPcodeCause
401INVALID_TOKENJWT missing, malformed, with an invalid signature or with another typ
401INVALID_AUDIENCEaud from another app or another environment
401INVALID_ISSUERissuer different from Kobana's Authorization Server
401TOKEN_EXPIREDexp in the past
401TOKEN_REVOKEDrevoked jti
403UNKNOWN_ORGANIZATIONthe org points to an organization Kadu does not know
403INSUFFICIENT_SCOPEtoken without the required scope
403USER_PERMISSION_DENIED, NOT_A_MEMBERthe role of the token's person does not allow it, or they do not belong to the organization (Permissions)
429RATE_LIMITEDtoken limit exceeded (Limits)
503AS_UNAVAILABLEthe signing keys could not be fetched from the Authorization Server. Verification itself is local, so this only happens at the moments listed in When the Authorization Server is called. It is not a problem with your token: try again after Retry-After. It is a 5xx on purpose: a 401 would send you to debug a good credential during an outage on our side.

See Errors for the full envelope.

Best practices​

  • Never commit a token to git. Use environment variables or a vault (AWS Secrets Manager, Vault…).
  • Use one token per integration: you can revoke one without taking down the others.
  • Ask for the minimum scopes: kadu/executions for whoever only follows, kadu/tasks write for whoever runs, delete only for whoever deletes.
  • Revoke any leaked token immediately.