ํฌ์ŠคํŠธ

OAuth2 Deepdive

OAuth2 Deepdive

๐Ÿ”” Oauth2 ์ ์šฉ ์ „
๐Ÿ”” Oauth2 ์‹ค์Šต

๊ฐœ์š”

์ตœ๊ทผ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์—์„œ ๋„ค์ด๋ฒ„ ๋กœ๊ทธ์ธ์„ ์œ„ํ•ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์„ ์„ค์ •ํ•˜๊ณ  ๋กœ์ง์„ ๊ตฌํ˜„ํ–ˆ๋‹ค. ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ์— ๋งž๊ฒŒ ๊ฐ ๋‹จ๊ณ„๋ณ„๋กœ ์ง„ํ–‰์„ ํ–ˆ์œผ๋‚˜โ€ฆ

์•„๋ฌด๋ฆฌ ๋ด๋„ ๊ณผ์ • ํ•˜๋‚˜ ํ•˜๋‚˜๋ฅผ ๋‚ด ์†์œผ๋กœ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š”๊ฒŒ ๋งž๋Š”๊ฑด๊ฐ€? ๋ผ๋Š” ์˜๋ฌธ์„ ์‹œ์ž‘์œผ๋กœ ์ข€ ๋” ํšจ์œจ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ดค๊ณ , ๊ทธ๋ ‡๊ฒŒ Oauth 2.0์œผ๋กœ ๋กœ๊ทธ์ธํ•˜๋Š” ๋ฐฉ์‹์„ ์ฐพ๊ฒŒ ๋˜์—ˆ๋‹ค.

Oauth2 ์ ์šฉ ์ „

์ฝ”๋“œ ์Šคํƒ€์ผ์„ ๋– ๋‚˜์„œ ์ง€๊ธˆ ์™€์„œ ๋ณด๋‹ˆ ๋งˆ์น˜ Oauth 1.0์ฒ˜๋Ÿผ ๊ตฌํ˜„์„ ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ๋‚˜ ๋˜ํ•œ, ๊ฐ ํ”Œ๋กœ์šฐ๋งˆ๋‹ค ํ•˜๋“œ ์ฝ”๋”ฉ์€ ์ค„์ด๊ณ  ๋ณ€์ˆ˜๋‚˜ ํ•จ์ˆ˜๋ฅผ ์žฌํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด, ๋ณด์•ˆ ์š”์†Œ๋ฅผ ์ƒ๊ฐํ•˜๋ฉด์„œ ๊ตฌํ˜„ํ–ˆ์—ˆ๋‹ค.

์„œ๋“œ ํŒŒํ‹ฐ ๊ด€๋ จ API๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ, ์‘๋‹ต ๊ฐ’์„ Json์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด JsonNode๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ๋™์ผํ•˜๊ฒŒ ์ ์šฉํ•˜๋ ค๊ณ  ๋ณด๋‹ˆ ์†Œ์Šค๊ฐ€ ๋„ˆ๋ฌด ๊ธธ์–ด์ง„ ๊ฒƒ ๊ฐ™๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
private final NaverApiService naverApiService;
private final SnsInfoService snsInfoService;
private final UserService userService;

public String getNaverOauth2LoginUrl(HttpServletRequest request) {
    String authorizeUrl = BASE_URL+"/authorize";
    String callbackUrl = getServiceUrl()+"/login/naver/callback";
    String state = NakjiUtil.generateTokenState();
    request.getSession().setAttribute("state", state);

    return UriComponentsBuilder
            .fromHttpUrl(authorizeUrl)
            .queryParam("response_type", "code")
            .queryParam("client_id", secrets.naver().naverId())
            .queryParam("redirect_uri", callbackUrl)
            .queryParam("state", state)
            .toUriString();
}

public String processNaverLogin(LoginProfile profile, Oauth2AccessToken tokenInfo) {
    SnsInfo checkSnsInfo = snsInfoService.getOrCreateUser(profile, tokenInfo);
    Optional<User> user = userService.getUserByUserId(checkSnsInfo.getSnsId());
    if (user.isPresent()) {
        // Update user info

    } else {
        // Create user
    }
    return "redirect:/";
}

