Skip to content

Commit

Permalink
Add initial dev flow for an admin to update key
Browse files Browse the repository at this point in the history
  • Loading branch information
Sae126V committed Jan 16, 2024
1 parent 85e6d19 commit 2db3f7a
Show file tree
Hide file tree
Showing 30 changed files with 1,020 additions and 69 deletions.
18 changes: 18 additions & 0 deletions iam-login-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,24 @@
<version>1.7.1</version>
</dependency>

<!-- Spring Batch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import it.infn.mw.iam.audit.events.account.multi_factor_authentication.AuthenticatorAppEnabledEvent;
import it.infn.mw.iam.audit.events.account.multi_factor_authentication.RecoveryCodeVerifiedEvent;
import it.infn.mw.iam.audit.events.account.multi_factor_authentication.TotpVerifiedEvent;
import it.infn.mw.iam.config.mfa.IamTotpMfaProperties;
import it.infn.mw.iam.core.user.IamAccountService;
import it.infn.mw.iam.core.user.exception.MfaSecretAlreadyBoundException;
import it.infn.mw.iam.core.user.exception.MfaSecretNotFoundException;
Expand All @@ -53,21 +52,22 @@ public class DefaultIamTotpMfaService implements IamTotpMfaService, ApplicationE
private final SecretGenerator secretGenerator;
private final RecoveryCodeGenerator recoveryCodeGenerator;
private final CodeVerifier codeVerifier;
private final IamTotpMfaProperties iamTotpMfaProperties;
private ApplicationEventPublisher eventPublisher;
private final IamTotpMfaEncryptionAndDecryptionService iamTotpMfaEncryptionAndDecryptionService;

@Autowired
public DefaultIamTotpMfaService(IamAccountService iamAccountService,
IamTotpMfaRepository totpMfaRepository, SecretGenerator secretGenerator,
RecoveryCodeGenerator recoveryCodeGenerator, CodeVerifier codeVerifier,
ApplicationEventPublisher eventPublisher, IamTotpMfaProperties iamTotpMfaProperties) {
ApplicationEventPublisher eventPublisher,
IamTotpMfaEncryptionAndDecryptionService iamTotpMfaEncryptionAndDecryptionService) {
this.iamAccountService = iamAccountService;
this.totpMfaRepository = totpMfaRepository;
this.secretGenerator = secretGenerator;
this.recoveryCodeGenerator = recoveryCodeGenerator;
this.codeVerifier = codeVerifier;
this.eventPublisher = eventPublisher;
this.iamTotpMfaProperties = iamTotpMfaProperties;
this.iamTotpMfaEncryptionAndDecryptionService = iamTotpMfaEncryptionAndDecryptionService;
}

private void authenticatorAppEnabledEvent(IamAccount account, IamTotpMfa totpMfa) {
Expand Down Expand Up @@ -115,10 +115,10 @@ public IamTotpMfa addTotpMfaSecret(IamAccount account) throws IamTotpMfaInvalidA
IamTotpMfa totpMfa = new IamTotpMfa(account);

totpMfa.setSecret(IamTotpMfaEncryptionAndDecryptionUtil.encryptSecretOrRecoveryCode(
secretGenerator.generate(), iamTotpMfaProperties.getPasswordToEncryptOrDecrypt()));
secretGenerator.generate(), iamTotpMfaEncryptionAndDecryptionService.getCurrentPasswordFromService()));
totpMfa.setAccount(account);

Set<IamTotpRecoveryCode> recoveryCodes = generateRecoveryCodes(totpMfa);
Set<IamTotpRecoveryCode> recoveryCodes = generateRecoveryCodes(totpMfa, iamTotpMfaEncryptionAndDecryptionService.getCurrentPasswordFromService());
totpMfa.setRecoveryCodes(recoveryCodes);
totpMfaRepository.save(totpMfa);

Expand All @@ -140,7 +140,8 @@ public IamTotpMfa addTotpMfaRecoveryCodes(IamAccount account) {

IamTotpMfa totpMfa = totpMfaOptional.get();

Set<IamTotpRecoveryCode> recoveryCodes = generateRecoveryCodes(totpMfa);
Set<IamTotpRecoveryCode> recoveryCodes = generateRecoveryCodes(totpMfa,
iamTotpMfaEncryptionAndDecryptionService.getCurrentPasswordFromService());

// Attach to account
totpMfa.setRecoveryCodes(recoveryCodes);
Expand Down Expand Up @@ -214,7 +215,8 @@ public boolean verifyTotp(IamAccount account, String totp) throws IamTotpMfaInva

IamTotpMfa totpMfa = totpMfaOptional.get();
String mfaSecret = IamTotpMfaEncryptionAndDecryptionUtil.decryptSecretOrRecoveryCode(
totpMfa.getSecret(), iamTotpMfaProperties.getPasswordToEncryptOrDecrypt());
totpMfa.getSecret(), iamTotpMfaEncryptionAndDecryptionService
.whichPasswordToUseForEncryptAndDecrypt(totpMfa.getId(), totpMfa.isKeyUpdateRequest()));

