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

Commit

Permalink
Only allow user principals to refresh their token and limit the numbe…
Browse files Browse the repository at this point in the history
…r… (#52)

Only only user principals to refresh there token and limit the number of refreshes.
  • Loading branch information
fieldju authored Jul 6, 2017
1 parent 9952151 commit 3fe6fd8
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 8 deletions.
14 changes: 11 additions & 3 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ This endpoint will take a Users credentials and proxy the request to Vault to ge
"metadata": {
"username": "[email protected]",
"is_admin": "false",
"groups": "Lst-CDT.CloudPlatformEngine.FTE,Lst-digital.platform-tools.internal"
"groups": "Lst-CDT.CloudPlatformEngine.FTE,Lst-digital.platform-tools.internal",
"refresh_count": 1,
"max_refresh_count": 24
},
"lease_duration": 3600,
"renewable": true
Expand Down Expand Up @@ -97,7 +99,9 @@ This endpoint will take a Users credentials and proxy the request to Vault to ge
"metadata": {
"username": "[email protected]",
"is_admin": "false",
"groups": "Lst-CDT.CloudPlatformEngine.FTE,Lst-digital.platform-tools.internal"
"groups": "Lst-CDT.CloudPlatformEngine.FTE,Lst-digital.platform-tools.internal",
"refresh_count": 1,
"max_refresh_count": 24
},
"lease_duration": 3600,
"renewable": true
Expand All @@ -110,6 +114,8 @@ This endpoint will take a Users credentials and proxy the request to Vault to ge
### Refresh the user's token [GET]

This endpoint allows a user to exchange their current token for a new one with updated policies.
There is a limit to the number of times this call can be made with a token and is store in the metadata
refresh_count and max_refresh_count can be used to determine when a re-authentication is required.

+ Request (application/json)

Expand All @@ -133,7 +139,9 @@ This endpoint allows a user to exchange their current token for a new one with u
"metadata": {
"username": "[email protected]",
"is_admin": "false",
"groups": "Lst-CDT.CloudPlatformEngine.FTE,Lst-digital.platform-tools.internal"
"groups": "Lst-CDT.CloudPlatformEngine.FTE,Lst-digital.platform-tools.internal",
"refresh_count": 2,
"max_refresh_count": 24
},
"lease_duration": 3600,
"renewable": true
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/nike/cerberus/error/DefaultApiError.java
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@ public enum DefaultApiError implements ApiError {
*/
AUTHENTICATION_ERROR_INVALID_REGION(99229, "Invalid AWS region provided during authentication.", HttpServletResponse.SC_BAD_REQUEST),

/**
* The token has exceeded the amount of times it can be refreshed
*/
MAXIMUM_TOKEN_REFRESH_COUNT_REACHED(99230, "Maximum token refresh count reached, re-authentication required.", HttpServletResponse.SC_FORBIDDEN),

/**
* The token has exceeded the amount of times it can be refreshed
*/
IAM_PRINCIPALS_CANNOT_USE_USER_ONLY_RESOURCE(99231, "The requested resource is for User Principals only.", HttpServletResponse.SC_FORBIDDEN),

/**
* Generic not found error.
*/
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/nike/cerberus/security/VaultAuthPrincipal.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public class VaultAuthPrincipal implements Principal {

public static final String METADATA_KEY_AWS_REGION = "aws_region";

public static final String METADATA_KEY_TOKEN_REFRESH_COUNT = "refresh_count";

public static final String METADATA_KEY_MAX_TOKEN_REFRESH_COUNT = "max_refresh_count";

private final VaultClientTokenResponse clientToken;

private final Set<String> userGroupSet;
Expand All @@ -61,12 +65,22 @@ public class VaultAuthPrincipal implements Principal {

private final boolean isIamPrincipal;

private final Integer tokenRefreshCount;

public VaultAuthPrincipal(VaultClientTokenResponse clientToken) {
this.clientToken = clientToken;
this.roles = buildRoles(clientToken);
this.userGroupSet = extractUserGroups(clientToken);
this.username = extractUsername(clientToken);
this.isIamPrincipal = extractIsIamPrincipal(clientToken);
this.tokenRefreshCount = extractTokenRefreshCount(clientToken);
}

private Integer extractTokenRefreshCount(VaultClientTokenResponse clientToken) {
final Map<String, String> meta = clientToken.getMeta();
// if a Token that is the root token or created outside of CMS,
// then meta might be null and there will be no value set
return meta == null ? 0 : Integer.parseInt(meta.getOrDefault(METADATA_KEY_TOKEN_REFRESH_COUNT, "0"));
}

private boolean extractIsIamPrincipal(VaultClientTokenResponse clientToken) {
Expand Down Expand Up @@ -130,4 +144,8 @@ public Set<String> getUserGroups() {
public boolean isIamPrincipal() {
return isIamPrincipal;
}

public Integer getTokenRefreshCount() {
return tokenRefreshCount;
}
}
34 changes: 30 additions & 4 deletions src/main/java/com/nike/cerberus/service/AuthenticationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public class AuthenticationService {

public static final String SYSTEM_USER = "system";
public static final String ADMIN_GROUP_PROPERTY = "cms.admin.group";
public static final String MAX_TOKEN_REFRESH_COUNT = "auth.token.maxRefreshCount";
public static final String ADMIN_IAM_ROLES_PROPERTY = "cms.admin.roles";
public static final String USER_TOKEN_TTL_OVERRIDE = "cms.user.token.ttl.override";
public static final String IAM_TOKEN_TTL_OVERRIDE = "cms.iam.token.ttl.override";
Expand Down Expand Up @@ -117,6 +118,8 @@ public class AuthenticationService {
@Named(IAM_TOKEN_TTL_OVERRIDE)
String iamTokenTTL = DEFAULT_TOKEN_TTL;

private final int maxTokenRefreshCount;

@Inject
public AuthenticationService(final SafeDepositBoxDao safeDepositBoxDao,
final AwsIamRoleDao awsIamRoleDao,
Expand All @@ -127,6 +130,7 @@ public AuthenticationService(final SafeDepositBoxDao safeDepositBoxDao,
final VaultPolicyService vaultPolicyService,
final ObjectMapper objectMapper,
@Named(ADMIN_GROUP_PROPERTY) final String adminGroup,
@Named(MAX_TOKEN_REFRESH_COUNT) final int maxTokenRefreshCount,
final DateTimeSupplier dateTimeSupplier,
final AwsIamRoleArnParser awsIamRoleArnParser) {

Expand All @@ -141,6 +145,7 @@ public AuthenticationService(final SafeDepositBoxDao safeDepositBoxDao,
this.adminGroup = adminGroup;
this.dateTimeSupplier = dateTimeSupplier;
this.awsIamRoleArnParser = awsIamRoleArnParser;
this.maxTokenRefreshCount = maxTokenRefreshCount;
}

/**
Expand All @@ -156,7 +161,7 @@ public AuthResponse authenticate(final UserCredentials credentials) {

if (authResponse.getStatus() == AuthStatus.SUCCESS) {
authResponse.getData().setClientToken(generateToken(credentials.getUsername(),
authServiceConnector.getGroups(authResponse.getData())));
authServiceConnector.getGroups(authResponse.getData()), 0));
}

return authResponse;
Expand All @@ -175,7 +180,7 @@ public AuthResponse mfaCheck(final MfaCheckRequest mfaCheckRequest) {

if (authResponse.getStatus() == AuthStatus.SUCCESS) {
authResponse.getData().setClientToken(generateToken(authResponse.getData().getUsername(),
authServiceConnector.getGroups(authResponse.getData())));
authServiceConnector.getGroups(authResponse.getData()), 0));
}

return authResponse;
Expand Down Expand Up @@ -322,14 +327,33 @@ protected byte[] validateAuthPayloadSizeAndTruncateIfLargerThanMaxKmsSupportedSi
* @return The auth response directly from Vault with the token and metadata
*/
public AuthResponse refreshUserToken(final VaultAuthPrincipal authPrincipal) {

if (authPrincipal.isIamPrincipal()) {
throw ApiException.newBuilder()
.withApiErrors(DefaultApiError.IAM_PRINCIPALS_CANNOT_USE_USER_ONLY_RESOURCE)
.withExceptionMessage("The iam principal: %s attempted to use the user token refresh method")
.build();
}

Integer currentTokenRefreshCount = authPrincipal.getTokenRefreshCount();
if (currentTokenRefreshCount >= maxTokenRefreshCount) {
throw ApiException.newBuilder()
.withApiErrors(DefaultApiError.MAXIMUM_TOKEN_REFRESH_COUNT_REACHED)
.withExceptionMessage(String.format("The principal %s attempted to refresh its token but has " +
"reached the maximum number of refreshes allowed", authPrincipal.getName()))
.build();
}

revoke(authPrincipal.getClientToken().getId());

final AuthResponse authResponse = new AuthResponse();
authResponse.setStatus(AuthStatus.SUCCESS);
final AuthData authData = new AuthData();
authResponse.setData(authData);
authData.setUsername(authPrincipal.getName());
authData.setClientToken(generateToken(authPrincipal.getName(), authPrincipal.getUserGroups()));
authData.setClientToken(generateToken(authPrincipal.getName(),
authPrincipal.getUserGroups(),
currentTokenRefreshCount + 1));

return authResponse;
}
Expand Down Expand Up @@ -360,7 +384,7 @@ public void revoke(final String vaultToken) {
* @param userGroups The user's groups
* @return The auth response directly from Vault with the token and metadata
*/
private VaultAuthResponse generateToken(final String username, final Set<String> userGroups) {
private VaultAuthResponse generateToken(final String username, final Set<String> userGroups, int refreshCount) {
final Map<String, String> meta = Maps.newHashMap();
meta.put(VaultAuthPrincipal.METADATA_KEY_USERNAME, username);

Expand All @@ -370,6 +394,8 @@ private VaultAuthResponse generateToken(final String username, final Set<String>
}
meta.put(VaultAuthPrincipal.METADATA_KEY_IS_ADMIN, String.valueOf(isAdmin));
meta.put(VaultAuthPrincipal.METADATA_KEY_GROUPS, StringUtils.join(userGroups, ','));
meta.put(VaultAuthPrincipal.METADATA_KEY_TOKEN_REFRESH_COUNT, String.valueOf(refreshCount));
meta.put(VaultAuthPrincipal.METADATA_KEY_MAX_TOKEN_REFRESH_COUNT, String.valueOf(maxTokenRefreshCount));

final Set<String> policies = buildPolicySet(userGroups);

Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/cms.conf
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ disableCassandra=true
# This will be replaced at build-time with the version number of the final fat jar artifact.
service.version="@@RELEASE@@"

# This sets the maximum number of times a token can be refreshed
auth.token.maxRefreshCount=24

# Flyway
flyway.schemas=cms
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nike.backstopper.exception.ApiException;
import com.nike.cerberus.auth.connector.AuthConnector;
import com.nike.cerberus.aws.KmsClientFactory;
import com.nike.cerberus.dao.AwsIamRoleDao;
import com.nike.cerberus.dao.SafeDepositBoxDao;
import com.nike.cerberus.domain.IamPrincipalCredentials;
import com.nike.cerberus.error.DefaultApiError;
import com.nike.cerberus.record.AwsIamRoleKmsKeyRecord;
import com.nike.cerberus.record.AwsIamRoleRecord;
import com.nike.cerberus.record.SafeDepositBoxRoleRecord;
Expand All @@ -33,6 +35,7 @@
import com.nike.cerberus.util.DateTimeSupplier;
import com.nike.vault.client.VaultAdminClient;
import com.nike.vault.client.model.VaultAuthResponse;
import com.nike.vault.client.model.VaultClientTokenResponse;
import org.apache.commons.lang3.RandomStringUtils;
import org.assertj.core.util.Lists;
import org.assertj.core.util.Sets;
Expand All @@ -57,6 +60,7 @@
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -99,13 +103,15 @@ public class AuthenticationServiceTest {

private AuthenticationService authenticationService;

private static int MAX_LIMIT = 2;

@Before
public void setup() {
initMocks(this);
objectMapper = CmsConfig.configureObjectMapper();
authenticationService = new AuthenticationService(safeDepositBoxDao,
awsIamRoleDao, authConnector, kmsService, kmsClientFactory,
vaultAdminClient, vaultPolicyService, objectMapper, "foo",
vaultAdminClient, vaultPolicyService, objectMapper, "foo", MAX_LIMIT,
dateTimeSupplier, awsIamRoleArnParser);
}

Expand Down Expand Up @@ -292,4 +298,55 @@ public void tests_that_validateAuthPayloadSizeAndTruncateIfLargerThanMaxKmsSuppo
assertNotEquals(serializedAuth, actual);
assertTrue(actual.length < AuthenticationService.KMS_SIZE_LIMIT);
}

@Test
public void tests_that_refreshUserToken_throws_access_denied_when_an_iam_principal_tries_to_call_it() {
VaultAuthPrincipal principal = mock(VaultAuthPrincipal.class);

when(principal.isIamPrincipal()).thenReturn(true);

Exception e = null;
try {
authenticationService.refreshUserToken(principal);
} catch (Exception e2) {
e = e2;
}

assertTrue(e instanceof ApiException);
assertTrue(((ApiException) e).getApiErrors().contains(DefaultApiError.IAM_PRINCIPALS_CANNOT_USE_USER_ONLY_RESOURCE));
}

@Test
public void tests_that_refreshUserToken_refreshes_token_when_count_is_less_than_limit() {
VaultAuthPrincipal principal = mock(VaultAuthPrincipal.class);

when(principal.isIamPrincipal()).thenReturn(false);
when(principal.getTokenRefreshCount()).thenReturn(MAX_LIMIT - 1);

VaultClientTokenResponse response = mock(VaultClientTokenResponse.class);

when(principal.getClientToken()).thenReturn(response);

when(response.getId()).thenReturn("");

authenticationService.refreshUserToken(principal);
}

@Test
public void tests_that_refreshUserToken_throws_access_denied_token_when_count_is_eq_or_greater_than_limit() {
VaultAuthPrincipal principal = mock(VaultAuthPrincipal.class);

when(principal.isIamPrincipal()).thenReturn(false);
when(principal.getTokenRefreshCount()).thenReturn(MAX_LIMIT);

Exception e = null;
try {
authenticationService.refreshUserToken(principal);
} catch (Exception e2) {
e = e2;
}

assertTrue(e instanceof ApiException);
assertTrue(((ApiException) e).getApiErrors().contains(DefaultApiError.MAXIMUM_TOKEN_REFRESH_COUNT_REACHED));
}
}

0 comments on commit 3fe6fd8

Please sign in to comment.