blob: af818190f795c8297aa417d1ec9ed23e9b39d73e [file] [log] [blame]
/*
* Copyright 2017 The Mifos Initiative.
*
* 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 io.mifos.identity.internal.command.handler;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import io.mifos.anubis.api.v1.domain.AllowedOperation;
import io.mifos.anubis.api.v1.domain.TokenContent;
import io.mifos.anubis.api.v1.domain.TokenPermission;
import io.mifos.anubis.provider.InvalidKeyTimestampException;
import io.mifos.anubis.provider.TenantRsaKeyProvider;
import io.mifos.anubis.security.AmitAuthenticationException;
import io.mifos.anubis.token.*;
import io.mifos.core.command.annotation.Aggregate;
import io.mifos.core.command.annotation.CommandHandler;
import io.mifos.core.command.annotation.CommandLogLevel;
import io.mifos.core.lang.ApplicationName;
import io.mifos.core.lang.DateConverter;
import io.mifos.core.lang.ServiceException;
import io.mifos.core.lang.TenantContextHolder;
import io.mifos.core.lang.config.TenantHeaderFilter;
import io.mifos.core.lang.security.RsaPrivateKeyBuilder;
import io.mifos.core.lang.security.RsaPublicKeyBuilder;
import io.mifos.identity.api.v1.events.EventConstants;
import io.mifos.identity.internal.command.AuthenticationCommandResponse;
import io.mifos.identity.internal.command.PasswordAuthenticationCommand;
import io.mifos.identity.internal.command.RefreshTokenAuthenticationCommand;
import io.mifos.identity.internal.repository.*;
import io.mifos.identity.internal.service.RoleMapper;
import io.mifos.identity.internal.util.IdentityConstants;
import io.mifos.tool.crypto.HashGenerator;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import javax.annotation.Nullable;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Myrle Krantz
*/
@SuppressWarnings({"unused", "WeakerAccess"})
@Aggregate
@Component
public class AuthenticationCommandHandler {
private final Users users;
private final Roles roles;
private final PermittableGroups permittableGroups;
private final Signatures signatures;
private final Tenants tenants;
private final HashGenerator hashGenerator;
private final TenantAccessTokenSerializer tenantAccessTokenSerializer;
private final TenantRefreshTokenSerializer tenantRefreshTokenSerializer;
private final TenantRsaKeyProvider tenantRsaKeyProvider;
private final ApplicationSignatures applicationSignatures;
private final ApplicationPermissions applicationPermissions;
private final ApplicationPermissionUsers applicationPermissionUsers;
private final ApplicationCallEndpointSets applicationCallEndpointSets;
private final JmsTemplate jmsTemplate;
private final Gson gson;
private final Logger logger;
private final ApplicationName applicationName;
@Value("${identity.token.access.ttl:1200}") //Given in seconds. Default 20 minutes.
private int accessTtl;
@Value("${identity.token.refresh.ttl:54000}") //Given in seconds. Default 15 hours.
private int refreshTtl;
@Autowired
public AuthenticationCommandHandler(final Users users,
final Roles roles,
final PermittableGroups permittableGroups,
final Signatures signatures,
final Tenants tenants,
final HashGenerator hashGenerator,
@SuppressWarnings("SpringJavaAutowiringInspection")
final TenantAccessTokenSerializer tenantAccessTokenSerializer,
@SuppressWarnings("SpringJavaAutowiringInspection")
final TenantRefreshTokenSerializer tenantRefreshTokenSerializer,
@SuppressWarnings("SpringJavaAutowiringInspection")
final TenantRsaKeyProvider tenantRsaKeyProvider,
final ApplicationSignatures applicationSignatures,
final ApplicationPermissions applicationPermissions,
final ApplicationPermissionUsers applicationPermissionUsers,
final ApplicationCallEndpointSets applicationCallEndpointSets,
final JmsTemplate jmsTemplate,
final ApplicationName applicationName,
@Qualifier(IdentityConstants.JSON_SERIALIZER_NAME) final Gson gson,
@Qualifier(IdentityConstants.LOGGER_NAME) final Logger logger) {
this.users = users;
this.roles = roles;
this.permittableGroups = permittableGroups;
this.signatures = signatures;
this.tenants = tenants;
this.hashGenerator = hashGenerator;
this.tenantAccessTokenSerializer = tenantAccessTokenSerializer;
this.tenantRefreshTokenSerializer = tenantRefreshTokenSerializer;
this.tenantRsaKeyProvider = tenantRsaKeyProvider;
this.applicationSignatures = applicationSignatures;
this.applicationPermissions = applicationPermissions;
this.applicationPermissionUsers = applicationPermissionUsers;
this.applicationCallEndpointSets = applicationCallEndpointSets;
this.jmsTemplate = jmsTemplate;
this.gson = gson;
this.logger = logger;
this.applicationName = applicationName;
}
@CommandHandler(logStart = CommandLogLevel.DEBUG, logFinish = CommandLogLevel.DEBUG)
public AuthenticationCommandResponse process(final PasswordAuthenticationCommand command)
throws AmitAuthenticationException
{
final byte[] base64decodedPassword;
try {
base64decodedPassword = Base64Utils.decodeFromString(command.getPassword());
}
catch (final IllegalArgumentException e)
{
throw ServiceException.badRequest("Password was not base64 encoded.");
}
final PrivateTenantInfoEntity privateTenantInfo = checkedGetPrivateTenantInfo();
final PrivateSignatureEntity privateSignature = checkedGetPrivateSignature();
byte[] fixedSalt = privateTenantInfo.getFixedSalt().array();
final UserEntity user = getUser(command.getUseridentifier());
if (!this.hashGenerator.isEqual(
user.getPassword().array(),
base64decodedPassword,
fixedSalt,
user.getSalt().array(),
user.getIterationCount(),
256))
{
throw AmitAuthenticationException.userPasswordCombinationNotFound();
}
final TokenSerializationResult refreshToken = getRefreshToken(user, privateSignature);
final AuthenticationCommandResponse ret = getAuthenticationResponse(
applicationName.toString(),
Optional.empty(),
privateTenantInfo,
privateSignature,
user,
refreshToken.getToken(),
refreshToken.getExpiration());
fireAuthenticationEvent(user.getIdentifier());
return ret;
}
private PrivateSignatureEntity checkedGetPrivateSignature() {
final Optional<PrivateSignatureEntity> privateSignature = signatures.getPrivateSignature();
if (!privateSignature.isPresent()) {
logger.error("Authentication attempted on tenant with no valid signature{}.", TenantContextHolder.identifier());
throw ServiceException.internalError("Tenant has no valid signature.");
}
return privateSignature.get();
}
private PrivateTenantInfoEntity checkedGetPrivateTenantInfo() {
final Optional<PrivateTenantInfoEntity> privateTenantInfo = tenants.getPrivateTenantInfo();
if (!privateTenantInfo.isPresent()) {
logger.error("Authentication attempted on uninitialized tenant {}.", TenantContextHolder.identifier());
throw ServiceException.internalError("Tenant is not initialized.");
}
return privateTenantInfo.get();
}
private class TenantIdentityRsaKeyProvider implements TenantApplicationRsaKeyProvider {
@Override
public PublicKey getApplicationPublicKey(final String tokenApplicationName, final String timestamp) throws InvalidKeyTimestampException {
if (applicationName.toString().equals(tokenApplicationName))
return tenantRsaKeyProvider.getPublicKey(timestamp);
final ApplicationSignatureEntity signature = applicationSignatures.get(tokenApplicationName, timestamp)
.orElseThrow(() -> new InvalidKeyTimestampException(timestamp));
return new RsaPublicKeyBuilder()
.setPublicKeyMod(signature.getPublicKeyMod())
.setPublicKeyExp(signature.getPublicKeyExp())
.build();
}
}
@CommandHandler(logStart = CommandLogLevel.DEBUG, logFinish = CommandLogLevel.DEBUG)
public AuthenticationCommandResponse process(final RefreshTokenAuthenticationCommand command)
throws AmitAuthenticationException
{
final TokenDeserializationResult deserializedRefreshToken =
tenantRefreshTokenSerializer.deserialize(new TenantIdentityRsaKeyProvider(), command.getRefreshToken());
final PrivateTenantInfoEntity privateTenantInfo = checkedGetPrivateTenantInfo();
final PrivateSignatureEntity privateSignature = checkedGetPrivateSignature();
final UserEntity user = getUser(deserializedRefreshToken.getUserIdentifier());
final String sourceApplicationName = deserializedRefreshToken.getSourceApplication();
return getAuthenticationResponse(
sourceApplicationName,
Optional.ofNullable(deserializedRefreshToken.getEndpointSet()),
privateTenantInfo,
privateSignature,
user,
command.getRefreshToken(),
LocalDateTime.ofInstant(deserializedRefreshToken.getExpiration().toInstant(), ZoneId.of("UTC")));
}
private AuthenticationCommandResponse getAuthenticationResponse(
final String sourceApplicationName,
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
final Optional<String> callEndpointSet,
final PrivateTenantInfoEntity privateTenantInfo,
final PrivateSignatureEntity privateSignature,
final UserEntity user,
final String refreshToken,
final LocalDateTime refreshTokenExpiration) {
final Optional<LocalDateTime> passwordExpiration = getExpiration(user);
final int gracePeriod = privateTenantInfo.getTimeToChangePasswordAfterExpirationInDays();
if (pastGracePeriod(passwordExpiration, gracePeriod))
throw AmitAuthenticationException.passwordExpired();
final Set<TokenPermission> tokenPermissions;
if (sourceApplicationName.equals(applicationName.toString())) { //ie, this is a token for the identity manager.
if (pastExpiration(passwordExpiration)) {
tokenPermissions = identityEndpointsAllowedEvenWithExpiredPassword();
logger.info("Password expired {}", passwordExpiration.map(LocalDateTime::toString).orElse("empty"));
}
else {
tokenPermissions = getUserTokenPermissions(user);
}
}
else {
tokenPermissions = getApplicationTokenPermissions(user, sourceApplicationName, callEndpointSet);
}
final HashSet<TokenPermission> minifiedTokenPermissions = new HashSet<>(
tokenPermissions
.stream()
.collect(Collectors.toMap(TokenPermission::getPath,
tokenPermission -> tokenPermission,
(currentTokenPermission, newTokenPermission) -> {
newTokenPermission.getAllowedOperations()
.forEach(allowedOperation -> currentTokenPermission.getAllowedOperations().add(allowedOperation));
return currentTokenPermission;
})
)
.values()
);
logger.info("Access token for tenant '{}', user '{}', application '{}', and callEndpointSet '{}' being returned containing the permissions '{}'.",
TenantContextHolder.identifier().orElse("null"),
user.getIdentifier(),
sourceApplicationName,
callEndpointSet.orElse("null"),
minifiedTokenPermissions.toString());
final TokenSerializationResult accessToken = getAuthenticationResponse(
user.getIdentifier(),
minifiedTokenPermissions,
privateSignature,
sourceApplicationName);
return new AuthenticationCommandResponse(
accessToken.getToken(), DateConverter.toIsoString(accessToken.getExpiration()),
refreshToken, DateConverter.toIsoString(refreshTokenExpiration),
passwordExpiration.map(DateConverter::toIsoString).orElse(null));
}
private Optional<LocalDateTime> getExpiration(final UserEntity user)
{
if (user.getIdentifier().equals(IdentityConstants.SU_NAME))
return Optional.empty();
else
return Optional.of(LocalDateTime.of(
LocalDate.ofEpochDay(user.getPasswordExpiresOn().getDaysSinceEpoch()), //Convert from cassandra LocalDate to java LocalDate.
LocalTime.MIDNIGHT));
}
private UserEntity getUser(final String identifier) throws AmitAuthenticationException {
final Optional<UserEntity> user = users.get(identifier);
if (!user.isPresent()) {
this.logger.info("Attempt to get a user who doesn't exist: " + identifier);
throw AmitAuthenticationException.userPasswordCombinationNotFound();
}
return user.get();
}
private void fireAuthenticationEvent(final String userIdentifier) {
this.jmsTemplate.convertAndSend(
this.gson.toJson(userIdentifier),
message -> {
if (TenantContextHolder.identifier().isPresent()) {
//noinspection OptionalGetWithoutIsPresent
message.setStringProperty(
TenantHeaderFilter.TENANT_HEADER,
TenantContextHolder.identifier().get());
}
message.setStringProperty(EventConstants.OPERATION_HEADER,
EventConstants.OPERATION_AUTHENTICATE
);
return message;
}
);
}
private TokenSerializationResult getAuthenticationResponse(
final String userIdentifier,
final Set<TokenPermission> tokenPermissions,
final PrivateSignatureEntity privateSignatureEntity,
final String sourceApplication) {
final PrivateKey privateKey = new RsaPrivateKeyBuilder()
.setPrivateKeyExp(privateSignatureEntity.getPrivateKeyExp())
.setPrivateKeyMod(privateSignatureEntity.getPrivateKeyMod())
.build();
final TenantAccessTokenSerializer.Specification x =
new TenantAccessTokenSerializer.Specification()
.setKeyTimestamp(privateSignatureEntity.getKeyTimestamp())
.setPrivateKey(privateKey)
.setTokenContent(new TokenContent(new ArrayList<>(tokenPermissions)))
.setSecondsToLive(accessTtl)
.setUser(userIdentifier)
.setSourceApplication(sourceApplication);
return tenantAccessTokenSerializer.build(x);
}
private Set<TokenPermission> getUserTokenPermissions(
final UserEntity user) {
final Optional<RoleEntity> userRole = roles.get(user.getRole());
final Set<TokenPermission> tokenPermissions = userRole
.map(r -> r.getPermissions().stream().flatMap(this::mapPermissions).collect(Collectors.toSet()))
.orElse(new HashSet<>());
tokenPermissions.addAll(identityEndpointsForEveryUser());
return tokenPermissions;
}
private Set<TokenPermission> getApplicationTokenPermissions(
final UserEntity user,
final String sourceApplicationName,
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<String> callEndpointSet) {
//If the call endpoint set was given, but does not correspond to a stored call endpoint set, throw an exception.
//If it wasn't given then return all of the permissions for the application.
final Optional<ApplicationCallEndpointSetEntity> applicationCallEndpointSet = callEndpointSet.map(x -> {
final Optional<ApplicationCallEndpointSetEntity> optionalEndpointSetEntity =
applicationCallEndpointSets.get(sourceApplicationName, x);
if (optionalEndpointSetEntity.isPresent()) {
return optionalEndpointSetEntity.get();
} else {
throw AmitAuthenticationException.invalidToken();
}
});
final RoleEntity userRole = roles.get(user.getRole())
.orElseThrow(AmitAuthenticationException::userPasswordCombinationNotFound);
return applicationCallEndpointSet.map(x -> this.getApplicationCallEndpointSetTokenPermissions(user.getIdentifier(), userRole, x, sourceApplicationName))
.orElseGet(() -> this.getApplicationUserTokenPermissions(user.getIdentifier(), userRole, sourceApplicationName));
}
private Set<TokenPermission> getApplicationCallEndpointSetTokenPermissions(
final String userIdentifier,
final RoleEntity userRole,
final ApplicationCallEndpointSetEntity applicationCallEndpointSet,
final String sourceApplicationName) {
final List<PermissionType> permissionsForUser = userRole.getPermissions();
final Set<PermissionType> permissionsRequestedByApplication = applicationCallEndpointSet.getCallEndpointGroupIdentifiers().stream()
.map(x -> applicationPermissions.getPermissionForApplication(sourceApplicationName, x))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toSet());
final Stream<PermissionType> applicationRequestedPermissionsTheUserHas
= intersectPermissionList(permissionsForUser, permissionsRequestedByApplication.stream());
final Set<PermissionType> permissionsPossible = applicationRequestedPermissionsTheUserHas
.filter(x ->
applicationPermissionUsers.enabled(sourceApplicationName, x.getPermittableGroupIdentifier(), userIdentifier))
.collect(Collectors.toSet());
if (!permissionsPossible.containsAll(permissionsRequestedByApplication))
throw AmitAuthenticationException.applicationMissingPermissions(userIdentifier, sourceApplicationName);
return permissionsPossible.stream()
.flatMap(this::mapPermissions)
.collect(Collectors.toSet());
}
private Set<TokenPermission> getApplicationUserTokenPermissions(
final String userIdentifier,
final RoleEntity userRole,
final String sourceApplicationName) {
final List<PermissionType> permissionsForUser = userRole.getPermissions();
final List<PermissionType> permissionsRequestedByApplication = applicationPermissions.getAllPermissionsForApplication(sourceApplicationName);
final Stream<PermissionType> applicationRequestedPermissionsTheUserHas
= intersectPermissionList(permissionsForUser, permissionsRequestedByApplication.stream());
return applicationRequestedPermissionsTheUserHas
.filter(x ->
applicationPermissionUsers.enabled(sourceApplicationName, x.getPermittableGroupIdentifier(), userIdentifier))
.flatMap(this::mapPermissions)
.collect(Collectors.toSet());
}
private Stream<PermissionType> intersectPermissionList(
final List<PermissionType> permissionsForUser,
final Stream<PermissionType> permissionsRequestedByApplication) {
final Map<String, Set<AllowedOperationType>> keyedUserPermissions = transformToSearchablePermissions(permissionsForUser);
return permissionsRequestedByApplication
.map(x -> new PermissionType(
x.getPermittableGroupIdentifier(),
intersectSets(keyedUserPermissions.get(x.getPermittableGroupIdentifier()), x.getAllowedOperations())))
.filter(x -> !x.getAllowedOperations().isEmpty());
}
static <T> Set<T> intersectSets(
final @Nullable Set<T> allowedOperations1,
final @Nullable Set<T> allowedOperations2) {
if (allowedOperations1 == null || allowedOperations2 == null)
return Collections.emptySet();
final Set<T> ret = new HashSet<>(allowedOperations1);
ret.retainAll(allowedOperations2);
return ret;
}
static Map<String, Set<AllowedOperationType>> transformToSearchablePermissions(final List<PermissionType> permissionsForUser) {
final Collector<Set<AllowedOperationType>, Set<AllowedOperationType>, Set<AllowedOperationType>> setToSetCollector
= Collector.of(
HashSet::new,
Set::addAll,
(x, y) -> {
final Set<AllowedOperationType> ret = new HashSet<>();
ret.addAll(x);
ret.addAll(y);
return ret;
});
return permissionsForUser.stream().collect(
Collectors.groupingBy(PermissionType::getPermittableGroupIdentifier,
Collectors.mapping(PermissionType::getAllowedOperations, setToSetCollector)));
}
private Set<TokenPermission> identityEndpointsForEveryUser() {
final Set<TokenPermission> ret = identityEndpointsAllowedEvenWithExpiredPassword();
ret.add(new TokenPermission(
applicationName + "/applications/*/permissions/*/users/{useridentifier}/enabled",
Sets.newHashSet(AllowedOperation.READ, AllowedOperation.CHANGE, AllowedOperation.DELETE)));
ret.add(new TokenPermission(
applicationName + "/users/{useridentifier}/permissions",
Sets.newHashSet(AllowedOperation.READ)));
return ret;
}
private Set<TokenPermission> identityEndpointsAllowedEvenWithExpiredPassword() {
final Set<TokenPermission> ret = new HashSet<>();
ret.add(new TokenPermission(
applicationName + "/users/{useridentifier}/password",
Sets.newHashSet(AllowedOperation.READ, AllowedOperation.CHANGE, AllowedOperation.DELETE)));
ret.add(new TokenPermission(
applicationName + "/token/_current",
Sets.newHashSet(AllowedOperation.DELETE)));
return ret;
}
static boolean pastExpiration(
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<LocalDateTime> passwordExpiration) {
return passwordExpiration.map(x -> LocalDateTime.now().compareTo(x) >= 0).orElse(false);
}
static boolean pastGracePeriod(
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<LocalDateTime> passwordExpiration,
final long gracePeriod) {
return passwordExpiration.map(x -> (LocalDateTime.now().compareTo(x.plusDays(gracePeriod)) >= 0)).orElse(false);
}
private Stream<TokenPermission> mapPermissions(final PermissionType permission) {
return permittableGroups.get(permission.getPermittableGroupIdentifier())
.map(PermittableGroupEntity::getPermittables)
.map(Collection::stream)
.orElse(Stream.empty())
.filter(permittable -> isAllowed(permittable, permission))
.map(this::getTokenPermission);
}
private boolean isAllowed(final PermittableType permittable, final PermissionType permission) {
return permission.getAllowedOperations().contains(AllowedOperationType.fromHttpMethod(permittable.getMethod()));
}
private TokenPermission getTokenPermission(final PermittableType permittable) {
final HashSet<AllowedOperation> allowedOperations = new HashSet<>();
allowedOperations.add(RoleMapper.mapAllowedOperation(AllowedOperationType.fromHttpMethod(permittable.getMethod())));
return new TokenPermission(permittable.getPath(), allowedOperations);
}
private TokenSerializationResult getRefreshToken(final UserEntity user,
final PrivateSignatureEntity privateSignatureEntity) {
final PrivateKey privateKey = new RsaPrivateKeyBuilder()
.setPrivateKeyExp(privateSignatureEntity.getPrivateKeyExp())
.setPrivateKeyMod(privateSignatureEntity.getPrivateKeyMod())
.build();
final TenantRefreshTokenSerializer.Specification x =
new TenantRefreshTokenSerializer.Specification()
.setKeyTimestamp(privateSignatureEntity.getKeyTimestamp())
.setPrivateKey(privateKey)
.setSecondsToLive(refreshTtl)
.setUser(user.getIdentifier())
.setSourceApplication(applicationName.toString());
return tenantRefreshTokenSerializer.build(x);
}
}