Skip to content

Commit

Permalink
Merge pull request #1156 from alphagov/govsi-1161
Browse files Browse the repository at this point in the history
GOVSI-1161 - Update ID token VOT value and enforce ordering of CredentialTrustLevel
  • Loading branch information
JHjava authored Dec 3, 2021
2 parents 4212247 + 60bf5e7 commit 959531b
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,19 @@
import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
import com.nimbusds.openid.connect.sdk.Nonce;
import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import uk.gov.di.authentication.oidc.lambda.TokenHandler;
import uk.gov.di.authentication.shared.entity.ClientConsent;
import uk.gov.di.authentication.shared.entity.RefreshTokenStore;
import uk.gov.di.authentication.shared.entity.ServiceType;
import uk.gov.di.authentication.shared.entity.ValidScopes;
import uk.gov.di.authentication.shared.entity.VectorOfTrust;
import uk.gov.di.authentication.sharedtest.basetest.ApiGatewayHandlerIntegrationTest;
import uk.gov.di.authentication.sharedtest.helper.KeyPairHelper;

Expand All @@ -51,9 +56,11 @@
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;

import static java.util.Collections.singletonList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static uk.gov.di.authentication.sharedtest.matchers.APIGatewayProxyResponseEventMatcher.hasStatus;
Expand All @@ -71,15 +78,30 @@ void setup() {
handler = new TokenHandler(TEST_CONFIGURATION_SERVICE);
}

@Test
void shouldCallTokenResourceAndReturnAccessAndRefreshToken()
throws JOSEException, ParseException, JsonProcessingException {
private static Stream<Object> validVectorValues() {
return Stream.of(
Optional.of("Cl.Cm"),
Optional.of("Cl"),
Optional.of("Pm.Cl.Cm"),
Optional.of("Pm.Cl"),
Optional.of("Pl.Cl.Cm"),
Optional.of("Pl.Cl"),
Optional.of("Ph.Cl"),
Optional.of("Ph.Cl.Cm"),
Optional.empty());
}

@ParameterizedTest
@MethodSource("validVectorValues")
void shouldCallTokenResourceAndReturnAccessAndRefreshToken(Optional<String> vtr)
throws JOSEException, ParseException, JsonProcessingException,
java.text.ParseException {
KeyPair keyPair = KeyPairHelper.GENERATE_RSA_KEY_PAIR();
Scope scope =
new Scope(
OIDCScopeValue.OPENID.getValue(), OIDCScopeValue.OFFLINE_ACCESS.getValue());
setUpDynamo(keyPair, scope, new Subject());
var response = generateTokenRequest(keyPair, scope);
var response = generateTokenRequest(keyPair, scope, vtr);

assertThat(response, hasStatus(200));
JSONObject jsonResponse = JSONObjectUtils.parse(response.getBody());
Expand All @@ -93,6 +115,16 @@ void shouldCallTokenResourceAndReturnAccessAndRefreshToken()
.toSuccessResponse()
.getTokens()
.getBearerAccessToken());
if (vtr.isEmpty()) {
vtr = Optional.of(VectorOfTrust.getDefaults().getCredentialTrustLevel().getValue());
}
assertThat(
OIDCTokenResponse.parse(jsonResponse)
.getOIDCTokens()
.getIDToken()
.getJWTClaimsSet()
.getClaim("vot"),
equalTo(vtr.get()));
}

@Test
Expand All @@ -101,7 +133,7 @@ void shouldCallTokenResourceAndOnlyReturnAccessTokenWithoutOfflineAccessScope()
KeyPair keyPair = KeyPairHelper.GENERATE_RSA_KEY_PAIR();
Scope scope = new Scope(OIDCScopeValue.OPENID.getValue());
setUpDynamo(keyPair, scope, new Subject());
var response = generateTokenRequest(keyPair, scope);
var response = generateTokenRequest(keyPair, scope, Optional.empty());

assertThat(response, hasStatus(200));
JSONObject jsonResponse = JSONObjectUtils.parse(response.getBody());
Expand Down Expand Up @@ -206,21 +238,26 @@ private void setUpDynamo(KeyPair keyPair, Scope scope, Subject internalSubject)
userStore.updateConsent(TEST_EMAIL, clientConsent);
}

