인증 또는 권한 부여: 백엔드에 적합한 솔루션 결정
James Reed
Infrastructure Engineer · Leapcell

소개
백엔드 개발의 활기찬 세계에서 사용자 액세스를 안전하게 관리하는 것은 매우 중요합니다. 세련된 마이크로서비스 아키텍처를 구축하든 모놀리식 엔터프라이즈 애플리케이션을 구축하든 "이 사용자는 누구인가?"와 "이 사용자는 무엇을 할 수 있는가?"라는 질문은 필연적으로 발생합니다. 많은 개발자, 심지어 숙련된 아키텍트조차도 이 두 가지 별개의 질문에 답하기 위해 설계된 메커니즘 간의 개념적 모호함에 직면합니다. 이러한 혼란은 단순한 솔루션의 과잉 엔지니어링부터 더 중요하게는 보안 취약점 도입에 이르기까지 실수를 자주 초래합니다. 인증과 권한 부여의 정확한 역할, 특히 OpenID Connect(OIDC)와 OAuth 2.0이 이 환경에 어떻게 적합하는지 이해하는 것은 단순히 학문적인 것이 아닙니다. 이는 백엔드 시스템의 보안, 확장성 및 유지 관리성에 직접적인 영향을 미칩니다. 이 글은 이러한 개념을 명확히 하여 특정 백엔드 요구 사항에 가장 적합한 솔루션을 선택하도록 안내하는 것을 목표로 합니다.
핵심 개념 설명
OIDC 및 OAuth 2.0의 구체적인 내용으로 넘어가기 전에 이 토론의 기초가 되는 기본 용어에 대한 명확한 이해를 확립해 봅시다.
- 인증(Authentication): 사용자의 신원을 확인하는 프로세스입니다. "당신은 당신이라고 주장하는 사람인가?"라는 질문에 답합니다. 사용자 이름과 비밀번호로 애플리케이션에 로그인할 때 인증하는 것입니다.
 - 권한 부여(Authorization): 인증된 사용자 또는 애플리케이션이 수행할 수 있는 작업을 결정하는 프로세스입니다. "무엇에 액세스하거나 수행할 수 있는가?"라는 질문에 답합니다. 예를 들어, 인증된 사용자는 자신의 프로필을 볼 수는 있지만 다른 사용자의 프로필을 편집할 권한은 없을 수 있습니다.
 - ID 공급자(IdP): 사용자의 ID 정보를 생성, 유지 관리 및 관리한 다음 다른 애플리케이션(서비스 공급자)에 인증 서비스를 제공하는 서비스입니다. 예로는 Google, Facebook 또는 회사 LDAP 서버가 있습니다.
 - 리소스 서버: 보호된 리소스(예: API, 사용자 데이터)를 호스팅하는 서버입니다.
 - 클라이언트 애플리케이션: 사용자를 대신하여 보호된 리소스에 액세스하려는 애플리케이션입니다. 웹 애플리케이션, 모바일 앱 또는 다른 백엔드 서비스가 될 수 있습니다.
 
OAuth 2.0: 권한 부여 프레임워크
OAuth 2.0은 클라이언트 애플리케이션이 리소스 소유자(일반적으로 사용자)를 대신하여 클라이언트에 리소스 소유자의 자격 증명을 절대 노출하지 않고도 보호된 리소스에 "위임된 액세스"를 얻을 수 있도록 하는 권한 부여 프레임워크입니다. OIDC는 사용자 인증을 직접 다루지 않습니다. 대신, 클라이언트가 보호된 리소스에 액세스하는 데 사용되는 액세스 토큰을 얻을 수 있는 안전한 방법을 제공합니다.
OAuth 2.0 작동 방식
OAuth 2.0의 핵심 흐름, 일반적으로 인증 코드 부여 유형은 다음과 같습니다.
- 클라이언트 권한 부여 요청: 클라이언트 애플리케이션(예: 백엔드)이 사용자 브라우저를 인증 서버로 리디렉션하여 특정 리소스(범위)에 대한 액세스를 요청합니다.
 - 사용자 권한 부여: 인증 서버의 인터페이스에서 사용자가 인증 서버에 인증(자격 증명, 예: 사용자 이름/비밀번호 사용)하고 클라이언트 애플리케이션이 요청된 리소스에 액세스하도록 허가합니다.
 - 인증 서버에서 
