2FA
This commit is contained in:
parent
9548f0ea95
commit
3179d75ff7
|
@ -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>
|
||||
|
|
|
@ -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.getLastName(),
|
||||
registerUser.getEmail(),
|
||||
passwordService.hashPassword(registerUser.getPassword())
|
||||
passwordService.hashPassword(registerUser.getPassword()),
|
||||
null
|
||||
);
|
||||
|
||||
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)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = true)
|
||||
private String two_fa_secret;
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
public EncryptUtil(String secretKey) {
|
||||
System.out.println(secretKey);
|
||||
this.secretKey = generateKey(secretKey);
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
#twofaForm {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input,
|
||||
label,
|
||||
select,
|
||||
|
@ -467,4 +471,4 @@ blockquote footer {
|
|||
font-size: small;
|
||||
line-height: var(--line-height);
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue