[Security] Secure Coding(5-2) - OAuth2
๐ ์ํ์ด ์ฝ๋ฉ ์์ ์ ๋ฆฌ
OAuth2
๐OAuth2: ์ฌ์ฉ์ ์ธ์ฆ(Authentication)
๊ณผ ๊ถํ ๋ถ์ฌ(Authorization)
๋ฅผ ๋ถ๋ฆฌํ์ฌ, ์ 3์ ์ ํ๋ฆฌ์ผ์ด์
์ด ์ฌ์ฉ์์ ์์(Resource)์ ์์ ํ๊ฒ ์ ๊ทผํ ์ ์๋๋ก ์ง์ํ๋ ํ๋กํ ์ฝ
- ๋ณต์กํ ์ํธํ๋ ์๋ช ์์ด๋ HTTPS ์ฐ๊ฒฐ๋ง์ผ๋ก ์ก์ธ์ค ํ ํฐ์ ์ฃผ๊ณ ๋ฐ์ ๊ถํ์ ์์ํ ์ ์๋ค
๊ฐ๋ณ๊ณ , ์ ํ๋ฆฌ์ผ์ด์ ํ๊ฒฝ์ ๊ตฌ์ ๋ฐ์ง ์๋ ์ ์ฐ์ฑ์ ํ๋ณด, ๊ตฌํ ๋์ด๋๋ฅผ ํฌ๊ฒ ๋ฎ์ถค
- ์: ์ด๋ค ์ผํ๋ชฐ์์ โ๊ตฌ๊ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธโ ๋ฒํผ์ ๋๋ ์ ๋ ์ผํ๋ชฐ ์ฌ์ดํธ๊ฐ ์ฌ๋ฌ๋ถ์ ๊ตฌ๊ธ ๋น๋ฐ๋ฒํธ๋ฅผ ์๊ณ ์์ง ์์ง๋ง, ๊ตฌ๊ธ ์ธ์ฆ ์๋ฒ๊ฐ ์ฌ์ฉ์๋ฅผ ๋์ ํด ๋ฐ๊ธํด ์ค ์ก์ธ์ค ํ ํฐ๋ง ์ ๋ฌ๋ฐ์, ์ด๋ฉ์ผ ์ฃผ์๋ ํ๋กํ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ํ๋ฆ
OAuth2 ๊ตฌ์ฑ ์์
Client Application (ํด๋ผ์ด์ธํธ)
- ์ญํ : ์ฌ์ฉ์๋ฅผ ๋์ ํด ๋ฆฌ์์ค์ ์ ๊ทผํ๋ ค๋ ์ฑ/์น์ฌ์ดํธ
- ์๋ณ์ ๋ณด:
Client ID
(๊ณต๊ฐ),Client Secret
(๋น๋ฐ ํค)
โ ์ฃผ์ ๋์:
- ๊ถํ ์๋ฒ์ ์ธ์ฆ ์์ฒญ
- Authorization Code ๋ฐ์
- Access Token ์์ฒญ ๋ฐ ์์
- API ํธ์ถ
Resource Owner (๋ฆฌ์์ค ์์ ์)
- ์ญํ : ์ค์ ๋ฐ์ดํฐ์ ์ฃผ์ธ = ์ฌ์ฉ์(๋น์ )
โ ์ฃผ์ ๋์:
- ๊ถํ ์๋ฒ์ ๋ก๊ทธ์ธ ์ฐฝ์์ ์ธ์ฆ
ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญํ ๊ถํ์ ๋์/๊ฑฐ๋ถ
- ์ค์: ๋น๋ฐ๋ฒํธ๋ฅผ ํด๋ผ์ด์ธํธ์ ์ฃผ์ง ์๊ณ , ๊ถํ ์๋ฒ์๋ง ์ ๋ ฅ
Authorization Server (๊ถํ ์๋ฒ)
- ์ญํ : ์ฌ์ฉ์ ์ธ์ฆ + ํ ํฐ ๋ฐ๊ธ
โ ์ฃผ์ ๋์:
- ์ฌ์ฉ์ ๋ก๊ทธ์ธ ํ์ธ
Authorization Code
๋ฐ๊ธ (์์ ์ฝ๋)Access Token
๋ฐ๊ธ (์งง์ ์ ํจ๊ธฐ๊ฐ, ์ค์ API ํธ์ถ์ฉ)Refresh Token
๋ฐ๊ธ (๊ธด ์ ํจ๊ธฐ๊ฐ, ํ ํฐ ๊ฐฑ์ ์ฉ)- ๊ฒ์ฆ: Client ID/Secret ํ์ธ, Scope ๊ฒ์ฆ
Resource Server (๋ฆฌ์์ค ์๋ฒ)
- ์ญํ : ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ API ์ ๊ณต
โ ์ฃผ์ ๋์:
- Access Token์ด ์ ํจํ๊ฐ?
- Token์ scope์ ์ด API ์ ๊ทผ ๊ถํ์ด ์๋๊ฐ?
Token์ด ๋ง๋ฃ๋์ง ์์๋๊ฐ?
- ๋์: ๊ฒ์ฆ ํต๊ณผ ์ โ ๋ฐ์ดํฐ ๋ฐํ
OAuth2 ์ธ์ฆ ํ๋ฆ: Authorization Code Grant
- ์ฌ์ฉ์: ์ ํ๋ฆฌ์ผ์ด์ ์ ์๋น์ค ์ ์ ์๋
- ์ ํ๋ฆฌ์ผ์ด์ : ๊ทธ ์ฆ์ ๊ถํ ์๋ฒ์ ์ธ์ฆ ์์ฒญ ํ์ด์ง๋ฅผ ๋์ฐ๋๋ก redirect
- ๊ถํ ์๋ฒ: redirect๋ ๋ก๊ทธ์ธ ํ์ด์ง์์ ์ ๋ณด ์ ๋ ฅ ์์ฒญ
- ์ฌ์ฉ์: ๋ก๊ทธ์ธ ์ ๋ณด ์ ๋ ฅ ๋ฐ ๊ถํ โํ์ฉโ
- ๊ถํ ์๋ฒ:
Authirization code
, ์ฆ ์ธ๊ฐ ์ฝ๋๋ฅผ ๋ฐ๊ธํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ๋ฌ - ์ ํ๋ฆฌ์ผ์ด์
: ๋ฐ์ ์ธ๊ฐ ์ฝ๋ ๊ธฐ๋ฐ์ผ๋ก ๋ฆฌ์์ค ์๋ฒ ์ก์ธ์ค ํ ํฐ(
Access Token
) ์์ฒญ์ ๊ถํ ์๋ฒ์ ๋ณด๋ธ๋ค- ์ด๋ Client Secret๊ณผ ํจ๊ป ์ธ๊ฐ ์ฝ๋๋ฅผ ์ ์กํด์ ์์ ์ ์ธ์ฆ
- ๊ถํ ์๋ฒ: ์ด๋ฅผ ๊ฒ์ฆํ๊ณ
Access Token
์ ๋ฐ๊ธ - ์ ํ๋ฆฌ์ผ์ด์
: ๋ฐ๊ธ๋ฐ์
Access Token
์ผ๋ก ๋ฆฌ์์ค ์๋ฒ์ ํ์ํ ์ฌ์ฉ์ ๋ฆฌ์์ค ์์ฒญ - ๋ฆฌ์์ค ์๋ฒ: ๋ฐฉ๊ธ ๋ฐ๊ธ ๋ฐ์
Access Token
์ด ๋ง๋์ง ๊ถํ์๋ฒ์ ํฌ๋ก์ค ์ฒดํฌ - ์ ํจํ๋ฉด OK ์๋ต ๋ฐ์
- ๋ฆฌ์์ค ์๋ฒ: ์์ฒญํ ๋ฆฌ์์ค ์ ๊ณต
- ์ ํ๋ฆฌ์ผ์ด์ : ์๋น์ค ์ด์ฉ ํ๋ฉด ๋ ๋๋ง ํ ์ฌ์ฉ์์๊ฒ ์๋น์ค ์ ๊ณต
์ธ์ฆ ํ๋ฆ์ ์ํ์ค ๋ค์ด์ด๊ทธ๋จ
OAuth2์ ์ฅ๋จ์
โ ์ฅ์ :
- ๋น๋ฐ๋ฒํธ ๋
ธ์ถ ์ํ ๊ฐ์
- ์ฌ์ฉ์ ๋น๋ฐ๋ฒํธ๋ฅผ ํด๋ผ์ด์ธํธ์ ์ ๋ฌ X(๊ถํ์๋ฒ์์๋ง ๊ด๋ฆฌ) โ ๋ณด์์ฑ ํฅ์
- SSO (Single Sign-On) ์ง์
- ํ ๋ฒ์ ๋ก๊ทธ์ธ์ผ๋ก ์ฌ๋ฌ App์ ์ ๊ทผ ๊ฐ๋ฅ โ ํธ์์ฑ ํฅ์
- ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ
- Access Token, Refresh Token์ ํ์ฉํ์ฌ ์ธ์ ๊ด๋ฆฌ ๋ฐ ๊ถํ ๊ฒ์ฆ์ ํจ๊ณผ์
- ์ ํจ๊ธฐ๊ฐ์ ์งง๊ฒํ์ฌ ํ ํฐ ํ์ทจ ํผํด ์ต์ํ
- ๋ค์ํ ํด๋ผ์ด์ธํธ ์ง์
- ์ฌ๋ฌ ํด๋ผ์ด์ธํธ์ ์ ์ฐํ๊ฒ ์ ์ฉ ๊ฐ๋ฅ, ํ์ฅ์ฑ ํฅ์
- ์ธ๋ถ ์ธ์ฆ ์ฐ๋ ์ฉ์ด
OAuth2์ ๋จ์
โ๋จ์ :
- ๊ตฌํ ๋ณต์ก์ฑ
- ๋ค์ํ ์ธ์ฆํ๋ฆ๊ณผ ํ ํฐ ๊ด๋ฆฌ๊ฐ ๋ณต์กํ์ฌ ์๋ชป ๊ตฌํํ๋ฉด ๋ณด์ ์ทจ์ฝ์ ์ด ๋ฐ์
- ํด๋ผ์ด์ธํธ ๋น๋ฐ ๊ด๋ฆฌ ๋ฌธ์
- ํ์ค ํด์ ์ฐจ์ด
- OAuth2๋ ํ์ค ์์ฒด๊ฐ ์ ์ฐํ๊ฒ ๋์ด์์ด์ ์ฐ๋ ์์คํ ๊ฐ ๊ตฌํ ์ฐจ์ด ๋ฐ์
- ์ถ๊ฐ ๋ณด์ ๊ณ ๋ ค ํ์
PKCE
, ํ ํฐ ์ทจ์(Token Revocation
), ํ์์คํฌํ/Nonce ๋ฑ ์ถ๊ฐ ๋ณด์ ๋์ฑ ์ด ์์ผ๋ฉด ๋ฆฌํ๋ ์ด ๊ณต๊ฒฉ, ํ ํฐ ํ์ทจ ๋ฑ์ ์ํ ์กด์ฌ
- ์๋ฒ ๊ฐ ์ ๋ขฐ ๊ด๊ณ ๊ตฌ์ฑ ํ์
PKCE
: ์ค๊ฐ์ ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ๋ ๋ฐฉ์ - ์ธ๊ฐ ์ฝ๋ ๊ตํ ๋ณดํธToken Revocation
: ํ์ํ ๋ ์ด์ ์ ๋ฐ๊ธ๋ ํ ํฐ ์ฆ์ ๋ฌดํจํ ๊ฐ๋ฅํ์์คํฌํ/Nonce
: ์ผํ์ฉ ๊ฐ ์ฌ์ฉ - ๊ณผ๊ฑฐ ๊ฐ ์ฌ์ฌ์ฉ ๊ณต๊ฒฉ ๋ฌดํจํ
OAuth์ OAuth2 ๋น๊ต
OAuth2 ๊ตฌํ
1. Google Cloud ๊ฐ์
https://cloud.google.com/apis?hl=ko
2. ์ ํ๋ก์ ํธ ์์ฑ
3. ์๋น์ค ๋์ ๋ฐ ์์
4. ํ๋ก์ ํธ ๊ตฌ์ฑ
- ์ฑ ์ ๋ณด > ์ฑ ์ด๋ฆ:
OAuth2 Test
, ์ฌ์ฉ์ ์ง์ ์ด๋ฉ์ผ: ๊ฐ์ธ ๋ฉ์ผ ์ฃผ์ - ๋์ > ์ธ๋ถ ์ ํ
- ์ฐ๋ฝ์ฒ ์ ๋ณด: ๊ฐ์ธ ๋ฉ์ผ ์ฃผ์
- โ๋ง๋ค๊ธฐโ ๋ฒํผ ์ ํ
5. ํด๋ผ์ด์ธํธ ๋ง๋ค๊ธฐ
6. ๋ฐ์ดํฐ ์ก์ธ์ค ์ค์
7. ๋์ ์ค์
- ๋์ > ํ
์คํธ ์ฌ์ฉ์ > +
ADD USERS
์ ๋ณธ์ธ ๋ฉ์ผ ์ฃผ์ ์ ๋ ฅ
8. ์ค์ ํ์ธ ๋ฐ ํ ์คํธ
- ์๋ URL์ ํด๋ผ์ด์ธํธ ID๋ฅผ ๋ฃ์ด ๊ตฌ๊ธ ๋ก๊ทธ์ธ ์ฐฝ์ด ๋จ๋์ง ํ์ธ
https://accounts.google.com/o/oauth2/auth?client_id=ํด๋ผ์ด์ธํธID&redirect_uri=http://localhost:8000/login/oauth2/code/google&response_type=code&scope=https://www.googleapis.com/auth/userinfo.email+https://www.googleapis.com/auth/userinfo.profile
9. Authorization Code ๋ฐ๊ธ
- Google OAuth API ๋ฑ๋ก ํ์
- ์ฌ์ฉ์๊ฐ ๊ตฌ๊ธ ๋ก๊ทธ์ธ์ ๋ง์น๊ณ ๋๋ฉด
redirect_uri
์ Authorization Code๋ฅผ ์๋ตํด ์ค
10. LAB ํ๊ฒฝ์ ์์ค ์ถ๊ฐํด์ OAuth2 ์ฐ๋ ํ ์คํธ
LAB ์์ค์ฝ๋ application.properties ์ค์ ์ถ๊ฐ
- ์ด์ LAB ์ฝ๋๋ฅผ ์คํ์ํค๊ณ ์์ ์๋ํด๋ดค๋ URL์ ์ ๋ ฅํ์ฌ ๊ตฌ๊ธ ๋ก๊ทธ์ธ ์ฐฝ์ด ๋ํ๋๋ฉฐ, ๊ณ์ ์ ์ ํํ๊ณ ์งํํ๋ฉด STS ์ฝ์์ ๋ฉ์์ง๊ฐ ์ ๋๋ก ์ถ๋ ฅ๋๋ฉด ์ฑ๊ณต
Access Token ๋ฐ๊ธ
๊ธฐ์กด ์์ ๋ก๊ทธ์ธ ๋ฉ์๋๋ ์์ ํ๊ณ , getAccessToken ๋ฉ์๋๋ ์ถ๊ฐ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Authorization Code๋ฅผ ์ฌ์ฉํ์ฌ Access Token ์์ฒญ
//restTemplate.exchange()๋ฅผ ์ด์ฉํด Google OAuth ์๋ฒ์ ํต์
private String getAccessToken(String authorizationCode, String registrationId) {
String clientId = env.getProperty("oauth2." + registrationId + ".client-id");
String clientSecret = env.getProperty("oauth2." + registrationId + ".client-secret");
String redirectUri = env.getProperty("oauth2." + registrationId + ".redirect-uri");
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", authorizationCode);
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
params.add("redirect_uri", redirectUri);
params.add("grant_type", "authorization_code");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity entity = new HttpEntity(params, headers);
ResponseEntity<String> response = restTemplate.exchange(tokenUri, HttpMethod.POST, entity, String.class);
return objectMapper.readTree(response.getBody()).get("access_token").asText();
}
- Authorization code์ ํจ๊ป HTTP ์์ฒญ์ผ๋ก ๋ณด๋ด์ ์ก์ธ์ค ํ ํฐ์ ๋ฐ์์ค๋ ๊ฒ์ ํ์ธ
Resource Server์์ ์ ์ ์ ๋ณด ๋ฐ๊ธฐ
1
2
3
4
5
6
7
8
9
10
11
12
//Access Token์ Authorization: Bearer ํค๋์ ํฌํจํ์ฌ Google API ํธ์ถ.
//์ ์ ID, ์ด๋ฉ์ผ, ์ด๋ฆ ๋ฑ์ ์ ๋ณด๋ฅผ ๋ฐํ.
private JsonNode getUserResource(String accessToken, String registrationId) {
String resourceUri = env.getProperty("oauth2." + registrationId + ".resource-uri");
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
HttpEntity entity = new HttpEntity(headers);
return restTemplate.exchange(resourceUri, HttpMethod.GET, entity, JsonNode.class).getBody();
}
- ๊ฒ ์ ์ ๋ฆฌ์์ค๋ผ๋ ๋ฉ์๋๋ฅผ ์ถ๊ฐ
- ๋ฉ์๋ ์ฝ๋๋ฅผ ๋ณด๋ฉด, HTTP ํค๋์ ๋ฒ ์ด๋ฌ๋ผ๋ ํค์๋๋ฅผ ๋ฃ๊ณ , ์ก์ธ์ค ํ ํฐ์ ๋ฃ์
- ๊ทธ ์๋ต ๊ฐ์ JSON ํฌ๋งท์ผ๋ก ์์ ๋๋ฏ๋ก, ์์ด๋, ์ด๋ฉ์ผ, ๋๋ค์ ๋ฑ์ ์ ์ ํ ์ถ์ถํ์ฌ ์ฝ์์ ์ถ๋ ฅํจ
์ ์ ์ ๋ณด ํ ๋๋ก ๊ณ์ ์ฐ๋
1
2
3
4
5
6
7
8
9
10
11
// ์ปจํธ๋กค๋ฌ
MemberModel member = checkUserId(email);
if (member == null) {
throw new RuntimeException("User not found in local DB: " + email);
}
// ์๋น์ค
HttpSession session = request.getSession(true);
session.setAttribute("userId", member.getUserId());
session.setAttribute("userName", member.getUserName());
response.setHeader("Set-Cookie", "JSESSIONID=" + session.getId() + "; Path=/; HttpOnly; Secure");
๋ก๊ทธ์ธ ์ฑ๊ณต
OAuth2 ๋ณด์ ์ํ ๋ฐ ํด๊ฒฐ ๋ฐฉ๋ฒ
OAuth2 ๊ตฌํ ์ ๋ฐ์ ๊ฐ๋ฅํ ์ทจ์ฝ์
Redirect URI
๊ฒ์ฆ ๋ฏธํก - ์ค๊ฐ์ ๊ณต๊ฒฉ์๊ฐ ๋ผ์ด๋ฆ- State ํ๋ผ๋ฏธํฐ ๋ฏธ์ฌ์ฉ
- State ํ๋ผ๋ฏธํฐ: CSRF ๋ฐฉ์ด์ ์ฌ์ฉํ๋ ๋๋ค ํ ํฐ
PKCE
๋ฏธ์ ์ฉPKCE(Proof Key for Code Exchange)
: ๋ชจ๋ฐ์ผ ๋ฐ ๊ณต์ฉ ํด๋ผ์ด์ธํธ์์ Authorization ์ฝ๋ ํ์ทจ๋ฅผ ๋ง๊ธฐ ์ํ ๋ณด์ ๊ธฐ๋ฒ
OAuth2 ์ฃผ์ ์ทจ์ฝ์ ์ฌ๋ก
- Authorization Code ํ์ทจ
- ์ค๊ฐ์ ๊ณต๊ฒฉ(MITM)์ด๋ ์คํ ๋ฆฌ๋ค์ด๋ ํธ ์ทจ์ฝ์ ์ ์ด์ฉํด, ์ธ์ฆ ์ฝ๋๊ฐ ํ์ทจ๋์ด ๋ถ์ ์ ํ ํ ํฐ ๋ฐ๊ธ ์ํ
- Access Token ์ ์ถ
- HTTPS ๋ฏธ์ ์ฉ, ๋ถ์์ ํ ํ ํฐ ์ ์ฅ(์: ๋ก์ปฌ ์คํ ๋ฆฌ์ง, ๋ธ๋ผ์ฐ์ ์บ์ ๋ฑ)์ผ๋ก ํ ํฐ ๋ ธ์ถ ๊ฐ๋ฅ
- ํ ํฐ ์ฌ์ฌ์ฉ ๋ฐ ๋ฆฌํ๋ ์ด ๊ณต๊ฒฉ
- ๋ง๋ฃ๋์ง ์์ ํ ํฐ ๋๋ ์ค๋ณต ์์ฒญ ์ฒ๋ฆฌ ๋ฏธํก์ผ๋ก ์ธํ ๋ฆฌํ๋ ์ด ๊ณต๊ฒฉ, ๋ถ์ ์ ํ ์ ๊ทผ ๊ถํ ํ์ฌ
- ํด๋ผ์ด์ธํธ ์ํฌ๋ฆฟ ๊ด๋ฆฌ ์ทจ์ฝ์
- ํด๋ผ์ด์ธํธ ์ํฌ๋ฆฟ(Client Secret)์ด ๊ณต๊ฐ ํด๋ผ์ด์ธํธ์ ๋ ธ์ถ๋ ๊ฒฝ์ฐ, ์ ์์ ์ฌ์ฉ์๊ฐ ํ ํฐ ๋ฐ๊ธ ์๋ ๊ฐ๋ฅ
Access Token ์ ์ถ ๋ฐฉ์ง ์ ๋ต
- ์์ ํ ์ ์ก ๋ฐ ์ ์ฅ
- HTTPS/TLS ์ ์ฉ
- ๋ณด์ ์ฟ ํค ์ฌ์ฉ
- ํ ํฐ ๊ด๋ฆฌ ์ ์ฑ
๊ฐํ
- ์งง์ ์ ํจ๊ธฐ๊ฐ
- Refresh Token ๋์
- ํ ํฐ ์ทจ์ ๋ฐ ์ฌ๋ฐ๊ธ
- ์ ํ๋ฆฌ์ผ์ด์
๋ณด์ ๊ฐํ
- XSS ๋ฐฉ์ด: ์ ๋ ฅ๊ฐ ๊ฒ์ฆ, ์ธ์ฝ๋ฉ ๋ฐ CSP(Content Security Policy) ์ ์ฉ
- PKCE ์ฌ์ฉ: ๊ณต๊ฐ ํด๋ผ์ด์ธํธ์ ๊ฒฝ์ฐ, PKCE ๋์ ์ผ๋ก Authorization Code ํ์ทจ ๋ฐฉ์ง