Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020-2024 the original author or authors.
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -134,9 +134,30 @@ public Authentication authenticate(Authentication authentication) throws Authent
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}

if (deviceCode.isInvalidated() && !userCode.isInvalidated()) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}

// In https://www.rfc-editor.org/rfc/rfc8628.html#section-3.5,
// the following error codes are defined:

// expired_token
// The "device_code" has expired, and the device authorization
// session has concluded. The client MAY commence a new device
// authorization request but SHOULD wait for user interaction before
// restarting to avoid unnecessary polling.
if (deviceCode.isExpired()) {
// Invalidate the device code
authorization = OAuth2Authorization.from(authorization).invalidate(deviceCode.getToken()).build();
this.authorizationService.save(authorization);
if (this.logger.isWarnEnabled()) {
this.logger.warn(LogMessage.format("Invalidated device code used by registered client '%s'",
authorization.getRegisteredClientId()));
}
OAuth2Error error = new OAuth2Error(EXPIRED_TOKEN, null, DEVICE_ERROR_URI);
throw new OAuth2AuthenticationException(error);
}

// authorization_pending
// The authorization request is still pending as the end user hasn't
// yet completed the user-interaction steps (Section 3.3). The
Expand Down Expand Up @@ -165,23 +186,6 @@ public Authentication authenticate(Authentication authentication) throws Authent
throw new OAuth2AuthenticationException(error);
}

// expired_token
// The "device_code" has expired, and the device authorization
// session has concluded. The client MAY commence a new device
// authorization request but SHOULD wait for user interaction before
// restarting to avoid unnecessary polling.
if (deviceCode.isExpired()) {
// Invalidate the device code
authorization = OAuth2Authorization.from(authorization).invalidate(deviceCode.getToken()).build();
this.authorizationService.save(authorization);
if (this.logger.isWarnEnabled()) {
this.logger.warn(LogMessage.format("Invalidated device code used by registered client '%s'",
authorization.getRegisteredClientId()));
}
OAuth2Error error = new OAuth2Error(EXPIRED_TOKEN, null, DEVICE_ERROR_URI);
throw new OAuth2AuthenticationException(error);
}

if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated device token request parameters");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ public void authenticateWhenUserCodeIsNotInvalidatedThenThrowOAuth2Authenticatio
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
Authentication authentication = createAuthentication(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.token(createDeviceCode())
.token(createUserCode())
.build();
given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization);
Expand All @@ -209,7 +210,7 @@ public void authenticateWhenUserCodeIsNotInvalidatedThenThrowOAuth2Authenticatio
}

@Test
public void authenticateWhenDeviceCodeIsInvalidatedThenThrowOAuth2AuthenticationException() {
public void authenticateWhenDeviceCodeAndUserCodeAreInvalidatedThenThrowOAuth2AuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
Authentication authentication = createAuthentication(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
Expand All @@ -231,13 +232,36 @@ public void authenticateWhenDeviceCodeIsInvalidatedThenThrowOAuth2Authentication
verifyNoInteractions(this.tokenGenerator);
}

@Test
public void authenticateWhenDeviceCodeIsInvalidatedThenThrowOAuth2AuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
Authentication authentication = createAuthentication(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.token(createDeviceCode(), withInvalidated())
.token(createUserCode())
.build();
given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization);
// @formatter:off
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
// @formatter:on

verify(this.authorizationService).findByToken(DEVICE_CODE,
OAuth2DeviceCodeAuthenticationProvider.DEVICE_CODE_TOKEN_TYPE);
verifyNoMoreInteractions(this.authorizationService);
verifyNoInteractions(this.tokenGenerator);
}

@Test
public void authenticateWhenDeviceCodeIsExpiredThenThrowOAuth2AuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
Authentication authentication = createAuthentication(registeredClient);
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
.token(createExpiredDeviceCode())
.token(createUserCode(), withInvalidated())
.token(createUserCode())
.build();
given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization);
// @formatter:off
Expand Down