authorization_code로 리디렉션: 인증 서버가 사용자 브라우저를 클라이언트 애플리케이션으로 일회용authorization_code와 함께 다시 리디렉션합니다. - 클라이언트 코드와 토큰 교환: 클라이언트 애플리케이션이 클라이언트 자격 증명과 
authorization_code를 사용하여 인증 서버의 토큰 엔드포인트로 직접 요청을 보냅니다. - 인증 서버에서 토큰 발행: 인증 서버가 요청을 검증하고 
access_token(및 선택적으로refresh_token)을 발행합니다. - 클라이언트 보호된 리소스 액세스: 클라이언트 애플리케이션이 
access_token을 사용하여 리소스 서버로 요청을 합니다. 
OAuth 2.0 사용 시기
백엔드 애플리케이션이 다음을 수행해야 할 때 OAuth 2.0이 필요합니다.
- 사용자를 대신하여 타사 API에 액세스: 예를 들어, 애플리케이션이 사용자의 Facebook 피드에 게시하거나, Google 캘린더를 보거나, GitHub 계정에서 데이터를 가져오려는 경우입니다. 이 시나리오에서 백엔드는 "클라이언트 애플리케이션"이고 Facebook/Google/GitHub는 "리소스 서버"입니다.
 - 서비스 간의 세분화된 액세스 제어 활성화: 마이크로서비스 아키텍처에서 한 서비스가 다른 서비스의 데이터에 액세스해야 할 수 있지만, 특정 엔드포인트까지만 또는 특정 권한으로만 액세스해야 할 수 있습니다. OAuth 2.0은 이러한 서비스 간 권한 부여를 촉진할 수 있습니다.
 
