Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MFA support #661

Closed
wants to merge 87 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
dfe23f0
Initial HTML and JS for enabling MFA on user account through dashboard
Nov 16, 2021
baf4fe8
Button and modal for editing MFA settings now appears
Nov 18, 2021
577f5cb
Adding modal for enabling authenticator app
Nov 19, 2021
0a58578
Adding models for IamTotpMfa and IamTotpRecoveryCode
Nov 19, 2021
6fd8ccc
Adding modal for disabling authenticator app
Nov 19, 2021
dccc0b6
Merge branch 'iam-spring-update-oct-2021' of git://github.com/indigo-…
Nov 22, 2021
1a08d05
WIP: Increase the Cookie duration
rmiccoli Sep 2, 2021
fb229df
Allow config option in test client app to avoid disclosing tokens
andreaceccanti Sep 10, 2021
02ac0d2
Dynamic downscoping in test client app
andreaceccanti Sep 11, 2021
a2dcabb
Bumped version to v1.7.1
andreaceccanti Sep 11, 2021
80b3140
v1.7.1 changelog
andreaceccanti Sep 11, 2021
855ee1e
More CHANGELOG tweaks
andreaceccanti Sep 11, 2021
8943042
Initial HTML and JS for enabling MFA on user account through dashboard
Nov 16, 2021
900662c
Button and modal for editing MFA settings now appears
Nov 18, 2021
dc787f8
Adding modal for enabling authenticator app
Nov 19, 2021
474b287
Adding models for IamTotpMfa and IamTotpRecoveryCode
Nov 19, 2021
c7dd5be
Adding modal for disabling authenticator app
Nov 19, 2021
d77143b
Unifying controllers, adding auth app service
Nov 22, 2021
fac0a90
Merge branch 'issue-418-mfa-support' of https://github.com/sam-glende…
Nov 22, 2021
67d80a1
Syncing branch with origin
Nov 22, 2021
af13c9c
Merging changes for updating Spring boot and Java version. Squashed c…
Nov 22, 2021
e6e8b26
Making database schema changes for MySQL and H2
Nov 23, 2021
84c8434
Ignoring .factorypath autogenerated file
Nov 23, 2021
ccbc081
Backend for adding TOTP secret to a user
Nov 24, 2021
409adb4
Merge branch 'iam-spring-update-oct-2021' into issue-418-mfa-support
Nov 25, 2021
47baf5f
Merge branch 'iam-spring-update-oct-2021' of git://github.com/indigo-…
Nov 25, 2021
70469af
Merge branch 'iam-spring-update-oct-2021' into issue-418-mfa-support
Nov 25, 2021
ae8b887
Removing non-existent files
Nov 25, 2021
1be4737
MFA secret and QR code now display on authenticator app enabling modal
Nov 25, 2021
06f4ace
Dynamic mfa settings menu, toaster notifications on successful operation
Nov 26, 2021
3fb7e55
Adding license to files, adding basic input detection for MFA modals
Nov 26, 2021
ea1a536
Verification code now sends through post successfully
Nov 29, 2021
5b23238
Adding TOTP validation for enabling and disabling authenticator app MFA
Nov 29, 2021
e89f821
Autowiring QR generator and code verifier into AuthenticatorAppContro…
Nov 29, 2021
c4be542
Database migrations for totp_mfa now in separate file
Nov 30, 2021
dea8ff5
Adding tests for enabling and disabling TOTP MFA through IamAccountSe…
Nov 30, 2021
b794327
Adding tests for MFA settings and auth app controllers
Dec 1, 2021
0a0557c
Adding ROLE_PRE_AUTHENTICATED
Dec 2, 2021
b1dc5fe
Adding UUID for IamTotpMfa, fixing migration version numbers
Dec 2, 2021
cc77a0b
Removing UUID from IamTotpMfa due to being unnecessary
Dec 2, 2021
27c0e65
Adding java docs for files in iam-login-service
Dec 3, 2021
81b536f
Adding basic mfa login procedure
Dec 6, 2021
f0de8a8
Merge branch 'indigo-iam:iam-spring-update-oct-2021' into iam-spring-…
Dec 6, 2021
fafd97e
Merge branch 'iam-spring-update-oct-2021' into issue-418-mfa-support
Dec 6, 2021
c9feb7c
Improving TOTP input screen post-login, fixing Java 17 Docker upgrade…
Dec 7, 2021
b05cce2
Adding error handling for MFA failure, fixing CSS bugs
Dec 8, 2021
f67d9b7
Separating MFA verification controllers, adding "Back to login" button
Dec 8, 2021
82af215
Adding mechanism for looping through available factors of authenticat…
Dec 9, 2021
3479f43
Adding recovery code login procedure, backend for regenerating recove…
Dec 10, 2021
915c6a9
Fixing css bug that broke the btn-login class
Dec 13, 2021
6a13643
Fixing css bugs with MFA verify button
Dec 13, 2021
e13a169
Adding recovery code regeneration after use and webpage navigation
Dec 13, 2021
ad2b1ad
Adding ability to view and reset recovery codes
Dec 14, 2021
d378875
Fixing error message tests in AuthenticatorAppSettingsTests
Dec 14, 2021
a0d900d
Fixing enabling and disabing totpMfa tests for IamAccount
Dec 15, 2021
79e5248
Adding tests for MfaVerify and AuthenticatorAppVerify controllers
Dec 16, 2021
ffa735a
Adding licence header
Dec 16, 2021
4741225
Adding tests for recovery code management controller endpoints
Dec 17, 2021
7c997a0
Adding tests on account service for adding recovery codes, fixing clo…
Dec 17, 2021
48b89c2
Fixing submit button bug on dashboard modals
Jan 6, 2022
b018fe3
Recovery codes display to the user on MFA enabling
Jan 6, 2022
8a33dc8
Adding amr claim in OIDC id_token
Jan 17, 2022
4efb960
Removing unnecessary database migrations, extending authentication ob…
Jan 20, 2022
6ac8b54
Fixing login bugs with MfaAuthenticationToken, extending authenticati…
Jan 21, 2022
54842ff
Renaming MfaAuthenticationToken to ExtendedAuthenticationToken
Jan 21, 2022
2a03fbb
Renaming MfaAuthenticationFilter to ExtendedAuthenticationFilter
Jan 21, 2022
2c946de
Moving db migrations to correct directory
Jan 24, 2022
98d3c73
Removing unneeded method to regenerate recovery codes
Jan 24, 2022
f095d71
Adding creation time and update time to IamTotpMfa
Jan 24, 2022
f2af341
Fixing migrations from IamTotpMfa creation and update time fields
Jan 27, 2022
a7098a6
Removing YubiKey option and all references in code
Jan 27, 2022
143dfe1
Demonstrating reading of amr from request parameters to /token
Feb 3, 2022
a062373
Authentication method reference claim now successfully appears in id_…
Feb 17, 2022
666037b
OAuth2 client login flow now works for MFA users
Feb 24, 2022
11cbc2c
Fixing login credential failure handling
Feb 25, 2022
dc01d78
Fixing TOTP verification process
Feb 25, 2022
3553f26
Merging base branch and fixing conflicts
Feb 28, 2022
a231f5e
Adding recovery code verification to login flow
Mar 4, 2022
e34f016
Merge branch mfa-db-migrations into issue-418-mfa-support
Mar 4, 2022
bc39b71
Merge branch 'v1.8.0' into issue-418-mfa-support
Mar 4, 2022
6513b09
Adding comments, fixing assigned authorities bug
Mar 8, 2022
d9b461e
Merge branch 'issue-418-mfa-support' of https://github.com/sam-glende…
Mar 8, 2022
e6750ec
Fixing license header in verify JSP
Mar 8, 2022
566b1f3
Fixing existing MFA tests, improving controller functionality
Mar 10, 2022
958082c
Adding license header for IamTotpMfaServiceTests
Mar 10, 2022
469ac8e
Fixing bugs with MFA verification
Mar 11, 2022
b9bbb06
Merge remote-tracking branch 'sam/issue-418-mfa-support' into add-mfa…
enricovianello Oct 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions iam-login-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@
<artifactId>spring-security-oauth2</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
Expand Down Expand Up @@ -396,6 +401,13 @@
<artifactId>jaxb-runtime</artifactId>
</dependency>

