Skip to content
This repository was archived by the owner on Jan 12, 2024. It is now read-only.

Commit 7db244b

Browse files
authored
Feat/add jwt exchange (#1067)
* added endpoint for access token * Refined and updated tests * Added a feature flag to exchange endpoint * Corrected SpEL Value string Co-authored-by: Shawn Sherwood <shawn-sher@users.noreply.github.com>
1 parent ce6bba0 commit 7db244b

15 files changed

Lines changed: 458 additions & 9 deletions

File tree

cerberus-auth-connector-okta/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,8 @@ dependencies {
2626

2727
// The Okta SDKs pull in an outdated version of guava that the OWASP Dep checker doesn't like
2828
implementation group: 'com.google.guava', name: 'guava', version: "${versions.guava}"
29+
30+
// Okta jwt verfier libraries
31+
implementation 'com.okta.jwt:okta-jwt-verifier:0.5.7'
32+
implementation 'com.okta.jwt:okta-jwt-verifier-impl:0.5.7'
2933
}

cerberus-auth-connector-okta/src/main/java/com/nike/cerberus/auth/connector/okta/OktaAuthConnector.java

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static java.lang.Thread.sleep;
2020

2121
import com.google.common.base.Preconditions;
22+
import com.google.common.collect.ImmutableMap;
2223
import com.nike.backstopper.exception.ApiException;
2324
import com.nike.cerberus.auth.connector.AuthConnector;
2425
import com.nike.cerberus.auth.connector.AuthData;
@@ -30,16 +31,22 @@
3031
import com.okta.authn.sdk.FactorValidationException;
3132
import com.okta.authn.sdk.client.AuthenticationClient;
3233
import com.okta.authn.sdk.impl.resource.DefaultVerifyPassCodeFactorRequest;
34+
import com.okta.jwt.AccessTokenVerifier;
35+
import com.okta.jwt.Jwt;
36+
import com.okta.jwt.JwtVerificationException;
37+
import com.okta.jwt.JwtVerifiers;
3338
import com.okta.sdk.authc.credentials.TokenClientCredentials;
3439
import com.okta.sdk.client.Client;
3540
import com.okta.sdk.client.Clients;
3641
import com.okta.sdk.resource.group.GroupList;
3742
import com.okta.sdk.resource.user.User;
3843
import java.util.HashSet;
44+
import java.util.Map;
3945
import java.util.Set;
4046
import java.util.concurrent.CompletableFuture;
4147
import java.util.concurrent.TimeUnit;
4248
import org.springframework.beans.factory.annotation.Autowired;
49+
import org.springframework.beans.factory.annotation.Value;
4350
import org.springframework.stereotype.Component;
4451

4552
/** Okta version 1 API implementation of the AuthConnector interface. */
@@ -50,18 +57,36 @@ public class OktaAuthConnector implements AuthConnector {
5057

5158
private final Client sdkClient;
5259

60+
private final String jwtIssuer;
61+
62+
private final String jwtAudience;
63+
64+
protected AccessTokenVerifier jwtVerifier;
65+
5366
@Autowired
5467
public OktaAuthConnector(
5568
AuthenticationClient oktaAuthenticationClient,
56-
OktaConfigurationProperties oktaConfigurationProperties) {
69+
OktaConfigurationProperties oktaConfigurationProperties,
70+
@Value("${cerberus.auth.jwt.issuer}") String jwtIssuer,
71+
@Value("${cerberus.auth.jwt.audience}") String jwtAudience) {
5772
this.oktaAuthenticationClient = oktaAuthenticationClient;
5873
this.sdkClient = getSdkClient(oktaConfigurationProperties);
74+
this.jwtIssuer = jwtIssuer;
75+
this.jwtAudience = jwtAudience;
5976
}
6077

6178
/** Alternate constructor to facilitate unit testing */
62-
public OktaAuthConnector(AuthenticationClient oktaAuthenticationClient, Client sdkClient) {
79+
public OktaAuthConnector(
80+
AuthenticationClient oktaAuthenticationClient,
81+
Client sdkClient,
82+
String jwtIssuer,
83+
String jwtAudience,
84+
AccessTokenVerifier jwtVerifier) {
6385
this.oktaAuthenticationClient = oktaAuthenticationClient;
6486
this.sdkClient = sdkClient;
87+
this.jwtIssuer = jwtIssuer;
88+
this.jwtAudience = jwtAudience;
89+
this.jwtVerifier = jwtVerifier;
6590
}
6691

6792
private Client getSdkClient(OktaConfigurationProperties oktaConfigurationProperties) {
@@ -209,4 +234,65 @@ public Set<String> getGroups(AuthData authData) {
209234

210235
return groups;
211236
}
237+
238+
/**
239+
* Validates a JWT and retunrs the subject and userId in a map
240+
*
241+
* @param jwtString String jwt access token
242+
* @return Map of username and userId
243+
* @throws ApiException if JWT cannot be verified
244+
*/
245+
@Override
246+
public Map<String, String> getValidatedUserPrincipal(String jwtString) {
247+
try {
248+
Jwt jwt = this.getAccessTokenVerifier().decode(jwtString);
249+
Map<String, Object> claims = jwt.getClaims();
250+
251+
String username = claims.getOrDefault("sub", "").toString();
252+
String userId = claims.getOrDefault("uid", "").toString();
253+
254+
if (username.isEmpty() || userId.isEmpty()) {
255+
throw new JwtVerificationException("sub and uid claims are required");
256+
}
257+
258+
Map<String, String> principalInfoMap =
259+
ImmutableMap.of("username", username, "userId", userId);
260+
return principalInfoMap;
261+
} catch (JwtVerificationException jve) {
262+
throw this.buildJwtVerificationApiException(jve, "Failed to verify JWT access token");
263+
}
264+
}
265+
266+
/**
267+
* Convert JwtVerificationException to ApiException
268+
*
269+
* @param jve JwtVerificationException
270+
* @param msg Message
271+
* @return ApiException
272+
*/
273+
private ApiException buildJwtVerificationApiException(JwtVerificationException jve, String msg) {
274+
ApiException exc =
275+
ApiException.Builder.newBuilder()
276+
.withApiErrors(DefaultApiError.BEARER_TOKEN_INVALID)
277+
.withExceptionMessage(msg)
278+
.withExceptionCause(jve)
279+
.build();
280+
return exc;
281+
}
282+
283+
/**
284+
* Creates an access token verifier with the configured issuer and audience
285+
*
286+
* @return AccessTokenVerifier
287+
*/
288+
protected AccessTokenVerifier getAccessTokenVerifier() {
289+
if (this.jwtVerifier == null) {
290+
this.jwtVerifier =
291+
JwtVerifiers.accessTokenVerifierBuilder()
292+
.setIssuer(this.jwtIssuer)
293+
.setAudience(this.jwtAudience)
294+
.build();
295+
}
296+
return this.jwtVerifier;
297+
}
212298
}

cerberus-auth-connector-okta/src/test/java/com/nike/cerberus/auth/connector/okta/OktaAuthConnectorTest.java

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package com.nike.cerberus.auth.connector.okta;
1818

1919
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertNotNull;
2021
import static org.mockito.Matchers.any;
22+
import static org.mockito.Matchers.anyString;
2123
import static org.mockito.Mockito.*;
2224
import static org.mockito.MockitoAnnotations.initMocks;
2325

@@ -29,7 +31,12 @@
2931
import com.nike.cerberus.auth.connector.okta.statehandlers.MfaStateHandler;
3032
import com.okta.authn.sdk.client.AuthenticationClient;
3133
import com.okta.authn.sdk.impl.resource.DefaultVerifyPassCodeFactorRequest;
34+
import com.okta.jwt.AccessTokenVerifier;
35+
import com.okta.jwt.Jwt;
36+
import com.okta.jwt.JwtVerificationException;
3237
import com.okta.sdk.client.Client;
38+
import java.util.HashMap;
39+
import java.util.Map;
3340
import org.junit.Before;
3441
import org.junit.Test;
3542
import org.mockito.Mock;
@@ -48,7 +55,9 @@ public void setup() {
4855

4956
initMocks(this);
5057

51-
oktaAuthConnector = new OktaAuthConnector(client, sdkClient);
58+
this.oktaAuthConnector =
59+
new OktaAuthConnector(
60+
client, sdkClient, "https://foo.bar", "dogs", mock(AccessTokenVerifier.class));
5261
}
5362

5463
/////////////////////////
@@ -238,4 +247,84 @@ public void mfaCheckFails() throws Exception {
238247
// verify results
239248
assertEquals(expectedResponse, actualResponse);
240249
}
250+
251+
@Test
252+
public void testGetValidatedOktaPrincipalOkay() {
253+
try {
254+
Map<String, Object> claims = new HashMap<String, Object>();
255+
claims.put("sub", "tester");
256+
claims.put("uid", "freeter");
257+
258+
Jwt mockJwt = mock(Jwt.class);
259+
when(mockJwt.getClaims()).thenReturn(claims);
260+
AccessTokenVerifier verifier = mock(AccessTokenVerifier.class);
261+
when(verifier.decode(anyString())).thenReturn(mockJwt);
262+
OktaAuthConnector connector =
263+
new OktaAuthConnector(
264+
client, sdkClient, "https://foo.bar/oauth2/skiddleydee", "dogs", verifier);
265+
Map<String, String> principal = connector.getValidatedUserPrincipal("us");
266+
267+
assertEquals(principal.get("username"), "tester");
268+
assertEquals(principal.get("userId"), "freeter");
269+
} catch (JwtVerificationException jve) {
270+
assert false;
271+
}
272+
}
273+
274+
@Test(expected = ApiException.class)
275+
public void testGetValidatedOktaPrincipalMissingUserId() {
276+
try {
277+
Map<String, Object> claims = new HashMap<String, Object>();
278+
claims.put("sub", "tester");
279+
// claims.put("uid", "freeter");
280+
281+
Jwt mockJwt = mock(Jwt.class);
282+
when(mockJwt.getClaims()).thenReturn(claims);
283+
AccessTokenVerifier verifier = mock(AccessTokenVerifier.class);
284+
when(verifier.decode(anyString())).thenReturn(mockJwt);
285+
OktaAuthConnector connector =
286+
new OktaAuthConnector(
287+
client, sdkClient, "https://foo.bar/oauth2/skiddleydee", "dogs", verifier);
288+
Map<String, String> principal = connector.getValidatedUserPrincipal("us");
289+
290+
assertEquals(principal.get("username"), "tester");
291+
assertEquals(principal.get("userId"), "freeter");
292+
} catch (JwtVerificationException jve) {
293+
assert false;
294+
}
295+
}
296+
297+
@Test(expected = ApiException.class)
298+
public void testGetValidatedOktaPrincipalBadClaims() {
299+
try {
300+
Map<String, Object> claims = new HashMap<String, Object>();
301+
302+
Jwt mockJwt = mock(Jwt.class);
303+
when(mockJwt.getClaims()).thenReturn(claims);
304+
AccessTokenVerifier verifier = mock(AccessTokenVerifier.class);
305+
when(verifier.decode(anyString())).thenReturn(mockJwt);
306+
OktaAuthConnector connector =
307+
new OktaAuthConnector(
308+
client, sdkClient, "https://foo.bar/oauth2/skiddleydee", "dogs", verifier);
309+
310+
connector.getValidatedUserPrincipal("us");
311+
} catch (JwtVerificationException jve) {
312+
assert false;
313+
}
314+
}
315+
316+
@Test
317+
public void testGetAccessTokenVerifierInitialNull() {
318+
OktaAuthConnector connector =
319+
new OktaAuthConnector(
320+
client, sdkClient, "https://foo.bar/oauth2/skiddleydee", "dogs", null);
321+
AccessTokenVerifier verifier = connector.getAccessTokenVerifier();
322+
assertNotNull(verifier);
323+
}
324+
325+
@Test
326+
public void testGetAccessTokenVerifier() {
327+
AccessTokenVerifier verifier = this.oktaAuthConnector.getAccessTokenVerifier();
328+
assertNotNull(verifier);
329+
}
241330
}

cerberus-auth-connector-onelogin/src/main/java/com/nike/cerberus/auth/connector/onelogin/OneLoginAuthConnector.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import com.nike.cerberus.auth.connector.*;
2323
import com.nike.cerberus.error.DefaultApiError;
2424
import java.util.HashSet;
25+
import java.util.Map;
2526
import java.util.Set;
27+
import org.apache.commons.lang3.NotImplementedException;
2628
import org.apache.commons.lang3.StringEscapeUtils;
2729
import org.apache.commons.lang3.StringUtils;
2830
import org.springframework.beans.factory.annotation.Autowired;
@@ -255,4 +257,9 @@ protected SessionLoginTokenData createSessionLoginToken(
255257

256258
return createSessionLoginTokenResponse.getData().get(0);
257259
}
260+
261+
@Override
262+
public Map<String, String> getValidatedUserPrincipal(String jwtString) {
263+
throw new NotImplementedException("Not implemented for OneLogin");
264+
}
258265
}

cerberus-auth-connector-onelogin/src/test/java/com/nike/cerberus/auth/connector/onelogin/OneLoginAuthConnectorTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.nike.cerberus.auth.connector.AuthStatus;
2929
import com.nike.cerberus.error.DefaultApiError;
3030
import java.util.Set;
31+
import org.apache.commons.lang3.NotImplementedException;
3132
import org.junit.Before;
3233
import org.junit.Test;
3334

@@ -369,4 +370,15 @@ public void test_createSessionLoginToken_fails_with_when_MFA_setup_is_required()
369370
MFA_SETUP_REQUIRED.getHttpStatusCode(), ae.getApiErrors().get(0).getHttpStatusCode());
370371
}
371372
}
373+
374+
@Test
375+
public void testgetValidatedUserPrincipalNotImplemented() {
376+
NotImplementedException nie = null;
377+
try {
378+
oneLoginAuthConnector.getValidatedUserPrincipal("this won't work");
379+
} catch (NotImplementedException exc) {
380+
nie = exc;
381+
}
382+
assertNotNull(nie);
383+
}
372384
}

