This commit is contained in:
Lorenz Hohermuth 2025-07-04 08:55:22 +02:00
parent 9548f0ea95
commit 3179d75ff7
15 changed files with 241 additions and 18 deletions

View File

@ -88,6 +88,12 @@
<version>1.16.0</version>
</dependency>
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.5.0</version>
</dependency>
</dependencies>
<build>

View File

@ -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<Map<String, String>> 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<String, String> response = Map.of("secret", key.getKey(), "qrUrl", qrUrl);
return ResponseEntity.ok(response);
}
@CrossOrigin(origins = "${CROSS_ORIGIN}")
@PostMapping("/verify")
public ResponseEntity<String> 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");
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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;
}

View File

@ -32,4 +32,7 @@ public class User {
@Column(nullable = false)
private String password;
@Column(nullable = true)
private String two_fa_secret;
}

View File

@ -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<String, String> body = Map.of(
"secret", recaptchaSecret,
"response", token
);
String url = VERIFY_URL + "?secret=" + recaptchaSecret + "&response=" + token;
Map<String, Object> response = restTemplate.postForObject(url, null, Map.class);
Map<String, Object> response = restTemplate.getForObject(url, Map.class);
return (Boolean) response.get("success");
}
}

View File

@ -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());
}
}

View File

@ -27,7 +27,6 @@ public class EncryptUtil {
private SecretKey secretKey;
public EncryptUtil(String secretKey) {
System.out.println(secretKey);
this.secretKey = generateKey(secretKey);
}

View File

@ -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",

View File

@ -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);
}
};
};

View File

@ -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;
}
};

View File

@ -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;
}
}

View File

@ -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 (
<div>
<h2>Login user</h2>
<form onSubmit={handleSubmit}>
<form id='loginForm' onSubmit={handleSubmit}>
<section>
<aside>
<div>
@ -73,6 +87,11 @@ function LoginUser({ loginValues, setLoginValues }) {
/>
<button type="submit">Login</button>
</form>
<form id='twofaForm' onSubmit={verify2FA}>
<h2>2FA</h2>
<input type="number" value={code} onChange={(e) => setCode(e.target.value)} required />
<button type="button" onClick={verify2FA}>Check</button>
</form>
</div>
);
}

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { postUser } from "../../comunication/FetchUser";
import { generate2FACode } from '../../comunication/TwoFactorAuth';
/**
* RegisterUser
@ -20,6 +21,8 @@ function RegisterUser({ loginValues, setLoginValues }) {
const [credentials, setCredentials] = useState(initialState);
const [errorMessage, setErrorMessage] = useState('');
const [qrCode, setQrCode] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setErrorMessage('');
@ -41,15 +44,32 @@ function RegisterUser({ loginValues, setLoginValues }) {
try {
await postUser(credentials);
setLoginValues({ userName: credentials.email, password: credentials.password });
setCredentials(initialState);
navigate('/');
setLoginValues({ email: credentials.email, password: credentials.password });
//setCredentials(initialState);
//navigate("/")
await generateQRCode();
} catch (error) {
console.error('Failed to fetch to server:', error.message);
setErrorMessage(error.message);
}
};
const generateQRCode = async () => {
const res = await generate2FACode(credentials);
setQrCode(
<div id="qrCode" >
<h2>2FA Setup</h2>
<p>To Setup 2FA Scan this QR Code in Google Authenicator</p>
<img src={res.qrUrl} />
<p>Secret: {res.secret}</p>
<button onClick={() => {
setCredentials(initialState);
navigate("/");
}} >Ok</button>
</div>
)
};
return (
<div>
<h2>Register user</h2>
@ -117,6 +137,7 @@ function RegisterUser({ loginValues, setLoginValues }) {
</section>
<button type="submit">Register</button>
{errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
{qrCode}
</form>
</div>
);