// Verify provided TOTP
if (codeVerifier.isValidCode(mfaSecret, totp)) {
Expand All @@ -233,7 +235,7 @@ public boolean verifyTotp(IamAccount account, String totp) throws IamTotpMfaInva
* @return true if valid, false otherwise
*/
@Override
public boolean verifyRecoveryCode(IamAccount account, String recoveryCode) {
public boolean verifyRecoveryCode(IamAccount account, String recoveryCode) throws IamTotpMfaInvalidArgumentError {
Optional<IamTotpMfa> totpMfaOptional = totpMfaRepository.findByAccount(account);
if (!totpMfaOptional.isPresent()) {
throw new MfaSecretNotFoundException("No multi-factor secret is attached to this account");
Expand All @@ -250,7 +252,8 @@ public boolean verifyRecoveryCode(IamAccount account, String recoveryCode) {
for (IamTotpRecoveryCode recoveryCodeObject : accountRecoveryCodes) {
String recoveryCodeEncrypted = recoveryCodeObject.getCode();
String recoveryCodeString = IamTotpMfaEncryptionAndDecryptionUtil.decryptSecretOrRecoveryCode(
recoveryCodeEncrypted, iamTotpMfaProperties.getPasswordToEncryptOrDecrypt());
recoveryCodeEncrypted, iamTotpMfaEncryptionAndDecryptionService
.whichPasswordToUseForEncryptAndDecrypt(totpMfa.getId(), totpMfa.isKeyUpdateRequest()));

if (recoveryCode.equals(recoveryCodeString)) {
recoveryCodeVerifiedEvent(account, totpMfa);
Expand All @@ -262,15 +265,16 @@ public boolean verifyRecoveryCode(IamAccount account, String recoveryCode) {
}


private Set<IamTotpRecoveryCode> generateRecoveryCodes(IamTotpMfa totpMfa) throws IamTotpMfaInvalidArgumentError {
private Set<IamTotpRecoveryCode> generateRecoveryCodes(IamTotpMfa totpMfa, String password)
throws IamTotpMfaInvalidArgumentError {
String[] recoveryCodeStrings = recoveryCodeGenerator.generateCodes(RECOVERY_CODE_QUANTITY);
Set<IamTotpRecoveryCode> recoveryCodes = new HashSet<>();

for (String code : recoveryCodeStrings) {
IamTotpRecoveryCode recoveryCode = new IamTotpRecoveryCode(totpMfa);

recoveryCode.setCode(IamTotpMfaEncryptionAndDecryptionUtil.encryptSecretOrRecoveryCode(
code, iamTotpMfaProperties.getPasswordToEncryptOrDecrypt()));
code, password));
recoveryCodes.add(recoveryCode);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@

import dev.samstevens.totp.recovery.RecoveryCodeGenerator;
import it.infn.mw.iam.audit.events.account.multi_factor_authentication.RecoveryCodesResetEvent;
import it.infn.mw.iam.config.mfa.IamTotpMfaProperties;
import it.infn.mw.iam.core.user.exception.MfaSecretNotFoundException;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamTotpMfa;
Expand All @@ -45,17 +44,17 @@ public class DefaultIamTotpRecoveryCodeResetService
private final IamAccountRepository accountRepository;
private final IamTotpMfaRepository totpMfaRepository;
private final RecoveryCodeGenerator recoveryCodeGenerator;
private final IamTotpMfaProperties iamTotpMfaProperties;
private final IamTotpMfaEncryptionAndDecryptionService iamTotpMfaEncryptionAndDecryptionService;
private ApplicationEventPublisher eventPublisher;

@Autowired
public DefaultIamTotpRecoveryCodeResetService(IamAccountRepository accountRepository,
IamTotpMfaRepository totpMfaRepository, RecoveryCodeGenerator recoveryCodeGenerator,
IamTotpMfaProperties iamTotpMfaProperties) {
IamTotpMfaEncryptionAndDecryptionService iamTotpMfaEncryptionAndDecryptionService) {
this.accountRepository = accountRepository;
this.totpMfaRepository = totpMfaRepository;
this.recoveryCodeGenerator = recoveryCodeGenerator;
this.iamTotpMfaProperties = iamTotpMfaProperties;
this.iamTotpMfaEncryptionAndDecryptionService = iamTotpMfaEncryptionAndDecryptionService;
}

private void recoveryCodesResetEvent(IamAccount account, IamTotpMfa totpMfa) {
Expand All @@ -80,14 +79,24 @@ public IamAccount resetRecoveryCodes(IamAccount account) throws IamTotpMfaInvali
}

IamTotpMfa totpMfa = totpMfaOptional.get();

if (iamTotpMfaEncryptionAndDecryptionService.hasAdminTriggeredTheJob()) {
totpMfa.setKeyUpdateRequest(true);
}

String[] recoveryCodeStrings = recoveryCodeGenerator.generateCodes(RECOVERY_CODE_QUANTITY);
Set<IamTotpRecoveryCode> recoveryCodes = new HashSet<>();

for (String code : recoveryCodeStrings) {
IamTotpRecoveryCode recoveryCode = new IamTotpRecoveryCode(totpMfa);

if (iamTotpMfaEncryptionAndDecryptionService.hasAdminTriggeredTheJob()) {
recoveryCode.setKeyUpdateRequest(true);
}

recoveryCode.setCode(IamTotpMfaEncryptionAndDecryptionUtil.encryptSecretOrRecoveryCode(
code, iamTotpMfaProperties.getPasswordToEncryptOrDecrypt()));
code, iamTotpMfaEncryptionAndDecryptionService.whichPasswordToUseForEncryptAndDecrypt(totpMfa.getId(),
totpMfa.isKeyUpdateRequest())));
recoveryCodes.add(recoveryCode);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021
*
* 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 it.infn.mw.iam.api.account.multi_factor_authentication;

import java.util.concurrent.atomic.AtomicLong;

import javax.annotation.PostConstruct;

import org.springframework.stereotype.Service;

import it.infn.mw.iam.config.mfa.IamTotpMfaProperties;
import it.infn.mw.iam.persistence.model.IamTotpProcessedRecords;
import it.infn.mw.iam.persistence.repository.IamTotpMfaRepository;
import it.infn.mw.iam.persistence.repository.IamTotpProcessedRecordsRepository;

@Service
public class IamTotpMfaEncryptionAndDecryptionService {

private volatile boolean triggerIamTotpPasswordsJobsuccessful = false;
private final AtomicLong recordsProcessed = new AtomicLong(0);
private final AtomicLong revertRecordsProcessedCount = new AtomicLong(0);

private Long totalRecordsToProcess = 0L;
private final IamTotpMfaProperties iamTotpMfaProperties;
private final IamTotpMfaRepository iamtotpMfaRepository;
private final IamTotpProcessedRecordsRepository iamTotpProcessedRecordsRepository;

private volatile boolean checkIfadminTriggeredTheJob = false;

public IamTotpMfaEncryptionAndDecryptionService(
IamTotpMfaProperties iamTotpMfaProperties,
IamTotpMfaRepository iamTotpMfaRepository,
IamTotpProcessedRecordsRepository iamTotpProcessedRecordsRepository) {
this.iamTotpMfaProperties = iamTotpMfaProperties;
this.iamTotpProcessedRecordsRepository = iamTotpProcessedRecordsRepository;
this.iamtotpMfaRepository = iamTotpMfaRepository;
}

public boolean hasTriggerIamTotpPasswordsJobSuccessful() {
return triggerIamTotpPasswordsJobsuccessful;
}

public void setTriggerIamTotpPasswordsJobSuccessful(boolean triggerIamTotpPasswordsJobsuccessful) {
this.triggerIamTotpPasswordsJobsuccessful = triggerIamTotpPasswordsJobsuccessful;
}

public long getRecordsProcessed() {
return recordsProcessed.get();
}

public void incrementRecordsProcessedBy(long increment) {
recordsProcessed.addAndGet(increment);
}

public long getRevertRecordsProcessedCount() {
return revertRecordsProcessedCount.get();
}

public void incrementRevertRecordsProcessedCount(long increment) {
revertRecordsProcessedCount.addAndGet(increment);
}

public Long getTotalRecordsToProcess() {
return totalRecordsToProcess;
}

public void setTotalRecordsToProcess(Long totalRecordsToProcess) {
this.totalRecordsToProcess = totalRecordsToProcess;
}

public boolean hasAdminTriggeredTheJob() {
return checkIfadminTriggeredTheJob;
}

public void setCheckIfadminTriggeredTheJob(boolean adminTriggeredTheJob) {
this.checkIfadminTriggeredTheJob = adminTriggeredTheJob;
}

public String whichPasswordToUseForEncryptAndDecrypt(long currentIDOfIamTotpMfaSecret, boolean alreadyUpdated) {
if (alreadyUpdated) {
return iamTotpMfaProperties.getPasswordToEncryptOrDecrypt();
} else {
if (currentIDOfIamTotpMfaSecret > totalRecordsToProcess || hasTriggerIamTotpPasswordsJobSuccessful()) {
return iamTotpMfaProperties.getPasswordToEncryptOrDecrypt();
}

return iamTotpMfaProperties.getOldPasswordToEncryptAndDecrypt();
}
}

public boolean hasAdminRequestedToUpdateTheKey() {
return iamTotpMfaProperties.isUpdateKeyRequest()
&& iamTotpMfaProperties.getOldPasswordToEncryptAndDecrypt().length() > 0;
}

public String getOldPasswordFromService() {
return iamTotpMfaProperties.getOldPasswordToEncryptAndDecrypt();
}

public String getCurrentPasswordFromService() {
return iamTotpMfaProperties.getPasswordToEncryptOrDecrypt();
}

@PostConstruct
public void initialize() {
if (hasAdminRequestedToUpdateTheKey()) {
setTotalRecordsToProcess(iamtotpMfaRepository.count());

IamTotpProcessedRecords iamTotpProcessedRecords = new IamTotpProcessedRecords();
iamTotpProcessedRecords.setTotalRecordsToProcess(getTotalRecordsToProcess());
iamTotpProcessedRecordsRepository.save(iamTotpProcessedRecords);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.springframework.web.bind.annotation.ResponseBody;

import it.infn.mw.iam.api.common.NoSuchAccountError;
import it.infn.mw.iam.config.mfa.IamTotpMfaProperties;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamTotpMfa;
import it.infn.mw.iam.persistence.repository.IamAccountRepository;
Expand All @@ -44,12 +45,15 @@ public class MultiFactorSettingsController {
public static final String MULTI_FACTOR_SETTINGS_URL = "/iam/multi-factor-settings";
private final IamAccountRepository accountRepository;
private final IamTotpMfaRepository totpMfaRepository;
private final IamTotpMfaProperties iamTotpMfaProperties;

@Autowired
public MultiFactorSettingsController(IamAccountRepository accountRepository,
IamTotpMfaRepository totpMfaRepository) {
IamTotpMfaRepository totpMfaRepository,
IamTotpMfaProperties iamTotpMfaProperties) {
this.accountRepository = accountRepository;
this.totpMfaRepository = totpMfaRepository;
this.iamTotpMfaProperties = iamTotpMfaProperties;
}


Expand Down Expand Up @@ -77,6 +81,11 @@ public MultiFactorSettingsDTO getMultiFactorSettings() {
}

// add further factors if/when implemented
if (iamTotpMfaProperties.getPasswordToEncryptOrDecrypt().length() > 15) {
dto.setMfaFeatureDisabled(false);
} else {
dto.setMfaFeatureDisabled(true);
}

return dto;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class MultiFactorSettingsDTO {
@NotEmpty
private boolean authenticatorAppActive;

@NotEmpty
private boolean isMfaFeatureDisabled;

// add further factors if/when implemented

public MultiFactorSettingsDTO() {}
Expand All @@ -43,6 +46,19 @@ public boolean getAuthenticatorAppActive() {
return authenticatorAppActive;
}

/**
* @return true if authenticator app is active
*/
public boolean isMfaFeatureDisabled() {
return isMfaFeatureDisabled;
}

/**
* @return true if authenticator app is active
*/
public void setMfaFeatureDisabled(boolean isMfaFeatureDisabled) {
this.isMfaFeatureDisabled = isMfaFeatureDisabled;
}

/**
* @param authenticatorAppActive new status of authenticator app
Expand All @@ -54,6 +70,7 @@ public void setAuthenticatorAppActive(final boolean authenticatorAppActive) {
public JSONObject toJson() {
JSONObject json = new JSONObject();
json.put("authenticatorAppActive", authenticatorAppActive);
json.put("isMfaFeatureDisabled", isMfaFeatureDisabled);
return json;
}
}
Loading

0 comments on commit 2db3f7a

Please sign in to comment.