Discord OAuth2로 사용자 인증하기

Discord OAuth2로 사용자 인증하기

지금까지 Discord 연동은 대체로 봇의 관점이었습니다. 봇이 서버에서 명령에 반응하고 이벤트를 처리하는 이야기였죠. 이번 글은 시선을 돌려 사용자의 관점을 봅니다. 웹사이트의 “Discord로 로그인” 버튼은 어떻게 동작할까요? 그리고 사실 봇을 서버에 초대할 때 썼던 그 링크도 알고 보면 OAuth2였다는 걸 확인하게 됩니다.

OAuth2가 푸는 문제

내 웹 서비스에서 사용자의 Discord 닉네임이나 가입한 서버 목록을 보여주고 싶다고 해볼게요. 사용자에게 Discord 비밀번호를 달라고 할 수는 없습니다. 대신 사용자가 Discord에 “이 앱이 내 정보를 보는 걸 허락한다”고 동의하면, Discord가 우리 앱에 제한된 접근 권한(토큰)을 발급해 줍니다. 이게 OAuth2의 핵심인 위임이에요. 비밀번호는 절대 우리 손에 들어오지 않습니다.

인가 코드(Authorization Code) 흐름

가장 널리 쓰이는 방식이 인가 코드 흐름입니다. 네 단계로 돌아가요.

먼저 사용자를 Discord의 인가 URL로 보냅니다. 어떤 정보를 요청하는지는 scope에 담아요.

https://discord.com/oauth2/authorize
  ?response_type=code
  &client_id=내_애플리케이션_ID
  &redirect_uri=https://내사이트.com/callback
  &scope=identify%20guilds
  &state=무작위_값

이 링크를 누르면 사용자는 Discord의 동의 화면을 보고, 허락하면 우리가 지정한 redirect_uri로 돌아옵니다. 이때 주소 끝에 ?code=...가 붙어 와요.

https://내사이트.com/callback?code=받은_인가_코드&state=무작위_값

여기서 state는 보안 장치입니다. 우리가 보낸 값과 돌아온 값이 같은지 확인해서, 공격자가 위조한 요청을 걸러내는 CSRF 방어용이에요. 항상 넣는 습관을 들이는 게 좋습니다.

자주 쓰는 스코프

인가 URL의 scope에 무엇을 넣느냐에 따라 받을 수 있는 정보와 권한이 정해집니다. 필요한 만큼만 요청하는 게 원칙이에요. 사용자가 동의 화면에서 보는 항목이 곧 이 스코프거든요.

  • identify: 사용자 기본 프로필(이름, 아바타). 로그인 구현의 최소 단위입니다.
  • email: 이메일 주소까지. identify와 함께 자주 묶어 씁니다.
  • guilds: 사용자가 가입한 서버 목록.
  • guilds.join: 봇이 사용자를 특정 서버에 자동으로 추가.
  • connections: 연결된 외부 계정(트위치, 스팀 등).
  • applications.commands: 앱의 슬래시 커맨드를 사용자 단위로 설치.
  • bot: 봇을 서버에 추가(잠시 뒤 다룰 초대 링크용).
  • role_connections.write: 연결된 역할(linked roles) 메타데이터 갱신.

여러 스코프를 요청할 때는 URL에서 공백(%20)으로 구분합니다. scope=identify%20email%20guilds처럼요.

코드를 토큰으로 교환하기

받은 code는 그 자체로는 쓸모가 없고, 우리 서버 뒤에서 액세스 토큰(access token)으로 교환해야 합니다. 이때 처음으로 client_secret이 등장하는데, 이름 그대로 비밀이라 절대 프런트엔드에 노출하면 안 되고 서버에서만 다뤄야 해요.

const res = await fetch("https://discord.com/api/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code, // 콜백으로 받은 코드
    redirect_uri: "https://내사이트.com/callback",
    client_id: process.env.DISCORD_CLIENT_ID,
    client_secret: process.env.DISCORD_CLIENT_SECRET,
  }),
});

