One Keycloak to Rule Them All - Everybody Wants Access. Nobody Wants to Ask Permission
in Programming on Programming, Keycloack, Auth
In the previous two posts: post 1, post 2, we managed to solve user authentication and internal communication between our products.
Users can log in once.
Internal services can talk to each other.
We have Client Credentials, Token Exchange, and reasonably well-behaved access tokens.
Life is good.
Then a customer asks:
“Can we connect our own application to your platform?”
Sure. We have APIs.
“Great! Can our application access users’ contacts and automatically create workflows?”
Hmm.
“Also, can users authorize it themselves? Like when you connect an app to Google?”
Ah, yes. Like Google.
Just a small feature request. Nothing complicated. 🙃
The obvious solution. Again.
We’ve already got APIs secured with Keycloak.
Why not give external developers a client ID and client secret?
They can request a token using Client Credentials and call our APIs.
Simple.
Except there’s one rather important question.
Whose data are they allowed to access?
Suppose a third-party application called SuperAutomation wants to read Alice’s contacts.
A service account token represents the application itself, not Alice granting access to her data.
We could build custom permission mappings around that service identity, but now we’d need to manage user authorization, organization boundaries, and revocation ourselves.
And under absolutely no circumstances should we ask Alice to give SuperAutomation her Keycloak password.
It’s 2026. We should know better by now. 💀
What we actually want is delegated authorization.
In other words:
“Hey Alice, SuperAutomation wants permission to access some of your data. Is that okay?”
That’s what OAuth is for.
Meet our new external developer
Let’s imagine a developer builds SuperAutomation, a small application that connects to our platform.
The application needs to:
- Read contacts.
- Create workflows.
- Receive notifications about workflow completion.
We define permissions using OAuth scopes.
For example:
contacts:read
workflows:create
workflows:read
notifications:read
Instead of giving the application unlimited API access, we let it request the permissions it needs.
At least in theory.
Because there’s always that one integration requesting every available scope “just in case.”
Sure. And while we’re at it, would you like our production database password? 🙃
First, register your application
Before SuperAutomation can request user authorization, we need to identify it.
We create an OAuth client for the application.
Client ID: superautomation
Client Type: Confidential
Redirect URI:
https://superautomation.example.com/oauth/callback
For a server-side integration, the confidential client authenticates with a secret or another supported method.
For a browser-only or native application, we’d use a public client with PKCE instead.
We also need to restrict redirect URIs.
Please don’t accept arbitrary redirect URLs.
An authorization code being sent to an attacker-controlled callback isn’t a fun little edge case. It’s a security problem.
For a public integration ecosystem, we’d probably create a Developer Portal where developers can register applications and manage their credentials.
That portal would call our own registration backend, which in turn provisions the necessary Keycloak clients through administrative APIs.
We wouldn’t give every external developer access to the Keycloak Admin Console.
That would certainly make onboarding easier.
For approximately five minutes. 😅
OAuth scopes: Small permissions, big consequences
Let’s say we define two scopes:
contacts:read
workflows:create
The first allows an application to read contacts.
The second allows an application to request workflow creation.
In Keycloak, we can define corresponding client scopes, configure protocol mappers and authorization-related mappings, and assign them to eligible clients.
One important distinction: a Keycloak client scope isn’t automatically an API permission.
We still need to decide which claims represent each permission and how our APIs enforce them.
For our examples, assume the access token contains an OAuth scope claim and our APIs validate those values.
Also, client scopes assigned as optional let applications request them explicitly. Default scopes may be included without an explicit scope request.
This matters when building a meaningful consent experience.
We don’t want a user clicking “Allow” without understanding what the application is asking for.
The consent flow
Alice visits SuperAutomation and clicks:
“Connect to My Platform”
SuperAutomation redirects her browser to Keycloak.
The authorization request looks like this:
GET /realms/my-platform/protocol/openid-connect/auth
?client_id=superautomation
&response_type=code
&scope=openid contacts:read workflows:create
&redirect_uri=https://superautomation.example.com/oauth/callback # Send the user back to me after they approve my ambitious plans.
&state=<random-state> # CSRF protection. Because hackers love uninvited OAuth parties.
&code_challenge=<pkce-challenge> # Cryptographic proof that I started this whole adventure.
&code_challenge_method=S256 # SHA-256. Because pinky promises don't pass security audits.
Alice authenticates with Keycloak if she doesn’t already have a valid session.
Then Keycloak can display a consent screen.
Something like:
┌─────────────────────────────────────┐
│ │
│ SuperAutomation │
│ │
│ Wants access to your account │
│ │
│ This application requests: │
│ │
│ ✓ Read your contacts │
│ ✓ Create workflows │
│ │
│ [Deny] [Allow] │
│ │
└─────────────────────────────────────┘
That looks much better than asking for a password.
To enable this in Keycloak, we configure the client with Consent required, and ensure the relevant client scopes are displayed on the consent screen with meaningful descriptions.
If Alice approves, Keycloak redirects her back to SuperAutomation with an authorization code.
The application exchanges it for tokens.
And now it can call our APIs with the access token.
Assuming, of course, that the APIs recognize and enforce the granted scopes.
What does an authorized token look like?
A simplified access token might look like this:
{
"iss": "https://auth.example.com/realms/my-platform",
"sub": "alice-123",
"azp": "superautomation",
"aud": ["contacts-api"],
"scope": "openid contacts:read"
}
Notice a few things.
sub identifies Alice.
azp identifies the OAuth client.
aud identifies the intended API.
And scope describes the permissions granted for this token.
Now SuperAutomation can call:
GET /api/contacts
Authorization: Bearer <access-token>
The Contacts API validates the token, verifies the scope, and applies Alice’s normal resource-level access restrictions.
The application doesn’t magically become an administrator.
It can only perform operations allowed by the combination of the token’s permissions and the platform’s authorization rules.
The user said yes. The organization admin said no.
Now comes the fun part.
Suppose SuperAutomation requests:
contacts:read
workflows:create
payments:approve
Reading contacts might be acceptable with user consent.
Creating workflows might also be permitted, depending on the organization.
But approving payments?
Hold on.
That’s potentially a much more sensitive operation.
We probably don’t want any employee to authorize a random third-party application to approve company payments.
Especially if they click Allow. 😬
So we introduce two levels of approval.
Low-risk permissions: The user can authorize the application, subject to their existing permissions and organization policies.
Sensitive permissions: An organization administrator must approve the integration before those permissions can be used.
That gives us a useful separation:
Permission: contacts:read
|
v
User Consent
|
v
Access Granted
Permission: payments:approve
|
v
Organization Admin Approval
|
v
User Consent
|
v
Access Granted
The exact ordering and user experience can vary. For this design, we choose administrator approval first, followed by user consent for the permitted scopes.
But there’s a catch.
Keycloak’s built-in consent mechanism doesn’t automatically implement this exact organization-level approval workflow.
Which means we need to build something ourselves.
Of course we do. 🙃
What Keycloak does, and what we still need to build
Keycloak handles the identity and OAuth mechanics:
- Authentication.
- OAuth client registration and configuration.
- Authorization Code flow.
- Consent screens.
- Token issuance and validation support.
Our platform handles the business-specific authorization policies:
- Application approval by organization.
- Scope classification and approval status.
- Organization-level integration policies.
- Effective permissions for each connected application.
- Audit logging and revocation of organizational grants.
We can keep that information in a dedicated Integration Service.
For example:
Keycloak
Identity / OAuth
|
v
Developer Portal --> Integration Service
|
v
Approval Records
|
v
API Gateway
|
+-----------+-----------+
| | |
v v v
Contacts API Workflows API Payments API
And an approval record might look like:
{
"organizationId": "acme",
"clientId": "superautomation",
"approvedScopes": [
"contacts:read",
"workflows:create"
],
"pendingScopes": [
"payments:approve"
],
"status": "partially_approved"
}
The critical detail is where we enforce this policy.
Recording approval in a database isn’t enough.
We must make sure the application cannot obtain or successfully use a sensitive permission before approval.
One approach is to integrate the approval policy into the authorization process so unapproved scopes cannot be issued in usable tokens.
Another is to enforce the organization approval policy at the API gateway or resource API, in addition to validating the token.
Depending on the desired behavior, this could involve custom Keycloak extensions, controlled client-scope configuration, or an external policy enforcement layer.
For our platform, the important rule is that a token containing payments:approve must not bypass the organization’s approval requirements.
And no, trusting a frontend button to hide the payment action doesn’t count as authorization.
Nice try. 💀
But what happens when approval is revoked?
Suppose Acme’s administrator revokes SuperAutomation’s access.
We update our approval records.
Easy.
But SuperAutomation might still have an access token issued before the revocation.
If our APIs only validate JWT signatures and expiration times, that token can remain usable until it expires.
Oops.
This is why revocation needs deliberate design.
For sensitive permissions, we can check current organization approval state during API authorization.
We can also use short-lived access tokens, revoke appropriate grants and sessions, and prevent refresh or new token issuance where required.
The exact strategy depends on our security and latency requirements.
For example, a read-only contact integration might tolerate short-lived cached authorization decisions.
A payment approval integration probably deserves stronger, more immediate policy enforcement.
Not every API needs to call Keycloak or the Integration Service on every request.
But we shouldn’t pretend that revoking a permission in a database instantly destroys every JWT already issued.
Unfortunately, tokens aren’t that considerate.
Is user consent enough?
No.
And this is probably the most important lesson from the whole integration design.
User consent means the user has agreed to let an application request certain access.
It does not automatically mean:
- The organization permits that integration.
- The user has permission to access every requested resource.
- Every API should accept the resulting token.
- The application should keep those permissions forever.
We need multiple authorization boundaries.
The user approves delegation.
The organization controls which integrations and sensitive permissions are allowed.
And the resource API enforces access to the actual data or operation.
None of these layers replaces the others.
It’s a little more complicated than a single Allow button.
But that’s usually how security works .
We wanted a login page. We built an ecosystem.
Looking back, the progression was surprisingly natural.
First, we needed users to log in across multiple products.
So we introduced Keycloak and Single Sign-On.
Then our products needed to communicate with each other.
So we added service identities, audience restrictions, and token exchange.
Finally, external developers wanted to build integrations.
So we introduced OAuth clients, scopes, user consent, and organization-level approval.
None of these requirements was particularly unreasonable on its own.
Together, they turned a login system into an identity and authorization platform.
And that’s the main thing I’d take away from this journey:
Authentication, authorization, and delegated access are different problems.
Keycloak solves a large part of the identity and OAuth plumbing.
But deciding who should access which product, resource, or organization remains our responsibility.
Use the right mechanism for each interaction. Keep tokens scoped to their intended purpose. Don’t confuse user consent with administrator approval.
And if someone asks for “just a simple login feature,” maybe ask a few more questions before estimating it at two days.
Trust me. 🙃