diff --git a/backend/src/main/java/kr/momo/controller/attendee/AttendeeController.java b/backend/src/main/java/kr/momo/controller/attendee/AttendeeController.java index 241e198e8..d6fa0c1e2 100644 --- a/backend/src/main/java/kr/momo/controller/attendee/AttendeeController.java +++ b/backend/src/main/java/kr/momo/controller/attendee/AttendeeController.java @@ -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; @@ -15,12 +16,26 @@ @RequiredArgsConstructor public class AttendeeController { + private static final String ACCESS_TOKEN = "ACCESS_TOKEN"; + private final AttendeeService attendeeService; @PostMapping("/api/v1/meetings/{uuid}/login") - public MomoApiResponse login( - @PathVariable String uuid, @RequestBody @Valid AttendeeLoginRequest request - ) { - return new MomoApiResponse<>(attendeeService.login(uuid, request)); + public ResponseEntity login(@PathVariable String uuid, @RequestBody @Valid AttendeeLoginRequest request) { + String token = attendeeService.login(uuid, request); + 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(); } } diff --git a/backend/src/main/java/kr/momo/controller/auth/AuthArgumentResolver.java b/backend/src/main/java/kr/momo/controller/auth/AuthArgumentResolver.java index c09d5d2fe..9a0c65e7f 100644 --- a/backend/src/main/java/kr/momo/controller/auth/AuthArgumentResolver.java +++ b/backend/src/main/java/kr/momo/controller/auth/AuthArgumentResolver.java @@ -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; @@ -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; @@ -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 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(); } } diff --git a/backend/src/main/java/kr/momo/service/attendee/AttendeeService.java b/backend/src/main/java/kr/momo/service/attendee/AttendeeService.java index c16231c5e..731052eea 100644 --- a/backend/src/main/java/kr/momo/service/attendee/AttendeeService.java +++ b/backend/src/main/java/kr/momo/service/attendee/AttendeeService.java @@ -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; @@ -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)); @@ -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()); } } diff --git a/backend/src/main/java/kr/momo/service/attendee/dto/TokenResponse.java b/backend/src/main/java/kr/momo/service/attendee/dto/TokenResponse.java deleted file mode 100644 index 6f06cd7c4..000000000 --- a/backend/src/main/java/kr/momo/service/attendee/dto/TokenResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package kr.momo.service.attendee.dto; - -public record TokenResponse(String token) { -} diff --git a/backend/src/test/java/kr/momo/controller/attendee/AttendeeControllerTest.java b/backend/src/test/java/kr/momo/controller/attendee/AttendeeControllerTest.java index ceb544a4d..8613c405f 100644 --- a/backend/src/test/java/kr/momo/controller/attendee/AttendeeControllerTest.java +++ b/backend/src/test/java/kr/momo/controller/attendee/AttendeeControllerTest.java @@ -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; @@ -33,6 +35,9 @@ class AttendeeControllerTest { @Autowired private MeetingRepository meetingRepository; + @Autowired + private JwtManager jwtManager; + @BeforeEach void setUp() { RestAssured.port = port; @@ -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()); } } diff --git a/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java b/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java index 860a51422..59f54dd52 100644 --- a/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java +++ b/backend/src/test/java/kr/momo/controller/meeting/MeetingControllerTest.java @@ -153,9 +153,9 @@ void lock() { 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.OK.value()); @@ -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()); @@ -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()); @@ -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()); @@ -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()); @@ -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()); @@ -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"); } } diff --git a/backend/src/test/java/kr/momo/controller/schedule/ScheduleControllerTest.java b/backend/src/test/java/kr/momo/controller/schedule/ScheduleControllerTest.java index 320c03851..9ce091962 100644 --- a/backend/src/test/java/kr/momo/controller/schedule/ScheduleControllerTest.java +++ b/backend/src/test/java/kr/momo/controller/schedule/ScheduleControllerTest.java @@ -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 times = List.of(Timeslot.TIME_0100.getLocalTime(), Timeslot.TIME_0130.getLocalTime()); List dateTimes = List.of( @@ -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) @@ -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") diff --git a/backend/src/test/java/kr/momo/service/auth/JwtManagerTest.java b/backend/src/test/java/kr/momo/service/auth/JwtManagerTest.java index 1cee3e690..0ed9ce158 100644 --- a/backend/src/test/java/kr/momo/service/auth/JwtManagerTest.java +++ b/backend/src/test/java/kr/momo/service/auth/JwtManagerTest.java @@ -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 { @@ -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());