예시: 타사 API 액세스를 위한 OAuth 2.0 사용(개념적)
Flask와 Python으로 작성된 백엔드가 사용자의 Google Drive 파일에 액세스해야 한다고 가정해 보겠습니다.
# 이것은 매우 단순화되고 개념적인 것입니다. 실제 구현에서는 # Authlib 또는 google-auth-oauthlib와 같은 라이브러리를 사용하고 # 상태, PKCE 등을 처리합니다. from flask import Flask, redirect, url_for, session, request import requests import os app = Flask(__name__) app.secret_key = os.urandom(24) # 강력하고 영구적인 키로 대체하세요. GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID") GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET") GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth" GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token" GOOGLE_DRIVE_API_URL = "https://www.googleapis.com/drive/v3/files" REDIRECT_URI = "http://localhost:5000/callback" @app.route('/') def index(): if 'google_access_token' in session: return f"Hello, you're authenticated with Google. <a href='{url_for('list_drive_files')}'>List Drive Files</a>" return '<a href="/login_google">Login with Google</a>' @app.route('/login_google') def login_google(): params = { "client_id": GOOGLE_CLIENT_ID, "redirect_uri": REDIRECT_URI, "response_type": "code", "scope": "https://www.googleapis.com/auth/drive.readonly", # Drive에 대한 읽기 전용 액세스 요청 "access_type": "offline", # 새로고침 토큰을 얻기 위해 "prompt": "consent" } auth_url = f"{GOOGLE_AUTH_URL}?{'&'.join([f'{k}={v}' for k,v in params.items()])}" return redirect(auth_url) @app.route('/callback') def callback(): code = request.args.get('code') if not code: return "Authorization code missing!", 400 token_payload = { "client_id": GOOGLE_CLIENT_ID, "client_secret": GOOGLE_CLIENT_SECRET, "code": code, "grant_type": "authorization_code", "redirect_uri": REDIRECT_URI } response = requests.post(GOOGLE_TOKEN_URL, data=token_payload) token_data = response.json() if 'access_token' in token_data: session['google_access_token'] = token_data['access_token'] # 선택적으로 새로고침 토큰 저장 (장기 액세스를 위해) # session['google_refresh_token'] = token_data['refresh_token'] return redirect(url_for('index')) return "Failed to get access token.", 500 @app.route('/drive_files') def list_drive_files(): access_token = session.get('google_access_token') if not access_token: return "Not authenticated with Google Drive.", 401 headers = { "Authorization": f"Bearer {access_token}" } response = requests.get(GOOGLE_DRIVE_API_URL + "?q='me' in owners", headers=headers) if response.status_code == 200: files = response.json().get('files', []) file_names = [f['name'] for f in files] return f"Your Google Drive files: {', '.join(file_names)}" else: return f"Error accessing Google Drive: {response.text}", response.status_code if __name__ == '__main__': app.run(debug=True)
이 예에서 Flask 백엔드는 OAuth 2.0 클라이언트 역할을 합니다. 사용자의 인증을 Google에 위임한 다음, 사용자의 허가를 받아 사용자를 대신하여 Google Drive와 상호 작용하기 위한 access_token을 얻습니다. 사용자는 Google 비밀번호를 애플리케이션에 직접 입력할 필요가 없습니다.
OpenID Connect(OIDC): OAuth 2.0 위에서 작동하는 인증
OIDC는 OAuth 2.0 프레임워크 위에 구축된 인증 계층입니다. OAuth 2.0이 권한 부여(위임된 액세스)를 처리하는 반면, OIDC는 인증을 처리하고 최종 사용자의 신원에 대한 검증 가능한 정보를 제공합니다. 이는 목표를 달성하기 위해 OAuth 2.0 흐름을 사용하므로 OAuth 2.0을 이해하면 OIDC를 이해하는 데 절반은 완료된 것입니다.
OIDC 작동 방식
OIDC는 새로운 아티팩트인 ID 토큰을 도입합니다. 이것은 인증된 사용자에 대한 사용자 ID, 이름, 이메일 및 이메일이 확인되었는지 여부와 같은 클레임(증명)을 포함하는 JSON 웹 토큰(JWT)입니다. OIDC 흐름은 일반적으로 다음과 같습니다(OIDC에 대한 인증 코드 흐름 사용).
- 클라이언트 인증 요청: 클라이언트 애플리케이션이 사용자에게 OIDC 공급자(OAuth 2.0 인증 서버이기도 함)로 리디렉션합니다. 요청에는 OIDC 요청을 나타내는 
openid범위와 기타 원하는 범위(예:profile,email)가 포함됩니다. - 사용자 인증 및 동의: 사용자가 OIDC 공급자와 인증하고 클라이언트 애플리케이션과 신원 정보를 공유하는 데 동의합니다.
 - 인증 서버에서 
