Skip to content

Identity and tokens

Identity (AUTH_BASE_URL) issues the access tokens that Frontline and GTFS-RT require. All token requests are form-encoded POSTs to /connect/token. The Identity route reference lists every published route.

Your account is who you are. A grant type is how you prove it.

  • Service account: a machine identity for an external integration, created and owned by an approved admin. Its client_id is its own OAuth application, so it authenticates as itself. This is currently the only way for external integrators to get access; see get access.
  • Human account: an existing, approved person. Humans sign in through an approved public OAuth application (HUMAN_OAUTH_CLIENT_ID below), which is not the username. Get its ID from your MyBusz contact. New human accounts can’t currently be created.

An application can use only the grants it is registered for. Confidential applications must also authenticate as a client, from a trusted server.

Grant type Account Proof Token amr Refresh token
urn:custom:nonce-challenge Service account Nonce signed with a registered public key pubkey No
client_credentials Service account Client secret secret No
urn:custom:password-totp Human Password plus TOTP or recovery code pwd, otp Yes
urn:custom:passkey Human WebAuthn assertion passkey No

Service accounts use only the first two grants. Password, TOTP, and passkeys are for humans only. pubkey and passkey tokens get the higher Frontline rate limit.

Access tokens currently last one hour. Schedule renewal from expires_in.

Request under AUTH_BASE_URL Purpose
GET /.well-known/openid-configuration Issuer and endpoint metadata
GET /.well-known/jwks Public keys for verifying access-token signatures

Read issuer and jwks_uri from discovery instead of hardcoding them or pinning one signing key. Verify the signature, issuer, audience (mybusz-api), and expiry; decoding a JWT is not validation. Scopes come from the account’s current authority, not from what the client requests.

Send grant_type=client_credentials with client_id and client_secret, as in getting started.

Keep the private key on your own trusted signer.

1. Request a nonce.

Terminal window
curl --fail-with-body --silent --show-error \
--data-urlencode "clientId=${CLIENT_ID}" \
"${AUTH_BASE_URL}/connect/challenge"

The response is JSON with a nonce string. This form field is clientId; the token request uses client_id.

2. Sign the nonce. Sign the exact UTF-8 bytes of nonce (no quotes, newline, or JSON wrapping) as a detached signature, not a JWT or JWS. Encode it with standard Base64, not Base64url.

3. Exchange the signature. Set SIGNED_NONCE_BASE64 to the signature:

Terminal window
curl --fail-with-body --silent --show-error \
--data-urlencode 'grant_type=urn:custom:nonce-challenge' \
--data-urlencode "client_id=${CLIENT_ID}" \
--data-urlencode "signed_nonce=${SIGNED_NONCE_BASE64}" \
"${AUTH_BASE_URL}/connect/token"

Supported algorithms are ES256, ES384, ES512, RS256, RS384, and RS512, plus ML-DSA-44, ML-DSA-65, ML-DSA-87, SLH-DSA-SHA2-128s, and SLH-DSA-SHAKE-128s where the server runtime supports them.

  • Use the algorithm registered for your key, not your SDK’s default. A classical JWK without alg resolves by curve (P-256, P-384, P-521 to ES256, ES384, ES512), and RSA resolves to RS256.
  • ECDSA signatures use fixed-width r || s (IEEE P1363), not ASN.1 DER.
  • Post-quantum signatures use an empty context.

A nonce expires after five minutes and is used up by the exchange, even if the signature then fails. Each client ID has one pending nonce, and a new challenge replaces it, so run this flow one at a time per credential. After a failure or uncertain result, start again with a new challenge.

POST /client/pubkey (deprecated) lets an eligible secret-based machine client, including a service account, switch to public-key authentication. Call it with that client’s bearer token (amr=secret) and JSON publicKeyJson: a string containing a public JWK Set or supported public-key PEM. Never send a private key.

The switch is one-way. The secret is cleared, client_credentials stops working, and you must use the nonce flow from then on. There is no self-service rollback, and it can’t replace a key that is already registered.