const { access_token, refresh_token, expires_in } = await res.json();

응답에는 access_token과 함께 refresh_token, 만료까지 남은 시간 expires_in이 옵니다. 액세스 토큰은 수명이 짧아서, 만료되면 refresh_token으로 새로 발급받아요.

사용자 정보 가져오기

이제 액세스 토큰으로 사용자의 정보를 읽을 수 있습니다. REST API와 똑같은데, 인증 헤더만 Bot 대신 Bearer를 씁니다.

const user = await fetch("https://discord.com/api/v10/users/@me", {
  headers: { Authorization: `Bearer ${access_token}` },
}).then((r) => r.json());

console.log(user.username); // 로그인한 사용자의 이름

앞서 정리한 스코프에 따라 받을 수 있는 정보가 정해집니다. 사용자가 동의하지 않은 범위는 절대 넘어오지 않아요.

액세스 토큰 갱신하기

액세스 토큰은 수명이 짧아서 보통 일주일이면 만료됩니다(남은 시간이 expires_in에 초 단위로 담겨 와요). 만료될 때마다 사용자를 다시 로그인시킬 수는 없으니, 받아둔 refresh_token으로 새 토큰을 발급받습니다. 앞의 교환 코드에서 grant_typerefresh_token으로 바꾸면 돼요.

const res = await fetch("https://discord.com/api/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "refresh_token",
    refresh_token,
    client_id: process.env.DISCORD_CLIENT_ID,
    client_secret: process.env.DISCORD_CLIENT_SECRET,
  }),
});

const { access_token, refresh_token: newRefreshToken } = await res.json();

응답에는 새 access_token과 함께 갱신된 refresh_token이 올 수 있으니, 둘 다 안전한 곳에 저장해두고 다음 갱신 때 새 값을 쓰면 됩니다. 이 갱신을 백그라운드에서 자동으로 돌려두면 사용자는 한 번 로그인한 뒤로 다시 인증할 필요가 없어요.

봇 토큰과 사용자 토큰은 다르다

여기서 토큰이 두 종류라는 게 분명해집니다. 지금까지 봇이 쓰던 봇 토큰은 수명이 길고 API 전체에 접근하며 게이트웨이 연결에 쓰입니다. 반면 OAuth2로 받은 사용자 토큰(Bearer)은 특정 사용자가 위임한 범위에만, 그것도 만료 전까지만 쓸 수 있어요. 헤더도 각각 Authorization: Bot ...Authorization: Bearer ...로 구분됩니다.

봇 초대 링크의 정체

이제 처음에 예고한 비밀을 풀 차례예요. 봇을 서버에 초대할 때 썼던 링크를 떠올려 보면, 사실 그것도 OAuth2 인가 URL이었습니다. 다만 scopebot이었던 거죠.

https://discord.com/oauth2/authorize?client_id=내_ID&scope=bot&permissions=8192

bot 스코프는 코드 교환 없이 사용자가 곧장 “이 봇을 내 서버에 추가” 하고 끝나는 간소화된 흐름이에요. 그리고 뒤에 붙은 permissions=8192이 바로 권한과 역할에서 본 권한 비트값입니다. 8192는 메시지 관리 권한이었죠. 즉 봇 초대 링크는 “이 앱을 추가하고, 이만큼의 권한을 줘”라는 OAuth2 요청이었던 겁니다.

마치며

사용자 관점의 OAuth2를 따라가며 위임의 개념, Authorization Code 흐름, 토큰 교환, 그리고 봇 초대 링크의 정체까지 하나로 꿰었습니다. 봇 토큰과 사용자 토큰이 어떻게 다른지, 권한 비트값이 초대 링크에 어떻게 실리는지도 앞 편들과 이어지며 선명해졌고요.

마지막으로, 봇이 실시간으로 서버의 모든 일을 지켜보는 통로인 게이트웨이까지 들여다보면 Discord 연동의 큰 그림이 완성됩니다.

더 자세한 내용은 Discord OAuth2 문서를 참고하세요.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

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

Discord