sourcefuse / sourcefuse/loopback4-microservice-catalog
feat(authentication-service): case-insensitive email lookup in login verify providers
- Dominant language
- TypeScript
- Stars
- 297
- Forks
- 78
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 4
Description
Is your feature request related to a problem? Please describe.
Yes. In KeycloakVerifyProvider (services/authentication-service/src/modules/auth/providers/keycloak-verify.provider.ts), the existing user is resolved with a case-sensitive equality match:
let user: IAuthUser | null = await this.userRepository.findOne({
where: {
email: profile.email,
},
});
Email addresses are case-insensitive in practice (the domain part per RFC, and the local part as treated by virtually all providers — Gmail, Outlook, corporate IdPs). However, identity providers do not guarantee a canonical casing: Keycloak can return John.Doe@example.com while the user row was created as john.doe@example.com (e.g. via admin user creation, bulk import, or a different signup path).
When the casing differs, findOne returns null, so a valid existing user is either:
- rejected with
401 InvalidCredentials(when no signup provider creates users on the fly), or - JIT-provisioned as a duplicate user account by the signup provider, which is worse — the user silently loses access to their existing data.
The same case-sensitive email lookup pattern exists in the other verify/signup flows of the service (e.g. SAML verify provider, local login by username/email, forgot-password), so the issue is not limited to Keycloak.
Describe the solution you'd like
Make the email match case-insensitive in the login verify providers. Possible approaches:
- Normalize at query time — look up with a case-insensitive operator, e.g.
where: {email: {ilike: <escaped profile.email>}}(with%/_/\escaped since ILIKE is a pattern match), or lowercase both sides. - Normalize at write time (preferred long-term) — store
emaillowercased inusers/user_credentialson every create/update path, lowercase the incomingprofile.emailbefore lookup, and keep plain (index-friendly) equality. Optionally enforce with a unique functional index onLOWER(email).
Either approach could be gated behind an opt-in service config flag (e.g. emailCaseInsensitive: true on AuthServiceBindings.CONFIG) to avoid changing behavior for existing consumers, since tenants with mixed-case duplicate emails already in the DB would need to resolve collisions first.
Describe alternatives you've considered
- Overriding
Strategies.Passport.KEYCLOAK_VERIFIERin the consuming application with a copy of the stock provider that usesilike— works, but duplicates ~60 lines of library code that drifts on every upgrade. - Doing a case-insensitive fallback lookup inside the
KEYCLOAK_PRE_VERIFY_PROVIDERhook (the stock verifier passes the found-or-null user to it and uses its return value) — this is our current workaround, but it relies on a hook whose purpose is verification, not user resolution, and every consumer has to rediscover and re-implement it.
Additional context
Suggested acceptance criteria:
- A user whose stored email differs from the IdP profile email only by case can log in via Keycloak and is matched to their existing account (no duplicate JIT-provisioned user).
- Behavior is consistent across Keycloak, SAML, and local login flows.
- Lookup remains SQL-injection safe (parameterized) and documented regarding index usage.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.