authorization_code로 리디렉션: OIDC 공급자가 사용자에게authorization_code와 함께 클라이언트 애플리케이션으로 다시 리디렉션합니다. - 클라이언트에서 토큰 교환: 클라이언트 애플리케이션이 
authorization_code를 클라이언트 자격 증명과 함께 OIDC 공급자의 토큰 엔드포인트로 보냅니다. - OIDC 공급자에서 토큰 발행: OIDC 공급자가 
access_token,refresh_token(선택 사항) 및 중요한 **id_token**으로 응답합니다. - 클라이언트 
id_token확인 및 사용: 클라이언트 애플리케이션이id_token을 확인(서명, 발급자, 대상, 만료 확인)하여 사용자 신원을 확인합니다. 확인이 완료되면 클라이언트는id_token클레임에서 사용자 정보를 추출할 수 있습니다. - 클라이언트 
access_token을 사용하여 권한 부여: 추가 리소스 액세스가 요청된 경우(openid범위를 통해), 클라이언트는access_token을 사용하여 보호된 API를 호출할 수 있습니다. 
OIDC 사용 시기
백엔드 애플리케이션이 다음을 수행해야 할 때 OIDC가 필요합니다.
- 사용자 인증: 이것이 주요 사용 사례입니다. 사용자가 Google, GitHub 또는 기타 OIDC 호환 ID를 사용하여 애플리케이션에 로그인하도록 하려면 OIDC가 솔루션입니다.
 - Single Sign-On(SSO) 구현: OIDC는 SSO의 기본입니다. 사용자가 OIDC 공급자와 인증하면 해당 공급자에 연결된 여러 애플리케이션에 다시 인증하지 않고도 원활하게 액세스할 수 있습니다.
 - 사용자 프로필 정보 가져오기: 
id_token은 이름, 이메일, 프로필 사진과 같은 기본 사용자 속성을 ID 공급자로부터 직접 검색하는 표준화된 방법을 제공합니다. 
예시: OIDC를 사용한 사용자 인증(개념적)
백엔드가 Google의 OIDC 기능을 통해 사용자를 인증한 다음 세션을 관리한다고 가정해 보겠습니다.
# 다시, 매우 단순화되었습니다. Authlib와 같은 라이브러리는 이를 크게 단순화합니다. from flask import Flask, redirect, url_for, session, request, jsonify import requests import os import jwt # ID 토큰 검증을 위한 PyJWT app = Flask(__name__) app.secret_key = os.urandom(24) # 강력하고 영구적인 키로 대체하세요. GOOGLE_OIDC_CLIENT_ID = os.environ.get("GOOGLE_OIDC_CLIENT_ID") GOOGLE_OIDC_CLIENT_SECRET = os.environ.get("GOOGLE_OIDC_CLIENT_SECRET") GOOGLE_DISCOVERY_URL = "https://accounts.google.com/.well-known/openid-configuration" REDIRECT_URI = "http://localhost:5000/oidc_callback" # OIDC 메타데이터 가져오기 (예: authorization_endpoint, token_endpoint, jwks_uri) # 실제 앱에서는 이 데이터를 캐시하고 새로 고칩니다. response = requests.get(GOOGLE_DISCOVERY_URL) OIDC_METADATA = response.json() @app.route('/') def index(): if 'user_info' in session: user = session['user_info'] return f"Hello, {user.get('name', user.get('sub'))}! <a href='/logout'>Logout</a>" return '<a href="/login_oidc">Login with Google (OIDC)</a>' @app.route('/login_oidc') def login_oidc(): params = { "client_id": GOOGLE_OIDC_CLIENT_ID, "redirect_uri": REDIRECT_URI, "response_type": "code", "scope": "openid profile email", # OIDC를 위한 필수 'openid' 범위 "access_type": "offline", "prompt": "consent" } auth_url = f"{OIDC_METADATA['authorization_endpoint']}?{'&'.join([f'{k}={v}' for k,v in params.items()])}" return redirect(auth_url) @app.route('/oidc_callback') def oidc_callback(): code = request.args.get('code') if not code: return "Authorization code missing!", 400 token_payload = { "client_id": GOOGLE_OIDC_CLIENT_ID, "client_secret": GOOGLE_OIDC_CLIENT_SECRET, "code": code, "grant_type": "authorization_code", "redirect_uri": REDIRECT_URI } response = requests.post(OIDC_METADATA['token_endpoint'], data=token_payload) token_data = response.json() if 'id_token' in token_data: id_token = token_data['id_token'] # --- ID 토큰 검증 (중요 보안 단계) --- # 1. 발급자의 JWKS를 가져와 서명 검증 jwks_response = requests.get(OIDC_METADATA['jwks_uri']) jwks = jwks_response.json() # 실제 앱에서는 적절한 키 가져오기 및 유효성 검사와 함께 강력한 JWT 라이브러리를 사용합니다. # 이것은 단순화된 예시입니다. try: # jwks에서 올바른 키를 가져와야 합니다. header = jwt.get_unverified_header(id_token) key = None for jwk in jwks['keys']: if jwk['kid'] == header['kid']: key = jwk break if not key: raise ValueError("Matching JWK not found for KID") public_key = jwt.decode(id_token, key, algorithms=[header['alg']], audience=GOOGLE_OIDC_CLIENT_ID, issuer=OIDC_METADATA['issuer'], options={"verify_signature": True}) # 추가 검증 (예: nonce, 만료, 'azp' 클레임)이 여기에 들어갑니다. session['user_info'] = public_key return redirect(url_for('index')) except jwt.exceptions.PyJWTError as e: app.logger.error(f"ID Token verification failed: {e}") return "ID Token verification failed.", 401 return "Failed to get ID token.", 500 @app.route('/logout') def logout(): session.pop('user_info', None) # 선택적으로 액세스/새로고침 토큰이 저장된 경우 취소 return redirect(url_for('index')) if __name__ == '__main__': app.run(debug=True)
여기서 Flask 백엔드는 OIDC를 사용하여 Google을 통해 사용자를 인증합니다. 받은 id_token은 검증되고, 내부에 포함된 클레임은 애플리케이션에서 사용자 세션을 설정하는 데 사용됩니다. 이제 백엔드는 사용자가 누구인지 압니다. access_token(또한 수신됨)은 추가 권한이 요청된 경우 Google API를 호출하는 데 사용될 수 있지만, id_token 만으로도 신원을 확인할 수 있습니다.
인증 대 권한 부여: 명확한 구분
핵심은 OIDC는 인증(이 사용자는 누구인가?)을 위한 것이고 OAuth 2.0은 권한 부여(이 사용자는 무엇을 할 수 있는가?)를 위한 것이라는 점입니다.
- 주요 목표가 외부 ID 공급자(Google, Facebook, Okta, Auth0 등)를 사용하여 사용자가 귀하의 애플리케이션에 로그인하도록 하는 것이라면 OIDC가 필요합니다. 
id_token을 사용하여 신원을 확인합니다. - 주요 목표가 사용자를 대신하여(또는 다른 서비스라도) 다른 애플리케이션의 보호된 리소스에 안전하게 액세스하는 것이라면 OAuth 2.0이 필요합니다. 
access_token을 사용하여 권한이 부여된 요청을 합니다. 
둘 다 함께 사용하는 것이 일반적입니다. 예를 들어, 사용자가 애플리케이션에 로그인하고(OIDC를 통한 인증) 그런 다음 애플리케이션이 Google 캘린더에 액세스해야 할 수 있습니다(OAuth 2.0을 통한 권한 부여). 둘 다 단일 Google 상호 작용을 통해 시작됩니다. 초기 OIDC 흐름은 id_token과 access_token을 모두 반환합니다(적절한 범위가 요청되었다고 가정).
결론
백엔드에 OpenID Connect와 OAuth 2.0 중 하나를 선택하는 것은 기본 질문으로 귀결됩니다. 사용자의 신원을 확인하려는 것입니까, 아니면 사용자가 무엇을 할 수 있는지 관리하려는 것입니까? OIDC는 OAuth 2.0을 기반으로 강력한 인증 기능을 제공하여 사용자 신원을 증명하는 표준화된 방법을 제공합니다. 반대로 OAuth 2.0은 자격 증명을 공유하지 않고 보호된 리소스에 안전하게 액세스할 수 있는 위임된 권한 부여를 위한 업계 표준으로 남아 있습니다. 사용자 로그인 및 신원 정보의 경우 OIDC를 사용하십시오. 외부 API에 대한 위임된 액세스의 경우 OAuth 2.0을 사용하십시오.