OAuth Client Credentials: 사용자 없는 M2M 인증 제대로 이해하기

OAuth Client Credentials: 사용자 없는 M2M 인증 제대로 이해하기

지금까지 OAuth를 이야기할 때는 늘 사용자가 주인공이었습니다. 인가 코드(Authorization Code) + PKCE 흐름에서 봤듯이, 사용자가 로그인하고 동의하면 그 사용자를 대신할 토큰이 발급되는 식이었죠.

그런데 현실에는 로그인할 사람이 아예 없는 통신도 많습니다. 새벽 3시에 도는 결제 배치 작업이 결제 API를 호출하고, 백엔드 서비스 A가 내부 서비스 B의 API를 두드리고, CI 파이프라인이 배포 API를 부르는 상황을 떠올려 보세요. 여기엔 브라우저도, 동의 화면도, 사용자도 없습니다.

이렇게 서버가 서버를 부르는, 이른바 머신 투 머신(machine-to-machine, 이하 M2M) 통신을 인증하는 것이 바로 OAuth의 클라이언트 자격 증명(Client Credentials) 그랜트입니다. 여러 그랜트 타입을 훑어본 글에서 짧게 소개했는데, 이번 글에서는 이 흐름 하나만 깊게 파보겠습니다.

사용자가 아니라 클라이언트가 주체입니다

Client Credentials를 이해하는 열쇠는 “권한의 주체가 누구인가”입니다.

Authorization Code 흐름에서 토큰은 “사용자 dale의 캘린더를 읽을 권한”을 나타냈어요. 반면 Client Credentials로 받은 토큰은 사람과 무관합니다. “report-worker라는 서비스가 리포트 API를 읽을 권한”처럼, 클라이언트 애플리케이션 자체가 권한의 주체예요.

그래서 이 흐름에는 사용자를 인가 서버로 리다이렉트하는 단계가 통째로 사라집니다. 동의를 구할 사람이 없으니까요. 클라이언트는 자기 자격 증명만으로 토큰 엔드포인트에 곧장 요청을 보내 액세스 토큰(access token)을 받아 옵니다.

전체 흐름 한눈에 보기

흐름은 지금까지 본 OAuth 그랜트 중 가장 단순합니다.

sequenceDiagram
    autonumber
    participant C as 클라이언트 서비스
    participant AS as 인가 서버
    participant API as 자원 서버 (API)

    C->>AS: 토큰 요청<br/>(grant_type=client_credentials + 클라이언트 인증)
    AS-->>C: access token 발급 (사용자 정보 없음)
    C->>API: access token으로 API 호출
    Note over API: 서명, scope, aud 검증
    API-->>C: 응답

사용자 리다이렉트도, 인가 코드도, 콜백도 없습니다. 토큰을 받아서 API를 부르는 두 동작이 전부예요.

토큰 요청 보내기

클라이언트는 토큰 엔드포인트에 grant_type=client_credentials를 보내면서, 자기가 누구인지를 함께 증명합니다.

Client Credentials 토큰 요청
POST /token HTTP/1.1
Host: as.example.com
Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&scope=reports:read

scope로 필요한 권한 범위를 요청할 수 있는데, 인가 서버는 이 클라이언트에 허용된 범위 안에서만 스코프를 내어줍니다. 응답은 이렇게 돌아옵니다.

토큰 응답
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "reports:read"
}

여기서 눈여겨볼 것은 없는 것들입니다. 사용자를 나타내는 id_token이 없고, refresh_token도 없어요. Client Credentials는 사용자 세션을 오래 유지하는 문제가 아니라서, access token이 만료되면 같은 자격 증명으로 새 토큰을 받으면 그만이거든요.

클라이언트를 인증하는 방법들

토큰 요청에서 가장 중요한 부분은 “이 요청을 보낸 게 정말 그 클라이언트가 맞는가”를 증명하는 일입니다. 사용자 로그인이 없는 만큼, 클라이언트 인증이 이 흐름의 유일한 신뢰 근거이기 때문이에요. 방식은 몇 가지가 있습니다.

가장 흔한 것은 client_secret_basic으로, 위 예시처럼 client_idclient_secret을 HTTP Authorization: Basic 헤더에 실어 보내는 방식입니다. client_secret_post는 같은 값을 헤더 대신 요청 본문에 담는 변형이고요. 둘 다 공유 비밀(shared secret)을 그대로 전송하기 때문에, 반드시 HTTPS 위에서만 써야 합니다.

더 안전한 방식으로는 private_key_jwt가 있습니다. 클라이언트가 자기 개인 키로 서명한 짧은 JWT를 자격 증명으로 제시하는 방식이라, 비밀 값 자체가 네트워크를 오가지 않아요. 여기에 인증서 기반의 상호 TLS(mTLS)까지 더하면, 비밀 유출 위험을 한층 더 줄일 수 있습니다. 민감한 M2M 연동이라면 단순 client_secret보다 private_key_jwt나 mTLS를 우선 고려하는 편이 좋습니다.

받은 토큰 검증하기

토큰을 받아 API를 호출하는 쪽이 클라이언트라면, 그 토큰을 검증하는 쪽은 API를 제공하는 자원 서버입니다.

검증 자체는 사용자 토큰과 다르지 않아요. JWT라면 인가 서버가 공개한 JWKS로 서명을 확인하고, issexp, 그리고 이 토큰이 우리 API를 대상으로 발급됐는지 aud를 확인합니다. 그다음 토큰의 scope에 이 요청이 필요로 하는 권한이 들어 있는지 살피면 되고요.

토큰 엔드포인트나 JWKS 주소 같은 값은 인가 서버가 OAuth 메타데이터로 공개하므로, 클라이언트가 이 정보를 하드코딩하지 않고 자동으로 발견할 수 있습니다. 사용자 컨텍스트가 없다는 점만 빼면, 엔드포인트별 파라미터를 다루는 방식도 다른 그랜트와 같습니다.

API 키를 두고 굳이 OAuth를 쓰는 이유

서버 간 인증이라면 그냥 API 키 하나 발급해서 헤더에 실어 보내면 되지 않나 싶을 수 있어요. 실제로 많은 서비스가 그렇게 하고, 간단한 경우엔 그걸로 충분합니다. 그럼에도 Client Credentials를 택하는 데는 이유가 있는데요.

우선 실제로 API에 실려 다니는 것이 짧게 사는 토큰이라는 점입니다. 장기 비밀인 client_secret은 백엔드 깊숙이 두고, 그걸로 받은 access token은 보통 한 시간 안팎이면 만료돼요. API 키는 한 번 새어 나가면 폐기하기 전까지 계속 유효하지만, 짧은 토큰은 유출되더라도 노출되는 시간 창이 좁습니다. 또한 scope로 권한을 잘게 나눠, 같은 클라이언트라도 작업마다 필요한 만큼만 허용할 수 있고요. 게다가 검증이 표준화돼 있어서, 자원 서버는 JWKS 서명만 확인하면 매 요청마다 중앙 저장소를 조회하지 않아도 됩니다. 여러 서비스의 자격 증명을 인가 서버 한곳에서 발급하고 회전시킬 수 있다는 것도 운영에서는 큰 장점이에요.

흔히 저지르는 실수

첫 번째는 사용자 데이터 접근에 Client Credentials를 쓰려는 것입니다. 이 토큰은 특정 사용자를 대리하지 않으므로, “로그인한 사용자의 파일”에 접근하는 용도로 쓰면 안 됩니다. 사용자 동의가 필요한 데이터라면 Authorization Code + PKCE로 가야 해요.

두 번째는 client_secret을 브라우저나 모바일 앱 같은 공개 클라이언트에 두는 것입니다. 비밀을 숨길 수 없는 환경에서는 애초에 Client Credentials를 쓰면 안 됩니다. 이 흐름은 비밀을 안전하게 보관할 수 있는 백엔드 서비스만의 것이에요.

마지막은 스코프를 지나치게 넓게 잡는 것입니다. M2M 자격 증명은 만료되지 않고 오래 살아 있는 경우가 많아서, 유출되면 피해가 큽니다. 그러니 배치 작업에는 딱 그 작업에 필요한 최소 스코프만 부여하고, 자격 증명 회전(rotation) 계획도 미리 세워 두는 게 좋습니다.

마치며

Client Credentials는 “사용자가 없는 OAuth”입니다. 사람을 대리하는 대신 클라이언트 서비스 자신이 권한의 주체가 되고, 리다이렉트나 동의 없이 자격 증명만으로 토큰을 받아 서버 간 통신을 인증하죠. 그만큼 클라이언트 인증 방식과 비밀 관리가 이 흐름의 보안을 좌우합니다.

내 서비스를 M2M 제공자로 열어 다른 서비스가 이 방식으로 접근하게 하려면, WorkOS Connect로 내 앱을 OAuth 제공자로 만들기에서 M2M 애플리케이션 설정을 함께 살펴보시면 좋습니다.

더 자세한 규격은 OAuth 2.0 원본인 RFC 6749의 Client Credentials Grant 절을 참고하세요.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord