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.
| Scope | Grants |
|---|---|
core:read | Read access to Core API resources |
core:write | Create, update, and delete on Core API |
audit:read | Access to audit events — see Data Model → Audit Trail |
ai:invoke | Call Intelligence Service generation endpoints |
ai:search | Call Intelligence Service search endpoints — cheaper than generation, so scoped separately |
documents:upload | Upload documents through Core API and trigger indexing in Intelligence Service |
webhooks:manage | Configure 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;
}
}