com.nike.cerberus.service.AuthenticationService.java Source code

Java tutorial

Introduction

Here is the source code for com.nike.cerberus.service.AuthenticationService.java

Source

/*
 * Copyright (c) 2016 Nike, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.nike.cerberus.service;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.kms.AWSKMSClient;
import com.amazonaws.services.kms.model.EncryptRequest;
import com.amazonaws.services.kms.model.EncryptResult;
import com.amazonaws.services.kms.model.InvalidArnException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.nike.backstopper.exception.ApiException;
import com.nike.cerberus.auth.connector.AuthConnector;
import com.nike.cerberus.auth.connector.AuthData;
import com.nike.cerberus.auth.connector.AuthResponse;
import com.nike.cerberus.auth.connector.AuthStatus;
import com.nike.cerberus.aws.KmsClientFactory;
import com.nike.cerberus.dao.AwsIamRoleDao;
import com.nike.cerberus.dao.SafeDepositBoxDao;
import com.nike.cerberus.domain.IamRoleAuthResponse;
import com.nike.cerberus.domain.IamRoleCredentials;
import com.nike.cerberus.domain.MfaCheckRequest;
import com.nike.cerberus.domain.UserCredentials;
import com.nike.cerberus.error.DefaultApiError;
import com.nike.cerberus.record.AwsIamRoleKmsKeyRecord;
import com.nike.cerberus.record.AwsIamRoleRecord;
import com.nike.cerberus.record.SafeDepositBoxRoleRecord;
import com.nike.cerberus.security.VaultAuthPrincipal;
import com.nike.cerberus.util.DateTimeSupplier;
import com.nike.vault.client.VaultAdminClient;
import com.nike.vault.client.VaultServerException;
import com.nike.vault.client.model.VaultAuthResponse;
import com.nike.vault.client.model.VaultTokenAuthRequest;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * Authentication service for Users and IAM roles to be able to authenticate and get an assigned Vault token.
 */
@Singleton
public class AuthenticationService {

    public static final String SYSTEM_USER = "system";
    public static final String ADMIN_GROUP_PROPERTY = "cms.admin.group";
    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";
    public static final String LOOKUP_SELF_POLICY = "lookup-self";
    public static final String DEFAULT_TOKEN_TTL = "1h";

    private final SafeDepositBoxDao safeDepositBoxDao;
    private final AwsIamRoleDao awsIamRoleDao;
    private final AuthConnector authServiceConnector;
    private final KmsService kmsService;
    private final KmsClientFactory kmsClientFactory;
    private final VaultAdminClient vaultAdminClient;
    private final VaultPolicyService vaultPolicyService;
    private final ObjectMapper objectMapper;
    private final String adminGroup;
    private final DateTimeSupplier dateTimeSupplier;

    @Inject(optional = true)
    @Named(ADMIN_IAM_ROLES_PROPERTY)
    String adminRoleArns;

    private Set<String> adminRoleArnSet;

    @Inject(optional = true)
    @Named(USER_TOKEN_TTL_OVERRIDE)
    String userTokenTTL = DEFAULT_TOKEN_TTL;

    @Inject(optional = true)
    @Named(IAM_TOKEN_TTL_OVERRIDE)
    String iamTokenTTL = DEFAULT_TOKEN_TTL;

    @Inject
    public AuthenticationService(final SafeDepositBoxDao safeDepositBoxDao, final AwsIamRoleDao awsIamRoleDao,
            final AuthConnector authConnector, final KmsService kmsService, final KmsClientFactory kmsClientFactory,
            final VaultAdminClient vaultAdminClient, final VaultPolicyService vaultPolicyService,
            final ObjectMapper objectMapper, @Named(ADMIN_GROUP_PROPERTY) final String adminGroup,
            final DateTimeSupplier dateTimeSupplier) {

        this.safeDepositBoxDao = safeDepositBoxDao;
        this.awsIamRoleDao = awsIamRoleDao;
        this.authServiceConnector = authConnector;
        this.kmsService = kmsService;
        this.kmsClientFactory = kmsClientFactory;
        this.vaultAdminClient = vaultAdminClient;
        this.vaultPolicyService = vaultPolicyService;
        this.objectMapper = objectMapper;
        this.adminGroup = adminGroup;
        this.dateTimeSupplier = dateTimeSupplier;
    }

    /**
     * Enables a user to authenticate with their credentials and get back a Vault token with any policies they
     * are entitled to.  If a MFA check is required, the details are contained within the auth response.
     *
     * @param credentials User credentials for the authenticating user
     * @return The auth response
     */
    public AuthResponse authenticate(final UserCredentials credentials) {
        final AuthResponse authResponse = authServiceConnector.authenticate(credentials.getUsername(),
                new String(credentials.getPassword(), Charset.defaultCharset()));

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

        return authResponse;
    }

    /**
     * Enables a user to execute an MFA check to complete authentication and get a Vault token.
     *
     * @param mfaCheckRequest Request containing the MFA token details
     * @return The auth response
     */
    public AuthResponse mfaCheck(final MfaCheckRequest mfaCheckRequest) {
        final AuthResponse authResponse = authServiceConnector.mfaCheck(mfaCheckRequest.getStateToken(),
                mfaCheckRequest.getDeviceId(), mfaCheckRequest.getOtpToken());

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

        return authResponse;
    }