private AuthenticationRequest generateAuthRequest(Scope scope) {
private AuthenticationRequest generateAuthRequest(Scope scope, Optional<String> vtr) {
ResponseType responseType = new ResponseType(ResponseType.Value.CODE);
State state = new State();
Nonce nonce = new Nonce();
return new AuthenticationRequest.Builder(
responseType,
scope,
new ClientID(CLIENT_ID),
URI.create("http://localhost/redirect"))
.state(state)
.nonce(nonce)
.build();
AuthenticationRequest.Builder builder =
new AuthenticationRequest.Builder(
responseType,
scope,
new ClientID(CLIENT_ID),
URI.create("http://localhost/redirect"))
.state(state)
.nonce(nonce);

vtr.ifPresent(v -> builder.customParameter("vtr", v));

return builder.build();
}

private APIGatewayProxyResponseEvent generateTokenRequest(KeyPair keyPair, Scope scope)
private APIGatewayProxyResponseEvent generateTokenRequest(
KeyPair keyPair, Scope scope, Optional<String> vtr)
throws JOSEException, JsonProcessingException {
PrivateKey privateKey = keyPair.getPrivate();
LocalDateTime localDateTime = LocalDateTime.now().plusMinutes(5);
Expand All @@ -233,8 +270,20 @@ private APIGatewayProxyResponseEvent generateTokenRequest(KeyPair keyPair, Scope
new PrivateKeyJWT(
claimsSet, JWSAlgorithm.RS256, (RSAPrivateKey) privateKey, null, null);
String code = new AuthorizationCode().toString();
VectorOfTrust vectorOfTrust = VectorOfTrust.getDefaults();
if (vtr.isPresent()) {
JSONArray jsonArray = new JSONArray();
jsonArray.add(vtr.get());
vectorOfTrust =
VectorOfTrust.parseFromAuthRequestAttribute(
singletonList(jsonArray.toJSONString()));
}
redis.addAuthCodeAndCreateClientSession(
code, "a-client-session-id", TEST_EMAIL, generateAuthRequest(scope).toParameters());
code,
"a-client-session-id",
TEST_EMAIL,
generateAuthRequest(scope, vtr).toParameters(),
vectorOfTrust);
Map<String, List<String>> customParams = new HashMap<>();
customParams.put(
"grant_type", Collections.singletonList(GrantType.AUTHORIZATION_CODE.getValue()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,7 @@ public APIGatewayProxyResponseEvent handleRequest(
String vot =
clientSession
.getEffectiveVectorOfTrust()
.getCredentialTrustLevel()
.getValue();
.retrieveVectorOfTrustForToken();

OIDCTokenResponse tokenResponse =
tokenService.generateTokenResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@
import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
import net.minidev.json.JSONArray;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import uk.gov.di.authentication.shared.entity.AuthCodeExchangeData;
import uk.gov.di.authentication.shared.entity.ClientConsent;
import uk.gov.di.authentication.shared.entity.ClientRegistry;
Expand Down Expand Up @@ -67,6 +70,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import static java.util.Collections.singletonList;
import static org.hamcrest.MatcherAssert.assertThat;
Expand Down Expand Up @@ -129,10 +133,16 @@ public void setUp() {
redisConnectionService);
}

@Test
public void shouldReturn200ForSuccessfulTokenRequest() throws JOSEException {
VectorOfTrust vot = mock(VectorOfTrust.class);
when(vot.getCredentialTrustLevel()).thenReturn(CredentialTrustLevel.MEDIUM_LEVEL);
private static Stream<String> validVectorValues() {
return Stream.of(
"Cl.Cm", "Cl", "Pm.Cl.Cm", "Pm.Cl", "Pl.Cl.Cm", "Pl.Cl", "Ph.Cl", "Ph.Cl.Cm");
}

@ParameterizedTest
@MethodSource("validVectorValues")
public void shouldReturn200ForSuccessfulTokenRequest(String vectorValue) throws JOSEException {
JSONArray jsonArray = new JSONArray();
jsonArray.add(vectorValue);
KeyPair keyPair = generateRsaKeyPair();
UserProfile userProfile = generateUserProfile();
SignedJWT signedJWT =
Expand Down Expand Up @@ -163,19 +173,22 @@ public void shouldReturn200ForSuccessfulTokenRequest() throws JOSEException {
new AuthCodeExchangeData()
.setEmail(TEST_EMAIL)
.setClientSessionId(CLIENT_SESSION_ID)));

AuthenticationRequest authenticationRequest = generateAuthRequest(jsonArray.toJSONString());
VectorOfTrust vtr =
VectorOfTrust.parseFromAuthRequestAttribute(
authenticationRequest.getCustomParameter("vtr"));
when(clientSessionService.getClientSession(CLIENT_SESSION_ID))
.thenReturn(
new ClientSession(
generateAuthRequest().toParameters(), LocalDateTime.now(), vot));
authenticationRequest.toParameters(), LocalDateTime.now(), vtr));
when(dynamoService.getUserProfileByEmail(eq(TEST_EMAIL))).thenReturn(userProfile);
when(tokenService.generateTokenResponse(
CLIENT_ID,
INTERNAL_SUBJECT,
SCOPES,
Map.of("nonce", NONCE),
PUBLIC_SUBJECT,
VOT,
vtr.retrieveVectorOfTrustForToken(),
userProfile.getClientConsent(),
clientRegistry.isInternalService()))
.thenReturn(tokenResponse);
Expand Down Expand Up @@ -486,6 +499,7 @@ private PrivateKeyJWT generatePrivateKeyJWT(PrivateKey privateKey) throws JOSEEx
private ClientRegistry generateClientRegistry(KeyPair keyPair) {
return new ClientRegistry()
.setClientID(CLIENT_ID)
.setInternalService(false)
.setClientName("test-client")
.setRedirectUrls(singletonList(REDIRECT_URI))
.setScopes(SCOPES.toStringList())
Expand Down Expand Up @@ -547,6 +561,13 @@ private APIGatewayProxyResponseEvent generateApiGatewayRequest(
}

private AuthenticationRequest generateAuthRequest() {
JSONArray jsonArray = new JSONArray();
jsonArray.add("Cl.Cm");
jsonArray.add("Cl");
return generateAuthRequest(jsonArray.toJSONString());
}

private AuthenticationRequest generateAuthRequest(String vtr) {
ResponseType responseType = new ResponseType(ResponseType.Value.CODE);
State state = new State();
return new AuthenticationRequest.Builder(
Expand All @@ -556,6 +577,7 @@ private AuthenticationRequest generateAuthRequest() {
URI.create(REDIRECT_URI))
.state(state)
.nonce(NONCE)
.customParameter("vtr", vtr)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ public void addAuthCodeAndCreateClientSession(
String authCode,
String clientSessionId,
String email,
Map<String, List<String>> authRequest)
Map<String, List<String>> authRequest,
VectorOfTrust vtr)
throws JsonProcessingException {
redis.saveWithExpiry(
AUTH_CODE_PREFIX.concat(authCode),
Expand All @@ -177,8 +178,7 @@ public void addAuthCodeAndCreateClientSession(
redis.saveWithExpiry(
CLIENT_SESSION_PREFIX.concat(clientSessionId),
objectMapper.writeValueAsString(
new ClientSession(
authRequest, LocalDateTime.now(), VectorOfTrust.getDefaults())),
new ClientSession(authRequest, LocalDateTime.now(), vtr)),
300);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package uk.gov.di.authentication.shared.entity;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

public enum CredentialTrustLevel {
LOW_LEVEL("Cl"),
Expand All @@ -18,23 +16,10 @@ public String getValue() {
return value;
}

public static CredentialTrustLevel retrieveCredentialTrustLevel(List<String> vtrSets) {
public static CredentialTrustLevel retrieveCredentialTrustLevel(String vtrSets) {

return Arrays.stream(values())
.filter(
tl ->
vtrSets.stream()
.anyMatch(
set ->
new HashSet<>(
Arrays.asList(
set.split("\\.")))
.equals(
new HashSet<>(
Arrays.asList(
tl.getValue()
.split(
"\\."))))))
.filter(tl -> vtrSets.equals(tl.getValue()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Invalid CredentialTrustLevel"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import java.util.Objects;
import java.util.stream.Collectors;

import static java.lang.String.format;
import static net.minidev.json.parser.JSONParser.DEFAULT_PERMISSIVE_MODE;

public class VectorOfTrust {
Expand Down Expand Up @@ -63,11 +62,7 @@ public static VectorOfTrust parseFromAuthRequestAttribute(List<String> vtr) {
LOGGER.error("Error when parsing vtr attribute", e);
throw new IllegalArgumentException("Invalid VTR attribute", e);
}
List<String> vtrSets = new ArrayList<>();
for (int i = 0; i < vtrJsonArray.size(); i++) {
vtrSets.add((String) vtrJsonArray.get(i));
}
VectorOfTrust vectorOfTrust = parseVtrSet(vtrSets);
VectorOfTrust vectorOfTrust = parseVtrSet(vtrJsonArray);
LOGGER.info("VTR has been processed at vectorOfTrust: {}", vectorOfTrust.toString());

return vectorOfTrust;
Expand All @@ -77,34 +72,35 @@ public static VectorOfTrust getDefaults() {
return new VectorOfTrust(CredentialTrustLevel.getDefault());
}

private static VectorOfTrust parseVtrSet(List<String> vtrSet) {
public String retrieveVectorOfTrustForToken() {
if (Objects.isNull(levelOfConfidence)) {
return credentialTrustLevel.getValue();
} else {
return levelOfConfidence.getValue() + "." + credentialTrustLevel.getValue();
}
}

private static VectorOfTrust parseVtrSet(JSONArray vtrJsonArray) {
List<VectorOfTrust> vectorOfTrusts = new ArrayList<>();
for (String vtr : vtrSet) {
LevelOfConfidence loc = null;
CredentialTrustLevel ctl;
String[] splitVtr = vtr.split("\\.");
for (Object obj : vtrJsonArray) {
String vtr = (String) obj;
var splitVtr = vtr.split("\\.");

List<String> levelOfConfidence =
List<LevelOfConfidence> levelOfConfidence =
Arrays.stream(splitVtr)
.filter((a) -> a.startsWith("P"))
.filter(a -> a.startsWith("P"))
.map(LevelOfConfidence::retrieveLevelOfConfidence)
.collect(Collectors.toList());
if (splitVtr.length > 3 || (splitVtr.length == 3 && levelOfConfidence.isEmpty())) {
throw new IllegalArgumentException(
format("Invalid number of attributes in VTR, %s", vtr));
}
if (!levelOfConfidence.isEmpty()) {
if (levelOfConfidence.size() > 1 || !vtr.startsWith("P")) {
throw new IllegalArgumentException(
format("Invalid Identity vector value exists in VTS: %s", vtr));
}
loc = LevelOfConfidence.retrieveLevelOfConfidence(levelOfConfidence.get(0));
ctl =
CredentialTrustLevel.retrieveCredentialTrustLevel(
List.of(vtr.substring(vtr.indexOf(".") + 1)));
if (levelOfConfidence.isEmpty()) {
var ctl = CredentialTrustLevel.retrieveCredentialTrustLevel(vtr);
vectorOfTrusts.add(new VectorOfTrust(ctl));
} else {
ctl = CredentialTrustLevel.retrieveCredentialTrustLevel(List.of(vtr));
var loc = levelOfConfidence.get(0);
var ctl =
CredentialTrustLevel.retrieveCredentialTrustLevel(
vtr.substring(vtr.indexOf(".") + 1));
vectorOfTrusts.add(new VectorOfTrust(ctl, loc));
}
vectorOfTrusts.add(new VectorOfTrust(ctl, loc));
}

return vectorOfTrusts.stream()
Expand Down
Loading

0 comments on commit 959531b

Please sign in to comment.