Skip to content

Commit

Permalink
TOTP MFA Update
Browse files Browse the repository at this point in the history
  • Loading branch information
kadraman committed Sep 23, 2024
1 parent 1644722 commit 28f23eb
Show file tree
Hide file tree
Showing 18 changed files with 152 additions and 301 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-log4j2:2.4.13"
implementation "org.mockito:mockito-bom:${mockitoVersion}"
implementation "org.hibernate.validator:hibernate-validator:6.1.0.Final"
implementation "com.warrenstrange:googleauth:1.5.0"
testImplementation "org.hibernate.validator:hibernate-validator:6.1.0.Final"
testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}") {
exclude group: "com.vaadin.external.google", module:"android-json"
Expand Down
244 changes: 0 additions & 244 deletions gradle.lockfile

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public void onAuthenticationSuccess(HttpServletRequest request,
log.debug("User is ADMIN, bypassing verification...");
bypassVerification(request, response, authentication);
} else {
session.setAttribute("otpType", mfaType);
switch (mfaType) {
case MFA_NONE:
log.debug("Multi factor authentication is not enabled, bypassing verification...");
Expand Down
16 changes: 12 additions & 4 deletions src/main/java/com/microfocus/example/entity/CustomUserDetails.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
Insecure Web App (IWA)
Copyright (C) 2020-2022 Micro Focus or one of its affiliates
Copyright (C) 2020-2024 Micro Focus or one of its affiliates
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand All @@ -28,7 +28,7 @@ Insecure Web App (IWA)

/**
* Custom User Details implementation
* @author Kevin A. Lee
* @author kadraman
*/
public class CustomUserDetails implements UserDetails {

Expand Down Expand Up @@ -74,6 +74,10 @@ public boolean isCredentialsNonExpired() {
return true;
}

public String getSecret() {
return user.getSecret();
}

public String getName() {
return user.getFirstName();
}
Expand All @@ -86,9 +90,13 @@ public String getMobile() {
return user.getPhone();
}

public MfaType getMfaType() { return user.getMfaType(); }
public MfaType getMfaType() {
return user.getMfaType();
}

public boolean isEnabled() { return user.getEnabled(); }
public boolean isEnabled() {
return user.getEnabled();
}

public User getUserDetails() {
return user;
Expand Down
15 changes: 14 additions & 1 deletion src/main/java/com/microfocus/example/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public BCryptPasswordEncoder passwordEncoder() {
@Transient
private String confirmPassword;

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String secret;

@NotEmpty(message = "{user.firstname.notEmpty}")
@Size(min = 2, max = 40, message = "{user.firstname.invalidLength}")
@Column(name = "first_name")
Expand Down Expand Up @@ -127,11 +130,12 @@ public User() {
this.mfaType = MfaType.MFA_NONE;
}

public User(UUID id, String username, String password, String firstName, String lastName, String email, String phone,
public User(UUID id, String username, String password, String secret, String firstName, String lastName, String email, String phone,
String address, String city, String state, String zip, String country, boolean enabled, MfaType mfaType) {
this.id = id;
this.username = username;
this.password = password;
this.secret = secret;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
Expand Down Expand Up @@ -179,6 +183,15 @@ public void setConfirmPassword(String confirmPassword) {
this.confirmPassword = confirmPassword;
}

@JsonIgnore
public String getSecret() {
return secret;
}

public void setSecret(String secret) {
this.secret = secret;
}

public String getFirstName() {
return firstName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public Integer execute(Connection con) throws SQLException {
utmp = new User(results.getObject("id", UUID.class),
results.getString("username"),
results.getString("password"),
results.getString("secret"),
results.getString("first_name"),
results.getString("last_name"),
results.getString("email"),
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/microfocus/example/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Insecure Web App (IWA)
import com.microfocus.example.web.form.admin.AdminNewUserForm;
import com.microfocus.example.web.form.admin.AdminPasswordForm;
import com.microfocus.example.web.form.admin.AdminUserForm;
import com.warrenstrange.googleauth.GoogleAuthenticator;

import org.apache.commons.lang3.RandomStringUtils;
import org.json.simple.parser.ParseException;
import org.slf4j.Logger;
Expand Down Expand Up @@ -479,4 +481,8 @@ public void deleteMessageById(UUID id) {
public void markMessageAsReadById(UUID id) { messageRepository.markMessageAsReadById(id); }
*/

private String generateSecretKey() {
return new GoogleAuthenticator().createCredentials().getKey();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
Insecure Web App (IWA)
Copyright (C) 2020-2022 Micro Focus or one of its affiliates
Copyright (C) 2020-2024 Micro Focus or one of its affiliates
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -32,7 +32,8 @@ Insecure Web App (IWA)

/**
* Verification Service to hide business logic / database persistence for MFA
* @author Kevin A. Lee
*
* @author kadraman
*/
@Service
@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Insecure Web App (IWA)
import com.microfocus.example.config.LocaleConfiguration;
import com.microfocus.example.config.handlers.CustomAuthenticationSuccessHandler;
import com.microfocus.example.entity.CustomUserDetails;
import com.microfocus.example.entity.MfaType;
import com.microfocus.example.entity.SMS;
import com.microfocus.example.exception.VerificationRequestFailedException;
import com.microfocus.example.payload.request.EmailRequest;
import com.microfocus.example.service.EmailSenderService;
Expand All @@ -30,6 +32,8 @@ Insecure Web App (IWA)
import com.microfocus.example.utils.EmailUtils;
import com.microfocus.example.utils.JwtUtils;
import com.microfocus.example.utils.WebUtils;
import com.warrenstrange.googleauth.GoogleAuthenticator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -122,33 +126,35 @@ public String otpLogin(HttpServletRequest request, Model model, Principal princi
if (model.containsAttribute("otp")) {
log.debug("OTP already set, revalidating");
} else {
// create an otp
try {
// generate OTP "one-time-password" for user
int otp = verificationService.generateOTP(userId);
log.debug("Generated OTP '" + String.valueOf(otp) + "' for user id: " + userId);

EmailRequest emailRequest = new EmailRequest(emailFromAddress, email,
"[IWA Pharmacy Direct] Your One Time Passcode", String.valueOf(otp));
try {
log.debug("Sending OTP {} via email to {}", String.valueOf(otp), email);
EmailUtils.sendEmail(emailRequest);
} catch (Exception ex) {
log.error(ex.getLocalizedMessage());
switch (loggedInUser.getMfaType()) {
case MFA_EMAIL:
log.debug("Generated OTP '" + String.valueOf(otp) + "' for user id: " + userId);
EmailRequest emailRequest = new EmailRequest(emailFromAddress, email,
"[IWA Pharmacy Direct] Your One Time Passcode", String.valueOf(otp));
log.debug("Sending OTP {} via email to {}", String.valueOf(otp), email);
EmailUtils.sendEmail(emailRequest);
break;
case MFA_SMS:
SMS sms = new SMS();
sms.setTo(mobile);
sms.setMessage("Your IWA Pharmacy Direct security code is " + String.valueOf(otp));
log.debug("Sending OTP {} via SMS to {}", String.valueOf(otp), mobile);
String sid = smsSenderService.sendSms(sms);
break;
case MFA_APP:
log.debug("Using Authenticator App to validate TOTP");
break;
default:
log.error("Unknown MFA Type");
}

/*SMS sms = new SMS();
sms.setTo(mobile);
sms.setMessage("Your IWA Pharmacy Direct security code is " + String.valueOf(otp));
try {
log.debug("Sending OTP {} via SMS to {}", String.valueOf(otp), mobile);
String sid = smsSenderService.sendSms(sms);
} catch (Exception ex) {
log.error(ex.getLocalizedMessage());
}*/
} catch (VerificationRequestFailedException ex) {
log.error(ex.getLocalizedMessage());
// TODO: handle
} catch (Exception ex) {
log.error(ex.getLocalizedMessage());
}
}
return "login_mfa";
Expand All @@ -158,36 +164,54 @@ public String otpLogin(HttpServletRequest request, Model model, Principal princi
public String otpLogin(HttpServletRequest request, HttpServletResponse response,
@RequestParam("otp") Optional<String> otp,
Model model, Principal principal) {
HttpSession session = request.getSession(true);
Authentication authentication = (Authentication) principal;
CustomUserDetails loggedInUser = (CustomUserDetails) ((Authentication) principal).getPrincipal();
String userId = loggedInUser.getId().toString();
String optStr = Optional.of(otp).get().orElse(null);
String otpStr = Optional.of(otp).get().orElse(null);

if (optStr == null || optStr.isEmpty()) {
if (otpStr == null || otpStr.isEmpty()) {
log.error("insufficient parameters");
model.addAttribute("message", "Please supply a One Time Passcode (OTP).");
model.addAttribute("alertClass", "alert-danger");
this.setModelDefaults(model, null, "optLogin");
this.setModelDefaults(model, null, "login_mfa");
return "login_mfa";
}

int otpNum = Integer.valueOf(optStr).intValue();
int otpNum = Integer.valueOf(otpStr).intValue();
// validate OTP "one-time-password" for user
if (otpNum > 0) {
log.debug("Verifying OTP '{}' for user with id: {} ", otpNum, userId);
int serverOtp = verificationService.getOtp(userId);
if (serverOtp > 0) {
if (otpNum == serverOtp) {
log.debug("User '{}' verified OTP successfully", userId);
verificationService.clearOTP(userId);

// if
if (loggedInUser.getMfaType().equals(MfaType.MFA_APP)) {
String secret = loggedInUser.getSecret();
log.debug("Validating TOTP {} with user secret {}", otpNum, secret);
GoogleAuthenticator gAuth = new com.warrenstrange.googleauth.GoogleAuthenticator();
if (gAuth.authorize(secret, otpNum)) {
log.debug("User '{}' verified TOTP successfully", userId);
} else {
log.debug("User '{}' failed OTP verification", userId);
model.addAttribute("message", "Your OTP is incorrect, please try-again!");
log.debug("User '{}' failed TOTP verification", userId);
model.addAttribute("message", "Your code is incorrect, please try-again!");
model.addAttribute("alertClass", "alert-danger");
return "login_mfa";
}
} else {
// TODO: fail
log.debug("Validating OTP...");
int serverOtp = verificationService.getOtp(userId);
if (serverOtp > 0) {
if (otpNum == serverOtp) {
log.debug("User '{}' verified OTP successfully", userId);
verificationService.clearOTP(userId);
} else {
log.debug("User '{}' failed OTP verification", userId);
model.addAttribute("message", "Your OTP is incorrect, please try-again!");
model.addAttribute("alertClass", "alert-danger");
return "login_mfa";
}
} else {
// TODO: fail
}
}
} else {
// TODO: fail
Expand Down
9 changes: 7 additions & 2 deletions src/main/resources/data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ VALUES ('32e7db01-86bc-4687-9ecb-d79b265ac14f', 'user1', '$2a$10$YFhTnHpCL.Z0Ev0
INSERT INTO users (id, username, password, first_name, last_name, email, phone, address, city, state, zip, country, date_created, enabled, mfa_type)
VALUES ('db4cfab1-ff1d-4bca-a662-394771841383', 'user2', '$2a$10$YFhTnHpCL.Z0Ev0j1CbEUub7sIWmN7Qd5RmnU8g5ekuoapV7Zdx32',
'Sarah', 'Shopper', 'user2@localhost.com', '+44808123456', '1 Somewhere Street', 'London', 'Greater London', 'SW1', 'United Kingdom', CURDATE(), true, 'MFA_EMAIL');
INSERT INTO users (id, username, password, secret, first_name, last_name, email, phone, address, city, state, zip, country, date_created, enabled, mfa_type)
VALUES ('a730c051-b5c2-454c-b669-679f06d99731', 'user3', '$2a$10$YFhTnHpCL.Z0Ev0j1CbEUub7sIWmN7Qd5RmnU8g5ekuoapV7Zdx32', 'IJGK2F5OH6E4CD2NK6Q4BSREMGJSJXEC',
'Susan', 'Shopper', 'user3@localhost.com', '+44808123456', '1 Somewhere Street', 'London', 'Greater London', 'SW1', 'United Kingdom', CURDATE(), true, 'MFA_APP');
INSERT INTO users (id, username, password, first_name, last_name, email, phone, address, city, state, zip, country, date_created, enabled)
VALUES ('92a82f45-7a03-42f3-80f8-ce4e9892409d', 'api', '$2a$10$YFhTnHpCL.Z0Ev0j1CbEUub7sIWmN7Qd5RmnU8g5ekuoapV7Zdx32',
'Api', 'User', 'api@localhost.com', '+44808123456', '1 Somewhere Street', 'London', 'Greater London', 'SW1', 'United Kingdom', CURDATE(), true);
INSERT INTO users (id, username, password, first_name, last_name, email, phone, address, city, state, zip, country, date_created, enabled, verify_code)
VALUES ('a730c051-b5c2-454c-b669-679f06d99731', 'user3', '$2a$10$YFhTnHpCL.Z0Ev0j1CbEUub7sIWmN7Qd5RmnU8g5ekuoapV7Zdx32',
'Steve', 'Shopper', 'user3@localhost.com', '+44808123456', '1 Somewhere Street', 'London', 'Greater London', 'SW1', 'United Kingdom', CURDATE(), false, 'AwUjqPvDLVxjzTEChhQXMDMJxBlWvZoG');
VALUES ('3f91dc76-97fc-45d5-8db0-3bee04326a86', 'user4', '$2a$10$YFhTnHpCL.Z0Ev0j1CbEUub7sIWmN7Qd5RmnU8g5ekuoapV7Zdx32',
'Steve', 'Shopper', 'user4@localhost.com', '+44808123456', '1 Somewhere Street', 'London', 'Greater London', 'SW1', 'United Kingdom', CURDATE(), false, 'AwUjqPvDLVxjzTEChhQXMDMJxBlWvZoG');
INSERT INTO user_authorities (authority_id, user_id)
VALUES ('05970e74-c82b-4e21-b100-f8184d6e3454', 'e18c8bcc-935d-444d-a194-3a32a3b35a49');
INSERT INTO user_authorities (authority_id, user_id)
Expand All @@ -32,6 +35,8 @@ VALUES ('6bdd6188-d659-4390-8d37-8f090d2ed69a', 'db4cfab1-ff1d-4bca-a662-3947718
INSERT INTO user_authorities (authority_id, user_id)
VALUES ('6bdd6188-d659-4390-8d37-8f090d2ed69a', 'a730c051-b5c2-454c-b669-679f06d99731');
INSERT INTO user_authorities (authority_id, user_id)
VALUES ('6bdd6188-d659-4390-8d37-8f090d2ed69a', '3f91dc76-97fc-45d5-8db0-3bee04326a86');
INSERT INTO user_authorities (authority_id, user_id)
VALUES ('dfc1d81b-4a7e-4248-80f7-8445ee5cb68e', '92a82f45-7a03-42f3-80f8-ce4e9892409d');
INSERT INTO products (id, code, name, rating, summary, description, image, price, in_stock, time_to_stock, available)
VALUES ('eec467c8-5de9-4c7c-8541-7b31614d31a0', 'SWA234-A568-00010', 'Solodox 750', 4,
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ create table users
id UUID not null,
username varchar(255) not null,
password varchar(255),
secret varchar(255),
date_created timestamp,
first_name varchar(255) not null,
last_name varchar(255) not null,
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

<form th:action="@{/j_spring_security_check}" method="post" class="form-login">
<div class="invisible">Site message</div>
<h1 class="h3 mb-3 font-weight-normal">Enter your details</h1>
<h1 class="h4 mb-3 font-weight-normal">Enter your details</h1>

<div class="form-group">
<input type="text" id="email" name="email" class="form-control"
Expand Down
34 changes: 25 additions & 9 deletions src/main/resources/templates/login_mfa.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,27 @@
<div class="row justify-content-center align-items-center">

<form th:action="@{/login_mfa}" method="post" class="form-login">
<h1 class="h3 mb-3 font-weight-normal">Two-Step Verification</h1>
<h1 class="h4 mb-3 font-weight-normal">Multi-factor Authentication</h1>

<!--p>
For added security, please enter the One Time Passcode (OTP)
that has been sent to a phone number ending <span th:text="${session.mobileDigits}">XX</span>
</p-->
<p>
For added security, please enter the One Time Passcode (OTP)
that has been sent to your email address.
</p>
<th:block th:switch="${#strings.toString(session.otpType)}">
<div th:case="'MFA_EMAIL'">
<p>
For added security, please enter the One Time Passcode (OTP)
that has been sent to your email address.
</p>
</div>
<div th:case="'MFA_SMS'">
<p>
For added security, please enter the One Time Passcode (OTP)
that has been sent to a phone number ending <span th:text="${session.mobileDigits}">XX</span>.
</p>
</div>
<div th:case="'MFA_APP'">
<p>
For added security, please enter the code from your Authenticator App.</span>
</p>
</div>
</th:block>

<div class="form-group">
<input type="text" id="otp" name="otp" class="form-control"
Expand All @@ -54,6 +65,11 @@ <h1 class="h3 mb-3 font-weight-normal">Two-Step Verification</h1>
</strong>
</div>
</div>
<div th:if="${otpError}" class="pb-2">
<div class="text-danger">
<strong th:text="${message}">message</strong>
</div>
</div>

<button class="btn btn-lg btn-primary btn-block" name="login-submit" id="login-submit" type="submit">Submit</button>
</form>
Expand Down
Loading

0 comments on commit 28f23eb

Please sign in to comment.