    /**
     * Enables an IAM role to authenticate and get back an encrypted payload that the role is only able to decrypt with
     * KMS.
     * @param credentials IAM role credentials
     * @return Encrypted auth response
     */
    public IamRoleAuthResponse authenticate(IamRoleCredentials credentials) {
        final String keyId;
        try {
            keyId = getKeyId(credentials);
        } catch (AmazonServiceException e) {
            if ("InvalidArnException".equals(e.getErrorCode())) {
                throw ApiException.newBuilder().withApiErrors(DefaultApiError.AUTH_IAM_ROLE_REJECTED)
                        .withExceptionCause(e)
                        .withExceptionMessage(String.format(
                                "Failed to lazily provision KMS key for arn:aws:iam::%s:role/%s in region: %s",
                                credentials.getAccountId(), credentials.getRoleName(), credentials.getRegion()))
                        .build();
            }
            throw e;
        }

        final Set<String> policies = buildPolicySet(credentials.getAccountId(), credentials.getRoleName());
        String arn = String.format("arn:aws:iam::%s:role/%s", credentials.getAccountId(),
                credentials.getRoleName());

        final Map<String, String> meta = Maps.newHashMap();
        meta.put(VaultAuthPrincipal.METADATA_KEY_AWS_ACCOUNT_ID, credentials.getAccountId());
        meta.put(VaultAuthPrincipal.METADATA_KEY_AWS_IAM_ROLE_NAME, credentials.getRoleName());
        meta.put(VaultAuthPrincipal.METADATA_KEY_AWS_REGION, credentials.getRegion());
        meta.put(VaultAuthPrincipal.METADATA_KEY_USERNAME, arn);

        // We will allow specific ARNs access to the user portions of the API
        if (getAdminRoleArnSet().contains(arn)) {
            meta.put(VaultAuthPrincipal.METADATA_KEY_IS_ADMIN, Boolean.toString(true));
        }

        final VaultTokenAuthRequest tokenAuthRequest = new VaultTokenAuthRequest().setPolicies(policies)
                .setMeta(meta).setTtl(iamTokenTTL).setNoDefaultPolicy(true);

        final VaultAuthResponse authResponse = vaultAdminClient.createOrphanToken(tokenAuthRequest);

        byte[] authResponseJson;
        try {
            authResponseJson = objectMapper.writeValueAsBytes(authResponse);
        } catch (JsonProcessingException e) {
            throw ApiException.newBuilder().withApiErrors(DefaultApiError.INTERNAL_SERVER_ERROR)
                    .withExceptionCause(e).withExceptionMessage(
                            "Failed to write IAM role authentication response as JSON for encrypting.")
                    .build();
        }
        final byte[] encryptedAuthResponse = encrypt(credentials.getRegion(), keyId, authResponseJson);

        IamRoleAuthResponse iamRoleAuthResponse = new IamRoleAuthResponse();
        iamRoleAuthResponse.setAuthData(Base64.encodeBase64String(encryptedAuthResponse));
        return iamRoleAuthResponse;
    }

