2FA
This commit is contained in:
parent
9548f0ea95
commit
3179d75ff7
|
@ -88,6 +88,12 @@
|
||||||
<version>1.16.0</version>
|
<version>1.16.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.warrenstrange</groupId>
|
||||||
|
<artifactId>googleauth</artifactId>
|
||||||
|
<version>1.5.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,7 +84,8 @@ public class UserController {
|
||||||
registerUser.getFirstName(),
|
registerUser.getFirstName(),
|
||||||
registerUser.getLastName(),
|
registerUser.getLastName(),
|
||||||
registerUser.getEmail(),
|
registerUser.getEmail(),
|
||||||
passwordService.hashPassword(registerUser.getPassword())
|
passwordService.hashPassword(registerUser.getPassword()),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
User savedUser = userService.createUser(user);
|
User savedUser = userService.createUser(user);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -32,4 +32,7 @@ public class User {
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
@Column(nullable = true)
|
||||||
|
private String two_fa_secret;
|
||||||
}
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
package ch.bbw.pr.tresorbackend.service;
|
package ch.bbw.pr.tresorbackend.service;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
||||||
|
import java.net.http.HttpHeaders;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -16,15 +18,9 @@ public class RecaptchaService {
|
||||||
|
|
||||||
public boolean verifyToken(String token) {
|
public boolean verifyToken(String token) {
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
Map<String, String> body = Map.of(
|
|
||||||
"secret", recaptchaSecret,
|
|
||||||
"response", token
|
|
||||||
);
|
|
||||||
|
|
||||||
String url = VERIFY_URL + "?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");
|
return (Boolean) response.get("success");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,6 @@ public class EncryptUtil {
|
||||||
private SecretKey secretKey;
|
private SecretKey secretKey;
|
||||||
|
|
||||||
public EncryptUtil(String secretKey) {
|
public EncryptUtil(String secretKey) {
|
||||||
System.out.println(secretKey);
|
|
||||||
this.secretKey = generateKey(secretKey);
|
this.secretKey = generateKey(secretKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
|
|
|
@ -11,7 +11,6 @@ export const postSecret = async ({loginValues, content}) => {
|
||||||
const path = process.env.REACT_APP_API_PATH; // "/api"
|
const path = process.env.REACT_APP_API_PATH; // "/api"
|
||||||
const portPart = port ? `:${port}` : ''; // port is optional
|
const portPart = port ? `:${port}` : ''; // port is optional
|
||||||
const API_URL = `${protocol}://${host}${portPart}${path}`;
|
const API_URL = `${protocol}://${host}${portPart}${path}`;
|
||||||
console.log(loginValues)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/secrets`, {
|
const response = await fetch(`${API_URL}/secrets`, {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -364,6 +364,10 @@ form header {
|
||||||
padding: 1.5rem 0;
|
padding: 1.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#twofaForm {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
label,
|
label,
|
||||||
select,
|
select,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { loginUser, captchaCheck } from '../../comunication/FetchUser';
|
import { loginUser, captchaCheck } from '../../comunication/FetchUser';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
import ReCAPTCHA from 'react-google-recaptcha';
|
||||||
|
import { generate2FACode, verify2FACode } from '../../comunication/TwoFactorAuth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LoginUser
|
* LoginUser
|
||||||
|
@ -10,6 +11,8 @@ import ReCAPTCHA from 'react-google-recaptcha';
|
||||||
function LoginUser({ loginValues, setLoginValues }) {
|
function LoginUser({ loginValues, setLoginValues }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [recaptchaToken, setRecaptchaToken] = useState(null);
|
const [recaptchaToken, setRecaptchaToken] = useState(null);
|
||||||
|
const [code, setCode] = useState(0);
|
||||||
|
|
||||||
|
|
||||||
const handleCaptcha = (token) => {
|
const handleCaptcha = (token) => {
|
||||||
setRecaptchaToken(token);
|
setRecaptchaToken(token);
|
||||||
|
@ -29,18 +32,29 @@ function LoginUser({ loginValues, setLoginValues }) {
|
||||||
let isLoginValid = false
|
let isLoginValid = false
|
||||||
isLoginValid = await loginUser(loginValues);
|
isLoginValid = await loginUser(loginValues);
|
||||||
if (isLoginValid || captchaData.success) {
|
if (isLoginValid || captchaData.success) {
|
||||||
|
document.getElementById('loginForm').style.display = "none";
|
||||||
|
document.getElementById('twofaForm').style.display = "block";
|
||||||
setLoginValues({ email: loginValues.email, password: loginValues.password });
|
setLoginValues({ email: loginValues.email, password: loginValues.password });
|
||||||
navigate('/');
|
return false // that the page doesnt reload
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch to server:', error.message);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Login user</h2>
|
<h2>Login user</h2>
|
||||||
<form onSubmit={handleSubmit}>
|
<form id='loginForm' onSubmit={handleSubmit}>
|
||||||
<section>
|
<section>
|
||||||
<aside>
|
<aside>
|
||||||
<div>
|
<div>
|
||||||
|
@ -73,6 +87,11 @@ function LoginUser({ loginValues, setLoginValues }) {
|
||||||
/>
|
/>
|
||||||
<button type="submit">Login</button>
|
<button type="submit">Login</button>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { postUser } from "../../comunication/FetchUser";
|
import { postUser } from "../../comunication/FetchUser";
|
||||||
|
import { generate2FACode } from '../../comunication/TwoFactorAuth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RegisterUser
|
* RegisterUser
|
||||||
|
@ -20,6 +21,8 @@ function RegisterUser({ loginValues, setLoginValues }) {
|
||||||
const [credentials, setCredentials] = useState(initialState);
|
const [credentials, setCredentials] = useState(initialState);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
|
||||||
|
const [qrCode, setQrCode] = useState(null);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
|
@ -41,15 +44,32 @@ function RegisterUser({ loginValues, setLoginValues }) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await postUser(credentials);
|
await postUser(credentials);
|
||||||
setLoginValues({ userName: credentials.email, password: credentials.password });
|
setLoginValues({ email: credentials.email, password: credentials.password });
|
||||||
setCredentials(initialState);
|
//setCredentials(initialState);
|
||||||
navigate('/');
|
//navigate("/")
|
||||||
|
await generateQRCode();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch to server:', error.message);
|
console.error('Failed to fetch to server:', error.message);
|
||||||
setErrorMessage(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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Register user</h2>
|
<h2>Register user</h2>
|
||||||
|
@ -117,6 +137,7 @@ function RegisterUser({ loginValues, setLoginValues }) {
|
||||||
</section>
|
</section>
|
||||||
<button type="submit">Register</button>
|
<button type="submit">Register</button>
|
||||||
{errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
|
{errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
|
||||||
|
{qrCode}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue