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

Commit

Permalink
Feat/add jwt exchange (#1067)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
shawn-sher and shawn-sher authored Jan 25, 2023
1 parent ce6bba0 commit 7db244b
Show file tree
Hide file tree
Showing 15 changed files with 458 additions and 9 deletions.
4 changes: 4 additions & 0 deletions cerberus-auth-connector-okta/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ dependencies {

// The Okta SDKs pull in an outdated version of guava that the OWASP Dep checker doesn't like
implementation group: 'com.google.guava', name: 'guava', version: "${versions.guava}"

// Okta jwt verfier libraries
implementation 'com.okta.jwt:okta-jwt-verifier:0.5.7'
implementation 'com.okta.jwt:okta-jwt-verifier-impl:0.5.7'
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static java.lang.Thread.sleep;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.nike.backstopper.exception.ApiException;
import com.nike.cerberus.auth.connector.AuthConnector;
import com.nike.cerberus.auth.connector.AuthData;
Expand All @@ -30,16 +31,22 @@
import com.okta.authn.sdk.FactorValidationException;
import com.okta.authn.sdk.client.AuthenticationClient;
import com.okta.authn.sdk.impl.resource.DefaultVerifyPassCodeFactorRequest;
import com.okta.jwt.AccessTokenVerifier;
import com.okta.jwt.Jwt;
import com.okta.jwt.JwtVerificationException;
import com.okta.jwt.JwtVerifiers;
import com.okta.sdk.authc.credentials.TokenClientCredentials;
import com.okta.sdk.client.Client;
import com.okta.sdk.client.Clients;
import com.okta.sdk.resource.group.GroupList;
import com.okta.sdk.resource.user.User;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

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

private final Client sdkClient;

private final String jwtIssuer;

private final String jwtAudience;

protected AccessTokenVerifier jwtVerifier;

@Autowired
public OktaAuthConnector(
AuthenticationClient oktaAuthenticationClient,
OktaConfigurationProperties oktaConfigurationProperties) {
OktaConfigurationProperties oktaConfigurationProperties,
@Value("${cerberus.auth.jwt.issuer}") String jwtIssuer,
@Value("${cerberus.auth.jwt.audience}") String jwtAudience) {
this.oktaAuthenticationClient = oktaAuthenticationClient;
this.sdkClient = getSdkClient(oktaConfigurationProperties);
this.jwtIssuer = jwtIssuer;
this.jwtAudience = jwtAudience;
}

/** Alternate constructor to facilitate unit testing */
public OktaAuthConnector(AuthenticationClient oktaAuthenticationClient, Client sdkClient) {
public OktaAuthConnector(
AuthenticationClient oktaAuthenticationClient,
Client sdkClient,
String jwtIssuer,
String jwtAudience,
AccessTokenVerifier jwtVerifier) {
this.oktaAuthenticationClient = oktaAuthenticationClient;
this.sdkClient = sdkClient;
this.jwtIssuer = jwtIssuer;
this.jwtAudience = jwtAudience;
this.jwtVerifier = jwtVerifier;
}

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

return groups;
}

/**
* Validates a JWT and retunrs the subject and userId in a map
*
* @param jwtString String jwt access token
* @return Map of username and userId
* @throws ApiException if JWT cannot be verified
*/
@Override
public Map<String, String> getValidatedUserPrincipal(String jwtString) {
try {
Jwt jwt = this.getAccessTokenVerifier().decode(jwtString);
Map<String, Object> claims = jwt.getClaims();

String username = claims.getOrDefault("sub", "").toString();
String userId = claims.getOrDefault("uid", "").toString();

if (username.isEmpty() || userId.isEmpty()) {
throw new JwtVerificationException("sub and uid claims are required");
}

Map<String, String> principalInfoMap =
ImmutableMap.of("username", username, "userId", userId);
return principalInfoMap;
} catch (JwtVerificationException jve) {
throw this.buildJwtVerificationApiException(jve, "Failed to verify JWT access token");
}
}

/**
* Convert JwtVerificationException to ApiException
*
* @param jve JwtVerificationException
* @param msg Message
* @return ApiException
*/
private ApiException buildJwtVerificationApiException(JwtVerificationException jve, String msg) {
ApiException exc =
ApiException.Builder.newBuilder()
.withApiErrors(DefaultApiError.BEARER_TOKEN_INVALID)
.withExceptionMessage(msg)
.withExceptionCause(jve)
.build();
return exc;
}

/**
* Creates an access token verifier with the configured issuer and audience
*
* @return AccessTokenVerifier
*/
protected AccessTokenVerifier getAccessTokenVerifier() {
if (this.jwtVerifier == null) {
this.jwtVerifier =
JwtVerifiers.accessTokenVerifierBuilder()
.setIssuer(this.jwtIssuer)
.setAudience(this.jwtAudience)
.build();
}
return this.jwtVerifier;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package com.nike.cerberus.auth.connector.okta;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;

Expand All @@ -29,7 +31,12 @@
import com.nike.cerberus.auth.connector.okta.statehandlers.MfaStateHandler;
import com.okta.authn.sdk.client.AuthenticationClient;
import com.okta.authn.sdk.impl.resource.DefaultVerifyPassCodeFactorRequest;
import com.okta.jwt.AccessTokenVerifier;
import com.okta.jwt.Jwt;
import com.okta.jwt.JwtVerificationException;
import com.okta.sdk.client.Client;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
Expand All @@ -48,7 +55,9 @@ public void setup() {

initMocks(this);

oktaAuthConnector = new OktaAuthConnector(client, sdkClient);
this.oktaAuthConnector =
new OktaAuthConnector(
client, sdkClient, "https://foo.bar", "dogs", mock(AccessTokenVerifier.class));
}

/////////////////////////
Expand Down Expand Up @@ -238,4 +247,84 @@ public void mfaCheckFails() throws Exception {
// verify results
assertEquals(expectedResponse, actualResponse);
}

@Test
public void testGetValidatedOktaPrincipalOkay() {
try {
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("sub", "tester");
claims.put("uid", "freeter");

Jwt mockJwt = mock(Jwt.class);
when(mockJwt.getClaims()).thenReturn(claims);
AccessTokenVerifier verifier = mock(AccessTokenVerifier.class);
when(verifier.decode(anyString())).thenReturn(mockJwt);
OktaAuthConnector connector =
new OktaAuthConnector(
client, sdkClient, "https://foo.bar/oauth2/skiddleydee", "dogs", verifier);
Map<String, String> principal = connector.getValidatedUserPrincipal("us");

assertEquals(principal.get("username"), "tester");
assertEquals(principal.get("userId"), "freeter");
} catch (JwtVerificationException jve) {
assert false;
}
}

@Test(expected = ApiException.class)
public void testGetValidatedOktaPrincipalMissingUserId() {
try {
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("sub", "tester");
// claims.put("uid", "freeter");

Jwt mockJwt = mock(Jwt.class);
when(mockJwt.getClaims()).thenReturn(claims);
AccessTokenVerifier verifier = mock(AccessTokenVerifier.class);
when(verifier.decode(anyString())).thenReturn(mockJwt);
OktaAuthConnector connector =
new OktaAuthConnector(
client, sdkClient, "https://foo.bar/oauth2/skiddleydee", "dogs", verifier);
Map<String, String> principal = connector.getValidatedUserPrincipal("us");

assertEquals(principal.get("username"), "tester");
assertEquals(principal.get("userId"), "freeter");
} catch (JwtVerificationException jve) {
assert false;
}
}

@Test(expected = ApiException.class)
public void testGetValidatedOktaPrincipalBadClaims() {
try {
Map<String, Object> claims = new HashMap<String, Object>();

Jwt mockJwt = mock(Jwt.class);
when(mockJwt.getClaims()).thenReturn(claims);
AccessTokenVerifier verifier = mock(AccessTokenVerifier.class);
when(verifier.decode(anyString())).thenReturn(mockJwt);
OktaAuthConnector connector =
new OktaAuthConnector(
client, sdkClient, "https://foo.bar/oauth2/skiddleydee", "dogs", verifier);

connector.getValidatedUserPrincipal("us");
} catch (JwtVerificationException jve) {
assert false;
}
}

@Test
public void testGetAccessTokenVerifierInitialNull() {
OktaAuthConnector connector =
new OktaAuthConnector(
client, sdkClient, "https://foo.bar/oauth2/skiddleydee", "dogs", null);
AccessTokenVerifier verifier = connector.getAccessTokenVerifier();
assertNotNull(verifier);
}

@Test
public void testGetAccessTokenVerifier() {
AccessTokenVerifier verifier = this.oktaAuthConnector.getAccessTokenVerifier();
assertNotNull(verifier);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import com.nike.cerberus.auth.connector.*;
import com.nike.cerberus.error.DefaultApiError;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -255,4 +257,9 @@ protected SessionLoginTokenData createSessionLoginToken(

return createSessionLoginTokenResponse.getData().get(0);
}

@Override
public Map<String, String> getValidatedUserPrincipal(String jwtString) {
throw new NotImplementedException("Not implemented for OneLogin");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.nike.cerberus.auth.connector.AuthStatus;
import com.nike.cerberus.error.DefaultApiError;
import java.util.Set;
import org.apache.commons.lang3.NotImplementedException;
import org.junit.Before;
import org.junit.Test;

Expand Down Expand Up @@ -369,4 +370,15 @@ public void test_createSessionLoginToken_fails_with_when_MFA_setup_is_required()
MFA_SETUP_REQUIRED.getHttpStatusCode(), ae.getApiErrors().get(0).getHttpStatusCode());
}
}

@Test
public void testgetValidatedUserPrincipalNotImplemented() {
NotImplementedException nie = null;
try {
oneLoginAuthConnector.getValidatedUserPrincipal("this won't work");
} catch (NotImplementedException exc) {
nie = exc;
}
assertNotNull(nie);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.nike.cerberus.auth.connector;

import java.util.Map;
import java.util.Set;

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

Set<String> getGroups(final AuthData data);

Map<String, String> getValidatedUserPrincipal(String jwtString);
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public enum DefaultApiError implements ApiError {
AUTH_TOKEN_INVALID(
99105, "X-Vault-Token or X-Cerberus-Token header is malformed or invalid.", SC_UNAUTHORIZED),

/** Authorization Bearer header was blank or invalid. */
BEARER_TOKEN_INVALID(99100, "Authorization Bearer header was blank or invalid.", SC_UNAUTHORIZED),

/** Supplied credentials are invalid. */
AUTH_BAD_CREDENTIALS(99106, "Invalid credentials", SC_UNAUTHORIZED),

Expand Down
4 changes: 4 additions & 0 deletions cerberus-web/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ dependencies {
implementation "io.jsonwebtoken:jjwt-api:${versions.jjwt}"
implementation "io.jsonwebtoken:jjwt-impl:${versions.jjwt}"
implementation "io.jsonwebtoken:jjwt-jackson:${versions.jjwt}"
implementation 'com.okta.jwt:okta-jwt-verifier:0.5.7'
implementation 'com.okta.jwt:okta-jwt-verifier-impl:0.5.7'




//dist tracing
Expand Down
Loading

0 comments on commit 7db244b

Please sign in to comment.