Skip to content

Commit 9716b64

Browse files
dorshaclaude
andauthored
feat: add IDPResponse to AuthenticationInfo for SSO exchange (#321)
Add IDPResponse type with idpGroups, idpSAMLAttributes, and idpOIDCClaims fields. Wire through from JWTResponse to AuthenticationInfo so SDK consumers can access IDP data after SSO token exchange. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 87e1c3a commit 9716b64

9 files changed

Lines changed: 161 additions & 4 deletions

File tree

src/main/java/com/descope/model/auth/AuthenticationInfo.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@ public class AuthenticationInfo {
1212
private Token refreshToken;
1313
private UserResponse user;
1414
private Boolean firstSeen;
15+
private IDPResponse idpResponse;
16+
17+
public AuthenticationInfo(Token token, Token refreshToken, UserResponse user, Boolean firstSeen) {
18+
this(token, refreshToken, user, firstSeen, null);
19+
}
1520
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.descope.model.auth;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
@Data
10+
@NoArgsConstructor
11+
@AllArgsConstructor
12+
public class IDPResponse {
13+
private List<String> idpGroups;
14+
private Map<String, Object> idpSAMLAttributes;
15+
private Map<String, Object> idpOIDCClaims;
16+
}

src/main/java/com/descope/model/jwt/response/JWTResponse.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.descope.model.jwt.response;
22

3+
import com.descope.model.auth.IDPResponse;
34
import com.descope.model.user.response.UserResponse;
45
import lombok.AllArgsConstructor;
56
import lombok.Data;
@@ -17,4 +18,11 @@ public class JWTResponse {
1718
private Integer cookieExpiration;
1819
private UserResponse user;
1920
private Boolean firstSeen;
21+
private IDPResponse idpResponse;
22+
23+
public JWTResponse(String sessionJwt, String refreshJwt, String cookieDomain, String cookiePath,
24+
Integer cookieMaxAge, Integer cookieExpiration, UserResponse user, Boolean firstSeen) {
25+
this(sessionJwt, refreshJwt, cookieDomain, cookiePath, cookieMaxAge, cookieExpiration, user,
26+
firstSeen, null);
27+
}
2028
}

src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public AuthenticationInfo validateAndRefreshSessionWithTokensAuthenticationInfo(
9494
} else if (StringUtils.isNotBlank(sessionToken)) {
9595
try {
9696
Token refresh = validateAndCreateToken(refreshToken);
97-
return new AuthenticationInfo(validateSessionWithToken(sessionToken), refresh, null, null);
97+
return new AuthenticationInfo(validateSessionWithToken(sessionToken), refresh, null, null, null);
9898
} catch (Exception e) {
9999
if (StringUtils.isNotBlank(refreshToken)) {
100100
return refreshSessionWithTokenAuthenticationInfo(refreshToken);

src/main/java/com/descope/sdk/auth/impl/AuthenticationsBase.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ AuthenticationInfo getAuthenticationInfo(JWTResponse jwtResponse) {
132132
refreshToken = validateAndCreateToken(jwtResponse.getRefreshJwt());
133133
}
134134
return new AuthenticationInfo(
135-
sessionToken, refreshToken, jwtResponse.getUser(), jwtResponse.getFirstSeen());
135+
sessionToken, refreshToken, jwtResponse.getUser(), jwtResponse.getFirstSeen(),
136+
jwtResponse.getIdpResponse());
136137
}
137138

138139
@SuppressWarnings("unchecked")

src/main/java/com/descope/sdk/mgmt/impl/JwtServiceImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ private AuthenticationInfo validateAndCreateAuthInfo(JWTResponse jwtResponse) th
138138
}
139139
Token sessionToken = validateAndCreateToken(jwtResponse.getSessionJwt());
140140
Token refreshToken = validateAndCreateToken(jwtResponse.getRefreshJwt());
141-
return new AuthenticationInfo(sessionToken, refreshToken, jwtResponse.getUser(), jwtResponse.getFirstSeen());
141+
return new AuthenticationInfo(sessionToken, refreshToken, jwtResponse.getUser(), jwtResponse.getFirstSeen(),
142+
jwtResponse.getIdpResponse());
142143
}
143144

144145
private URI composeUpdateJwtUri() {

src/test/java/com/descope/sdk/TestUtils.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public class TestUtils {
6262
1234567,
6363
1234567890,
6464
MOCK_USER_RESPONSE,
65-
true);
65+
true,
66+
null);
6667
public static final Map<String, Object> TENANTS_AUTHZ = mapOf("permissions", Arrays.asList("tp1", "tp2"), "roles",
6768
Arrays.asList("tr1", "tr2"));
6869
public static final Token MOCK_TOKEN = Token.builder()

src/test/java/com/descope/sdk/auth/impl/OAuthServiceImplTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
import static org.mockito.Mockito.mockStatic;
1717

1818
import com.descope.model.auth.AuthenticationInfo;
19+
import com.descope.model.auth.IDPResponse;
1920
import com.descope.model.auth.OAuthResponse;
2021
import com.descope.model.client.Client;
2122
import com.descope.model.jwt.Token;
23+
import com.descope.model.jwt.response.JWTResponse;
2224
import com.descope.model.jwt.response.SigningKeysResponse;
2325
import com.descope.model.magiclink.LoginOptions;
2426
import com.descope.model.user.response.UserResponse;
@@ -111,6 +113,65 @@ void testExchangeToken() {
111113
Assertions.assertThat(user.getLoginIds()).isNotEmpty();
112114
}
113115

116+
@Test
117+
void testExchangeTokenWithoutIDPResponse() {
118+
JWTResponse jwtResponseNoIdp = new JWTResponse(
119+
"someSessionJwt", "someRefreshJwt", "", "/", 1234567, 1234567890,
120+
MOCK_JWT_RESPONSE.getUser(), true);
121+
122+
ApiProxy apiProxy = mock(ApiProxy.class);
123+
doReturn(jwtResponseNoIdp).when(apiProxy).post(any(), any(), any());
124+
doReturn(new SigningKeysResponse(Arrays.asList(MOCK_SIGNING_KEY))).when(apiProxy).get(any(),
125+
eq(SigningKeysResponse.class));
126+
127+
AuthenticationInfo authenticationInfo;
128+
try (MockedStatic<ApiProxyBuilder> mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) {
129+
mockedApiProxyBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);
130+
try (MockedStatic<JwtUtils> mockedJwtUtils = mockStatic(JwtUtils.class)) {
131+
mockedJwtUtils.when(() -> JwtUtils.getToken(anyString(), any())).thenReturn(MOCK_TOKEN);
132+
authenticationInfo = oauthService.exchangeToken("somecode");
133+
}
134+
}
135+
136+
Assertions.assertThat(authenticationInfo).isNotNull();
137+
Assertions.assertThat(authenticationInfo.getUser()).isNotNull();
138+
Assertions.assertThat(authenticationInfo.getIdpResponse()).isNull();
139+
}
140+
141+
@Test
142+
void testExchangeTokenWithIDPResponse() {
143+
IDPResponse idpResponse = new IDPResponse(
144+
Arrays.asList("users"),
145+
null,
146+
mapOf("email_verified", true, "locale", "en-US"));
147+
JWTResponse jwtResponseWithIdp = new JWTResponse(
148+
"someSessionJwt", "someRefreshJwt", "", "/", 1234567, 1234567890,
149+
MOCK_JWT_RESPONSE.getUser(), true, idpResponse);
150+
151+
ApiProxy apiProxy = mock(ApiProxy.class);
152+
doReturn(jwtResponseWithIdp).when(apiProxy).post(any(), any(), any());
153+
doReturn(new SigningKeysResponse(Arrays.asList(MOCK_SIGNING_KEY))).when(apiProxy).get(any(),
154+
eq(SigningKeysResponse.class));
155+
156+
AuthenticationInfo authenticationInfo;
157+
try (MockedStatic<ApiProxyBuilder> mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) {
158+
mockedApiProxyBuilder.when(() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);
159+
try (MockedStatic<JwtUtils> mockedJwtUtils = mockStatic(JwtUtils.class)) {
160+
mockedJwtUtils.when(() -> JwtUtils.getToken(anyString(), any())).thenReturn(MOCK_TOKEN);
161+
authenticationInfo = oauthService.exchangeToken("somecode");
162+
}
163+
}
164+
165+
Assertions.assertThat(authenticationInfo).isNotNull();
166+
Assertions.assertThat(authenticationInfo.getIdpResponse()).isNotNull();
167+
Assertions.assertThat(authenticationInfo.getIdpResponse().getIdpGroups())
168+
.isEqualTo(Arrays.asList("users"));
169+
Assertions.assertThat(authenticationInfo.getIdpResponse().getIdpSAMLAttributes()).isNull();
170+
Assertions.assertThat(authenticationInfo.getIdpResponse().getIdpOIDCClaims())
171+
.containsEntry("email_verified", true)
172+
.containsEntry("locale", "en-US");
173+
}
174+
114175
void testExampleRequireBrowser() throws Exception {
115176
System.out.println(oauthService.start(OAUTH_PROVIDER_GOOGLE, "https://localhost/kuku", null));
116177
String encodedCode = "";

src/test/java/com/descope/sdk/auth/impl/SamlLinkServiceImplTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static com.descope.sdk.TestUtils.MOCK_TOKEN;
66
import static com.descope.sdk.TestUtils.MOCK_URL;
77
import static com.descope.sdk.TestUtils.PROJECT_ID;
8+
import static com.descope.utils.CollectionUtils.mapOf;
89
import static org.mockito.ArgumentMatchers.any;
910
import static org.mockito.ArgumentMatchers.anyString;
1011
import static org.mockito.ArgumentMatchers.eq;
@@ -13,9 +14,11 @@
1314
import static org.mockito.Mockito.mockStatic;
1415

1516
import com.descope.model.auth.AuthenticationInfo;
17+
import com.descope.model.auth.IDPResponse;
1618
import com.descope.model.auth.SAMLResponse;
1719
import com.descope.model.client.Client;
1820
import com.descope.model.jwt.Token;
21+
import com.descope.model.jwt.response.JWTResponse;
1922
import com.descope.model.jwt.response.SigningKeysResponse;
2023
import com.descope.model.magiclink.LoginOptions;
2124
import com.descope.model.user.response.UserResponse;
@@ -88,4 +91,65 @@ void testExchangeToken() {
8891
Assertions.assertThat(user.getUserId()).isNotBlank();
8992
Assertions.assertThat(user.getLoginIds()).isNotEmpty();
9093
}
94+
95+
@Test
96+
void testExchangeTokenWithoutIDPResponse() {
97+
JWTResponse jwtResponseNoIdp = new JWTResponse(
98+
"someSessionJwt", "someRefreshJwt", "", "/", 1234567, 1234567890,
99+
MOCK_JWT_RESPONSE.getUser(), true);
100+
101+
ApiProxy apiProxy = mock(ApiProxy.class);
102+
doReturn(jwtResponseNoIdp).when(apiProxy).post(any(), any(), any());
103+
doReturn(new SigningKeysResponse(Arrays.asList(MOCK_SIGNING_KEY)))
104+
.when(apiProxy).get(any(), eq(SigningKeysResponse.class));
105+
106+
AuthenticationInfo authenticationInfo;
107+
try (MockedStatic<ApiProxyBuilder> mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) {
108+
mockedApiProxyBuilder.when(
109+
() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);
110+
try (MockedStatic<JwtUtils> mockedJwtUtils = mockStatic(JwtUtils.class)) {
111+
mockedJwtUtils.when(() -> JwtUtils.getToken(anyString(), any())).thenReturn(MOCK_TOKEN);
112+
authenticationInfo = samlService.exchangeToken("somecode");
113+
}
114+
}
115+
116+
Assertions.assertThat(authenticationInfo).isNotNull();
117+
Assertions.assertThat(authenticationInfo.getUser()).isNotNull();
118+
Assertions.assertThat(authenticationInfo.getIdpResponse()).isNull();
119+
}
120+
121+
@Test
122+
void testExchangeTokenWithIDPResponse() {
123+
IDPResponse idpResponse = new IDPResponse(
124+
Arrays.asList("engineering", "devops"),
125+
mapOf("department", "engineering", "title", "Staff Engineer"),
126+
null);
127+
JWTResponse jwtResponseWithIdp = new JWTResponse(
128+
"someSessionJwt", "someRefreshJwt", "", "/", 1234567, 1234567890,
129+
MOCK_JWT_RESPONSE.getUser(), true, idpResponse);
130+
131+
ApiProxy apiProxy = mock(ApiProxy.class);
132+
doReturn(jwtResponseWithIdp).when(apiProxy).post(any(), any(), any());
133+
doReturn(new SigningKeysResponse(Arrays.asList(MOCK_SIGNING_KEY)))
134+
.when(apiProxy).get(any(), eq(SigningKeysResponse.class));
135+
136+
AuthenticationInfo authenticationInfo;
137+
try (MockedStatic<ApiProxyBuilder> mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) {
138+
mockedApiProxyBuilder.when(
139+
() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);
140+
try (MockedStatic<JwtUtils> mockedJwtUtils = mockStatic(JwtUtils.class)) {
141+
mockedJwtUtils.when(() -> JwtUtils.getToken(anyString(), any())).thenReturn(MOCK_TOKEN);
142+
authenticationInfo = samlService.exchangeToken("somecode");
143+
}
144+
}
145+
146+
Assertions.assertThat(authenticationInfo).isNotNull();
147+
Assertions.assertThat(authenticationInfo.getIdpResponse()).isNotNull();
148+
Assertions.assertThat(authenticationInfo.getIdpResponse().getIdpGroups())
149+
.isEqualTo(Arrays.asList("engineering", "devops"));
150+
Assertions.assertThat(authenticationInfo.getIdpResponse().getIdpSAMLAttributes())
151+
.containsEntry("department", "engineering")
152+
.containsEntry("title", "Staff Engineer");
153+
Assertions.assertThat(authenticationInfo.getIdpResponse().getIdpOIDCClaims()).isNull();
154+
}
91155
}

0 commit comments

Comments
 (0)