public String processNaverLoginCallback(String state, String code, HttpSession session) {
    Oauth2AccessToken token = getToken(state, code, session);
    if (token == null) {
        throw new BadRequestException(ErrorCode.INVALID_PARAMETER);
    }

    try (Response response = naverApiService.naverProfileConnection(token.tokenType(), token.accessToken())) {
        if (response.status() == 200) {
            JsonNode resultJson = NakjiUtil.readBody(response.body().asInputStream());
            if ("00".equals(resultJson.get("resultcode")) && "success".equals(resultJson.get("message"))) {
                JsonNode profileInfo = resultJson.get("response");
                LoginProfile profile = new LoginProfile("naver",
                        Optional.ofNullable(profileInfo.get("id")).map(JsonNode::asText).orElse(""),
                        Optional.ofNullable(profileInfo.get("nickname")).map(JsonNode::asText).orElse(""),
                        Optional.ofNullable(profileInfo.get("gender")).map(JsonNode::asText).orElse(""),
                        Optional.ofNullable(profileInfo.get("age")).map(JsonNode::asText).orElse("")
                );

                return processNaverLogin(profile, token);
            } else {
                throw new Exception();
            }
        } else {
            log.info("NaverLoginService.getNaverProfileUrl Request Status: {}, Body: {}", response.status(), response.body().toString());
            //throw new BadRequestException("Bad request with status: " + response.status());
        }
    } catch (Exception e) {
        log.error("NaverLoginService.getNaverProfileUrl Error: ", e);
    }

    return "";
}

public Oauth2AccessToken getToken(String state, String code, HttpSession session) {
    try (Response response = getGenerateToken(state, code, session)) {
        if (response.status() == 200) {
            JsonNode resultJson = NakjiUtil.readBody(response.body().asInputStream());
            Oauth2AccessToken resultToken = new Oauth2AccessToken(
                    Optional.ofNullable(resultJson.get("access_token")).map(JsonNode::asText).orElse(""),
                    Optional.ofNullable(resultJson.get("refresh_token")).map(JsonNode::asText).orElse(""),
                    Optional.ofNullable(resultJson.get("token_type")).map(JsonNode::asText).orElse(""),
                    Optional.ofNullable(resultJson.get("expires_in")).map(JsonNode::asInt).orElse(0),
                    Optional.ofNullable(resultJson.get("error")).map(JsonNode::asText).orElse(""),
                    Optional.ofNullable(resultJson.get("error_description")).map(JsonNode::asText).orElse("")
            );

            if ("invalid_request".equals(resultToken.errorCode())) {
                throw new BadRequestException(ErrorCode.INVALID_PARAMETER);
            } else if ("unauthorized_client".equals(resultToken.errorCode())) {
                throw new UnauthorizedException(ErrorCode.INVALID_AUTH_CODE);
            }

            return resultToken;
        } else {
            log.info("NaverLoginService.getToken Request Status: {}, Body: {}", response.status(), response.body().toString());
            //throw new BadRequestException("Bad request with status: " + response.status());
        }
    } catch (Exception e) {
        log.error("NaverLoginService.getToken Error: ", e);
    }

    return null;
}

private Response getGenerateToken(String state, String code, HttpSession session) {
    String storedState = (String)session.getAttribute("state");
    if (!state.equals(storedState)) {
        throw new UnauthorizedException(ErrorCode.INVALID_AUTH_PATH);
    }

    return Feign.builder()
            .encoder(new GsonEncoder())
            .decoder(new GsonDecoder())
            .target(NaverGenerateTokenClient.class, BASE_URL)
            .generateToken(secrets.naver().naverId(), secrets.naver().naverKey(), state, code);
}

Oauth2 ์ ์šฉ ํ›„

gpt์™€ ์ผ๋ถ€ ๋ธ”๋กœ๊ทธ๋ฅผ ์ฐพ์•„๋ณด๋ฉด์„œ ๋‚ด ๋ฐฉ์‹๋Œ€๋กœ ์ ์šฉํ•ด๋ดค๋‹ค. ์šฐ์„  ๋กœ๊ทธ์ธ์€ ์ž˜ ๋˜๊ณ , ์ดˆ๊ธฐ ์„ธํŒ…๋งŒ ํ•ด๋‘๋ฉด ๊ทธ ์ดํ›„์—๋Š” ์†Œ์Šค๋ฅผ ์žฌํ™œ์šฉํ•˜๊ฑฐ๋‚˜ ์กฐ๊ธˆ๋งŒ ๋” ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ํ•ด๊ฒฐ๋  ๊ฒƒ ๊ฐ™๋‹ค.

๋‹ค์Œ์—๋Š” ํŽ˜์ด์ง€๋งˆ๋‹ค ์—ญํ•  ๋ถ€์—ฌํ•˜๋Š” ๊ฒƒ๊ณผ jwt์„ ์ด์šฉํ•œ ๋กœ๊ทธ์ธ์„ ์ถ”๊ฐ€ํ•  ์˜ˆ์ •์ด๋‹ค.

