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

✨ implement refresh token #113

Merged
merged 12 commits into from
Jul 31, 2024
Merged

✨ implement refresh token #113

merged 12 commits into from
Jul 31, 2024

Conversation

HaiSeong
Copy link

이름이나 패키지 변경으로 file changed 수가 많습니다.
생각보다 변경된건 많지 않아요.

  • 로그인 or 회원가입시
    • 클라이언트가 google Id token을 바디에 담아 요청
    • refresh token과 access token을 바디에 담아서 응답
  • 회원 인증 기능 필요시
    • 사용자가 authorization header에 "Bearer {access token}" 을 담아 같이 요청
    • 유효한 토큰인 경우 인자에 @LoginUser UserInfo userInfo에 정보가 담김
  • access token 만료시
    • 만료된 access token으로 회원 인증 기능을 요청하면 401 반환
    • 클라이언트는 refresh api를 호출
      • 이때 refresh token을 바디에 담아 보냄
      • access token과 refresh token을 다시 만들어서 보내줌

@HaiSeong HaiSeong added ✨ feature new feature BE Backend labels Jul 27, 2024
@HaiSeong HaiSeong requested review from geoje and tackyu July 27, 2024 14:03
@HaiSeong HaiSeong self-assigned this Jul 27, 2024
Copy link

Overall Project 91.99% 🍏
Files changed 100% 🍏

File Coverage
GoogleLoginResponse.java 100% 🍏
TokenRefreshRequest.java 100% 🍏
TokenRefreshResponse.java 100% 🍏
GoogleSignUpResponse.java 100% 🍏
LoginController.java 100% 🍏
JwtTokenManager.java 100% 🍏
TokenType.java 100% 🍏
TokenExtractor.java 100% 🍏
TokenPayload.java 100% 🍏
LoginService.java 96.65% 🍏
LoginUserArgumentResolver.java 80.95% 🍏

Copy link

Overall Project 91.99% 🍏
Files changed 100% 🍏

File Coverage
GoogleLoginResponse.java 100% 🍏
TokenRefreshRequest.java 100% 🍏
TokenRefreshResponse.java 100% 🍏
GoogleSignUpResponse.java 100% 🍏
LoginController.java 100% 🍏
JwtTokenManager.java 100% 🍏
TokenType.java 100% 🍏
TokenExtractor.java 100% 🍏
TokenPayload.java 100% 🍏
LoginService.java 96.65% 🍏
LoginUserArgumentResolver.java 80.95% 🍏

@HaiSeong
Copy link
Author

Copy link
Contributor

@geoje geoje left a comment

Choose a reason for hiding this comment

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

properties 가시성만 조금 손 본다면 모든게 좋습니다~~ 주말동안 고생허셨어요!!

@@ -7,6 +7,12 @@ jasypt:
encryptor:
password: ${JASYPT_PASSWORD}

jwt:
access-token:
expire-in-millis: 86400000
Copy link
Contributor

Choose a reason for hiding this comment

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

Duration 클래스로 가독성을 높여 보는 것은 어떻까요? 86400000ms1d 랑 같은 값인 것 같아요.

그리고 받을 때는 타입이 long 이 아닌 Duration 으로 받으면 될 것 같아요!

https://www.baeldung.com/configuration-properties-in-spring-boot#1-duration

Copy link
Author

Choose a reason for hiding this comment

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

오 좋은데요! long을 쓰니 무조건 ms 단위로 썼는데 훨신 좋아보여요!

access-token:
expire-in-millis: 86400000
refresh-token:
expire-in-millis: 2419200000
Copy link
Contributor

Choose a reason for hiding this comment

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

Duration 사용 마찬가지 입니다!

Copy link
Author

Choose a reason for hiding this comment

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

해결했습니다!
1d, 30d로 변경했슴당


