diff --git a/183_12_1_tresorbackend_rupe-master/pom.xml b/183_12_1_tresorbackend_rupe-master/pom.xml index 0b8aba1..5804d6d 100644 --- a/183_12_1_tresorbackend_rupe-master/pom.xml +++ b/183_12_1_tresorbackend_rupe-master/pom.xml @@ -88,6 +88,12 @@ 1.16.0 + + com.warrenstrange + googleauth + 1.5.0 + + diff --git a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/controller/TwoFactorAuthController.java b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/controller/TwoFactorAuthController.java new file mode 100644 index 0000000..b70795c --- /dev/null +++ b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/controller/TwoFactorAuthController.java @@ -0,0 +1,48 @@ +package ch.bbw.pr.tresorbackend.controller; + +import ch.bbw.pr.tresorbackend.model.Check2FACode; +import ch.bbw.pr.tresorbackend.model.Generate2FACode; +import ch.bbw.pr.tresorbackend.service.TwoFactorAuthService; +import com.warrenstrange.googleauth.GoogleAuthenticator; +import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@AllArgsConstructor +@RequestMapping("/api/2fa") +public class TwoFactorAuthController { + + private TwoFactorAuthService twoFactorAuthService; + private final GoogleAuthenticator gAuth = new GoogleAuthenticator(); + + + @CrossOrigin(origins = "${CROSS_ORIGIN}") + @PostMapping("/generate-secret") + public ResponseEntity> generateSecret(@Valid @RequestBody Generate2FACode generate2FACode) { + GoogleAuthenticatorKey key = gAuth.createCredentials(); + twoFactorAuthService.addTwoFactorSecretToUser(key.getKey(), generate2FACode.getEmail(), generate2FACode.getPassword()); + + String qrUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL("Tresor", generate2FACode.getEmail(), key); + Map response = Map.of("secret", key.getKey(), "qrUrl", qrUrl); + return ResponseEntity.ok(response); + } + + @CrossOrigin(origins = "${CROSS_ORIGIN}") + @PostMapping("/verify") + public ResponseEntity verifyCode(@Valid @RequestBody Check2FACode check2FACode) { + String secret = twoFactorAuthService.getTwoFactorSecretFromUser(check2FACode.getEmail(), check2FACode.getPassword()); + if (secret == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found"); + } + boolean isValid = gAuth.authorize(secret, check2FACode.getCode()); + return isValid ? ResponseEntity.ok("2FA success") : ResponseEntity.status(401).body("Invalid code"); + } +} \ No newline at end of file diff --git a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/controller/UserController.java b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/controller/UserController.java index 463a241..160c754 100644 --- a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/controller/UserController.java +++ b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/controller/UserController.java @@ -84,7 +84,8 @@ public class UserController { registerUser.getFirstName(), registerUser.getLastName(), registerUser.getEmail(), - passwordService.hashPassword(registerUser.getPassword()) + passwordService.hashPassword(registerUser.getPassword()), + null ); User savedUser = userService.createUser(user); diff --git a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/Check2FACode.java b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/Check2FACode.java new file mode 100644 index 0000000..055a661 --- /dev/null +++ b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/Check2FACode.java @@ -0,0 +1,19 @@ +package ch.bbw.pr.tresorbackend.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.Value; + +@Value +public class Check2FACode { + + @NotEmpty(message = "Email cannot be empty") + @Email(message = "Invalid email format") + String email; + + @NotEmpty(message = "Password cannot be empty") + String password; + + int code; +} diff --git a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/Generate2FACode.java b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/Generate2FACode.java new file mode 100644 index 0000000..bdb4c48 --- /dev/null +++ b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/Generate2FACode.java @@ -0,0 +1,16 @@ +package ch.bbw.pr.tresorbackend.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import lombok.*; + +@Value +public class Generate2FACode { + + @NotEmpty(message = "Email cannot be empty") + @Email(message = "Invalid email format") + String email; + + @NotEmpty(message = "Password cannot be empty") + String password; +} diff --git a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/User.java b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/User.java index 01d4f6c..a0a6958 100644 --- a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/User.java +++ b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/model/User.java @@ -32,4 +32,7 @@ public class User { @Column(nullable = false) private String password; + + @Column(nullable = true) + private String two_fa_secret; } \ No newline at end of file diff --git a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/service/RecaptchaService.java b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/service/RecaptchaService.java index 4ba0626..97ee1a2 100644 --- a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/service/RecaptchaService.java +++ b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/service/RecaptchaService.java @@ -1,9 +1,11 @@ package ch.bbw.pr.tresorbackend.service; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.springframework.beans.factory.annotation.Value; +import java.net.http.HttpHeaders; import java.util.Map; @Service @@ -16,15 +18,9 @@ public class RecaptchaService { public boolean verifyToken(String token) { RestTemplate restTemplate = new RestTemplate(); - Map body = Map.of( - "secret", recaptchaSecret, - "response", token - ); - String url = VERIFY_URL + "?secret=" + recaptchaSecret + "&response=" + token; - Map response = restTemplate.postForObject(url, null, Map.class); - + Map response = restTemplate.getForObject(url, Map.class); return (Boolean) response.get("success"); } } diff --git a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/service/TwoFactorAuthService.java b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/service/TwoFactorAuthService.java new file mode 100644 index 0000000..5fd20d4 --- /dev/null +++ b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/service/TwoFactorAuthService.java @@ -0,0 +1,28 @@ +package ch.bbw.pr.tresorbackend.service; + +import ch.bbw.pr.tresorbackend.model.EncryptCredentials; +import ch.bbw.pr.tresorbackend.model.User; +import ch.bbw.pr.tresorbackend.util.EncryptUtil; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class TwoFactorAuthService { + + private UserService userService; + + public void addTwoFactorSecretToUser(String secret, String email, String encryptedKey) { + User user = userService.findByEmail(email); + EncryptUtil eu = new EncryptUtil(encryptedKey); + String encryptedSecret = eu.encrypt(secret); + user.setTwo_fa_secret(encryptedSecret); + userService.updateUser(user); + } + + public String getTwoFactorSecretFromUser(String email, String encryptedKey) { + User user = userService.findByEmail(email); + EncryptUtil eu = new EncryptUtil(encryptedKey); + return eu.decrypt(user.getTwo_fa_secret()); + } +} diff --git a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/util/EncryptUtil.java b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/util/EncryptUtil.java index 66c0962..f70348a 100644 --- a/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/util/EncryptUtil.java +++ b/183_12_1_tresorbackend_rupe-master/src/main/java/ch/bbw/pr/tresorbackend/util/EncryptUtil.java @@ -27,7 +27,6 @@ public class EncryptUtil { private SecretKey secretKey; public EncryptUtil(String secretKey) { - System.out.println(secretKey); this.secretKey = generateKey(secretKey); } diff --git a/183_12_2_tresorfrontend_rupe-master/package.json b/183_12_2_tresorfrontend_rupe-master/package.json index fb4acae..1aecea5 100644 --- a/183_12_2_tresorfrontend_rupe-master/package.json +++ b/183_12_2_tresorfrontend_rupe-master/package.json @@ -7,6 +7,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.8", + "qrcode.react": "^4.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-google-recaptcha": "^3.1.0", diff --git a/183_12_2_tresorfrontend_rupe-master/src/comunication/FetchSecrets.js b/183_12_2_tresorfrontend_rupe-master/src/comunication/FetchSecrets.js index 7d2bd00..6027c22 100644 --- a/183_12_2_tresorfrontend_rupe-master/src/comunication/FetchSecrets.js +++ b/183_12_2_tresorfrontend_rupe-master/src/comunication/FetchSecrets.js @@ -4,14 +4,13 @@ */ //Post secret to server -export const postSecret = async ({loginValues, content}) => { +export const postSecret = async ({ loginValues, content }) => { const protocol = process.env.REACT_APP_API_PROTOCOL; // "http" const host = process.env.REACT_APP_API_HOST; // "localhost" const port = process.env.REACT_APP_API_PORT; // "8080" const path = process.env.REACT_APP_API_PATH; // "/api" const portPart = port ? `:${port}` : ''; // port is optional const API_URL = `${protocol}://${host}${portPart}${path}`; - console.log(loginValues) try { const response = await fetch(`${API_URL}/secrets`, { @@ -72,4 +71,4 @@ export const getSecretsforUser = async (loginValues) => { console.error('Failed to get secrets:', error.message); throw new Error('Failed to get secrets. ' || error.message); } -}; \ No newline at end of file +}; diff --git a/183_12_2_tresorfrontend_rupe-master/src/comunication/TwoFactorAuth.js b/183_12_2_tresorfrontend_rupe-master/src/comunication/TwoFactorAuth.js new file mode 100644 index 0000000..95819a2 --- /dev/null +++ b/183_12_2_tresorfrontend_rupe-master/src/comunication/TwoFactorAuth.js @@ -0,0 +1,63 @@ +export const generate2FACode = async (loginValues) => { + const protocol = process.env.REACT_APP_API_PROTOCOL; // "http" + const host = process.env.REACT_APP_API_HOST; // "localhost" + const port = process.env.REACT_APP_API_PORT; // "8080" + const path = process.env.REACT_APP_API_PATH; // "/api" + const portPart = port ? `:${port}` : ''; // port is optional + const API_URL = `${protocol}://${host}${portPart}${path}`; + console.log(loginValues) + + try { + const response = await fetch(`${API_URL}/2fa/generate-secret`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: loginValues.email, + password: loginValues.password, + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Server response failed.'); + } + + const data = await response.json(); + console.log('Secret successfully posted:', data); + return data; + } catch (error) { + console.error('Error posting secret:', error.message); + throw new Error('Failed to save secret. ' || error.message); + } +}; + + +export const verify2FACode = async ({ loginValues, code }) => { + const protocol = process.env.REACT_APP_API_PROTOCOL; // "http" + const host = process.env.REACT_APP_API_HOST; // "localhost" + const port = process.env.REACT_APP_API_PORT; // "8080" + const path = process.env.REACT_APP_API_PATH; // "/api" + const portPart = port ? `:${port}` : ''; // port is optional + const API_URL = `${protocol}://${host}${portPart}${path}`; + + try { + const response = await fetch(`${API_URL}/2fa/verify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: loginValues.email, + password: loginValues.password, + code: code + }) + }); + + console.log('2FA successfully verifyed'); + return response.ok; + } catch (error) { + return false; + } +}; diff --git a/183_12_2_tresorfrontend_rupe-master/src/css/mvp.css b/183_12_2_tresorfrontend_rupe-master/src/css/mvp.css index 36085b8..1a59b89 100644 --- a/183_12_2_tresorfrontend_rupe-master/src/css/mvp.css +++ b/183_12_2_tresorfrontend_rupe-master/src/css/mvp.css @@ -364,6 +364,10 @@ form header { padding: 1.5rem 0; } +#twofaForm { + display: none; +} + input, label, select, @@ -467,4 +471,4 @@ blockquote footer { font-size: small; line-height: var(--line-height); padding: 1.5rem 0; -} \ No newline at end of file +} diff --git a/183_12_2_tresorfrontend_rupe-master/src/pages/user/LoginUser.jsx b/183_12_2_tresorfrontend_rupe-master/src/pages/user/LoginUser.jsx index d394575..5135ede 100644 --- a/183_12_2_tresorfrontend_rupe-master/src/pages/user/LoginUser.jsx +++ b/183_12_2_tresorfrontend_rupe-master/src/pages/user/LoginUser.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { loginUser, captchaCheck } from '../../comunication/FetchUser'; import ReCAPTCHA from 'react-google-recaptcha'; +import { generate2FACode, verify2FACode } from '../../comunication/TwoFactorAuth'; /** * LoginUser @@ -10,6 +11,8 @@ import ReCAPTCHA from 'react-google-recaptcha'; function LoginUser({ loginValues, setLoginValues }) { const navigate = useNavigate(); const [recaptchaToken, setRecaptchaToken] = useState(null); + const [code, setCode] = useState(0); + const handleCaptcha = (token) => { setRecaptchaToken(token); @@ -29,18 +32,29 @@ function LoginUser({ loginValues, setLoginValues }) { let isLoginValid = false isLoginValid = await loginUser(loginValues); if (isLoginValid || captchaData.success) { + document.getElementById('loginForm').style.display = "none"; + document.getElementById('twofaForm').style.display = "block"; setLoginValues({ email: loginValues.email, password: loginValues.password }); - navigate('/'); + return false // that the page doesnt reload } } catch (error) { console.error('Failed to fetch to server:', error.message); } }; + const verify2FA = async () => { + const res = await verify2FACode({ loginValues, code }); + if (res) { + navigate('/') + return + } + alert('2fa code not valid'); + }; + return (

Login user

-
+
{errorMessage &&

{errorMessage}

} + {qrCode}
);