IT 시스템 개발에서 사용자 인증(Authentication)과 인가(Authorization)는 마치 건물의 기초 공사와 같습니다. 튼튼하지 않으면 언제든 무너질 수 있는 치명적인 약점이 되죠. 직접 여러 프로젝트를 진행하면서 사용자 ID/PW를 직접 관리하는 방식의 한계와 위험성을 수없이 경험했습니다. 특히 수많은 서비스가 서로 연동되는 현대 웹 환경에서는 더욱 그렇습니다. 혹시 지금도 사용자 인증/인가 시스템 구축 때문에 골머리를 앓고 계신가요? 사용자 정보 보안은 물론, 서비스 확장성과 개발 편의성까지 놓치고 싶지 않다면, OAuth 2.0과 OpenID Connect는 더 이상 선택이 아닌 필수가 되고 있습니다. 제가 직접 이 두 가지 기술을 활용하여 안전하고 유연한 인증/인가 시스템을 구축해 본 경험을 바탕으로, 실전 가이드와 노하우를 공유해 드립니다.
📑 목차
사용자 인증/인가, 왜 이렇게 어렵고 중요할까요?
개발 초기에는 사용자 ID와 비밀번호를 데이터베이스에 저장하고 직접 로그인 로직을 구현하는 것이 가장 쉽고 빠르게 느껴질 수 있습니다. 하지만 서비스가 성장하고 다양한 외부 서비스와 연동하기 시작하면 문제가 발생합니다. 사용자 비밀번호를 직접 관리하는 것은 보안 책임을 온전히 개발팀이 져야 한다는 의미이며, 이는 엄청난 부담입니다. 만약 데이터 유출 사고라도 발생한다면, 서비스의 신뢰도는 바닥으로 떨어지겠죠.
또한, 사용자가 한 번의 로그인으로 여러 서비스에 접근할 수 있게 하는 싱글 사인 온(SSO, Single Sign-On)이나, 특정 서비스에서 다른 서비스의 리소스에 접근할 권한을 부여하는 기능은 직접 구현하기 매우 복잡하고 오류 발생 확률이 높습니다. 예를 들어, 사용자가 내 서비스에 로그인한 후, 이메일 서비스의 주소록을 내 서비스에서 사용하고 싶을 때, 사용자의 이메일 ID/PW를 직접 받아와서 처리하는 것은 상상만 해도 위험하고 비효율적입니다. 이 모든 문제에 대한 해답이 바로 표준화된 인증/인가 프레임워크에 있습니다.
제가 실제로 겪었던 한 사례를 공유하자면, 기존 시스템은 자체 인증 방식을 사용했습니다. 이후 외부 API 연동이 필요해졌을 때, 각 API마다 인증 방식을 맞춰주거나, 보안에 취약한 방식으로 사용자 정보를 주고받아야 하는 상황에 직면했습니다. 결국, 전체 시스템의 인증 부분을 OAuth 2.0과 OpenID Connect 기반으로 리팩토링하는 대규모 작업을 진행해야 했고, 이 과정에서 초기 설계의 중요성을 뼈저리게 느꼈습니다.
OAuth 2.0, 인가(Authorization)의 핵심 플레이어
OAuth 2.0은 본질적으로 인가(Authorization)를 위한 프레임워크입니다. 즉, "어떤 사용자가 어떤 리소스에 접근할 수 있는가?"를 다루죠. 사용자의 ID와 비밀번호를 직접 주고받지 않고도, 안전하게 특정 리소스에 대한 접근 권한을 위임할 수 있게 해줍니다. 쉽게 말해, 사용자가 직접 자신의 비밀번호를 알려주지 않고도, "내 사진 앱이 내 클라우드 저장소에 있는 사진에 접근하는 것을 허용한다"고 승인하는 방식입니다.
OAuth 2.0의 핵심 구성 요소는 다음과 같습니다:
- 리소스 소유자(Resource Owner): 보호된 리소스에 대한 접근 권한을 부여하는 사용자 (예: 나)
- 클라이언트(Client): 리소스 소유자를 대신하여 보호된 리소스에 접근하려는 애플리케이션 (예: 사진 앱)
- 인가 서버(Authorization Server): 클라이언트에게 접근 토큰(Access Token)을 발급하는 서버
- 리소스 서버(Resource Server): 보호된 리소스를 호스팅하며, 접근 토큰을 사용하여 요청을 승인하는 서버 (예: 클라우드 저장소)
가장 널리 사용되는 인가 코드 부여(Authorization Code Grant) 방식의 흐름은 다음과 같습니다.
- 클라이언트가 리소스 소유자를 인가 서버로 리다이렉트하여 권한 요청.
- 리소스 소유자가 인가 서버에서 클라이언트의 리소스 접근을 승인.
- 인가 서버가 클라이언트에게 인가 코드(Authorization Code)를 발급하여 리다이렉트.
- 클라이언트가 인가 코드를 인가 서버에 전달하여 접근 토큰(Access Token) 및 갱신 토큰(Refresh Token) 요청.
- 인가 서버가 클라이언트에게 토큰 발급.
- 클라이언트가 접근 토큰을 사용하여 리소스 서버에 보호된 리소스 요청.
- 리소스 서버가 접근 토큰의 유효성을 검증하고 리소스 제공.
이 과정에서 중요한 점은 클라이언트가 사용자의 비밀번호를 절대 직접 알 필요가 없다는 것입니다. 단지 인가 서버로부터 받은 토큰만으로 리소스에 접근할 수 있습니다. 제가 직접 시스템을 구축하면서 Authorization Code Grant와 더불어 PKCE(Proof Key for Code Exchange)를 적극 활용했습니다. 특히 모바일 앱이나 SPA(Single Page Application)와 같은 Public Client에서 Authorization Code Grant를 사용할 경우, 인가 코드가 중간에 탈취되어 악용될 위험이 있는데, PKCE는 이를 효과적으로 방어해주는 역할을 합니다. PKCE는 인가 코드 요청 시 `code_challenge`를 생성하고, 토큰 요청 시 `code_verifier`를 함께 보내어 일치 여부를 확인하는 방식입니다.
# 인가 코드 요청 (PKCE 적용)
GET /oauth/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=YOUR_REDIRECT_URI&
scope=read_profile%20write_data&
state=RANDOM_STRING&
code_challenge=CODE_CHALLENGE_VALUE&
code_challenge_method=S256
위 코드 예시처럼 `code_challenge`와 `code_challenge_method`를 추가하여 요청하면, 인가 서버는 이 정보를 저장하고 있다가 클라이언트가 토큰을 요청할 때 `code_verifier`와 비교하여 요청의 유효성을 한 번 더 검증합니다. 이로 인해 만약 중간에 인가 코드가 탈취되더라도, `code_verifier`를 모르면 접근 토큰을 발급받을 수 없게 됩니다.
OpenID Connect, 인증(Authentication)의 완벽한 파트너
OAuth 2.0이 인가를 위한 프레임워크라면, OpenID Connect(OIDC)는 OAuth 2.0 위에 구축된 인증(Authentication) 프로토콜입니다. 즉, "사용자가 누구인가?"를 확인하는 데 초점을 맞춥니다. OIDC는 OAuth 2.0의 토큰 교환 메커니즘을 그대로 활용하면서, 사용자 신원 정보를 담은 ID 토큰(ID Token)을 추가로 제공합니다. 이 ID 토큰은 JWT(JSON Web Token) 형식으로 되어 있으며, 사용자의 고유 ID, 이름, 이메일 등 기본적인 프로필 정보를 담고 있어 클라이언트가 사용자를 식별하는 데 사용됩니다.
OIDC의 핵심은 다음과 같습니다.
- ID 토큰(ID Token): 사용자의 신원 정보를 담은 JWT. 클라이언트가 사용자를 인증하고 식별하는 데 사용됩니다.
- 사용자 정보 엔드포인트(UserInfo Endpoint): ID 토큰에 포함되지 않은 추가적인 사용자 프로필 정보를 요청할 수 있는 API 엔드포인트.
- 표준화된 스코프(Standard Scopes): `openid`, `profile`, `email` 등 OIDC에서 정의된 표준 스코프를 통해 클라이언트가 어떤 사용자 정보를 요청할지 명시합니다.
실제 OIDC 흐름은 OAuth 2.0의 인가 코드 부여 방식과 매우 유사합니다. 클라이언트가 인가 서버에 `response_type=code id_token`과 `scope=openid profile email`과 같은 OIDC 스코프를 추가하여 요청하면, 인가 서버는 인가 코드와 함께 ID 토큰을 발급합니다. 클라이언트는 이 ID 토큰을 검증하여 사용자의 신원을 확인할 수 있습니다. ID 토큰은 JWT이기 때문에, 서명(Signature)을 통해 위변조 여부를 쉽게 확인할 수 있으며, 만료 시간(exp), 발급자(iss), 대상(aud) 등의 클레임(Claim)을 통해 유효성을 검증합니다.
// ID 토큰 (JWT 디코딩 시 예시)
{
"iss": "https://auth.example.com",
"sub": "user12345",
"aud": "my_client_id",
"exp": 1678886400,
"iat": 1678882800,
"auth_time": 1678882700,
"nonce": "some_random_nonce",
"name": "홍길동",
"email": "hong.gildong@example.com",
"preferred_username": "gildong"
}
위 예시처럼 ID 토큰에는 사용자의 식별 정보가 포함되어 있어, 클라이언트가 별도의 API 호출 없이 사용자를 인증하고 최소한의 프로필 정보를 얻을 수 있습니다. 저는 OIDC를 도입하면서 JWT 라이브러리를 사용하여 ID 토큰의 서명 검증과 클레임 유효성 검사를 자동화했습니다. 특히 `aud` 클레임이 클라이언트 ID와 일치하는지, `exp` 클레임으로 만료 여부를 확인하는 것은 필수적인 보안 절차입니다. 또한, 리플레이 공격을 방지하기 위해 `nonce` 값을 사용하는 것도 중요합니다.
두 기술의 시너지: 함께 구축하는 안전한 시스템
OAuth 2.0과 OpenID Connect는 상호 보완적인 관계입니다. OAuth 2.0이 리소스 접근 권한을 위임하는 인가에 집중한다면, OpenID Connect는 이 과정에서 사용자의 인증 정보를 클라이언트에 안전하게 전달하는 역할을 합니다. 이 둘을 함께 사용함으로써, 우리는 사용자 인증과 인가를 아우르는 강력하고 안전한 시스템을 구축할 수 있습니다.
OAuth 2.0 vs OpenID Connect 비교
| 구분 | OAuth 2.0 | OpenID Connect |
|---|---|---|
| 주요 목적 | 리소스 인가(Authorization) 위임 | 사용자 인증(Authentication) 및 신원 정보 제공 |
| 무엇을 발급? | Access Token, Refresh Token | ID Token (JWT), Access Token, Refresh Token |
| 무엇에 초점? | 클라이언트가 보호된 리소스에 접근하는 방식 | 사용자의 신원(Identity) 확인 |
| 단독 사용 가능? | 예 (인가 목적으로) | 아니오 (OAuth 2.0 기반 위에서 동작) |
| 주요 사용처 | API 접근 제어, 타사 서비스 연동 | 싱글 사인 온 (SSO), 소셜 로그인, 사용자 인증 |
실제 시스템 구축 흐름
제가 구축한 시스템에서는 다음과 같은 흐름으로 두 기술을 활용했습니다.
- 사용자 로그인 요청: 웹/모바일 클라이언트가 사용자에게 "Google로 로그인", "카카오로 로그인" 등 OIDC/OAuth를 지원하는 ID 공급자(Identity Provider)를 통한 로그인 옵션을 제공합니다.
- 인가 서버 리다이렉션: 사용자가 ID 공급자를 선택하면, 클라이언트는 해당 ID 공급자의 인가 서버로 `scope=openid profile email`과 같은 OIDC 스코프와 함께 인가 코드 요청을 보냅니다. (이때 PKCE 적용)
- 사용자 인증 및 동의: ID 공급자 인가 서버에서 사용자는 자신의 계정으로 로그인하고, 클라이언트가 요청한 정보(프로필, 이메일 등)에 대한 접근을 승인합니다.
- 인가 코드 및 ID 토큰 수신: ID 공급자 인가 서버는 사용자 동의 후, 클라이언트의 리다이렉션 URI로 인가 코드와 함께 ID 토큰(JWT)을 보냅니다.
- 토큰 교환: 클라이언트는 받은 인가 코드를 ID 공급자 인가 서버의 토큰 엔드포인트에 보내 접근 토큰, 갱신 토큰, 그리고 ID 토큰을 다시 요청합니다.
- 사용자 인증 및 세션 생성: 클라이언트는 수신한 ID 토큰의 유효성을 검증(서명, 만료 시간, 발급자, 대상 등 확인)하여 사용자를 인증하고, 자체 서비스 내부에 사용자 세션을 생성합니다. 이때 ID 토큰의 `sub` 클레임을 사용하여 사용자를 고유하게 식별하고, 필요한 경우 `UserInfo` 엔드포인트에서 추가 정보를 가져옵니다.
- 리소스 접근: 이후 클라이언트는 발급받은 접근 토큰을 사용하여 자체 리소스 서버(API 서버) 또는 외부 연동 서비스의 API에 접근합니다. 리소스 서버는 접근 토큰의 유효성을 검증하여 인가를 처리합니다.
이러한 통합 흐름을 통해, 사용자 인증은 ID 공급자에게 위임하고, 우리 서비스는 오직 필요한 리소스에 대한 인가만 관리하게 되어 보안 취약점을 줄이고, 개발 복잡도를 현저히 낮출 수 있었습니다. 특히 ID 공급자가 제공하는 최신 보안 기능을 자동으로 활용할 수 있다는 점이 큰 장점이었습니다.
실제 구현 시 고려사항과 팁
이론은 쉽지만, 실제 시스템에 적용할 때는 여러 가지 고려사항이 발생합니다. 제가 직접 겪었던 경험을 바탕으로 몇 가지 팁을 공유합니다.
1. 토큰 관리 전략
- 접근 토큰(Access Token): 수명이 짧게 설정하는 것이 보안상 유리합니다 (예: 1시간). 클라이언트 측에서는 이 토큰을 메모리나 안전한 저장소(HTTP Only Cookie, Web Worker 내부 저장소 등)에 보관해야 합니다. 저는 HTTP Only Cookie를 사용하여 XSS 공격의 위험을 최소화했습니다.
- 갱신 토큰(Refresh Token): 수명이 길지만, 접근 토큰보다 더 강력하게 보호되어야 합니다. 탈취 시 더 큰 위험을 초래할 수 있기 때문입니다. 갱신 토큰은 반드시 DB에 저장하고, 사용 시마다 재사용 여부를 확인하고, 일회성 사용(Refresh Token Rotation)을 강제하는 것이 좋습니다. 즉, 갱신 토큰을 사용하여 새로운 접근 토큰을 발급받을 때, 기존 갱신 토큰은 무효화하고 새로운 갱신 토큰을 발급하는 방식입니다.
- ID 토큰(ID Token): 클라이언트가 사용자를 인증하는 데 사용되며, 한 번 검증 후에는 더 이상 사용되지 않으므로, 장기간 저장할 필요는 없습니다.
초기에 갱신 토큰 관리를 단순히 로컬 스토리지에 저장하는 방식으로 구현했다가, 보안 취약점 진단에서 지적을 받은 경험이 있습니다. 결국 서버 측에서 갱신 토큰을 관리하고, Refresh Token Rotation을 적용하여 보안 수준을 크게 강화했습니다. 이 과정에서 토큰 재발급 로직을 구현하는 데 추가적인 노력이 필요했지만, 장기적인 관점에서는 필수적인 투자였습니다.
2. 에러 처리 및 사용자 경험
인증/인가 과정에서 네트워크 오류, 토큰 만료, 잘못된 요청 등 다양한 에러가 발생할 수 있습니다. 각 단계별로 예상 가능한 에러 시나리오를 정의하고, 사용자에게 친절하게 안내하거나 자동으로 재시도하는 로직을 구현해야 합니다. 예를 들어, 접근 토큰이 만료되었을 경우, 갱신 토큰을 사용하여 자동으로 재발급을 시도하고, 이마저도 실패하면 사용자에게 재로그인을 요청하는 방식으로 구현했습니다. 이 과정에서 사용자에게 불필요한 불편함을 주지 않으면서도 보안을 유지하는 균형점을 찾는 것이 중요했습니다.
3. 보안 강화 방안
- CORS(Cross-Origin Resource Sharing) 설정: API 서버에서 허용되는 Origin을 명확히 지정하여 비인가된 도메인으로부터의 요청을 차단합니다.
- HTTPS 강제: 모든 통신은 반드시 HTTPS를 통해 이루어져야 합니다. 토큰과 같은 민감한 정보가 평문으로 노출되는 것을 방지합니다.
- 클라이언트 비밀(Client Secret) 관리: 클라이언트 시크릿은 절대 클라이언트 측 코드에 노출되어서는 안 됩니다. 서버 측에서 안전하게 관리하고, 필요한 경우 환경 변수나 비밀 관리 시스템을 사용해야 합니다.
- 로그 모니터링: 인증/인가 관련 로그를 상세하게 기록하고 모니터링하여 비정상적인 접근 시도를 빠르게 감지하고 대응할 수 있도록 합니다.
저희 팀은 클라이언트 시크릿을 Git에 실수로 커밋할 뻔한 아찔한 경험을 했습니다. 다행히 코드 리뷰 단계에서 발견되어 큰 문제로 이어지지는 않았지만, 이런 기본적인 보안 수칙을 철저히 지키는 것이 얼마나 중요한지 다시 한번 깨달았습니다. 결국, 환경 변수와 CI/CD 파이프라인을 통해 클라이언트 시크릿을 안전하게 주입하는 시스템을 구축했습니다.
직접 구축해 본 결과와 얻은 교훈
OAuth 2.0과 OpenID Connect를 도입하여 사용자 인증 및 인가 시스템을 구축한 결과, 다음과 같은 긍정적인 변화를 경험할 수 있었습니다.
- 보안성 강화: 사용자 비밀번호를 직접 관리하지 않게 되면서 보안 부담이 크게 줄었습니다. ID 공급자의 강력한 보안 인프라를 활용할 수 있게 되었고, 토큰 기반 인증 덕분에 각 요청의 보안 취약점도 줄었습니다.
- 개발 효율성 증대: 복잡한 인증/인가 로직을 직접 구현할 필요가 없어지면서, 개발팀은 핵심 비즈니스 로직에 집중할 수 있게 되었습니다. 평균적으로 한 달 이상 걸렸던 인증 모듈 개발 기간이 1주일 내외로 단축되었습니다.
- 확장성 및 유연성 확보: 새로운 외부 서비스 연동이나 소셜 로그인 추가가 훨씬 용이해졌습니다. 표준 프로토콜을 사용하기 때문에, 다양한 ID 공급자와의 통합이 매끄럽게 이루어졌습니다.
- 사용자 경험 개선: 싱글 사인 온(SSO) 기능 덕분에 사용자는 여러 서비스에 일일이 로그인할 필요 없이 편리하게 접근할 수 있게 되었습니다. 이는 사용자 만족도 향상으로 이어졌습니다.
물론 도입 초기에는 OAuth 2.0의 다양한 Grant Type과 OIDC의 JWT 구조, 토큰 검증 로직 등을 이해하는 데 시간이 필요했습니다. 특히 `scope`나 `claim`의 정확한 의미를 파악하고, 각 토큰의 생명주기를 어떻게 관리할 것인지 설계하는 과정에서 많은 고민이 있었습니다. 하지만 한 번 제대로 구축해 놓으니, 이후 유지보수와 기능 확장이 훨씬 수월해졌습니다.
가장 큰 교훈은 "표준을 따르는 것이 장기적으로 가장 효율적이고 안전한 길"이라는 것입니다. 처음에는 복잡해 보일 수 있지만, 이미 수많은 전문가들이 검증하고 개선해 온 프로토콜을 활용하는 것은 잠재적인 위험을 줄이고 안정성을 확보하는 가장 확실한 방법입니다. 특히 보안 분야에서는 직접 모든 것을 구현하기보다는, 검증된 표준과 라이브러리를 적극 활용하는 것이 현명한 선택이라고 생각합니다.
마무리하며: 미래의 인증/인가 시스템을 위한 제언
지금까지 OAuth 2.0과 OpenID Connect를 활용하여 안전한 사용자 인증 및 인가 시스템을 구축한 실전 경험을 공유해 드렸습니다. 이 두 기술은 현대 웹 서비스의 보안과 확장성을 위한 필수적인 요소라고 확신합니다. 사용자 데이터를 안전하게 보호하고, 개발 리소스를 효율적으로 사용하며, 사용자에게는 편리한 경험을 제공하는 데 이보다 좋은 조합은 찾기 어려울 것입니다.
물론, 모든 시스템이 똑같은 방식으로 이 기술들을 적용할 수는 없습니다. 서비스의 특성과 보안 요구사항에 맞춰 적절한 Grant Type을 선택하고, 토큰 관리 전략을 세밀하게 설계해야 합니다. 하지만 제가 공유한 실전 팁과 고려사항들이 여러분의 시스템 구축에 작은 도움이 되기를 바랍니다.
이 글을 읽으면서 궁금한 점이나, 여러분이 겪었던 OAuth 2.0과 OpenID Connect 관련 경험이 있다면 댓글로 자유롭게 공유해 주세요. 함께 고민하고 배우는 과정이 더 나은 시스템을 만드는 데 큰 힘이 될 것입니다.