|
| 1 | +--- |
| 2 | +title: "[Security] Secure Coding(5-2) - OAuth2" |
| 3 | + |
| 4 | +categories: [Security, Secure Coding] |
| 5 | +tags: |
| 6 | + - [Security, Cyberattacks, 보안, 시큐어 코딩, OAuth2, Access Token, Refres Token] |
| 7 | +toc: true |
| 8 | +toc_sticky: true |
| 9 | + |
| 10 | +date: 2025-10-01 |
| 11 | +last_modified_at: 2025-10-01 |
| 12 | +--- |
| 13 | +>🔒 시큐어 코딩 수업 정리 |
| 14 | +
|
| 15 | +## OAuth2 |
| 16 | +📚**<span style="color: #008000">OAuth2</span>**: `사용자 인증(Authentication)`과 `권한 부여(Authorization)`를 **분리**하여, **제3자 애플리케이션이 사용자의 자원(Resource)에 안전하게 접근**할 수 있도록 지원하는 프로토콜 |
| 17 | +* 복잡한 암호화나 서명 없이도 **HTTPS 연결**만으로 액세스 토큰을 주고받아 권한을 위임할 수 있다 |
| 18 | +* 가볍고, 애플리케이션 환경에 구애받지 않는 유연성을 확보, 구현 난이도를 크게 낮춤 |
| 19 | + |
| 20 | +* 예: 어떤 쇼핑몰에서 ‘구글 계정으로 로그인’ 버튼을 눌렀을 때 쇼핑몰 사이트가 여러분의 구글 비밀번호를 알고 있지 않지만, 구글 인증 서버가 사용자를 대신해 발급해 준 액세스 토큰만 전달받아, 이메일 주소나 프로필 정보를 가져오는 흐름 |
| 21 | + |
| 22 | +### OAuth2 구성 요소 |
| 23 | + |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +#### Client Application (클라이언트) |
| 29 | +* **역할**: 사용자를 대신해 리소스에 접근하려는 앱/웹사이트 |
| 30 | +* **식별정보**: `Client ID` (공개), `Client Secret` (비밀 키) |
| 31 | + |
| 32 | +✅**주요 동작**: |
| 33 | +* 권한 서버에 인증 요청 |
| 34 | +* Authorization Code 받음 |
| 35 | +* Access Token 요청 및 수신 |
| 36 | +* API 호출 |
| 37 | + |
| 38 | +#### Resource Owner (리소스 소유자) |
| 39 | +* **역할**: 실제 데이터의 주인 = 사용자(당신) |
| 40 | + |
| 41 | +✅**주요 동작**: |
| 42 | +* 권한 서버의 로그인 창에서 인증 |
| 43 | +* 클라이언트가 요청한 권한에 동의/거부 |
| 44 | + |
| 45 | +* **중요**: 비밀번호를 클라이언트에 주지 않고, 권한 서버에만 입력 |
| 46 | + |
| 47 | +#### Authorization Server (권한 서버) |
| 48 | +* **역할**: 사용자 인증 + 토큰 발급 |
| 49 | + |
| 50 | +✅**주요 동작**: |
| 51 | +* 사용자 로그인 확인 |
| 52 | +* `Authorization Code` 발급 (임시 코드) |
| 53 | +* `Access Token` 발급 (짧은 유효기간, 실제 API 호출용) |
| 54 | +* `Refresh Token` 발급 (긴 유효기간, 토큰 갱신용) |
| 55 | + |
| 56 | +* **검증**: Client ID/Secret 확인, Scope 검증 |
| 57 | + |
| 58 | +#### Resource Server (리소스 서버) |
| 59 | +* **역할**: 실제 데이터를 저장하고 API 제공 |
| 60 | + |
| 61 | +✅**주요 동작**: |
| 62 | +* Access Token이 유효한가? |
| 63 | +* Token의 scope에 이 API 접근 권한이 있는가? |
| 64 | +* Token이 만료되지 않았는가? |
| 65 | + |
| 66 | +* **동작**: 검증 통과 시 → **데이터 반환** |
| 67 | + |
| 68 | +--- |
| 69 | + |
| 70 | +### OAuth2 인증 흐름: Authorization Code Grant |
| 71 | + |
| 72 | + |
| 73 | + |
| 74 | +1. **사용자**: 애플리케이션에 서비스 접속 시도 |
| 75 | +2. **애플리케이션**: 그 즉시 권한 서버의 인증 요청 페이지를 띄우도록 redirect |
| 76 | +3. **권한 서버**: redirect된 로그인 페이지에서 정보 입력 요청 |
| 77 | +4. **사용자**: 로그인 정보 입력 및 권한 '허용' |
| 78 | +5. **권한 서버:** `Authirization code`, 즉 인가 코드를 발급하여 애플리케이션에 전달 |
| 79 | +6. **애플리케이션:** 받은 인가 코드 기반으로 리소스 서버 액세스 토큰(`Access Token`) 요청을 권한 서버에 보낸다 |
| 80 | + - 이때 Client Secret과 함께 인가 코드를 전송해서 자신을 인증 |
| 81 | +7. **권한 서버:** 이를 검증하고 `Access Token`을 발급 |
| 82 | +8. **애플리케이션:** 발급받은 `Access Token`으로 리소스 서버에 필요한 사용자 리소스 요청 |
| 83 | +9. **리소스 서버:** 방금 발급 받은 `Access Token`이 맞는지 권한서버와 크로스 체크 |
| 84 | +10. 유효하면 OK 응답 받음 |
| 85 | +11. **리소스 서버**: 요청한 리소스 제공 |
| 86 | +12. **애플리케이션:** 서비스 이용 화면 렌더링 후 사용자에게 서비스 제공 |
| 87 | + |
| 88 | + |
| 89 | +> 인증 흐름의 시퀀스 다이어그램 |
| 90 | +
|
| 91 | +--- |
| 92 | + |
| 93 | +### OAuth2의 장단점 |
| 94 | +✅**장점:** |
| 95 | + |
| 96 | +1. **비밀번호 노출 위험 감소** |
| 97 | + * 사용자 비밀번호를 클라이언트에 전달 X(권한서버에서만 관리) → 보안성 향상 |
| 98 | +2. **SSO (Single Sign-On) 지원** |
| 99 | + * 한 번의 로그인으로 여러 App에 접근 가능 → 편의성 향상 |
| 100 | +3. **토큰 기반 인증** |
| 101 | + * Access Token, Refresh Token을 활용하여 세션 관리 및 권한 검증에 효과적 |
| 102 | + * 유효기간을 짧게하여 토큰 탈취 피해 최소화 |
| 103 | +4. **다양한 클라이언트 지원** |
| 104 | + * 여러 클라이언트에 유연하게 적용 가능, 확장성 향상 |
| 105 | +5. **외부 인증 연동 용이** |
| 106 | + |
| 107 | +### OAuth2의 단점 |
| 108 | +❌**단점:** |
| 109 | + |
| 110 | +1. **구현 복잡성** |
| 111 | + * 다양한 인증흐름과 토큰 관리가 복잡하여 잘못 구현하면 보안 취약점이 발생 |
| 112 | +2. **클라이언트 비밀 관리 문제** |
| 113 | +3. **표준 해석 차이** |
| 114 | + * OAuth2는 표준 자체가 유연하게 되어있어서 연동 시스템 간 구현 차이 발생 |
| 115 | +4. **추가 보안 고려 필요** |
| 116 | + * `PKCE`, 토큰 취소(`Token Revocation`), 타임스탬프/Nonce 등 추가 보안 대책이 없으면 리플레이 공격, 토큰 탈취 등의 위험 존재 |
| 117 | +5. **서버 간 신뢰 관계 구성 필요** |
| 118 | + |
| 119 | +* `PKCE`: 중간자 공격을 방어하는 방식 - 인가 코드 교환 보호 |
| 120 | +* `Token Revocation`: 필요할 때 이전에 발급된 토큰 즉시 무효화 가능 |
| 121 | +* `타임스탬프/Nonce`: 일회용 값 사용 - 과거 값 재사용 공격 무효화 |
| 122 | + |
| 123 | +### OAuth와 OAuth2 비교 |
| 124 | + |
| 125 | + |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +## OAuth2 구현 |
| 130 | + |
| 131 | +#### 1. Google Cloud 가입 |
| 132 | +https://cloud.google.com/apis?hl=ko |
| 133 | + |
| 134 | +#### 2. 새 프로젝트 생성 |
| 135 | + |
| 136 | + |
| 137 | + |
| 138 | +#### 3. 서비스 동의 및 시작 |
| 139 | + |
| 140 | + |
| 141 | + |
| 142 | +#### 4. 프로젝트 구성 |
| 143 | +* 앱 정보 > 앱 이름: `OAuth2 Test`, 사용자 지원 이메일: 개인 메일 주소 |
| 144 | +* 대상 > 외부 선택 |
| 145 | +* 연락처 정보: 개인 메일 주소 |
| 146 | +* '만들기' 버튼 선택 |
| 147 | + |
| 148 | +#### 5. 클라이언트 만들기 |
| 149 | + |
| 150 | + |
| 151 | + |
| 152 | +#### 6. 데이터 액세스 설정 |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | +#### 7. 대상 설정 |
| 157 | +* 대상 > 테스트 사용자 > + `ADD USERS`에 본인 메일 주소 입력 |
| 158 | + |
| 159 | +#### 8. 설정 확인 및 테스트 |
| 160 | + |
| 161 | + |
| 162 | + |
| 163 | +* 아래 URL에 클라이언트 ID를 넣어 구글 로그인 창이 뜨는지 확인 |
| 164 | +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 |
| 165 | + |
| 166 | +#### 9. Authorization Code 발급 |
| 167 | +* **Google OAuth API 등록** 필요 |
| 168 | +* 사용자가 구글 로그인을 마치고 나면 `redirect_uri`에 Authorization Code를 응답해 줌 |
| 169 | + |
| 170 | +#### 10. LAB 환경에 소스 추가해서 OAuth2 연동 테스트 |
| 171 | + |
| 172 | + |
| 173 | +> LAB 소스코드 application.properties 설정 추가 |
| 174 | +
|
| 175 | + |
| 176 | + |
| 177 | + |
| 178 | + |
| 179 | + |
| 180 | + |
| 181 | +* 이제 LAB 코드를 실행시키고 앞서 시도해봤던 URL을 입력하여 구글 로그인 창이 나타나며, 계정을 선택하고 진행하면 STS 콘솔에 메시지가 제대로 |
| 182 | +출력되면 성공 |
| 183 | + |
| 184 | + |
| 185 | + |
| 186 | +#### Access Token 발급 |
| 187 | + |
| 188 | + |
| 189 | +> 기존 소셜로그인 메서드도 수정하고, getAccessToken 메서드도 추가 |
| 190 | +
|
| 191 | +```java |
| 192 | +//Authorization Code를 사용하여 Access Token 요청 |
| 193 | +//restTemplate.exchange()를 이용해 Google OAuth 서버와 통신 |
| 194 | + |
| 195 | +private String getAccessToken(String authorizationCode, String registrationId) { |
| 196 | + String clientId = env.getProperty("oauth2." + registrationId + ".client-id"); |
| 197 | + String clientSecret = env.getProperty("oauth2." + registrationId + ".client-secret"); |
| 198 | + String redirectUri = env.getProperty("oauth2." + registrationId + ".redirect-uri"); |
| 199 | + |
| 200 | + MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); |
| 201 | + params.add("code", authorizationCode); |
| 202 | + params.add("client_id", clientId); |
| 203 | + params.add("client_secret", clientSecret); |
| 204 | + params.add("redirect_uri", redirectUri); |
| 205 | + params.add("grant_type", "authorization_code"); |
| 206 | + |
| 207 | + HttpHeaders headers = new HttpHeaders(); |
| 208 | + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| 209 | + |
| 210 | + HttpEntity entity = new HttpEntity(params, headers); |
| 211 | + ResponseEntity<String> response = restTemplate.exchange(tokenUri, HttpMethod.POST, entity, String.class); |
| 212 | + |
| 213 | + return objectMapper.readTree(response.getBody()).get("access_token").asText(); |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +* Authorization code와 함께 HTTP 요청으로 보내서 액세스 토큰을 받아오는 것을 확인 |
| 218 | + |
| 219 | +#### Resource Server에서 유저정보 받기 |
| 220 | + |
| 221 | + |
| 222 | + |
| 223 | +```java |
| 224 | +//Access Token을 Authorization: Bearer 헤더에 포함하여 Google API 호출. |
| 225 | +//유저 ID, 이메일, 이름 등의 정보를 반환. |
| 226 | + |
| 227 | +private JsonNode getUserResource(String accessToken, String registrationId) { |
| 228 | + String resourceUri = env.getProperty("oauth2." + registrationId + ".resource-uri"); |
| 229 | + |
| 230 | + HttpHeaders headers = new HttpHeaders(); |
| 231 | + headers.set("Authorization", "Bearer " + accessToken); |
| 232 | + |
| 233 | + HttpEntity entity = new HttpEntity(headers); |
| 234 | + return restTemplate.exchange(resourceUri, HttpMethod.GET, entity, JsonNode.class).getBody(); |
| 235 | +} |
| 236 | +``` |
| 237 | + |
| 238 | +* 겟 유저 리소스라는 메서드를 추가 |
| 239 | +* 메서드 코드를 보면, HTTP 헤더에 베어러라는 키워드를 넣고, 액세스 토큰을 넣음 |
| 240 | +* 그 응답 값은 JSON 포맷으로 수신되므로, 아이디, 이메일, 닉네임 등을 적절히 추출하여 콘솔에 출력함 |
| 241 | + |
| 242 | +#### 유저 정보 토대로 계정 연동 |
| 243 | + |
| 244 | + |
| 245 | + |
| 246 | +```java |
| 247 | +// 컨트롤러 |
| 248 | +MemberModel member = checkUserId(email); |
| 249 | +if (member == null) { |
| 250 | + throw new RuntimeException("User not found in local DB: " + email); |
| 251 | +} |
| 252 | + |
| 253 | +// 서비스 |
| 254 | +HttpSession session = request.getSession(true); |
| 255 | +session.setAttribute("userId", member.getUserId()); |
| 256 | +session.setAttribute("userName", member.getUserName()); |
| 257 | +response.setHeader("Set-Cookie", "JSESSIONID=" + session.getId() + "; Path=/; HttpOnly; Secure"); |
| 258 | +``` |
| 259 | + |
| 260 | +#### 로그인 성공 |
| 261 | + |
| 262 | + |
| 263 | + |
| 264 | +--- |
| 265 | + |
| 266 | +## OAuth2 보안 위협 및 해결 방법 |
| 267 | + |
| 268 | +### OAuth2 구현 시 발생 가능한 취약점 |
| 269 | +* `Redirect URI` 검증 미흡 - 중간에 공격자가 끼어듦 |
| 270 | +* **State 파라미터** 미사용 |
| 271 | + * State 파라미터: CSRF 방어에 사용하는 랜덤 토큰 |
| 272 | +* `PKCE` 미적용 |
| 273 | + * `PKCE(Proof Key for Code Exchange)`: 모바일 및 공용 클라이언트에서 Authorization 코드 탈취를 막기 위한 보안 기법 |
| 274 | + |
| 275 | +### OAuth2 주요 취약점 사례 |
| 276 | +* **Authorization Code 탈취** |
| 277 | + * 중간자 공격(MITM)이나 오픈 리다이렉트 취약점을 이용해, 인증 코드가 탈취되어 부적절한 토큰 발급 위험 |
| 278 | +* **Access Token 유출** |
| 279 | + * HTTPS 미적용, 불안전한 토큰 저장(예: 로컬 스토리지, 브라우저 캐시 등)으로 토큰 노출 가능 |
| 280 | +* **토큰 재사용 및 리플레이 공격** |
| 281 | + * 만료되지 않은 토큰 또는 중복 요청 처리 미흡으로 인한 리플레이 공격, 부적절한 접근 권한 행사 |
| 282 | +* **클라이언트 시크릿 관리 취약점** |
| 283 | + * 클라이언트 시크릿(Client Secret)이 공개 클라이언트에 노출될 경우, 악의적 사용자가 토큰 발급 시도 가능 |
| 284 | + |
| 285 | +--- |
| 286 | + |
| 287 | +### Access Token 유출 방지 전략 |
| 288 | +* **안전한 전송 및 저장** |
| 289 | + * HTTPS/TLS 적용 |
| 290 | + * 보안 쿠키 사용 |
| 291 | +* **토큰 관리 정책 강화** |
| 292 | + * 짧은 유효기간 |
| 293 | + * Refresh Token 도입 |
| 294 | + * 토큰 취소 및 재발급 |
| 295 | +* **애플리케이션 보안 강화** |
| 296 | + * XSS 방어: 입력값 검증, 인코딩 및 CSP(Content Security Policy) 적용 |
| 297 | + * PKCE 사용: 공개 클라이언트의 경우, PKCE 도입으로 Authorization Code 탈취 방지 |
0 commit comments