    /**
     * Since tokens are immutable, there are certain situations where refreshing the token used by a user is
     * necessary.  Anytime permissions change, this is required to reflect that to the user.
     *
     * @param authPrincipal The principal for the caller
     * @return The auth response directly from Vault with the token and metadata
     */
    public AuthResponse refreshUserToken(final VaultAuthPrincipal authPrincipal) {
        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()));

        return authResponse;
    }

    /**
     * Requests Vault revoke the specified token.  If the token doesn't exist, we simply ignore and move along.
     *
     * @param vaultToken Token to be revoked
     */
    public void revoke(final String vaultToken) {
        try {
            vaultAdminClient.revokeOrphanToken(vaultToken);
        } catch (VaultServerException vse) {
            if (vse.getCode() != HttpStatus.SC_BAD_REQUEST) {
                throw ApiException.newBuilder().withApiErrors(DefaultApiError.INTERNAL_SERVER_ERROR)
                        .withExceptionCause(vse)
                        .withExceptionMessage("Unexpected response from Vault when revoking a token!").build();
            }
        }
    }

    /**
     * Creates a token request and executes it returning the auth response.
     *
     * @param username The user requesting a token
     * @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) {
        final Map<String, String> meta = Maps.newHashMap();
        meta.put(VaultAuthPrincipal.METADATA_KEY_USERNAME, username);

        boolean isAdmin = false;
        if (userGroups.contains(this.adminGroup)) {
            isAdmin = true;
        }
        meta.put(VaultAuthPrincipal.METADATA_KEY_IS_ADMIN, String.valueOf(isAdmin));
        meta.put(VaultAuthPrincipal.METADATA_KEY_GROUPS, StringUtils.join(userGroups, ','));

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

        final VaultTokenAuthRequest tokenAuthRequest = new VaultTokenAuthRequest().setDisplayName(username)
                .setPolicies(policies).setMeta(meta).setTtl(userTokenTTL).setNoDefaultPolicy(true);

        return vaultAdminClient.createOrphanToken(tokenAuthRequest);
    }

    /**
     * Builds the policy set to be associated with the to-be generated Vault token.  The lookup-self policy is
     * included by default.  All other associated policies are based on the groups the user is a member of.
     *
     * @param groups Groups the user is a member of
     * @return Set of policies to be associated
     */
    private Set<String> buildPolicySet(final Set<String> groups) {
        final Set<String> policies = Sets.newHashSet(LOOKUP_SELF_POLICY);
        final List<SafeDepositBoxRoleRecord> sdbRoles = safeDepositBoxDao
                .getUserAssociatedSafeDepositBoxRoles(groups);

        sdbRoles.forEach(i -> {
            policies.add(vaultPolicyService.buildPolicyName(i.getSafeDepositBoxName(), i.getRoleName()));
        });

        return policies;
    }

    /**
     * Builds the policy set to be associated with the to-be generated Vault token.  The lookup-self policy is
     * included by default.  All other associated policies are based on what permissions are granted to the IAM role.
     *
     * @param accountId AWS account ID
     * @param iamRoleName IAM role name
     * @return Set of policies to be associated
     */
    private Set<String> buildPolicySet(final String accountId, final String iamRoleName) {
        final Set<String> policies = Sets.newHashSet(LOOKUP_SELF_POLICY);
        final List<SafeDepositBoxRoleRecord> sdbRoles = safeDepositBoxDao
                .getIamRoleAssociatedSafeDepositBoxRoles(accountId, iamRoleName);

        sdbRoles.forEach(i -> {
            policies.add(vaultPolicyService.buildPolicyName(i.getSafeDepositBoxName(), i.getRoleName()));
        });

        return policies;
    }

    /**
     * Looks up the KMS key id associated with the iam role + region.  If the IAM role exists, but its the first time
     * we've seen the region, we provision a key for usage and return it.
     *
     * @param credentials IAM role credentials
     * @return KMS Key id
     */
    private String getKeyId(IamRoleCredentials credentials) {
        final Optional<AwsIamRoleRecord> iamRole = awsIamRoleDao.getIamRole(credentials.getAccountId(),
                credentials.getRoleName());

        if (!iamRole.isPresent()) {
            throw ApiException.newBuilder().withApiErrors(DefaultApiError.AUTH_IAM_ROLE_INVALID)
                    .withExceptionMessage(
                            String.format("The IAM Role: %s for Acct id: %s was not configured for any SDB",
                                    credentials.getRoleName(), credentials.getAccountId()))
                    .build();
        }

        final Optional<AwsIamRoleKmsKeyRecord> kmsKey = awsIamRoleDao.getKmsKey(iamRole.get().getId(),
                credentials.getRegion());

        final String kmsKeyId;

        if (!kmsKey.isPresent()) {
            kmsKeyId = kmsService.provisionKmsKey(iamRole.get().getId(), credentials.getAccountId(),
                    credentials.getRoleName(), credentials.getRegion(), SYSTEM_USER, dateTimeSupplier.get());
        } else {
            kmsKeyId = kmsKey.get().getAwsKmsKeyId();
        }

        return kmsKeyId;
    }

    /**
     * Encrypts the data provided using KMS based on the provided region and key id.
     *
     * @param regionName Region where key is located
     * @param keyId Key id
     * @param data Data to be encrypted
     * @return encrypted data
     */
    private byte[] encrypt(final String regionName, final String keyId, final byte[] data) {
        Region region;
        try {
            region = Region.getRegion(Regions.fromName(regionName));
        } catch (IllegalArgumentException iae) {
            throw ApiException.newBuilder().withApiErrors(DefaultApiError.AUTH_IAM_ROLE_AWS_REGION_INVALID)
                    .withExceptionCause(iae).build();
        }

        final AWSKMSClient kmsClient = kmsClientFactory.getClient(region);

        try {
            final EncryptResult encryptResult = kmsClient
                    .encrypt(new EncryptRequest().withKeyId(keyId).withPlaintext(ByteBuffer.wrap(data)));

            return encryptResult.getCiphertextBlob().array();
        } catch (AmazonClientException ace) {
            throw ApiException.newBuilder().withApiErrors(DefaultApiError.INTERNAL_SERVER_ERROR)
                    .withExceptionCause(ace)
                    .withExceptionMessage(
                            String.format("Unexpected error communicating with AWS KMS for region %s.", regionName))
                    .build();
        }
    }

    private Set<String> getAdminRoleArnSet() {
        if (adminRoleArnSet == null) {
            adminRoleArnSet = new HashSet<>();
            if (StringUtils.isNotBlank(adminRoleArns)) {
                String[] roles = adminRoleArns.split(",");
                if (roles.length > 0) {
                    Arrays.stream(roles).forEach(role -> {
                        adminRoleArnSet.add(role.trim());
                    });
                }
            }
        }
        return adminRoleArnSet;
    }
}