security.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
  security:
    oauth2:
      client:
        registration:
          naver:
            client-id: ${NAVER_CLIENT_ID}
            client-secret: ${NAVER_CLIENT_SECRET}
            authorization-grant-type: authorization_code
            redirect-uri: ${SERVICE_URL}${NAVER_REDIRECT_URI}
            scope: name, nickname, email, gender, birthyear, profile_image
        provider:
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response

SecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(authorize -> authorize
                        ...
                )
                .oauth2Login(oauth2 -> oauth2
                        .loginPage("/login")
                        .defaultSuccessUrl("/")
                        .failureUrl("/login?error")
                        .userInfoEndpoint(userInfo -> userInfo
                                .userService(customOAuth2UserService)
                        )
                )
                .logout(logout -> logout
                        .logoutSuccessUrl("/")
                        .invalidateHttpSession(true)
                        .deleteCookies("JSESSIONID")
                )
                .build();
    }
}

OAuthAttributes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Builder
public record OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, User user) {
    public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
        if ("naver".equals(registrationId)) {
            return ofNaver(userNameAttributeName, attributes);
        } else {
            return null;
        }
    }

    private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
        Map<String, Object> response = (Map<String, Object>) attributes.get("response");

        return OAuthAttributes.builder()
                .attributes(attributes)
                .nameAttributeKey(userNameAttributeName)
                .user(User.builder()
                        .oauthId((String) response.get("id"))
                        .provider("naver")
                        .name((String) response.get("name"))
                        .nickname((String) response.get("nickname"))
                        .profileImage((String) response.get("profile_image"))
                        .email((String) response.get("email"))
                        .gender((String) response.get("gender"))
                        .birthYear((String) response.get("birthYear"))
                        .role(Role.USER)
                        .build())
                .build();
    }

    public User toEntity() {
        return user;
    }
}

OAuth2UserService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final UserRepository userRepository;
    private final HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.getClientRegistration()
                                        .getProviderDetails()
                                        .getUserInfoEndpoint()
                                        .getUserNameAttributeName();

        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
        User user = saveOrUpdate(attributes);
        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
                attributes.getAttributes(),
                attributes.getNameAttributeKey());
    }

    private User saveOrUpdate(OAuthAttributes attributes) {
        User userInfo = attributes.getUser();
        User target = userRepository.findByOauthId(userInfo.getOauthId())
                .map(entity -> entity.update(userInfo.getNickname(), userInfo.getProfileImage(), userInfo.getEmail()))
                .orElse(attributes.toEntity());

        return userRepository.save(target);
    }
}

ํ›„๊ธฐ

์—ฌ๋‹ด์ด์ง€๋งŒ ํ‚ค๊ฐ’์„ ๊ด€๋ฆฌํ•  ๋•Œ, ํ•ด๋‹น ์„ค์ • ํŒŒ์ผ์„ ์ €์žฅ์†Œ์—๋Š” ์ปค๋ฐ‹ํ•˜์ง€ ์•Š์•˜๋‹ค. ๋‹ค๋ฅธ PC์—์„œ ์ ‘์†ํ•  ๋•Œ๋Š” ๊ฐœ์ธ ํด๋ผ์šฐ๋“œ์—์„œ ํ‚ค๊ฐ’์ด ์žˆ๋Š” ํŒŒ์ผ์„ ๋ถˆ๋Ÿฌ์™€์„œ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, ์ด๋ฒˆ์— oauth๋ฅผ ๊ฒฝํ—˜ํ•˜๋ฉด์„œ ์ ์  ๋ถˆํŽธํ•ด์งˆ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.

๊ทธ๋ž˜์„œ dotenv๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๊ธฐ์กด ํ™˜๊ฒฝ์„ค์ • ๊ฐ’๋“ค์€ env ํŒŒ์ผ์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ์‹์œผ๋กœ ์ˆ˜์ •ํ–ˆ๋‹ค.
ํฐ ์ฐจ์ด๋Š” ์—†์„ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋‚ด๊ฐ€ ํ™˜๊ฒฝ์„ค์ •ํ•  ๋•Œ ์–ด๋–ค ๊ตฌ์กฐ๋กœ ์„ค์ •ํ–ˆ๋Š”์ง€๋Š” ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฒƒ์ด ๊ฐœ์„ ์ ์ด๋‹ค.

์ฐธ๊ณ  ์ž๋ฃŒ

Spring io
Velog
ChatGPT

์ด ๊ธฐ์‚ฌ๋Š” ์ €์ž‘๊ถŒ์ž์˜ CC BY 4.0 ๋ผ์ด์„ผ์Šค๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

ยฉ yuuuuuuyu. ์ผ๋ถ€ ๊ถŒ๋ฆฌ ๋ณด์œ 

Powered by Jekyll with Chirpy theme