Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 기존 jwt 토큰 응답 방식을 쿠키로 변경하고 path 및 보안 설정 #131

Merged
merged 5 commits into from
Aug 1, 2024
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package kr.momo.controller.attendee;

import jakarta.validation.Valid;
import kr.momo.controller.MomoApiResponse;
import kr.momo.service.attendee.AttendeeService;
import kr.momo.service.attendee.dto.AttendeeLoginRequest;
import kr.momo.service.attendee.dto.TokenResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -15,12 +16,26 @@
@RequiredArgsConstructor
public class AttendeeController {

private static final String ACCESS_TOKEN = "ACCESS_TOKEN";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금해서 SET-COOKIE 에 들어갈 key-value 이름 컨벤션도 찾아봤는데 역시나 이상없네요 👍


private final AttendeeService attendeeService;

@PostMapping("/api/v1/meetings/{uuid}/login")
public MomoApiResponse<TokenResponse> login(
@PathVariable String uuid, @RequestBody @Valid AttendeeLoginRequest request
) {
return new MomoApiResponse<>(attendeeService.login(uuid, request));
public ResponseEntity<Void> login(@PathVariable String uuid, @RequestBody @Valid AttendeeLoginRequest request) {
String token = attendeeService.login(uuid, request);
String path = String.format("/api/v1/meetings/%s/", uuid);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 오타가 맞을까요?

Suggested change
String path = String.format("/api/v1/meetings/%s/", uuid);
String path = String.format("/api/v1/meetings/%s", uuid);


return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, createCookie(token, path))
.build();
}

private String createCookie(String value, String path) {
return ResponseCookie.from(ACCESS_TOKEN, value)
.httpOnly(true)
.secure(true)
.path(path)
.build()
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package kr.momo.controller.auth;

import kr.momo.exception.MomoException;
import kr.momo.exception.code.AuthErrorCode;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Optional;
import kr.momo.service.auth.JwtManager;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
Expand All @@ -16,8 +18,7 @@
@RequiredArgsConstructor
public class AuthArgumentResolver implements HandlerMethodArgumentResolver {

private static final String AUTHORIZATION = "Authorization";
private static final String BEARER = "Bearer ";
private static final String ACCESS_TOKEN = "ACCESS_TOKEN";

private final JwtManager jwtManager;

Expand All @@ -34,15 +35,21 @@ public Long resolveArgument(
@NonNull NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
String token = getToken(webRequest);
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
Cookie[] cookies = request.getCookies();
String token = getCookieValue(cookies).orElse("");

return jwtManager.extract(token);
}

private String getToken(NativeWebRequest webRequest) {
String header = webRequest.getHeader(AUTHORIZATION);
if (header == null) {
throw new MomoException(AuthErrorCode.NOT_FOUND_TOKEN);
private Optional<String> getCookieValue(Cookie[] cookies) {
if (cookies == null) {
return Optional.empty();
}
return header.replaceFirst(BEARER, "");

return Arrays.stream(cookies)
.filter(cookie -> ACCESS_TOKEN.equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import kr.momo.exception.MomoException;
import kr.momo.exception.code.MeetingErrorCode;
import kr.momo.service.attendee.dto.AttendeeLoginRequest;
import kr.momo.service.attendee.dto.TokenResponse;
import kr.momo.service.auth.JwtManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -25,7 +24,7 @@ public class AttendeeService {
private final JwtManager jwtManager;

@Transactional
public TokenResponse login(String uuid, AttendeeLoginRequest request) {
public String login(String uuid, AttendeeLoginRequest request) {
Meeting meeting = meetingRepository.findByUuid(uuid)
.orElseThrow(() -> new MomoException(MeetingErrorCode.INVALID_UUID));

Expand All @@ -37,14 +36,14 @@ public TokenResponse login(String uuid, AttendeeLoginRequest request) {
.orElseGet(() -> signup(meeting, name, password));
}

private TokenResponse verifyPassword(Attendee attendee, AttendeePassword password) {
private String verifyPassword(Attendee attendee, AttendeePassword password) {
attendee.verifyPassword(password);
return new TokenResponse(jwtManager.generate(attendee.getId()));
return jwtManager.generate(attendee.getId());
}

private TokenResponse signup(Meeting meeting, AttendeeName name, AttendeePassword password) {
private String signup(Meeting meeting, AttendeeName name, AttendeePassword password) {
Attendee attendee = new Attendee(meeting, name, password, Role.GUEST);
attendeeRepository.save(attendee);
return new TokenResponse(jwtManager.generate(attendee.getId()));
return jwtManager.generate(attendee.getId());
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package kr.momo.controller.attendee;

import static org.assertj.core.api.Assertions.assertThat;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import kr.momo.domain.attendee.Attendee;
import kr.momo.domain.attendee.AttendeeRepository;
import kr.momo.domain.attendee.Role;
import kr.momo.domain.meeting.Meeting;
import kr.momo.domain.meeting.MeetingRepository;
import kr.momo.fixture.AttendeeFixture;
import kr.momo.fixture.MeetingFixture;
import kr.momo.service.attendee.dto.AttendeeLoginRequest;
import kr.momo.service.auth.JwtManager;
import kr.momo.support.IsolateDatabase;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -33,6 +35,9 @@ class AttendeeControllerTest {
@Autowired
private MeetingRepository meetingRepository;

@Autowired
private JwtManager jwtManager;

@BeforeEach
void setUp() {
RestAssured.port = port;
Expand All @@ -42,16 +47,18 @@ void setUp() {
@Test
void login() {
Meeting meeting = meetingRepository.save(MeetingFixture.COFFEE.create());
Attendee attendee = AttendeeFixture.HOST_JAZZ.create(meeting);
attendeeRepository.save(new Attendee(meeting, attendee.name(), attendee.password(), Role.GUEST));
Attendee attendee = attendeeRepository.save(AttendeeFixture.HOST_JAZZ.create(meeting));

AttendeeLoginRequest request = new AttendeeLoginRequest(attendee.name(), attendee.password());

RestAssured.given().log().all()
String token = RestAssured.given().log().all()
.contentType(ContentType.JSON)
.body(request)
.when().post("/api/v1/meetings/{uuid}/login", meeting.getUuid())
.then().log().all()
.statusCode(HttpStatus.OK.value());
.statusCode(HttpStatus.OK.value())
.extract().cookie("ACCESS_TOKEN");

assertThat(jwtManager.extract(token)).isEqualTo(attendee.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ void lock() {
String token = getToken(attendee, meeting);

RestAssured.given().log().all()
.cookie("ACCESS_TOKEN", token)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixture 에 상수로 추가해줘도 될 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋네요 반영하겠습니다.

.contentType(ContentType.JSON)
.pathParam("uuid", meeting.getUuid())
.header("Authorization", "Bearer " + token)
.when().patch("/api/v1/meetings/{uuid}/lock")
.then().log().all()
.statusCode(HttpStatus.OK.value());
Expand All @@ -170,9 +170,9 @@ void lockWithInvalidUUID() {
String token = getToken(attendee, meeting);

RestAssured.given().log().all()
.cookie("ACCESS_TOKEN", token)
.contentType(ContentType.JSON)
.pathParam("uuid", invalidUUID)
.header("Authorization", "Bearer " + token)
.when().patch("/api/v1/meetings/{uuid}/lock")
.then().log().all()
.statusCode(HttpStatus.NOT_FOUND.value());
Expand All @@ -186,9 +186,9 @@ void lockWithNoPermission() {
String token = getToken(attendee, meeting);

RestAssured.given().log().all()
.cookie("ACCESS_TOKEN", token)
.contentType(ContentType.JSON)
.pathParam("uuid", meeting.getUuid())
.header("Authorization", "Bearer " + token)
.when().patch("/api/v1/meetings/{uuid}/lock")
.then().log().all()
.statusCode(HttpStatus.FORBIDDEN.value());
Expand All @@ -202,9 +202,9 @@ void unlock() {
String token = getToken(attendee, meeting);

RestAssured.given().log().all()
.cookie("ACCESS_TOKEN", token)
.contentType(ContentType.JSON)
.pathParam("uuid", meeting.getUuid())
.header("Authorization", "Bearer " + token)
.when().patch("/api/v1/meetings/{uuid}/unlock")
.then().log().all()
.statusCode(HttpStatus.OK.value());
Expand All @@ -219,9 +219,9 @@ void unlockWithInvalidUUID() {
String token = getToken(attendee, meeting);

RestAssured.given().log().all()
.cookie("ACCESS_TOKEN", token)
.contentType(ContentType.JSON)
.pathParam("uuid", invalidUUID)
.header("Authorization", "Bearer " + token)
.when().patch("/api/v1/meetings/{uuid}/unlock")
.then().log().all()
.statusCode(HttpStatus.BAD_REQUEST.value());
Expand All @@ -235,9 +235,9 @@ void unlockWithNoPermission() {
String token = getToken(attendee, meeting);

RestAssured.given().log().all()
.cookie("ACCESS_TOKEN", token)
.contentType(ContentType.JSON)
.pathParam("uuid", meeting.getUuid())
.header("Authorization", "Bearer " + token)
.when().patch("/api/v1/meetings/{uuid}/unlock")
.then().log().all()
.statusCode(HttpStatus.FORBIDDEN.value());
Expand All @@ -252,6 +252,6 @@ private String getToken(Attendee attendee, Meeting meeting) {
.when().post("/api/v1/meetings/{uuid}/login", meeting.getUuid())
.then().log().all()
.statusCode(HttpStatus.OK.value())
.extract().jsonPath().getString("data.token");
.extract().cookie("ACCESS_TOKEN");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ void create() {
.when().post("/api/v1/meetings/{uuid}/login", meeting.getUuid())
.then().log().all()
.statusCode(HttpStatus.OK.value())
.extract().jsonPath().getString("data.token");
.extract().cookie("ACCESS_TOKEN");

List<LocalTime> times = List.of(Timeslot.TIME_0100.getLocalTime(), Timeslot.TIME_0130.getLocalTime());
List<DateTimesCreateRequest> dateTimes = List.of(
Expand All @@ -85,7 +85,7 @@ void create() {
ScheduleCreateRequest scheduleCreateRequest = new ScheduleCreateRequest(attendee.name(), dateTimes);

RestAssured.given().log().all()
.header("Authorization", "Bearer " + token)
.cookie("ACCESS_TOKEN", token)
.pathParam("uuid", meeting.getUuid())
.contentType(ContentType.JSON)
.body(scheduleCreateRequest)
Expand Down Expand Up @@ -130,10 +130,10 @@ void findMySchedule() {
.when().post("/api/v1/meetings/{uuid}/login", meeting.getUuid())
.then().log().all()
.statusCode(HttpStatus.OK.value())
.extract().jsonPath().getString("data.token");
.extract().cookie("ACCESS_TOKEN");

RestAssured.given().log().all()
.header("Authorization", "Bearer " + token)
.cookie("ACCESS_TOKEN", token)
.pathParam("uuid", meeting.getUuid())
.contentType(ContentType.JSON)
.when().get("/api/v1/meetings/{uuid}/attendees/me/schedules")
Expand Down
13 changes: 8 additions & 5 deletions backend/src/test/java/kr/momo/service/auth/JwtManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import kr.momo.fixture.MeetingFixture;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

class JwtManagerTest {

Expand All @@ -30,11 +33,11 @@ void generate() {
assertThat(attendeeId).isEqualTo(attendee.getId());
}

@DisplayName("토큰이 올바르지 않을 경우 예외를 발생시킨다.")
@Test
void throwExceptionForInvalidToken() {
String token = "invalidToken";

@DisplayName("토큰이 null이거나 올바르지 않을 경우 예외를 발생시킨다.")
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"invalidToken"})
void throwExceptionForInvalidToken(String token) {
assertThatThrownBy(() -> jwtManager.extract(token))
.isInstanceOf(MomoException.class)
.hasMessage(AuthErrorCode.INVALID_TOKEN.message());
Expand Down