<!-- Secret, TOTP, QR code and recovery code generator for multi-factor authentication -->
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp</artifactId>
<version>1.7.1</version>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
import org.springframework.stereotype.Component;

import it.infn.mw.iam.authn.util.Authorities;
import it.infn.mw.iam.core.ExtendedAuthenticationToken;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.repository.IamAccountRepository;

@SuppressWarnings("deprecation")
@Component
public class AccountUtils {

IamAccountRepository accountRepo;

@Autowired
Expand All @@ -56,14 +58,27 @@ public boolean isAdmin(Authentication auth) {
return auth.getAuthorities().contains(Authorities.ROLE_ADMIN);
}

public boolean isPreAuthenticated(Authentication auth) {
if (auth == null || auth.getAuthorities() == null) {
return false;
}

return auth.getAuthorities().contains(Authorities.ROLE_PRE_AUTHENTICATED);
}

public boolean isAuthenticated() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

return isAuthenticated(auth);
}

public boolean isAuthenticated(Authentication auth) {
return !(isNull(auth) || auth instanceof AnonymousAuthenticationToken);
if (isNull(auth) || auth instanceof AnonymousAuthenticationToken) {
return false;
} else if (auth instanceof ExtendedAuthenticationToken && !auth.isAuthenticated()) {
return false;
}
return true;
}

