Authentication in the CatalystOne Developer Platform
This guide explains how authentication works in the CatalystOne Developer Platform. It is intended for developers who are building integrations, web applications, SPAs, or mobile experiences that need to call CatalystOne APIs. The goal is to help you choose the right authentication mechanism, understand how registration works, and implement the flow correctly from end to end.
At a high level, the platform uses OAuth 2.0 and issues JWT bearer tokens from the CatalystOne Authorization Server. There are two main ways to authenticate. The first is machine-to-machine authentication using the Client Credentials grant. The second is named user authentication using Authorization Code with PKCE. Which one you choose depends entirely on whether your integration acts as a system or on behalf of a signed-in user.
Choosing the right authentication mechanism
If your integration runs in the background, has no interactive user, and simply needs to call APIs as a service, you should use Client Credentials. This is the typical choice for scheduled jobs, backend integrations, provisioning connectors, and other headless system-to-system scenarios.
If your application needs a person to sign in and you want API access to reflect that person's identity and permissions, you should use Authorization Code with PKCE. This is the right choice for browser applications, mobile apps, portals, or integrations where user context matters.
In practice, the platform exposes these two models through two setup experiences:
Client Configuration is used for machine-to-machine integrations
App Registration is used for named-user OAuth applications.
Base URL and key endpoints
GET /oauth2/authorizefor starting the browser-based user sign-in flowPOST /oauth2/tokenfor exchanging credentials, authorization codes, or refresh tokens for access tokensGET /oauth2/jwksfor retrieving the signing keys used to validate JWTsGET /api/oauth2/consentsfor listing a user's current app consentsDELETE /api/oauth2/consents/{clientId}for revoking a previously granted consent
Machine-to-machine authentication with Client Credentials
Machine-to-machine authentication is designed for backend integrations where no user is present. In this flow, your integration authenticates using a client ID and client secret, and the authorization server returns a bearer token that can be used to call the APIs the client has been granted access to.
Before this flow can be used, you must create a Client Configuration. This configuration defines which APIs the client may access, which scopes it may request, and whether any additional constraints or data filters should be enforced at runtime. In other words, the Client Configuration is where you define the client's permissions and boundaries before any token is ever issued.
Once the client has been configured and a secret has been generated, your service can request a token using HTTP Basic authentication. The client_id and client_secret are joined as client_id:client_secret, Base64-encoded, and sent as Authorization: Basic <encoded> — they are not included in the request body.
curl -X POST 'https://api.catalystone.io/auth2/oauth2/token' \
-u '<client_id>:<client_secret>' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'scope=user.read'
If the request is valid, the server returns a standard OAuth token response containing an access_token, a token_type of Bearer, an expires_in value, and the granted scopes. For machine-to-machine clients, the access token lifetime is currently one hour, so expires_in is typically 3600 seconds.
This is the best option when the integration itself is the actor. If, instead, the external application needs to act as a person, you should move to App Registration and the named-user flow described next.
Named user authentication with Authorization Code and PKCE
Named user authentication is used when an external application needs a real CatalystOne user to sign in and grant access. In this model, the application does not simply authenticate as itself. Instead, the user is redirected to the authorization server, signs in, reviews consent where required, and the application receives a token that represents both the application and the user.
This flow is configured through App Registration.
App Registration and client types
When you create an App Registration, you provide the application's name, an optional description, and one or more allowed redirect URIs. You also choose the application's client type.
The platform supports two client types:
Confidential applications, which can safely store a client secret in a trusted backend
Public applications, such as single-page applications and mobile apps, which do not use a client secret and must rely on PKCE
This distinction matters because it affects how the token exchange works. A confidential client can authenticate with a client secret. A public client cannot. Confidential applications support client secret generation and regeneration. Public applications do not use client secrets and rely on PKCE.
You can change an existing registration's client type at any time if your application's needs evolve. For example, if a public SPA later needs backend integration capabilities, you can transition it to confidential and start using a client secret.
Legacy clients that do not support PKCE
Some older integrations cannot send PKCE parameters (code_challenge /code_verifier). Support for these cases is limited by client type:
Public applications: PKCE is always required.
Confidential applications: PKCE is recommended and should be enabled by default, but can be disabled for legacy compatibility when needed.
If you disable PKCE for a confidential application, treat it as an exception path and apply additional controls:
keep redirect URIs tightly scoped and HTTPS-only,
protect and rotate the client secret,
request only the minimum scopes required,
plan migration to PKCE-enabled behavior when the client supports it.
Redirect URI requirements
Redirect URIs are validated before an app registration is accepted.
In normal production scenarios, you should use https:// redirect URIs. For local development, the platform also permits http://localhost and http://127.0.0.1, with or without an explicit port. Mobile deep links are supported as well, using URIs such as myapp://oauth/callback.
More specifically, the following redirect URI patterns are accepted:
https://<host>/...http://localhost[:port]/...http://127.0.0.1[:port]/...custom deep links such as
myapp://oauth/callback
To protect applications from unsafe or misleading redirect targets, a few patterns are explicitly rejected. Redirect URIs may not contain userinfo such as https://user:pass@host/.... For plain HTTP, only localhost and 127.0.0.1 are accepted, and the port must either be omitted or fall within 1..65535. Custom schemes must use the scheme://... deep-link format, and unsafe schemes such as file, javascript, data, and blob are not allowed.
How the Authorization Code flow works
Once an app has been registered, the Authorization Code flow begins by generating PKCE values. Your application creates a high-entropy code_verifier, derives a code_challenge from it using SHA-256 and base64url encoding, and also creates a random state value. The state is important for CSRF protection and must be validated when the user returns to your app.
Your application then redirects the user's browser to the authorization endpoint. This is a browser redirect, not a backend API call.
# Example URL to open in the browser
https://api.catalystone.io/auth2/oauth2/authorize?response_type=code&client_id=<client_id>&redirect_uri=<urlencoded_redirect_uri>&scope=openid%20profile&state=<random_state>&code_challenge=<code_challenge>&code_challenge_method=S256
After the user signs in and completes consent if needed, the authorization server redirects the browser back to the registered redirect URI. The response contains an authorization code and the same `state` value your application originally sent.<redirect_uri>?code=<authorization_code>&state=<same_state>
At that point, your application exchanges the authorization code for tokens.
For a public client, there is no client secret. The client authenticates by including the client_id directly in the request body, alongside the grant_type, the received code, the same redirect_uri, and the original code_verifier. No Authorization header is sent.
curl -X POST 'https://api.catalystone.io/auth2/oauth2/token' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=<client_id>' \
--data-urlencode 'code=<authorization_code>' \
--data-urlencode 'redirect_uri=<redirect_uri>' \
--data-urlencode 'code_verifier=<code_verifier>'
For a confidential client, the client authenticates using HTTP Basic — client_id and client_secret are joined as client_id:client_secret, Base64-encoded, and sent as Authorization: Basic <encoded>. The client_id is not repeated in the request body.
curl -X POST 'https://api.catalystone.io/auth2/oauth2/token' \
-u '<client_id>:<client_secret>' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=<authorization_code>' \
--data-urlencode 'redirect_uri=<redirect_uri>' \
--data-urlencode 'code_verifier=<code_verifier>'
If everything matches — including the redirect URI, PKCE values, and client behavior — the authorization server returns a bearer access token. For App Registration clients, the current access token lifetime is 5 minutes, and refresh tokens are issued with a 60 minute lifetime.
Refresh tokens
Confidential clients can use the refresh token grant to obtain a new access token without asking the user to sign in again immediately. As with the authorization code exchange, client_id and client_secret are Base64-encoded as client_id:client_secret and sent via Authorization: Basic <encoded> — not in the request body. Public clients do not use a client secret and should instead send the user through the authorization flow again when their token expires.
curl -X POST 'https://api.catalystone.io/auth2/oauth2/token' \
-u '<client_id>:<client_secret>' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=<refresh_token>'
Consent management
For named-user applications, consent is part of the user experience and can also be managed after the fact. Users can review and revoke previously granted application consents directly in the CatalystOne application. The platform also exposes endpoints that let an authenticated user inspect which applications they have consented to and revoke those consents when needed.
To list the current user's consents, call:
curl -X GET 'https://api.catalystone.io/auth2/api/oauth2/consents' \
-H 'Authorization: Bearer <named_user_access_token>'
To revoke consent for a specific application, call:
curl -X DELETE 'https://api.catalystone.io/auth2/api/oauth2/consents/<app_client_id>' \
-H 'Authorization: Bearer <named_user_access_token>'
These results are tenant-scoped, which means the server derives the tenant from the authenticated token and only returns or revokes consents within that tenant context.
Security guidance
OAuth flows are only safe when a few foundational rules are followed consistently. Outside local development, always use HTTPS. Never log client secrets, authorization codes, access tokens, or refresh tokens. When using Authorization Code with PKCE, always send code_challenge_method=S256, generate a unique code_verifier for each authorization request, and verify the state value exactly on the callback.
It is also good practice to request only the scopes your application truly needs, to store secrets and refresh tokens in secure storage, and to rotate secrets immediately if you suspect they may have been exposed.
Troubleshooting common errors
Most authentication issues fall into a small set of OAuth errors.
An invalid_client error usually means the client credentials are wrong, missing, or being used in the wrong kind of flow. A common example is trying to use a client secret with a public application, or using the wrong secret for a confidential one.
An invalid_grant error usually points to something wrong in the authorization code exchange. The authorization code may have expired, may already have been used, the redirect URI might not match exactly, or the PKCE code_verifier may not match the original code_challenge.
An invalid_request error generally means one or more required fields are missing or malformed. This can happen if values are not URL-encoded correctly, if the request body is incomplete, or if the wrong parameter set is used for the chosen grant type.
Quick reference
If you only need the short version, use the following rules:
Use
grant_type=client_credentialsfor machine-to-machine integrations.Use
grant_type=authorization_codewith PKCE for user sign-in flows.Treat refresh tokens as a confidential-client capability.
Use
GET /oauth2/jwksto retrieve the public signing keys used to validate issued JWTs.