public JwtTokenManager(
@Value("${jwt.secret}") String secret,
@Value("${jwt.expire-in-millis}") long tokenExpirationMills
@Value("${jwt.access-token.expire-in-millis}") long accessTokenExpirationMills,
@Value("${jwt.refresh-token.expire-in-millis}") long refreshTokenExpirationMills
Copy link
Contributor

Choose a reason for hiding this comment

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

properties 에 코멘트 남겨놓은 것을 추가하면 여기에 타입으로 long 이 아닌 Duration 을 줄 수 있을 것 같아요!

@@ -48,7 +49,17 @@ void loginWithGoogleWithEmailAlreadyRegistered() throws FirebaseAuthException {
when(firebaseToken.getEmail()).thenReturn(email);
when(firebaseAuth.verifyIdToken(idToken)).thenReturn(firebaseToken);

GoogleLoginResponse actual = RestAssured.given().log().all()
GoogleLoginResponse actual = RestAssured.given(spec).log().all()
.filter(document(DEFAULT_RESTDOCS_PATH,
Copy link
Contributor

Choose a reason for hiding this comment

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

document 메서드의 2, 3번째 인자로 descriptionsummary가 있는데 API Docs 사용자인 안드로이드 측에서 사용하기 쉽도록 내용을 추가해주는건 어떨까요?!

Copy link
Author

Choose a reason for hiding this comment

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

추가 했습니다!

다만 이슈가 조금 있습니다.

    @Test
    @DisplayName("이미 가입된 계정으로 로그인하면 이미 가입되었다고 알리고 access token과 refresh token을 반환한다.")
    void loginWithGoogleWithEmailAlreadyRegistered() throws FirebaseAuthException {
        String email = "[email protected]";
        String idToken = "test.id.token";

        FirebaseToken firebaseToken = mock(FirebaseToken.class);
        when(firebaseToken.getEmail()).thenReturn(email);
        when(firebaseAuth.verifyIdToken(idToken)).thenReturn(firebaseToken);

        GoogleLoginResponse actual = RestAssured.given(spec).log().all()
                .filter(document(DEFAULT_RESTDOCS_PATH,
                        "이미 가입된 계정으로 로그인하여 토큰을 반환합니다.",
                        "구글 로그인 API",
                        requestFields(
                                fieldWithPath("idToken").description("Google ID Token")
                        ),
                        responseFields(
                                fieldWithPath("accessToken").description("JWT Access Token"),
                                fieldWithPath("refreshToken").description("JWT Refresh Token"),
                                fieldWithPath("registered").description("사용자 등록 여부")
                        )
                ))
                .contentType(ContentType.JSON)
                .body(new GoogleLoginRequest(idToken))
                .when().post("/api/oauth/google/login")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(GoogleLoginResponse.class);

        assertAll(
                () -> assertThat(actual.accessToken()).isNotNull(),
                () -> assertThat(actual.refreshToken()).isNotNull(),
                () -> assertThat(actual.registered()).isTrue()
        );
    }

    @Test
    @DisplayName("처음 로그인하면 가입되어 있지 않음을 알리고 access token과 refresh token을 반환하지 않는다.")
    void loginWithGoogleWithEmailNotRegistered() throws FirebaseAuthException {
        String email = "[email protected]";
        String idToken = "test.id.token";

        FirebaseToken firebaseToken = mock(FirebaseToken.class);
        when(firebaseToken.getEmail()).thenReturn(email);
        when(firebaseAuth.verifyIdToken(idToken)).thenReturn(firebaseToken);

        GoogleLoginResponse actual = RestAssured.given(spec).log().all()
                .filter(document(DEFAULT_RESTDOCS_PATH,
                        "처음 로그인하여 등록되지 않은 계정을 알립니다.",
                        "구글 로그인 API",
                        requestFields(
                                fieldWithPath("idToken").description("Google ID Token")
                        ),
                        responseFields(
                                fieldWithPath("accessToken").description("JWT Access Token").optional(),
                                fieldWithPath("refreshToken").description("JWT Refresh Token").optional(),
                                fieldWithPath("registered").description("사용자 등록 여부")
                        )
                ))
                .contentType(ContentType.JSON)
                .body(new GoogleLoginRequest(idToken))
                .when().post("/api/oauth/google/login")
                .then().log().all()
                .statusCode(200)
                .extract()
                .as(GoogleLoginResponse.class);

        assertAll(
                () -> assertThat(actual.accessToken()).isNull(),
                () -> assertThat(actual.refreshToken()).isNull(),
                () -> assertThat(actual.registered()).isFalse()
        );
    }

이렇게 한 메서드에 두가지 테스트가 있을때 description이 달라지게 되는데 뒤에 실행되는 테스트의 description으로 덮어씌워지는것 같습니다.

스크린샷 2024-07-31 오전 1 51 57

이 경우에 두가지 디스크립션이 모두 필요하면 어떻게 해야할까요?

@hyxrxn @geoje

@@ -59,12 +70,13 @@ void loginWithGoogleWithEmailAlreadyRegistered() throws FirebaseAuthException {

assertAll(
Copy link
Contributor

Choose a reason for hiding this comment

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

토큰이 보통 해싱 기반으로 작동되는 것으로 아는데, 길이가 똑같지 않나요?
NotNull 체크 뿐만 아니라 길이 체크도 하면 테스트의 신뢰도가 훨씬 올라갈 것 같아요!

Copy link
Author

@HaiSeong HaiSeong Jul 30, 2024

Choose a reason for hiding this comment

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

제 생각에 페이로드에 담기는 정보의 양에 따라 토큰의 길이가 가변적일것 같습니다.

다만 말씀하신대로 테스트의 신뢰도를 위해서 토큰의 형식을 보여주는지 여부는 필요할것 같네요!

@@ -73,7 +85,17 @@ void loginWithGoogleWithEmailNotRegistered() throws FirebaseAuthException {
when(firebaseToken.getEmail()).thenReturn(email);
when(firebaseAuth.verifyIdToken(idToken)).thenReturn(firebaseToken);

GoogleLoginResponse actual = RestAssured.given().log().all()
GoogleLoginResponse actual = RestAssured.given(spec).log().all()
.filter(document(DEFAULT_RESTDOCS_PATH,
Copy link
Contributor

Choose a reason for hiding this comment

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

여기도 description summary >_<

@@ -84,6 +106,7 @@ void loginWithGoogleWithEmailNotRegistered() throws FirebaseAuthException {

assertAll(
() -> assertThat(actual.accessToken()).isNull(),
() -> assertThat(actual.refreshToken()).isNull(),
() -> assertThat(actual.registered()).isFalse()
Copy link
Contributor

Choose a reason for hiding this comment

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

길이체크...?!?! :)

import org.springframework.web.context.request.NativeWebRequest;

@SpringBootTest
class LoginUserArgumentResolverTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

ArgumentResolver 를 테스트할 수도 있겠군요! 👍

Copy link

Overall Project 92.53% 🍏
Files changed 100% 🍏

File Coverage
GoogleLoginResponse.java 100% 🍏
TokenRefreshRequest.java 100% 🍏
TokenRefreshResponse.java 100% 🍏
GoogleSignUpResponse.java 100% 🍏
UserController.java 100% 🍏
LoginController.java 100% 🍏
JwtTokenManager.java 100% 🍏
TokenType.java 100% 🍏
TokenExtractor.java 100% 🍏
TokenPayload.java 100% 🍏
LoginUserArgumentResolver.java 97.62% 🍏
LoginService.java 96.65% 🍏

Copy link

Overall Project 92.64% 🍏
Files changed 100% 🍏

File Coverage
UserService.java 100% 🍏
GoogleLoginResponse.java 100% 🍏
TokenRefreshRequest.java 100% 🍏
TokenRefreshResponse.java 100% 🍏
GoogleSignUpResponse.java 100% 🍏
UserController.java 100% 🍏
LoginController.java 100% 🍏
JwtTokenManager.java 100% 🍏
TokenType.java 100% 🍏
TokenExtractor.java 100% 🍏
TokenPayload.java 100% 🍏
UsernameCheckResponse.java 100% 🍏
LoginUserArgumentResolver.java 97.62% 🍏
LoginService.java 96.65% 🍏

@HaiSeong
Copy link
Author

이제는 문서를 안줄 수가 없어 이 브랜치에 같이 올립니다.

  • 케이엠의 요청으로 사용자 username 중복 검증 api를 만들었습니다.
  • @WithLoginUserTest 어노테이션이 Authorization Header를 두개 붙이는 버그 수정했습니다.

@HaiSeong HaiSeong merged commit 406a25c into be/dev Jul 31, 2024
1 check passed
@geoje geoje deleted the be/feat/69 branch August 7, 2024 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BE Backend ✨ feature new feature
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

3 participants