public Optional<IamAccount> getAuthenticatedUserAccount(Authentication authn) {
Expand All @@ -72,7 +87,7 @@ public Optional<IamAccount> getAuthenticatedUserAccount(Authentication authn) {
}

Authentication userAuthn = authn;

if (authn instanceof OAuth2Authentication) {
OAuth2Authentication oauth = (OAuth2Authentication) authn;
if (oauth.getUserAuthentication() == null) {
Expand All @@ -86,13 +101,13 @@ public Optional<IamAccount> getAuthenticatedUserAccount(Authentication authn) {
}

public Optional<IamAccount> getAuthenticatedUserAccount() {

Authentication auth = SecurityContextHolder.getContext().getAuthentication();

return getAuthenticatedUserAccount(auth);
}
public Optional<IamAccount> getByAccountId(String accountId){

public Optional<IamAccount> getByAccountId(String accountId) {
return accountRepo.findByUuid(accountId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
/**
* 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.HashSet;
import java.util.Optional;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.recovery.RecoveryCodeGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import it.infn.mw.iam.audit.events.account.multi_factor_authentication.AuthenticatorAppDisabledEvent;
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.core.user.IamAccountService;
import it.infn.mw.iam.core.user.exception.MfaSecretAlreadyBoundException;
import it.infn.mw.iam.core.user.exception.MfaSecretNotFoundException;
import it.infn.mw.iam.core.user.exception.TotpMfaAlreadyEnabledException;
import it.infn.mw.iam.persistence.model.IamAccount;
import it.infn.mw.iam.persistence.model.IamTotpMfa;
import it.infn.mw.iam.persistence.model.IamTotpRecoveryCode;
import it.infn.mw.iam.persistence.repository.IamTotpMfaRepository;

@Service
public class DefaultIamTotpMfaService implements IamTotpMfaService, ApplicationEventPublisherAware {

public static final int RECOVERY_CODE_QUANTITY = 6;

private final IamAccountService iamAccountService;
private final IamTotpMfaRepository totpMfaRepository;
private final SecretGenerator secretGenerator;
private final RecoveryCodeGenerator recoveryCodeGenerator;
private final CodeVerifier codeVerifier;
private ApplicationEventPublisher eventPublisher;

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

private void authenticatorAppEnabledEvent(IamAccount account, IamTotpMfa totpMfa) {
eventPublisher.publishEvent(new AuthenticatorAppEnabledEvent(this, account, totpMfa));
}

private void authenticatorAppDisabledEvent(IamAccount account, IamTotpMfa totpMfa) {
eventPublisher.publishEvent(new AuthenticatorAppDisabledEvent(this, account, totpMfa));
}

private void totpVerifiedEvent(IamAccount account, IamTotpMfa totpMfa) {
eventPublisher.publishEvent(new TotpVerifiedEvent(this, account, totpMfa));
}

private void recoveryCodeVerifiedEvent(IamAccount account, IamTotpMfa totpMfa) {
eventPublisher.publishEvent(new RecoveryCodeVerifiedEvent(this, account, totpMfa));
}

@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = applicationEventPublisher;
}

/**
* Generates and attaches a TOTP MFA secret to a user account, along with a set of recovery codes
* This is pre-emptive to actually enabling TOTP MFA on the account - the secret is written for
* server-side TOTP verification during the user's enabling of MFA on their account
*
* @param account the account to add the secret to
* @return the new TOTP secret
*/
@Override
public IamTotpMfa addTotpMfaSecret(IamAccount account) {
Optional<IamTotpMfa> totpMfaOptional = totpMfaRepository.findByAccount(account);
if (totpMfaOptional.isPresent()) {
if (totpMfaOptional.get().isActive()) {
throw new MfaSecretAlreadyBoundException(
"A multi-factor secret is already assigned to this account");
}

totpMfaRepository.delete(totpMfaOptional.get());
}

// Generate secret
IamTotpMfa totpMfa = new IamTotpMfa(account);
totpMfa.setSecret(secretGenerator.generate());
totpMfa.setAccount(account);

Set<IamTotpRecoveryCode> recoveryCodes = generateRecoveryCodes(totpMfa);
totpMfa.setRecoveryCodes(recoveryCodes);
totpMfaRepository.save(totpMfa);
return totpMfa;
}

/**
* Adds a set of recovery codes to a given account's TOTP secret.
*
* @param account the account to add recovery codes to
* @return the affected TOTP secret
*/
@Override
public IamTotpMfa addTotpMfaRecoveryCodes(IamAccount account) {
Optional<IamTotpMfa> totpMfaOptional = totpMfaRepository.findByAccount(account);
if (!totpMfaOptional.isPresent()) {
throw new MfaSecretNotFoundException("No multi-factor secret is attached to this account");
}

IamTotpMfa totpMfa = totpMfaOptional.get();

Set<IamTotpRecoveryCode> recoveryCodes = generateRecoveryCodes(totpMfa);

// Attach to account
totpMfa.setRecoveryCodes(recoveryCodes);
totpMfa.touch();
return totpMfa;
}

/**
* Enables TOTP MFA on a provided account. Relies on the account already having a non-active TOTP
* secret attached to it
*
* @param account the account to enable TOTP MFA on
* @return the newly-enabled TOTP secret
*/
@Override
public IamTotpMfa enableTotpMfa(IamAccount account) {
Optional<IamTotpMfa> totpMfaOptional = totpMfaRepository.findByAccount(account);
if (!totpMfaOptional.isPresent()) {
throw new MfaSecretNotFoundException("No multi-factor secret is attached to this account");
}

IamTotpMfa totpMfa = totpMfaOptional.get();
if (totpMfa.isActive()) {
throw new TotpMfaAlreadyEnabledException("TOTP MFA is already enabled on this account");
}

totpMfa.setActive(true);
totpMfa.touch();
totpMfaRepository.save(totpMfa);
iamAccountService.saveAccount(account);
authenticatorAppEnabledEvent(account, totpMfa);
return totpMfa;
}

/**
* Disables TOTP MFA on a provided account. Relies on the account having an active TOTP secret
* attached to it. Disabling means to delete the secret entirely (if a user chooses to enable
* again, a new secret is generated anyway)
*
* @param account the account to disable TOTP MFA on
* @return the newly-disabled TOTP MFA
*/
@Override
public IamTotpMfa disableTotpMfa(IamAccount account) {
Optional<IamTotpMfa> totpMfaOptional = totpMfaRepository.findByAccount(account);
if (!totpMfaOptional.isPresent()) {
throw new MfaSecretNotFoundException("No multi-factor secret is attached to this account");
}

IamTotpMfa totpMfa = totpMfaOptional.get();
totpMfaRepository.delete(totpMfa);

iamAccountService.saveAccount(account);
authenticatorAppDisabledEvent(account, totpMfa);
return totpMfa;
}

/**
* Verifies a provided TOTP against an account multi-factor secret
*
* @param account the account whose secret we will check against
* @param totp the TOTP to validate
* @return true if valid, false otherwise
*/
@Override
public boolean verifyTotp(IamAccount account, String totp) {
Optional<IamTotpMfa> totpMfaOptional = totpMfaRepository.findByAccount(account);
if (!totpMfaOptional.isPresent()) {
throw new MfaSecretNotFoundException("No multi-factor secret is attached to this account");
}

IamTotpMfa totpMfa = totpMfaOptional.get();
String mfaSecret = totpMfa.getSecret();

// Verify provided TOTP
if (codeVerifier.isValidCode(mfaSecret, totp)) {
totpVerifiedEvent(account, totpMfa);
return true;
}

return false;
}

/**
* Verifies a provided recovery code against an account
*
* @param account the account we will check against
* @param recoveryCode the recovery code to validate
* @return true if valid, false otherwise
*/
@Override
public boolean verifyRecoveryCode(IamAccount account, String recoveryCode) {
Optional<IamTotpMfa> totpMfaOptional = totpMfaRepository.findByAccount(account);
if (!totpMfaOptional.isPresent()) {
throw new MfaSecretNotFoundException("No multi-factor secret is attached to this account");
}

IamTotpMfa totpMfa = totpMfaOptional.get();
if (!totpMfa.isActive()) {
throw new MfaSecretNotFoundException("No multi-factor secret is attached to this account");
}

// Check for a matching recovery code
Set<IamTotpRecoveryCode> accountRecoveryCodes = totpMfa.getRecoveryCodes();
for (IamTotpRecoveryCode recoveryCodeObject : accountRecoveryCodes) {
String recoveryCodeString = recoveryCodeObject.getCode();
if (recoveryCode.equals(recoveryCodeString)) {
recoveryCodeVerifiedEvent(account, totpMfa);
return true;
}
}

return false;
}

private Set<IamTotpRecoveryCode> generateRecoveryCodes(IamTotpMfa totpMfa) {
String[] recoveryCodeStrings = recoveryCodeGenerator.generateCodes(RECOVERY_CODE_QUANTITY);
Set<IamTotpRecoveryCode> recoveryCodes = new HashSet<>();
for (String code : recoveryCodeStrings) {
IamTotpRecoveryCode recoveryCode = new IamTotpRecoveryCode(totpMfa);
recoveryCode.setCode(code);
recoveryCodes.add(recoveryCode);
}
return recoveryCodes;
}
}
Loading
Loading