Setting up auth via Keycloak
After the technical skeleton, the tournament app runs as a preview system. Frontend, API gateway, service, database, and RabbitMQ all start. That’s not domain functionality yet, but it’s an important technical vertical slice.
The next step looks like infrastructure again at first glance.
Authentication.
So: login. Tokens. Guards. Interceptors. JWT strategy. Keycloak configuration.
Not particularly exciting. At least not compared to a drag-and-drop group assignment or a schedule generator.
Still, auth belongs early in this project’s journey. Because as soon as personal or club-related data comes into play, the question isn’t just:
Can the app display something?
But:
Who’s allowed to see or change which data?
Why auth this early?
Section titled “Why auth this early?”The tournament app isn’t just going to be a public results list. Down the line, it manages players, tournaments, groups, results, and history.
Some of that data is harmless. Some of it is sensitive enough that you shouldn’t handle it carelessly. Especially since this is about youth tournaments, data minimization matters. But even data-minimal applications need access protection.
For the MVP, that doesn’t mean every role rule has to be perfectly modeled yet.
It does mean, though:
- not everyone gets to manage players
- not everyone gets to create tournaments
- not everyone gets to record results
- parents or spectators should eventually see, at most, a limited live view
- secure data access needs to be prepared for
Login, then, isn’t just a convenience feature. It’s the foundation that lets later domain functionality be properly secured.
Authentication is not authorization
Section titled “Authentication is not authorization”A common mistake is lumping login and permission checks together.
Authentication answers:
Who are you?
Authorization answers:
What are you allowed to do?
This step is mostly about authentication, and about the technical groundwork for later authorization.
The user should be able to log in. The frontend should get an access token. The API gateway should validate that token. After that, an authenticated user should be available in the request context.
Roles like admin, trainer, referee, or parent are already known as a direction. But the complete authorization model comes later.
That’s deliberate.
Here too: technical vertical slice first, domain functionality second.
Why Keycloak?
Section titled “Why Keycloak?”For this project, I’m using Keycloak as the identity provider.
There are several reasons for that.
For one, Keycloak is already familiar in my architecture lab. So I don’t have to evaluate a new auth provider first or build my own login solution. For another, Keycloak supports established standards like OpenID Connect and OAuth 2.0.
For a modern web app, the Authorization Code flow with PKCE is especially relevant. It gives the frontend a user-friendly, browser-based login without having to handle passwords itself. The backend gets a signed access token that it can validate independently.
That’s pleasant from both a developer and a user perspective:
- users log in through a familiar login page
- the frontend doesn’t have to store passwords
- the backend gets standardized JWTs
- roles and claims can be managed centrally
- more clients can be added later
That said, Keycloak isn’t always intuitive. A new realm with new clients, scopes, and mappers gets set up quickly — but not necessarily set up correctly right away.
That’s exactly why this step deserves its own field report.
Target flow
Section titled “Target flow”For the tournament app, this step produces the following chain:
Angular App ↓ Login via KeycloakAccess Token ↓ Bearer Token per HTTP InterceptorTournament API Gateway ↓ JWT Strategy / GuardAuthenticatedUser in the request contextThe frontend handles login, logout, and attaching the token to requests.
The API gateway validates the token. The frontend doesn’t decide whether a user is valid. The backend checks signature, issuer, audience, and expiration.
That matters.
A frontend can hide UI elements. But it must never be the final authority on access control.
Angular: initializing Keycloak and sending the token
Section titled “Angular: initializing Keycloak and sending the token”In the frontend, Keycloak gets integrated into the Angular standalone app.
The high-level setup:
- Keycloak configuration via environment or runtime config
- initialization at app start
- an auth guard for protected routes
- an HTTP interceptor for API requests
- login/logout in the app shell
The interceptor is unspectacular here, but central.
It makes sure requests to the tournament API automatically get a bearer token:
Authorization: Bearer <access-token>That way, not every feature has to know how auth technically works. Player management, the tournament list, or tournament day should be able to focus on their own domain work later. The token mechanics stay a cross-cutting concern.
That’s exactly the point of a good skeleton: it reduces later friction.
A later feature prompt no longer has to say:
Build player management and integrate login, tokens, guards, and API auth along the way.
Just:
Build player management within the existing auth structure.
API gateway: JWT strategy, Passport, and guards
Section titled “API gateway: JWT strategy, Passport, and guards”The API gateway is the only external backend interface for the frontend.
This is where the access token gets validated.
Technically, that happens through a JWT strategy, Passport, and guards. The guard protects routes. The strategy checks the token and derives an internal user context from it.
A simplified target model looks like this:
export interface AuthenticatedUser { userId: string; subject: string; username?: string; email?: string; roles: string[];}What matters here: userId and subject come from the token’s sub claim.
Not from the email address.
Not from the display name.
Not from preferred_username.
Those values are fine to display or use for debugging. But they aren’t a stable technical identity.
Why sub matters so much
Section titled “Why sub matters so much”For later data access, the backend needs a stable user identifier.
When a trainer creates a tournament, a referee records results, or a user is later only allowed to see their own data, the system needs a reliable reference:
created_by_user_idupdated_by_user_idowner_user_idOr, more neutrally:
created_by_subjectupdated_by_subjectowner_subjectThe exact field name matters less than where the value actually comes from.
The technical user ID should come from sub.
sub stands for subject, and in the OpenID Connect context it’s the stable identity of the authenticated user within the identity provider.
For this project, the rule is:
Technical user ID = token.subIf sub is missing, the API gateway shouldn’t silently fall back to email or preferred_username. It should reject the request.
That’s inconvenient, but correct.
Because a missing stable identity claim isn’t a domain edge case. It’s a broken auth configuration.
Preparing data access
Section titled “Preparing data access”There’s still no real player management, no tournament data, and no result logic.
Even so, this step is domain-relevant.
Because once data does exist later, the backend can associate it with the authenticated user.
Examples:
A trainer only sees tournaments for their own club.A referee can only record results for an active tournament.A parent link only sees data that's been explicitly shared for live viewing.A user only sees data they're authorized to see.For now, that’s still a long way off. But without a solid auth foundation, that future would get shaky very fast.
Login, then, isn’t the end of the security architecture. It’s the beginning of it.
Manual setup in the Keycloak admin console
Section titled “Manual setup in the Keycloak admin console”Part of this step deliberately doesn’t happen in code.
The realm and clients get created in the Keycloak admin console.
For this project, that means:
Realm:tournament
Frontend Client:tournament-frontend
API Client:tournament-apiThe frontend client uses the Authorization Code flow with PKCE. Redirect URIs and web origins have to work for both local development and the preview environment.
The API gateway expects an access token that’s meant for the tournament API.
And that’s exactly where a typical Keycloak pitfall shows up.
Pitfall: valid token, wrong audience
Section titled “Pitfall: valid token, wrong audience”After the first integration, login worked in the frontend. sub was present in the access token too.
Still, the API gateway rejected requests to /auth/me.
The guard logged, roughly:
jwt audience invalid. expected: tournament-apiSo the token wasn’t invalid as such.
It was signed. It had an issuer. It had a subject. It was a genuine access token.
But it wasn’t issued for this API.
The token’s audience was:
"aud": "account"But the API gateway expected:
tournament-apiThat’s an important distinction.
A token can be valid and still not be intended for the recipient.
The fix was an audience mapper in Keycloak. The frontend client, or an assigned client scope, makes sure tournament-api ends up in the access token’s audience.
After that, the token contains, roughly:
"aud": ["account", "tournament-api"]or, depending on configuration:
"aud": "tournament-api"Only then does the API accept the token.
Why the audience check stays important
Section titled “Why the audience check stays important”You could configure the API gateway more loosely and just drop the audience check.
That would be more convenient.
But it would be the wrong direction.
Audience answers the question:
Who is this token meant for?
If a backend accepts tokens that aren’t intended for it, the security boundary gets blurrier. Especially in a system with multiple apps, APIs, or future clients, the API gateway shouldn’t just check whether a token is valid — it should also check whether it’s meant for this API.
That’s why the audience check stays active.
The Keycloak setup gets adjusted, not the API validation watered down.
Result of the auth vertical slice
Section titled “Result of the auth vertical slice”By the end of this step, the auth chain works:
Frontend Login ↓Access token contains sub ↓Access token contains aud: tournament-api ↓HTTP interceptor sends bearer token ↓API gateway validates JWT ↓/auth/me returns the authenticated userThat still doesn’t implement any domain role logic.
But the foundation is in place.
The API gateway now knows who’s making the request. It can evaluate roles later. It can scope data access to the authenticated user. And it can reject requests when the identity isn’t reliably established.
What’s deliberately still open
Section titled “What’s deliberately still open”This step stays bounded too.
Not included:
- no complete role matrix
- no use-case-specific authorization checks
- no club or tenant logic
- no parent live-view sharing
- no player or tournament data
- no UI for permission management
That’s not a shortcoming. It’s intentional.
This step is meant to make auth technically sound. Domain authorization comes later, once the concrete use cases get built.
Review questions
Section titled “Review questions”For auth integrations, it’s not enough for login to work visually.
Important review questions at this step:
- Is the Authorization Code flow with PKCE actually used?
- Does the configuration come from environment or runtime config?
- Are there no hardcoded URLs?
- Does the interceptor only send tokens to matching API targets?
- Does the API gateway validate signature, issuer, audience, and expiration?
- Is
subused as the stable technical user ID? - Is there no silent fallback to email or display name?
- Are roles extracted from the token, but not yet over-interpreted?
- Is
/auth/meprotected? - Does a request without a token fail?
- Does a request with the wrong audience fail?
- Does the setup work both locally and in preview?
This kind of review is especially worth it with Keycloak. The admin UI allows plenty of combinations that work technically but don’t match the intended authorization model.
Conclusion
Section titled “Conclusion”Auth is a classic infrastructure step.
Afterward, you don’t see a tournament list. No groups. No results. No polished parent view.
Still, the step matters.
Because from now on, the system can do more than just run. It can attribute requests to a user. It can restrict data access later. It can evaluate roles. And it can prevent a valid but wrongly addressed token from just getting accepted.
Keycloak isn’t always intuitive here. New realms, client scopes, mappers, and audiences in particular are pitfalls if you haven’t set them up a few times before.
But that’s exactly why this step belongs in the journey.
Not as a polished auth tutorial.
But as a real field report:
Login isn’t done until the backend trusts the token for a good reason.