Companion repository for ROPC Is Dead: How to Get User Tokens Without It.
RFC 9700 (January 2025) prohibits OAuth2 Resource Owner Password Credentials (grant_type=password) — the strongest "MUST NOT" in IETF vocabulary. OAuth 2.1 removes it entirely. This PoC demonstrates the two industry-standard replacements that CLIs like Azure CLI and AWS CLI already use:
- Authorization Code + PKCE (default) — opens a browser, catches the callback on localhost
- Device Authorization Grant (
--use-device-code) — displays a URL and code for headless environments
flowchart LR
CLI["CLI Client\n(Spring Boot CLI)"]
KC["Keycloak 26\n(Auth Server)\nPort 8180"]
Browser["Browser\n(User)"]
RS["Resource Server\nPort 8080"]
CLI -- "OAuth2 flow" --> KC
Browser -- "Login / Approve" --> KC
CLI -- "Bearer token" --> RS
| Component | Description |
|---|---|
| cli-client | Spring Boot CLI app with two OAuth2 flows. No web server — uses java.net.http.HttpClient for all HTTP calls. |
| resource-server | Spring Boot OAuth2 Resource Server. Validates JWTs against Keycloak and exposes GET /api/userinfo. |
| Keycloak 26 | Authorization server with pre-configured realm (ropc-demo), public client (cli-client), and test user. ROPC is explicitly disabled. |
- JDK 17+
- Maven 3.8+
- Docker & Docker Compose
docker compose up -dWait ~30 seconds for the realm import to complete. Keycloak runs on http://localhost:8180 (admin/admin).
cd resource-server
mvn spring-boot:runThe resource server starts on port 8080 and validates JWTs issued by Keycloak.
cd cli-client
mvn spring-boot:runWhat happens:
- The CLI generates a PKCE code verifier and challenge
- A temporary HTTP server starts on localhost (port 6363, fallback 6364/6365)
- Your browser opens the Keycloak login page
- You log in as
testuser/testpassword - Keycloak redirects back to localhost with an authorization code
- The CLI exchanges the code + PKCE verifier for an access token
- The CLI calls the protected
/api/userinfoendpoint and prints the result
cd cli-client
mvn spring-boot:run -Dspring-boot.run.arguments="--use-device-code"What happens:
- The CLI requests a device code from Keycloak
- The CLI displays a verification URL and user code
- You open the URL in any browser (even on a different device)
- You enter the code and log in as
testuser/testpassword - The CLI polls until authorization completes
- The CLI calls the protected
/api/userinfoendpoint and prints the result
{"sub":"...","preferred_username":"testuser","email":"testuser@example.com","scope":"openid profile","issued_at":"...","expires_at":"..."}The realm is auto-imported from keycloak/realm-export.json:
| Setting | Value |
|---|---|
| Realm | ropc-demo |
| Client | cli-client (public) |
| Auth Code flow | Enabled |
| Device flow | Enabled |
| ROPC (Direct Access Grants) | Disabled |
| Redirect URIs | http://localhost:6363/*, http://localhost:6364/*, http://localhost:6365/* |
| Test user | testuser / testpassword |
ROPC was one POST request — simple but dangerous. The client saw the user's password, MFA couldn't work, and the authorization server lost control of the login experience.
Auth Code + PKCE keeps the same convenience (browser opens automatically) while ensuring the CLI never sees the password. The PKCE extension prevents authorization code interception.
Device Authorization Grant solves the headless case — SSH sessions, containers, CI runners — where no browser is available on the same machine. The user authenticates on any device with a browser.
This dual-mode pattern (PKCE by default, device code as fallback) is what Azure CLI, AWS CLI, and GitHub CLI converged on. See the companion article for the full decision tree, HTTP-level examples, and migration checklist.
MIT