| /* |
| * 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.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.security.AmitAuthenticationException; |
| import io.mifos.anubis.token.TenantAccessTokenSerializer; |
| import io.mifos.anubis.token.TenantRefreshTokenSerializer; |
| import io.mifos.anubis.token.TokenDeserializationResult; |
| import io.mifos.anubis.token.TokenSerializationResult; |
| import io.mifos.core.command.annotation.Aggregate; |
| import io.mifos.core.command.annotation.CommandHandler; |
| 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.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 java.security.PrivateKey; |
| import java.time.LocalDate; |
| import java.util.*; |
| 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 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, |
| 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.jmsTemplate = jmsTemplate; |
| this.gson = gson; |
| this.logger = logger; |
| this.applicationName = applicationName; |
| } |
| |
| @CommandHandler |
| 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 LocalDate passwordExpiration = getExpiration(user); |
| |
| final TokenSerializationResult accessToken = getAccessToken( |
| user.getIdentifier(), |
| getTokenPermissions(user, passwordExpiration, privateTenantInfo.getTimeToChangePasswordAfterExpirationInDays()), |
| privateSignature); |
| |
| final TokenSerializationResult refreshToken = getRefreshToken(user, privateSignature); |
| |
| fireAuthenticationEvent(user.getIdentifier()); |
| |
| return new AuthenticationCommandResponse( |
| accessToken.getToken(), DateConverter.toIsoString(accessToken.getExpiration()), |
| refreshToken.getToken(), DateConverter.toIsoString(refreshToken.getExpiration()), |
| DateConverter.toIsoString(passwordExpiration)); |
| } |
| |
| 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(); |
| } |
| |
| @CommandHandler |
| public AuthenticationCommandResponse process(final RefreshTokenAuthenticationCommand command) |
| throws AmitAuthenticationException |
| { |
| final TokenDeserializationResult deserializedRefreshToken = |
| tenantRefreshTokenSerializer.deserialize(command.getRefreshToken()); |
| |
| final PrivateTenantInfoEntity privateTenantInfo = checkedGetPrivateTenantInfo(); |
| final PrivateSignatureEntity privateSignature = checkedGetPrivateSignature(); |
| |
| final UserEntity user = getUser(deserializedRefreshToken.getUserIdentifier()); |
| |
| final LocalDate passwordExpiration = getExpiration(user); |
| |
| final TokenSerializationResult accessToken = getAccessToken( |
| user.getIdentifier(), |
| getTokenPermissions(user, passwordExpiration, privateTenantInfo.getTimeToChangePasswordAfterExpirationInDays()), |
| privateSignature); |
| |
| return new AuthenticationCommandResponse( |
| accessToken.getToken(), DateConverter.toIsoString(accessToken.getExpiration()), |
| command.getRefreshToken(), DateConverter.toIsoString(deserializedRefreshToken.getExpiration()), |
| DateConverter.toIsoString(passwordExpiration)); |
| } |
| |
| private LocalDate getExpiration(final UserEntity user) |
| { |
| return LocalDate.ofEpochDay(user.getPasswordExpiresOn().getDaysSinceEpoch()); |
| } |
| |
| 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 getAccessToken( |
| final String identifier, |
| final Set<TokenPermission> tokenPermissions, |
| final PrivateSignatureEntity privateSignatureEntity) { |
| |
| 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(identifier); |
| |
| return tenantAccessTokenSerializer.build(x); |
| } |
| |
| private Set<TokenPermission> getTokenPermissions( |
| final UserEntity user, |
| final LocalDate passwordExpiration, |
| final long gracePeriod) throws AmitAuthenticationException { |
| final Optional<RoleEntity> userRole = roles.get(user.getRole()); |
| final Set<TokenPermission> tokenPermissions; |
| |
| if (pastGracePeriod(passwordExpiration, gracePeriod)) |
| throw AmitAuthenticationException.passwordExpired(); |
| |
| if (pastExpiration(passwordExpiration)) { |
| tokenPermissions = new HashSet<>(); |
| } |
| else { |
| tokenPermissions = userRole.map(r -> r.getPermissions().stream().flatMap(this::mapPermissions).collect(Collectors.toSet())) |
| .orElse(new HashSet<>()); |
| } |
| |
| tokenPermissions.add( |
| new TokenPermission( |
| applicationName + "/applications/*/permissions/*/users/{useridentifier}/enabled", |
| AllowedOperation.ALL)); |
| tokenPermissions.add( |
| new TokenPermission( |
| applicationName + "/users/{useridentifier}/password", |
| Collections.singleton(AllowedOperation.CHANGE))); |
| tokenPermissions.add( |
| new TokenPermission( |
| applicationName + "/users/{useridentifier}/permissions", |
| Collections.singleton(AllowedOperation.READ))); |
| tokenPermissions.add( |
| new TokenPermission( |
| applicationName + "/token/_current", |
| Collections.singleton(AllowedOperation.DELETE))); |
| |
| return tokenPermissions; |
| } |
| |
| static boolean pastExpiration(final LocalDate passwordExpiration) { |
| return LocalDate.now().compareTo(passwordExpiration) >= 0; |
| } |
| |
| static boolean pastGracePeriod(final LocalDate passwordExpiration, final long gracePeriod) { |
| return LocalDate.now().compareTo(passwordExpiration.plusDays(gracePeriod)) >= 0; |
| } |
| |
| 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) { |
| return new TokenPermission( |
| permittable.getPath(), |
| Collections.singleton(RoleMapper.mapAllowedOperation(AllowedOperationType.fromHttpMethod(permittable.getMethod())))); |
| } |
| |
| 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); |
| } |
| } |