8000 Feature #43649: Add Phone OTP Authenticator by joaoccarvalhoo · Pull Request #49606 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Feature #43649: Add Phone OTP Authenticator - #49606

Open
joaoccarvalhoo wants to merge 1 commit into
keycloak:mainfrom
joaoccarvalhoo:feature/issue-43649
Open

joaoccarvalhoo wants to merge 1 commit into
keycloak:mainfrom
joaoccarvalhoo:feature/issue-43649

Conversation

@joaoccarvalhoo
Copy link
Copy Markdown

Implement Phone OTP browser authenticator with code generation, hashing, TTL, max attempts, and resend cooldown.

Add PhoneOtpAuthenticatorFactory and PhoneOtpUtils for shared OTP utilities.

Implement VerifyPhoneNumber required action to collect and verify user phone numbers.

Add integration tests and page objects (PhoneOtpFlowTest, PhoneOtpLoginPage, PhoneOtpCodePage, VerifyPhoneNumberPage, VerifyPhoneNumberCodePage) covering login, invalid codes, resend cooldown, and required-action flow.

Closes #43649

Implement Phone OTP browser authenticator with code generation,
hashing, TTL, max attempts, and resend cooldown.

Add PhoneOtpAuthenticatorFactory and PhoneOtpUtils for shared OTP
utilities.

Implement VerifyPhoneNumber required action to collect and verify
user phone numbers.

Add integration tests and page objects (PhoneOtpFlowTest,
PhoneOtpLoginPage, PhoneOtpCodePage, VerifyPhoneNumberPage,
VerifyPhoneNumberCodePage) covering login, invalid codes, resend
cooldown, and required-action flow.

Closes keycloak#43649

Signed-off-by: Joao Carvalho <joao.cipriano.carvalho@tecnico.ulisboa.pt>
Co-authored-by: Chloe Romão <chloe.romao@tecnico.ulisboa.pt>
@joaoccarvalhoo
joaoccarvalhoo requested review from a team as code owners June 2, 2026 14:45
@gauravbsinghal
Copy link
Copy Markdown

Hi @joaoccarvalhoo, I'm sharing this from Niro, a PR pentesting tool we're building to help OSS maintainers catch security regressions before merge. I ran it against this PR and it found the following issue.

Any unauthenticated caller can test phone numbers against the niro realm and learn which numbers are registered users, giving them a reusable account-enumeration oracle for targeted attacks.

Reproduction Steps

Precondition: none; no credentials are required.

  1. Start a fresh browser session and request:
    GET http://host.docker.internal:8080/realms/niro/protocol/openid-connect/auth?client_id=niro-phone-otp-browser&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&response_type=code&scope=openid
    The response is the phone OTP login page (data-page-id="login-phone-otp-login") with a session-bound form action under /realms/niro/login-actions/authenticate?... .
  2. Submit a known registered phone number to that returned form action. Example observed request:
    POST http://host.docker.internal:8080/realms/niro/login-actions/authenticate?session_code=ZgpqM0UJUQD5Pg2sbdDY0ANrsesw8Tfj7-ChDBWa1iw&execution=ccb42468-0a6a-4612-8708-724455d63ff9&client_id=niro-phone-otp-browser&tab_id=rBr9sXeYaiw&client_data=eyJydSI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9jYWxsYmFjayIsInJ0IjoiY29kZSJ9
    Content-Type: application/x-www-form-urlencoded
    Body: phoneNumber=%2B15550002001
    Expected secure behavior: the response should be indistinguishable from an unknown-number submission.
    Observed vulnerable behavior: HTTP 200 returned data-page-id="login-phone-otp-code" and the page text "We sent a one-time code to +15550002001.".
  3. Start a second fresh session with the same GET from step 1 and submit an unregistered number to that new session-bound form action. Example observed request:
    POST http://host.docker.internal:8080/realms/niro/login-actions/authenticate?session_code=BYayNCiTFkKQ57sLTtThMH45-B7b3R5DkyVJViu56GY&execution=ccb42468-0a6a-4612-8708-724455d63ff9&client_id=niro-phone-otp-browser&tab_id=loD2o-6h7o8&client_data=eyJydSI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9jYWxsYmFjayIsInJ0IjoiY29kZSJ9
    Content-Type: application/x-www-form-urlencoded
    Body: phoneNumber=%2B19999999999
    Observed vulnerable behavior: HTTP 200 returned data-page-id="login-phone-otp-login" and the error "Phone number not found.".
  4. Repeat steps 2 and 3 in fresh sessions. I reproduced the same distinction twice: +15550002001 always advanced to login-phone-otp-code, while +19999999999 always stayed on login-phone-otp-login with "Phone number not found.".

Remediation Guidance

In services/src/main/java/org/keycloak/authentication/authenticators/browser/PhoneOtpAuthenticator.java, remove the distinct invalid-phone response from the phone lookup path. handlePhoneNumberStep/findUserByPhone should return the same template and generic message for both existing and non-existing phone numbers, while only generating/storing/sending an OTP when exactly one user is found. Do not surface Messages.PHONE_OTP_INVALID_PHONE to unauthenticated callers; keep Errors.USER_NOT_FOUND only in server-side events/logs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phone/Code registration and sign-in, out of the box

3 participants

0