Skip to main content

Authentication and Authorization

Authentication

BCMLogic Next uses Keycloak as its identity and access management layer, implementing OAuth 2.0 with OIDC. Keycloak handles human user logins (via the BCMLogic Next web application) and issues tokens for service-to-service integrations using the client credentials grant.

The token endpoint follows the standard Keycloak URL pattern:

https://auth.bcmlogic.com/realms/{realm}/protocol/openid-connect/token

Service-to-Service Flow

Request a token from Keycloak using your client credentials:

curl -X POST https://auth.bcmlogic.com/realms/bcmlogic/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=core:read core:write ai:invoke"
async function getToken(clientId: string, clientSecret: string, scopes: string[]): Promise<string> {
const response = await fetch(
'https://auth.bcmlogic.com/realms/bcmlogic/protocol/openid-connect/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope: scopes.join(' '),
}),
}
);
const { access_token } = await response.json();
return access_token;
}

The response is a JWT bearer token signed by Keycloak. Tokens are valid for 1 hour by default. Cache the token and refresh before expiry — do not fetch a new token on every request.

Include the token on every API request:

Authorization: Bearer eyJhbGc...

Do not put credentials in source control. Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, etc.).

OIDC Discovery

Keycloak exposes a standard OIDC discovery document. Use it to resolve endpoints programmatically rather than hardcoding URLs:

https://auth.bcmlogic.com/realms/bcmlogic/.well-known/openid-configuration

Scopes

Authorisation is scope-based. Request the minimum set of scopes your integration needs — least privilege simplifies incident response.

ScopeGrants
core:readRead access to Core API resources
core:writeCreate, update, and delete on Core API
audit:readAccess to audit events — see Data Model → Audit Trail
ai:invokeCall Intelligence Service generation endpoints
ai:searchCall Intelligence Service search endpoints — cheaper than generation, so scoped separately
documents:uploadUpload documents through Core API and trigger indexing in Intelligence Service
webhooks:manageConfigure webhook endpoints in the developer portal

Scopes are configured on your client credentials in the developer portal.

Tenant Scoping

The bearer token carries a tenant_id claim. The gateway resolves the tenant from this claim and enforces it at every layer. You cannot escalate to another tenant by adjusting request bodies or URLs.

If your integration manages multiple tenants (e.g. a managed service provider), authenticate separately per tenant and hold separate token caches.

See ADR-006 for the full isolation model.

Token Rotation

Client secrets are rotatable through the developer portal. Rotate:

  • At least every 12 months as standard hygiene.
  • Immediately on suspected compromise.

Old secrets remain valid for a 30-day overlap period to allow rolling deployments without downtime.

Token caching pattern

class TokenCache {
private token: string | null = null;
private expiresAt = 0;

async get(clientId: string, clientSecret: string, scopes: string[]): Promise<string> {
// Refresh 60 seconds before actual expiry
if (Date.now() < this.expiresAt - 60_000 && this.token) {
return this.token;
}

const res = await fetch('https://auth.bcmlogic.com/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope: scopes.join(' '),
}),
});

const { access_token, expires_in } = await res.json();
this.token = access_token;
this.expiresAt = Date.now() + expires_in * 1000;
return this.token;
}
}