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 @@ -116,6 +116,7 @@
import static java.util.Collections.singletonMap;
import static java.util.Objects.isNull;
import static java.util.stream.Collectors.toSet;
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20;
import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.SUB;
import static org.cloudfoundry.identity.uaa.oauth.token.CompositeToken.ID_TOKEN;
import static org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.GRANT_TYPE_AUTHORIZATION_CODE;
Expand Down Expand Up @@ -609,9 +610,14 @@ protected <T extends AbstractExternalOAuthIdentityProviderDefinition<T>> Map<Str
ExternalOAuthCodeToken codeToken,
final IdentityProvider<T> identityProvider
) {
String idToken = getTokenFromCode(codeToken, identityProvider);
codeToken.setIdToken(idToken);
return getClaimsFromToken(idToken, identityProvider);
String tokenFieldName = getTokenFieldName(identityProvider.getConfig());
String token = getTokenFromCode(codeToken, identityProvider);
if ("access_token".equals(tokenFieldName) && token != null && OAUTH20.equals(identityProvider.getType())) {
Comment on lines +613 to +615
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded string 'access_token' should be extracted to a constant to improve maintainability and reduce the risk of typos.

Copilot uses AI. Check for mistakes.
codeToken.setAccessToken(token);
} else {
codeToken.setIdToken(token);
}
return getClaimsFromToken(token, identityProvider);
}

