--- tags: authentication, react, spring --- # Authenticating the User - Part 2 (2233) We will now work on completing the sign-in process. ## Backend Updates We will need to make a few more updates to our Spring Boot code. There are 2 ways a user can authenitcate to the API: (1) using a Firebase and (2) using the authorization token we give them. #### Response Wrapper Update the response class to have an additional parameter `HttpHeaders headers` ```java= @Data @RequiredArgsConstructor @AllArgsConstructor public class ResponseWrapper { private @NonNull int statusCode; private @NonNull String name; private @NonNull Object payload; private @Nullable HttpHeaders headers = null; public ResponseEntity getResponse() { return ResponseEntity.status(statusCode).headers(headers).body(Collections.singletonMap(name, payload)); } } ``` ## User Service We will need a few additional functions to for our log in to work. We need to be able fetch a user by their the uid and an easy way to update the last login value. ```java= public User getUserByUid(String uid) throws ExecutionException, InterruptedException { User user = null; Query query = db.collection("User") .whereEqualTo("uid", uid); ApiFuture<QuerySnapshot> future = query.get(); List<QueryDocumentSnapshot> docs = future.get().getDocuments(); if(docs.size() == 1) user = docs.get(0).toObject(User.class); return user; } public void updateLastLogin(String id){ DocumentReference docRef = db.collection("User").document(id); ApiFuture<WriteResult> writeResult = docRef.update("lastLogin", FieldValue.serverTimestamp()); } ``` ### application.yml Add to the `exposed-headers` 1. X-Auth-Token 2. Expires This allow us to pass these values back in the hearder on our CORS call. ### FirebaseAuthenticationFilter We will update the authentication filter to work with both the Firebase toke and our own JWT token. ```java= @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String authToken = extractAuthenticationTokenFromRequest(request); if (StringUtils.hasText(authToken)) { FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(authToken); String uid = decodedToken.getUid(); Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("USER")); Authentication authentication = new UsernamePasswordAuthenticationToken(uid, null, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); } else { authToken = extractAuthorizationTokenFromRequest(request); if (StringUtils.hasText(authToken)) { Claims claims = JwtUtil.getClaimsFromToken(authToken); String uid = claims.getSubject(); Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("USER")); Authentication authentication = new UsernamePasswordAuthenticationToken(uid, null, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); } } } catch (AuthenticationException e) { failureHandler.onAuthenticationFailure(request, response, e); return; } catch (FirebaseAuthException e) { throw new RuntimeException(e); } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e){ //TODO: Handle exception //throw new RuntimeException(e); } filterChain.doFilter(request, response); } private String extractAuthenticationTokenFromRequest(HttpServletRequest request) { String authToken = request.getHeader("Authorization"); if (StringUtils.hasText(authToken) && authToken.startsWith("Bearer ")) { return authToken.substring(7); } return null; } private String extractAuthorizationTokenFromRequest(HttpServletRequest request) { return request.getHeader("X-Auth-Token"); } ``` ### AuthenticationController Now we will update the authentication controller. ```java= @RestController @RequestMapping("/api") public class AuthenticationController { private final FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(); private final AuthenticationManager authenticationManager; @Value("${response.status}") private int statusCode; @Value("${response.name}") private String name; private Object payload; private ResponseWrapper response; private static final String CLASS_NAME = "Authentication"; private final Log logger = LogFactory.getLog(this.getClass()); public AuthenticationController(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @PostMapping("/register") public String register(@RequestBody RegistrationRequest registrationRequest) throws FirebaseAuthException { UserRecord.CreateRequest createRequest = new UserRecord.CreateRequest() .setEmail(registrationRequest.getEmail()) .setPassword(registrationRequest.getPassword()) .setDisplayName(registrationRequest.getDisplayName()); UserRecord userRecord = firebaseAuth.createUser(createRequest); return "User created with UID: " + userRecord.getUid(); } @PostMapping("/login") public ResponseEntity login(@RequestBody LoginRequest loginRequest) throws FirebaseAuthException { String token = ""; HttpHeaders headers = new HttpHeaders(); try { UserRecord userRecord = firebaseAuth.getUserByEmail(loginRequest.getEmail()); //logger.info(userRecord.getUid()); UserDetails userDetails = new FirebaseUserDetails(userRecord); //logger.info(userDetails); token = JwtUtil.generateToken(userDetails); logger.info(token); UserService service = new UserService(); User user = service.getUserByUid(userRecord.getUid()); payload = user; service.updateLastLogin(user.getUserId() ); statusCode = 200; name = "user"; headers.add("X-Auth-Token", token); Instant now = Instant.now(); Instant expiryDate = now.plus(1, ChronoUnit.HOURS); headers.add("Expires", String.valueOf(expiryDate.toEpochMilli())); } catch (ExecutionException | InterruptedException e) { payload = new ErrorMessage("Error signing in", CLASS_NAME, e.toString()); } response = new ResponseWrapper(statusCode, name, payload, headers); return response.getResponse(); } @GetMapping("/logout") public String logout() { SecurityContextHolder.getContext().setAuthentication(null); return "Logged out successfully"; } } ``` ## Front End Updates Let's start by adding the SignIn page. ## SignIn This is a stateless component. ```jsx= const context = useContext(AuthContext); const emailRef = useRef(""); const passwordRef = useRef(""); let navigate = useNavigate(); useEffect(()=>{ window.document.body.classList.add("text-center"); },[]) async function handleSubmit(event){ event.preventDefault(); await context.login(emailRef.current.value, passwordRef.current.value).then(()=>{ if(context.isLoggedIn && context.currentUser != null) { navigate("/"); } }) } return ( <main className="form-signin w-25 m-auto"> <form onSubmit={handleSubmit}> <img className="mb-4" src={logo} alt="" width="324" /> <h1 className="h3 mb-3 fw-normal">Please sign in</h1> <div className="form-floating"> <input type="email" className="form-control" id="floatingInput" placeholder="name@example.com" ref={emailRef}/> <label htmlFor="floatingInput">Email address</label> </div> <div className="form-floating"> <input type="password" className="form-control" id="floatingPassword" placeholder="Password" ref={passwordRef}/> <label htmlFor="floatingPassword">Password</label> </div> <button className="mt-3 w-100 btn btn-lg btn-primary" type="submit">Sign in</button> <p className="mt-5 mb-3 text-body-secondary"> &copy; 2017–2023</p> </form> </main> ); ``` Add this page to the router `App.js` as `/signin`. This is **not** a private route. ### AuthContext Now we need to update the AuthContext to work with our login. First we need to install the firebase library. ```bash npm i firebase --save ``` #### Create Web App on Firebase Our login is handled on the front end. To be able to do this, we will need to create a Firebase web application. ![](https://i.imgur.com/6cl71PO.gif) Add your firebaseConfig to the AuthContext as a variable. ```jsx= const AuthContext = createContext({ currentUser: {}, setCurrentUser: ()=>{}, isLoggedIn: false, login: () => {}, logout: () => {}, register: () =>{} }); const AuthProvider = ({ children }) => { const [isLoggedIn, setIsLoggedIn] = useState(localStorage.getItem("authorize") ? true : false); const [currentUser, setUser] = useState(localStorage.getItem("currentUser")); const fakeUser = { "userId": "WgpGQl7XD8WGgetGxvMF", "uid": "v8qSFHyaxgZhAAsCuADn4h5l7G32", "username": "moneypenny", "email": "pstone@example.com", "firstName": "Penny", "middleName": "", "lastName": "Stone", "intro": "Bazinga!", "profile": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus placerat orci eget tortor posuere sagittis. Nullam ullamcorper finibus sapien at imperdiet. Etiam et libero turpis. Vestibulum id lectus efficitur, interdum nibh vitae, semper nisi. Curabitur tellus ante, rutrum sed ullamcorper non, egestas ac massa. Vivamus accumsan tempor sapien nec consectetur. Phasellus vulputate mauris nec metus suscipit, ut vestibulum dui viverra.", "mobile": "", "registeredAt": { "seconds": 1674840783, "nanos": 287000000 }, "lastLogin": null } // Your web app's Firebase configuration const firebaseConfig = { apiKey: "", authDomain: "", projectId: "", storageBucket: "", messagingSenderId: "", appId: "" }; // Initialize Firebase initializeApp(firebaseConfig); const auth = getAuth(); const setCurrentUser = (user)=>{ setUser(user); } const login = async (email, password) => { await signInWithEmailAndPassword(auth,email, password).then( async cred => { let user = cred.user; let res = await user.getIdTokenResult(false); let token = res.token; let headers = {"Authorization": "Bearer " + token} await axios.post('http://localhost:8080/api/login', { "email": email, "password": "" }, { headers: headers, context: document.body }).then((response) => { setCurrentUser(response.data.user) setIsLoggedIn(true); localStorage.setItem("authorize", response.headers.get("X-Auth-Token")); localStorage.setItem("currentUser", JSON.stringify(response.data.user)); }).catch(e => { console.log(e) }) }).catch(e => console.log(e)) }; const register = async (username, password) => { try { await createUserWithEmailAndPassword(username, password); setIsLoggedIn(true); } catch (error) { console.log(error); } }; const logout = async () => { try { await signOut(); setIsLoggedIn(false); // Do something else here, like clear the token from local storage } catch (error) { console.log(error); } }; return ( <AuthContext.Provider value={{ currentUser, isLoggedIn, setCurrentUser, login, register, logout, }} > {children} </AuthContext.Provider> ); }; const AuthConsumer = AuthContext.Consumer; export { AuthContext, AuthProvider, AuthConsumer }; ``` ### Use Authorization Token Finally, now that you have the `X-Auth-Token`, you need to add this a header on each axios call that requires authentication/authorization. #### Example - MyPosts Add the header object to the constructor ```javascript! this.headers = { "X-Auth-Token": localStorage.getItem("authorize") } ``` Then update the axios calls: ```javascript! await axios .get(`http://localhost:8080/api/post/user/${this.userId}`, {headers: this.headers}) ``` ```javascript! await axios .put(`http://localhost:8080/api/post/${postId}`, { published: newValue }, {headers: this.headers}) ``` ```javascript! await axios .delete(`http://localhost:8080/api/post/${postId}`, {headers: this.headers}) ``` Go through all the files and update your axios calls.