Skip to content
Merged
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
Expand Up @@ -17,10 +17,18 @@

import jakarta.servlet.http.HttpServletRequest;

import java.util.Map;

@Controller
public class IntrospectEndpoint {
protected final Logger logger = LoggerFactory.getLogger(getClass());

// RFC 7662 section 2.2: an inactive-token response MUST contain only {"active": false}
// and SHOULD NOT include any other information about the token.
// IntrospectionClaims includes fields like `revocable` (a primitive boolean inherited from Claims)
// that cannot be null, so @JsonInclude(NON_NULL) can't suppress them in the inactive case.
private static final Map<String, Object> INACTIVE_TOKEN_RESPONSE = Map.of("active", false);

private final ResourceServerTokenServices resourceServerTokenServices;

public IntrospectEndpoint(
Expand All @@ -30,24 +38,19 @@ public IntrospectEndpoint(

@PostMapping("/introspect")
@ResponseBody
public IntrospectionClaims introspect(@RequestParam String token) {
IntrospectionClaims introspectionClaims = new IntrospectionClaims();

public Object introspect(@RequestParam String token) {
try {
OAuth2AccessToken oAuth2AccessToken = resourceServerTokenServices.readAccessToken(token);
if (oAuth2AccessToken.isExpired()) {
introspectionClaims.setActive(false);
return introspectionClaims;
return INACTIVE_TOKEN_RESPONSE;
}
resourceServerTokenServices.loadAuthentication(token);
introspectionClaims = UaaTokenUtils.getClaims(oAuth2AccessToken.getValue(), IntrospectionClaims.class);
IntrospectionClaims introspectionClaims = UaaTokenUtils.getClaims(oAuth2AccessToken.getValue(), IntrospectionClaims.class);
introspectionClaims.setActive(true);
} catch (InvalidTokenException _) {
introspectionClaims.setActive(false);
return introspectionClaims;
} catch (InvalidTokenException _) {
return INACTIVE_TOKEN_RESPONSE;
}

return introspectionClaims;
}

@RequestMapping(value = "/introspect")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import org.cloudfoundry.identity.uaa.oauth.common.exceptions.InvalidTokenException;
import org.cloudfoundry.identity.uaa.oauth.provider.token.ResourceServerTokenServices;
import org.cloudfoundry.identity.uaa.oauth.token.IntrospectionClaims;
import org.cloudfoundry.identity.uaa.util.JsonUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -40,7 +43,7 @@ void validToken() {
when(token.isExpired()).thenReturn(false);
when(token.getValue()).thenReturn(validToken);

IntrospectionClaims claims = introspectEndpoint.introspect(validToken);
IntrospectionClaims claims = (IntrospectionClaims) introspectEndpoint.introspect(validToken);
assertThat(claims.isActive()).isTrue();

verify(resourceServerTokenServices).readAccessToken(validToken);
Expand All @@ -55,24 +58,41 @@ void expiredTokenIsInactive() {
when(resourceServerTokenServices.readAccessToken(validToken)).thenReturn(token);
when(token.isExpired()).thenReturn(true);

IntrospectionClaims claims = introspectEndpoint.introspect(validToken);
assertThat(claims.isActive()).isFalse();
Object result = introspectEndpoint.introspect(validToken);
assertThat(result).isEqualTo(Map.of("active", false));
}

@Test
void invalidToken_inReadAccessToken() {
when(resourceServerTokenServices.readAccessToken(validToken)).thenThrow(new InvalidTokenException("Bla"));
IntrospectionClaims claims = introspectEndpoint.introspect(validToken);
assertThat(claims.isActive()).isFalse();
Object result = introspectEndpoint.introspect(validToken);
assertThat(result).isEqualTo(Map.of("active", false));
}

@Test
void invalidToken_inLoadAuthentication() {
OAuth2AccessToken token = mock(OAuth2AccessToken.class);
when(resourceServerTokenServices.readAccessToken(validToken)).thenReturn(token);
when(resourceServerTokenServices.loadAuthentication(validToken)).thenThrow(new InvalidTokenException("Bla"));
IntrospectionClaims claims = introspectEndpoint.introspect(validToken);
assertThat(claims.isActive()).isFalse();
Object result = introspectEndpoint.introspect(validToken);
assertThat(result).isEqualTo(Map.of("active", false));
}

@Test
void falseRevocableClaimIsNotSuppressedOnActiveToken() {
// A `false` value on an active token is real information and must still be
// returned, unlike the inactive-token case where no other fields are present.
String tokenWithRevocableFalse = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyZXZvY2FibGUiOmZhbHNlfQ.jS74pusAMo7VBsEN08rzpxMrk57ZMoRH3QX_gNUopJ4";
OAuth2AccessToken token = mock(OAuth2AccessToken.class);
when(resourceServerTokenServices.readAccessToken(tokenWithRevocableFalse)).thenReturn(token);
when(token.isExpired()).thenReturn(false);
when(token.getValue()).thenReturn(tokenWithRevocableFalse);

Object result = introspectEndpoint.introspect(tokenWithRevocableFalse);

assertThat(result).isInstanceOf(IntrospectionClaims.class);
assertThat(((IntrospectionClaims) result).isRevocable()).isFalse();
assertThat(JsonUtils.writeValueAsString(result)).contains("\"revocable\":false");
}

@Test
Expand All @@ -82,7 +102,7 @@ void claimsForValidToken() {
when(token.isExpired()).thenReturn(false);
when(token.getValue()).thenReturn(validToken);

IntrospectionClaims claimsResult = introspectEndpoint.introspect(validToken);
IntrospectionClaims claimsResult = (IntrospectionClaims) introspectEndpoint.introspect(validToken);

assertThat(claimsResult.isActive()).isTrue();
assertThat(claimsResult.getName()).isEqualTo("UAA username");
Expand All @@ -97,9 +117,8 @@ void invalidJSONInClaims() {
when(token.isExpired()).thenReturn(false);
when(token.getValue()).thenReturn(invalidToken);

IntrospectionClaims claimsResult = introspectEndpoint.introspect(invalidToken);
Object result = introspectEndpoint.introspect(invalidToken);

assertThat(claimsResult.isActive()).isFalse();
assertThat(claimsResult.getName()).isNull();
assertThat(result).isEqualTo(Map.of("active", false));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ void invalidToken() throws Exception {
.andExpect(content().contentType(APPLICATION_JSON));
}

@Test
void invalidTokenResponseContainsOnlyActiveField() throws Exception {
// RFC 7662 section 2.2: an inactive-token response SHOULD NOT include any
// additional information about the token.
mockMvc.perform(
post("/introspect")
.with(httpBasic(CLIENT_ID, CLIENT_SECRET))
.header(ACCEPT, APPLICATION_JSON_VALUE)
.header(CONTENT_TYPE, APPLICATION_FORM_URLENCODED_VALUE)
.param("token", "invalid-token"))
.andExpect(status().isOk())
.andExpect(content().contentType(APPLICATION_JSON))
.andExpect(content().string("{\"active\":false}"));
}

@Test
void deleteNotSupported() throws Exception {
mockMvc.perform(
Expand Down
Loading