protected <T extends AbstractExternalOAuthIdentityProviderDefinition<T>> Map<String, Object> getClaimsFromToken(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.apache.commons.lang3.RandomStringUtils;
import org.cloudfoundry.identity.uaa.authentication.AccountNotPreCreatedException;
import org.cloudfoundry.identity.uaa.authentication.UaaAuthentication;
import org.cloudfoundry.identity.uaa.authentication.UaaPrincipal;
import org.cloudfoundry.identity.uaa.authentication.event.IdentityProviderAuthenticationSuccessEvent;
import org.cloudfoundry.identity.uaa.authentication.manager.ExternalGroupAuthorizationEvent;
import org.cloudfoundry.identity.uaa.authentication.manager.InvitedUserAuthenticatedEvent;
Expand Down Expand Up @@ -95,6 +96,7 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20;
import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.ISS;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME;
import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME;
Expand All @@ -109,6 +111,7 @@
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
Expand Down Expand Up @@ -515,6 +518,55 @@ void discoveryURL_is_used() throws MalformedURLException {

}

@Test
void oauth20_flow_works_with_non_jwt_token() throws Exception {
String userInfoResponse = """
{
"login": "octocat",
"id": 1,
"type": "User",
"site_admin": false,
"name": "monalisa octocat",
"company": "GitHub",
"email": "[email protected]"
}""";

CompositeToken accessToken = getCompositeAccessToken();
accessToken.setIdTokenValue(null); //DOES NOT EXIST FOR OAUTH2.0
String oauth2TokenResponse = JsonUtils.writeValueAsString(accessToken);

//UAA exchanges the code for a token
mockUaaServer.expect(requestTo("http://localhost/oauth/token"))
.andExpect(header("Authorization", "Basic " + new String(Base64.encodeBase64("identity:identitysecret".getBytes()))))
.andExpect(header("Accept", "application/json"))
.andExpect(content().string(containsString("grant_type=authorization_code")))
.andExpect(content().string(containsString("code=the_code")))
.andExpect(content().string(containsString("redirect_uri=http%3A%2F%2Flocalhost%2Fcallback%2Fthe_origin")))
.andExpect(content().string(containsString("response_type=code")))
.andRespond(withStatus(OK).contentType(APPLICATION_JSON).body(oauth2TokenResponse));

//UAA retrieves user info using an access token
mockUaaServer.expect(requestTo(config.getUserInfoUrl().toString()))
.andRespond(withStatus(OK).contentType(APPLICATION_JSON).body(userInfoResponse));

IdentityProvider<RawExternalOAuthIdentityProviderDefinition> identityProvider = getOauth20Provider();
identityProvider.getConfig().setResponseType("code");
reset(provisioning);
when(provisioning.retrieveByOrigin(eq(ORIGIN), anyString())).thenReturn(identityProvider);

addTheUserOnAuth();

Authentication authentication = externalOAuthAuthenticationManager.authenticate(xCodeToken);
assertThat(authentication).isNotNull();
assertThat(authentication.getPrincipal()).isInstanceOf(UaaPrincipal.class);
UaaPrincipal principal = (UaaPrincipal) authentication.getPrincipal();
assertThat(principal).isNotNull();
assertThat(principal.getName()).isEqualTo("octocat");
assertThat(principal.getEmail()).isEqualTo("[email protected]");
mockUaaServer.verify();

}

@Test
void clientAuthInBody_is_used() {
config.setClientAuthInBody(true);
Expand Down Expand Up @@ -1291,6 +1343,31 @@ private IdentityProvider<OIDCIdentityProviderDefinition> getProvider() {
return identityProvider;
}

private IdentityProvider<RawExternalOAuthIdentityProviderDefinition> getOauth20Provider() throws Exception {
RawExternalOAuthIdentityProviderDefinition config = new RawExternalOAuthIdentityProviderDefinition()
.setAuthUrl(URI.create("http://localhost/oauth/authorize").toURL())
.setTokenUrl(URI.create("http://localhost/oauth/token").toURL())
.setIssuer("http://localhost/oauth/token")
.setShowLinkText(true)
.setLinkText("My oauth20 Provider")
.setRelyingPartyId("identity")
.setRelyingPartySecret("identitysecret")
.setUserInfoUrl(URI.create("http://localhost/userinfo").toURL())
.setTokenKey(PUBLIC_KEY);
config.setExternalGroupsWhitelist(Collections.singletonList("*"));
attributeMappings.put(USER_NAME_ATTRIBUTE_NAME, "login");
config.setAttributeMappings(attributeMappings);
config.setResponseType("code");

IdentityProvider<RawExternalOAuthIdentityProviderDefinition> identityProvider = new IdentityProvider<>();
identityProvider.setName("my oauth20 provider");
identityProvider.setIdentityZoneId(OriginKeys.UAA);
identityProvider.setType(OAUTH20);
identityProvider.setConfig(config);
identityProvider.setOriginKey(ORIGIN);
return identityProvider;
}

private void testTokenHasAuthoritiesFromIdTokenRoles() {
attributeMappings.put(GROUP_ATTRIBUTE_NAME, "scope");
mockToken();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.cloudfoundry.identity.uaa.provider.IdentityProvider;
import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning;
import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition;
import org.cloudfoundry.identity.uaa.provider.RawExternalOAuthIdentityProviderDefinition;
import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMember;
import org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupExternalMembershipManager;
import org.cloudfoundry.identity.uaa.user.UaaUser;
Expand All @@ -40,6 +41,7 @@
import org.cloudfoundry.identity.uaa.util.UaaTokenUtils;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager;
import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManagerImpl;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -83,6 +85,7 @@
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.fail;
import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20;
import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.AUD;
import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EMAIL;
import static org.cloudfoundry.identity.uaa.oauth.token.ClaimConstants.EXPIRY_IN_SECONDS;
Expand Down Expand Up @@ -1083,6 +1086,49 @@ void oidcPasswordGrantWithPrompts() throws MalformedURLException, JOSEException
assertThat(headers).doesNotContainKey("X-Forwarded-For");
}

@Test
void oauth20AuthorizationFlowWithUserInfo() throws Exception {

final ExternalOAuthCodeToken codeToken = new ExternalOAuthCodeToken("thecode", "some-origin", "http://google.com", null, null, "signedrequest");
final RestTemplate restTemplate = mock(RestTemplate.class);
final ResponseEntity<Map<String, Object>> responseEntity = mock(ResponseEntity.class);
when(responseEntity.getStatusCode()).thenReturn(HttpStatus.OK);
when(responseEntity.getBody()).thenReturn(Collections.emptyMap());
when(restTemplate.exchange(any(URI.class), eq(HttpMethod.GET), any(HttpEntity.class), any(ParameterizedTypeReference.class))).thenReturn(responseEntity);
authManager = new ExternalOAuthAuthenticationManager(
identityProviderProvisioning,
mock(IdentityZoneManager.class),
restTemplate,
restTemplate,
tokenEndpointBuilder,
new KeyInfoService("http://uaa.example.com"),
null
) {
@Override
protected <T extends AbstractExternalOAuthIdentityProviderDefinition<T>> String getTokenFromCode(
ExternalOAuthCodeToken codeToken,
IdentityProvider<T> config
) {
return "opaque-access-token";
}

@Override
public RestTemplate getRestTemplate(AbstractExternalOAuthIdentityProviderDefinition config) {
return restTemplate;
}
};
final RawExternalOAuthIdentityProviderDefinition config = new RawExternalOAuthIdentityProviderDefinition();
config.setResponseType("code");
config.setUserInfoUrl(URI.create("https://some.metadata.url/userinfo").toURL());
final IdentityProvider<AbstractExternalOAuthIdentityProviderDefinition> idp = new IdentityProvider<>();
idp.setType(OAUTH20);
idp.setConfig(config);
authManager.getClaimsFromToken(codeToken, idp);
assertThat(codeToken.getIdToken()).isNull();
assertThat(codeToken.getAccessToken()).isEqualTo("opaque-access-token");

}

private static void assertAuthorizationHeaderIsSetAndStartsWithBasic(final HttpHeaders headers) {
assertThat(headers).containsKey("Authorization");
final List<String> authorizationHeaders = headers.get("Authorization");
Expand Down
Loading