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`, { | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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