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 νμΌμμ κ°μ Έμ€λ λ°©μμΌλ‘ μμ νλ€.
ν° μ°¨μ΄λ μμ μ μμ§λ§, λ΄κ° νκ²½μ€μ ν λ μ΄λ€ κ΅¬μ‘°λ‘ μ€μ νλμ§λ λ³Ό μ μκ² λ κ²μ΄ κ°μ μ μ΄λ€.