How ExternalIdentityId works with Keycloak
Keycloak authenticates the user and issues a JWT containing a sub claim, which uniquely identifies the user in Keycloak.
After ASP.NET Core validates the JWT, the application extracts the sub claim and searches PostgreSQL for a user whose ExternalIdentityId matches it.
- If the user exists: return the local
User.Id. - If the user doesn't exist: create a new local user record, store the Keycloak
subasExternalIdentityId, and return the newly generated localUser.Id.
The flow, step by step
Keycloak issues a JWT
The JWT payload contains something like:
{
"sub": "abc-123-keycloak-id",
"email": "[email protected]",
"aud": "finance-api"
}
The sub value is the user's unique Keycloak identifier.
ASP.NET Core extracts the sub claim
backend/src/ Finance.Api/ CurrentUser.cs var sub = principal.FindFirstValue("sub") ?? throw new InvalidOperationException( "Token is missing the 'sub' claim.");
principal represents the authenticated user and contains the claims extracted from the validated JWT.
Search for the user in PostgreSQL
backend/src/ Finance.Application/ Users/ UserService.cs var userId = await db .Users.Where(u => u.ExternalIdentityId == externalIdentityId) .Select(u => u.Id) .FirstOrDefaultAsync(ct);
externalIdentityId is the sub value, passed in from CurrentUser. This searches the Users table for a matching Keycloak identifier. If a matching user exists, the method returns the local Id.
Create a local user if one doesn't exist
backend/src/ Finance.Application/ Users/ UserService.cs var user = new User { Id = Guid.NewGuid(), ExternalIdentityId = externalIdentityId, Email = email ?? "", CreatedAt = DateTime.UtcNow }; db.Users.Add(user); await db.SaveChangesAsync(ct); // in a try/catch return user.Id;
This creates a user in PostgreSQL and links the local user record to the Keycloak identity. If two first requests race to create the same user, the save is caught and the row the other request created is returned.