spring-projects/spring-security · warning · OAuth2AuthenticationException

access_denied

access_denied

Error message

OAuth 2.0 Parameter: client_id

What it means

In OAuth2DeviceAuthorizationConsentAuthenticationProvider.authenticate, when the user denies the device authorization consent, the provider invalidates the device code and user code, saves the updated authorization, and throws an OAuth2AuthenticationException with error code access_denied (parameter client_id). This is the standard RFC 8628 denial path: the resource owner explicitly declined the device's access request.

Source

Thrown at oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProvider.java:203

		if (authorities.isEmpty()) {
			// Authorization consent denied (or revoked)
			if (currentAuthorizationConsent != null) {
				this.authorizationConsentService.remove(currentAuthorizationConsent);
				if (this.logger.isTraceEnabled()) {
					this.logger.trace("Revoked authorization consent");
				}
			}
			authorization = OAuth2Authorization.from(authorization)
				.invalidate(deviceCodeToken.getToken())
				.invalidate(userCodeToken.getToken())
				.attributes((attrs) -> attrs.remove(OAuth2ParameterNames.STATE))
				.build();
			this.authorizationService.save(authorization);
			if (this.logger.isTraceEnabled()) {
				this.logger.trace("Invalidated device code and user code because authorization consent was denied");
			}
			throw createException(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID);
		}

		OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build();
		if (currentAuthorizationConsent == null || !authorizationConsent.equals(currentAuthorizationConsent)) {
			this.authorizationConsentService.save(authorizationConsent);
			if (this.logger.isTraceEnabled()) {
				this.logger.trace("Saved authorization consent");
			}
		}

		authorization = OAuth2Authorization.from(authorization)
			.authorizedScopes(authorizedScopes)
			.invalidate(userCodeToken.getToken())
			.attributes((attrs) -> attrs.remove(OAuth2ParameterNames.STATE))
			.attributes((attrs) -> attrs.remove(OAuth2ParameterNames.SCOPE))
			.build();
		this.authorizationService.save(authorization);

View on GitHub (pinned to 96852e8860)

Solutions

  1. This is expected user behavior: treat access_denied at the token endpoint as final and stop polling; show a 'authorization was denied' message on the device.
  2. To proceed, restart the entire device authorization flow so the user can approve.
  3. Check the client handles OAuth2ErrorCodes.ACCESS_DENIED from the token endpoint gracefully instead of retrying.
  4. Review consent page UX if users report accidental denials.

Example fix

// before: device client keeps polling after denial
// while (true) { post token endpoint; }
// after
// if (response.error === 'access_denied') { showError('Authorization denied'); exit; }
Defensive patterns

Strategy: try-catch

Try / catch

catch (OAuth2AuthenticationException e) { if ("access_denied".equals(e.getError().getErrorCode())) { stopTokenPolling(); showDeniedMessageOnDevice(); } }

Prevention

When it happens

Trigger: The user clicks 'Deny' on the device consent page; the consent request resolves to an existing authorization but the consent parameter indicates denial (deviceAuthorizationConsentAuthentication.isConsent... / denial branch), causing the access_denied exception after cleanup of device/user codes.

Common situations: An end user deliberately denies an app's device login attempt (expected behavior); a device client polling the token endpoint then receives authorization_pending/access_denied at the token endpoint; automated tests exercise the deny path; a confusing consent page causes users to accidentally deny.

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/ce355c34893fdbd8. Report an issue: GitHub.