Human grants need an existing, approved account and the approved public application’s client_id. They are not admin-only sign-in methods.

The account must be approved, not locked out, and have TOTP set up. This custom grant is not the standard OAuth password grant.

Terminal window
curl --fail-with-body --silent --show-error \
--data-urlencode 'grant_type=urn:custom:password-totp' \
--data-urlencode "client_id=${HUMAN_OAUTH_CLIENT_ID}" \
--data-urlencode "username=${USERNAME}" \
--data-urlencode "password=${PASSWORD}" \
--data-urlencode "totp_code=${TOTP_CODE}" \
"${AUTH_BASE_URL}/connect/token"

totp_code takes the current authenticator code or an unused recovery code; recovery codes are used up. This is the only grant that returns a refresh token.

Terminal window
curl --fail-with-body --silent --show-error \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode "client_id=${HUMAN_OAUTH_CLIENT_ID}" \
--data-urlencode "refresh_token=${REFRESH_TOKEN}" \
"${AUTH_BASE_URL}/connect/token"

Use the same application as the original sign-in. Refresh tokens currently last seven days. Refresh one request at a time and store the new tokens from each response. Refresh rechecks the account’s existence, approval, lockout, roles, and security stamp; if it fails, sign in again instead of retrying.

Passkeys work for existing human accounts without an admin role, but don’t bypass approval or account checks.

Run passkey ceremonies on an approved HTTPS browser origin that matches the server’s WebAuthn relying party; agree on the origin with your MyBusz contact first. Request bodies use the service’s FIDO2 JSON format, so binary challenges, credential IDs, and authenticator responses need converting. Don’t just JSON.stringify a browser credential. JSON fields are camelCase (totpCode), while the token form uses totp_code. The route reference doesn’t include these body schemas; get them from your MyBusz contact.

  1. POST /passkey/register-options with username, password, and totpCode. It returns the creation options.
  2. Run the WebAuthn credential-creation ceremony with those options.
  3. POST /passkey/register-complete with the same username, the serialized attestationResponse, and an optional friendlyName. It returns a credentialId, not a token.

The options expire after five minutes and are single-use. Each account can have one registration in progress.

  1. POST /passkey/authenticate-options with username, or {} for a discoverable credential (which must already be registered). Keep the returned sessionId and options.
  2. Run the WebAuthn authentication ceremony.
  3. POST /passkey/authenticate-complete with sessionId and the serialized assertionResponse. It returns an assertion_token.
  4. Form-POST /connect/token with grant_type=urn:custom:passkey, the application’s client_id, and assertion_token.

The options last five minutes and the assertion_token two minutes; both are single-use. Restart after expiry or an uncertain result. The token has amr=passkey and no refresh token.

  • POST /passkey/list with username, password, and totpCode returns passkeys, each with id, friendlyName, createdAt, lastUsedAt, and aaGuid.
  • POST /passkey/revoke with the same fields plus passkeyId deletes that passkey. passkeyId is the integer id from the list, not the Base64 credentialId.

Both require password and TOTP proof in the body; an access token isn’t enough. Removing a passkey doesn’t invalidate access tokens already issued.

  1. POST /account/reset-totp with username, password, and totpCode (current TOTP or an unused recovery code; a password alone isn’t enough). It returns totpSetupUri and totpSharedKey for the new secret.
  2. Add the new secret to your authenticator. TOTP sign-in is unavailable until you verify it, and existing refresh sessions can be invalidated.
  3. POST /account/verify-totp with username and a totpCode from the new authenticator. It returns new recoveryCodes; store them securely.

Form-POST /connect/revocation with token, an optional token_type_hint (access_token or refresh_token), and the application’s client identity and authentication. The application must be allowed to revoke that token, and the nonce grant can’t authenticate this endpoint. Success can be an empty response even for an already-invalid token; it isn’t a status check.

Transit APIs validate access tokens locally, so revoking a token doesn’t reject it everywhere immediately. On sign-out, also delete your local token copies.