Skip to content

Commit

Permalink
Refactor to save refresh token in local storage.
Browse files Browse the repository at this point in the history
  • Loading branch information
andrezz-b committed Jan 28, 2024
1 parent 98bcf39 commit a074c65
Show file tree
Hide file tree
Showing 7 changed files with 31 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ public ResponseEntity<?> loginUser(@Valid @RequestBody LoginDto loginDto, HttpSe
return new ResponseEntity<>(Map.of("message", "Invalid email or password"), HttpStatus.UNAUTHORIZED);
}

response.addCookie(userService.setRefreshTokenCookie(loginResponseData.getRefreshToken()));
return new ResponseEntity<>(
Map.of("token", loginResponseData.getToken(), "active", loginResponseData.isActive()),
Map.of("token", loginResponseData.getToken(), "active", loginResponseData.isActive(), "refreshToken", loginResponseData.getRefreshToken()),
HttpStatus.OK);
}

@GetMapping("/refresh")
public ResponseEntity<?> refreshToken(@CookieValue("refreshToken") String refreshToken) {
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody Map<String, String> body) {
try {
final String refreshToken = body.get("refreshToken");
String token = userService.refreshAccessToken(refreshToken);
boolean active = userService.getUserByToken(token).isActive();
return ResponseEntity.ok(Map.of("token", token, "active", active));
Expand All @@ -66,10 +66,8 @@ public ResponseEntity<?> refreshToken(@CookieValue("refreshToken") String refres
}

@GetMapping("/logout")
public ResponseEntity<Void> logout(@CookieValue(name = "refreshToken", required = false) String refreshToken, HttpServletResponse response) {
if (refreshToken != null) {
response.addCookie(userService.clearRefreshTokenCookie());
}
public ResponseEntity<Void> logout() {
// TODO: Invalidate refresh token
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;

import jakarta.servlet.http.Cookie;
import lombok.AllArgsConstructor;
import lombok.Getter;

Expand Down Expand Up @@ -76,28 +75,6 @@ public static class LoginResponse {
private boolean active;
}

public Cookie clearRefreshTokenCookie() {
return handleRefreshTokenCookie(null, true);
}

public Cookie setRefreshTokenCookie(String token) {
return handleRefreshTokenCookie(token, false);
}

private Cookie handleRefreshTokenCookie(String token, boolean clear) {
Cookie cookie = new Cookie("refreshToken", clear ? null : token);
cookie.setHttpOnly(true);
// This should be set if we setup HTTPS
// cookie.setSecure(true);
final int maxAge = clear
? 0
: (int) (JWTUtils.TOKEN_DURATION.get(TokenType.REFRESH) / 1000);
cookie.setMaxAge(maxAge);
cookie.setPath("/");
//cookie.setAttribute("SameSite", "None");
return cookie;
}

public User finishUserSetup(User user, SetupDto setupDto){
user.setActive(true);
user.setPreferences(setupDto.getPreferences());
Expand Down
19 changes: 14 additions & 5 deletions frontend/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import type { User } from "@/types/User";
import type { LoginData, RegisterData, TokenResponse } from "@/types/Auth";
import type {
LoginData,
RefreshTokenResponse,
RegisterData,
TokenResponse,
} from "@/types/Auth";
import { axiosPublic } from "./config/axios";
import { UseMutationOptions, useMutation } from "@tanstack/react-query";
import { AxiosError } from "axios";

const AuthService = {
useLogin: <T = TokenResponse>(
useLogin: <T = RefreshTokenResponse>(
mutationOptions?: Omit<
UseMutationOptions<T, Error, LoginData>,
"mutationFn"
Expand All @@ -27,12 +32,16 @@ const AuthService = {
useRefreshToken: <T = TokenResponse>(
mutationOptions?: Omit<UseMutationOptions<T>, "mutationFn">
) => {
// When in StrictMode useLocalStorage sometimes returns the default value so this is a workaround
const refreshToken = JSON.parse(localStorage.getItem("refreshToken") ?? "");
return useMutation({
mutationFn: async () => {
try {
const response = await axiosPublic.get<T>("/auth/refresh", {
withCredentials: true,
});
const response = await axiosPublic.post<T>(
"/auth/refresh",
{ refreshToken },
{ withCredentials: true }
);
return response.data;
} catch (e) {
const error = e as AxiosError<Record<string, string>>;
Expand Down
17 changes: 0 additions & 17 deletions frontend/src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,12 @@ export default function useLocalStorage<T>(
}

setValue(item ? JSON.parse(item) : defaultValue);

function handler(e: StorageEvent) {
if (e.key !== key) return;

const lsi = localStorage.getItem(key);
setValue(JSON.parse(lsi ?? ""));
}

window.addEventListener("storage", handler);

return () => {
window.removeEventListener("storage", handler);
};
}, [key, defaultValue]);

const setValueWrap = (value: T) => {
try {
setValue(value);

localStorage.setItem(key, JSON.stringify(value));
if (typeof window !== "undefined") {
window.dispatchEvent(new StorageEvent("storage", { key }));
}
} catch (e) {
console.error(e);
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/hooks/useLogout.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { useNavigate } from "react-router-dom";
import useAuth from "./useAuth";
import AuthService from "@/api/auth";
import useLocalStorage from "./useLocalStorage";

const useLogout = () => {
const { setAuth } = useAuth();
const navigate = useNavigate();
const [, setRefreshToken] = useLocalStorage("refreshToken", "");
const { mutate: logout } = AuthService.useLogout({
onSuccess: () => {
setAuth(null);
setRefreshToken("");
navigate("/login");
},
});
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/types/Auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface TokenResponse {
active: boolean;
}

export interface RefreshTokenResponse extends TokenResponse {
refreshToken: string;
}

export interface RegisterData {
email: string;
password: string;
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/views/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EmailAtIcon, LockIcon } from "../assets";
import { Link, useLocation, useNavigate } from "react-router-dom";
import AuthService from "@/api/auth";
import useAuth from "@/hooks/useAuth";
import useLocalStorage from "@/hooks/useLocalStorage";

const LoginSchema = Yup.object({
email: Yup.string().required("Required").email("Must be valid email"),
Expand All @@ -22,19 +23,21 @@ const LoginForm = () => {
const { setAuth } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const [, setRefreshToken] = useLocalStorage<string>("refreshToken", "");
const { mutateAsync: login } = AuthService.useLogin();
const from = location?.state?.from?.pathname ?? "/";

const handleSubmit = (values: LoginValues) => {
return login(values, {
onSuccess: ({ token, active }) => {
onSuccess: ({ token, active, refreshToken }) => {
setAuth({
user: {
email: values.email,
},
accessToken: token,
active,
});
setRefreshToken(refreshToken);
navigate(from, { replace: true });
},
});
Expand Down

0 comments on commit a074c65

Please sign in to comment.