cerberus-core/src/main/java/com/nike/cerberus/auth/connector/AuthConnector.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.nike.cerberus.auth.connector;
1818

19+
import java.util.Map;
1920
import java.util.Set;
2021

2122
public interface AuthConnector {
@@ -29,4 +30,6 @@ public interface AuthConnector {
2930
AuthResponse mfaCheck(final String stateToken, final String deviceId, final String otpToken);
3031

3132
Set<String> getGroups(final AuthData data);
33+
34+
Map<String, String> getValidatedUserPrincipal(String jwtString);
3235
}

cerberus-core/src/main/java/com/nike/cerberus/error/DefaultApiError.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ public enum DefaultApiError implements ApiError {
5151
AUTH_TOKEN_INVALID(
5252
99105, "X-Vault-Token or X-Cerberus-Token header is malformed or invalid.", SC_UNAUTHORIZED),
5353

54+
/** Authorization Bearer header was blank or invalid. */
55+
BEARER_TOKEN_INVALID(99100, "Authorization Bearer header was blank or invalid.", SC_UNAUTHORIZED),
56+
5457
/** Supplied credentials are invalid. */
5558
AUTH_BAD_CREDENTIALS(99106, "Invalid credentials", SC_UNAUTHORIZED),
5659

cerberus-web/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ dependencies {
7373
implementation "io.jsonwebtoken:jjwt-api:${versions.jjwt}"
7474
implementation "io.jsonwebtoken:jjwt-impl:${versions.jjwt}"
7575
implementation "io.jsonwebtoken:jjwt-jackson:${versions.jjwt}"
76+
implementation 'com.okta.jwt:okta-jwt-verifier:0.5.7'
77+
implementation 'com.okta.jwt:okta-jwt-verifier-impl:0.5.7'
78+
79+
7680

7781

7882
//dist tracing

0 commit comments

Comments
 (0)