GameVox로 로그인
사용자가 GameVox 계정으로 여러분의 웹사이트나 앱에 로그인할 수 있게 합니다. Discord와 같은 형태의 표준 OAuth2 인가 코드 흐름이며 봇이 필요 없습니다. 앱이 사용자 계정 정보의 일부를 요청하고, 사용자가 승인하면, 여러분의 서버가 토큰을 받습니다.
용도
- 로그인과 계정 연결. 사용자가 누구인지(변하지 않는 GameVox 사용자 ID, 사용자 이름, 아바타) 알 수 있고, 사용자가 허락하면 인증된 이메일도 받을 수 있습니다.
- 서버 선택기와 대시보드.
guilds로 사용자가 속한 서버와 설정을 관리할 수 있는 서버를 확인할 수 있습니다. - 사용자 대신 행동하기 위한 기능이 아닙니다. 사용자 토큰으로는 메시지를 보내거나, 음성에 참여하거나, GameVox에서 무엇이든 변경할 수 없습니다. GameVox 안에서 동작하는 기능에는 봇이 필요합니다.
시작하기 전에
- developers.gamevox.com에서 애플리케이션을 만드세요. OAuth2 탭의 클라이언트 ID가
client_id입니다. - OAuth2 탭에서 앱이 사용할 리디렉션 URI를 모두 추가하세요(최대 10개).
https://여야 하고(개발용으로는http://localhost/http://127.0.0.1도 가능),#fragment가 없어야 합니다. 보내는redirect_uri는 쿼리 문자열까지 포함해 그중 하나와 정확히 일치해야 합니다. - 클라이언트 시크릿을 복사하세요. 앱을 만들 때나 시크릿을 재설정할 때 한 번만 표시됩니다. 모든 토큰 요청에 필요하므로 서버에 보관하세요.
엔드포인트
GET https://gamevox.com/oauth2/authorize/
POST https://app-api.gamevox.com/api/oauth2/token
POST https://app-api.gamevox.com/api/oauth2/token/revoke
GET https://app-api.gamevox.com/api/users/@me
GET https://app-api.gamevox.com/api/users/@me/guilds
GET https://app-api.gamevox.com/api/oauth2/@me
PUT https://api.gamevox.com/api/users/@me/applications/{application.id}/role-connection /oauth2/authorize/의 끝 슬래시를 빼지 마세요. 토큰 및 사용자 엔드포인트는 /api/v10/ 아래에서도 응답하므로, 경로에 버전을 넣는 라이브러리도 그대로 동작합니다. 역할 연결은 app-api가 아니라 api.gamevox.com에 있습니다.
1. 사용자를 GameVox로 보내기
브라우저를 인가 URL로 리디렉션하세요(팝업이나 iframe이 아닌 일반 페이지 이동으로):
https://gamevox.com/oauth2/authorize/
?response_type=code
&client_id=123456789012345678
&scope=identify%20email
&redirect_uri=https%3A%2F%2Fyourapp.example.com%2Fauth%2Fgamevox
&state=Zx81kq0vT2 | 매개변수 | 필수 | 설명 |
|---|---|---|
response_type | 예 | 항상 code. |
client_id | 예 | 앱 OAuth2 탭의 클라이언트 ID. |
scope | 예 | 공백으로 구분합니다(URL 인코딩 시 %20 또는 +). 스코프를 참고하세요. bot은 넣지 마세요. bot이 있으면 이 URL은 서버 설치가 됩니다. |
redirect_uri | 예 | 등록한 리디렉션 URI 중 하나와 정확히 같은 값. |
state | 아니요 | 사용자 세션에 묶인, 추측할 수 없는 값(최대 1024자). 그대로 돌아오므로, 확인해서 사이트 간 요청 위조를 막으세요. |
code_challenge | 아니요 | 선택 사항인 PKCE 챌린지. 43자에서 128자 사이. |
code_challenge_method | 아니요 | 챌린지를 보낼 때는 S256. plain은 거부됩니다. |
prompt | 아니요 | none을 지정하면, 사용자가 이 스코프 전부에 대해 이미 앱을 승인한 경우 동의 화면을 건너뜁니다. 새로운 스코프가 있으면 여전히 묻습니다. |
사용자에게 보이는 화면
- gamevox.com의 동의 화면에 앱 이름, 아이콘, 설명, 로그인된 계정, 앱이 볼 수 있게 되는 정보를 쉬운 말로 정리한 목록, 그리고 이동할 사이트가 표시됩니다.
- 로그인하지 않은 상태라면 Log in을 누르면 GameVox 웹 앱을 거쳐 같은 화면으로 바로 돌아옵니다.
- 일반 정보 탭에서 서비스 약관과 개인정보 처리방침 링크를 설정하면 버튼 아래에 표시됩니다.
2. 콜백 처리하기
사용자가 Authorize를 클릭하면 GameVox가 코드와 함께 리디렉션 URI로 보냅니다:
https://yourapp.example.com/auth/gamevox?code=3vR0b...&state=Zx81kq0vT2 대신 Cancel을 클릭한 경우:
https://yourapp.example.com/auth/gamevox?error=access_denied
&error_description=The+user+denied+the+request&state=Zx81kq0vT2 state가 이 브라우저에 발급한 값이 아니면 콜백을 거부하세요.- 코드는 10분 후 만료되며 한 번만 사용할 수 있습니다. 서버에서 바로 교환하세요.
3. 코드를 토큰으로 교환하기
서버에서 폼 인코딩된 본문(JSON이 아닌 application/x-www-form-urlencoded)을 토큰 엔드포인트로 POST하세요:
curl -X POST https://app-api.gamevox.com/api/oauth2/token \
-u "123456789012345678:CS.your-client-secret" \
-d grant_type=authorization_code \
-d code=3vR0b... \
-d redirect_uri=https://yourapp.example.com/auth/gamevox {
"access_token": "GVO.mJ4c...",
"token_type": "Bearer",
"expires_in": 604800,
"refresh_token": "GVR.9tQe...",
"scope": "identify email"
} - 위와 같이 HTTP Basic(
client_id:client_secret)으로 인증하거나,client_id와client_secret을 폼 필드로 보내세요. redirect_uri는 1단계에서 보낸 값과 같아야 합니다. PKCE를 사용했다면code_verifier를 추가하세요.- 응답에는
Cache-Control: no-store가 붙습니다. 클라이언트 시크릿이나 토큰을 브라우저 코드나 URL에 절대 넣지 마세요.
4. API 호출하기
액세스 토큰을 Authorization: Bearer로 보내세요. GET /users/@me에는 identify 스코프가 필요합니다:
curl https://app-api.gamevox.com/api/users/@me \
-H "Authorization: Bearer GVO.mJ4c..." {
"id": "1557709980780474368",
"gamevox_id": "edf412bc-a7b8-4f2a-a810-8b9eb8c02462",
"username": "alice",
"discriminator": "0",
"global_name": "alice",
"avatar": null,
"gamevox_avatar_url": "https://...",
"email": "alice@example.com",
"verified": true
} id는 사용자의 변하지 않는 GameVox ID입니다. 계정 레코드의 키로 사용하세요. 사용자 이름은 바뀔 수 있습니다.gamevox_id는 같은 사용자를 GameVox 고유의 UUID 형식으로 나타낸 것입니다.email과verified는email스코프가 있고 사용자가 이메일을 등록한 경우에만 포함됩니다. GameVox는 인증된 주소만 저장하므로,email이 있으면verified는 항상true입니다.avatar는 항상null입니다(Discord 라이브러리가 이 값으로 Discord CDN 링크를 만들기 때문입니다). 대신gamevox_avatar_url을 사용하세요. 사용자에게 사진이 없으면 생략됩니다.
사용자의 서버
GET /users/@me/guilds에는 guilds 스코프가 필요합니다:
[
{ "id": "1557445672742031360", "name": "Raid Night", "icon": null,
"owner": true, "permissions": "2251799813685247", "features": [] },
{ "id": "1557445672742031361", "name": "Guild Hall", "icon": null,
"owner": false, "permissions": "1056", "features": [] }
] permissions는 서버를 고르기 위한 요약이며 완전한 계산 결과가 아닙니다. 소유자와 관리자에게는 모든 비트(ADMINISTRATOR포함), 서버 설정을 편집할 수 있는 사용자에게는MANAGE_GUILD(32)와VIEW_CHANNEL(1024), 그 밖의 사용자에게는VIEW_CHANNEL이 들어갑니다.- 응답 하나에 최대
limit개의 서버가 오래된 순서로 들어갑니다(1에서 200 사이, 기본값 200). Discord와 마찬가지로after나before에 이전 페이지에 있는 서버의id를 지정해 페이지를 넘깁니다.
현재 인가
GET /oauth2/@me는 어떤 스코프로도 사용할 수 있으며, 토큰이 어느 앱의 것인지, 무엇이 허용되었는지, 언제 만료되는지 알려 줍니다. identify가 있으면 user도 포함됩니다.
{
"application": { "id": "123456789012345678", "name": "RaidPlanner", ... },
"scopes": ["identify", "email"],
"expires": "2026-10-15T15:03:20Z",
"user": { "id": "1557709980780474368", "username": "alice", ... }
} 연결된 역할
role_connections.write로 사용자가 여러분의 서비스에서 어떤 계정을 쓰는지 기록할 수 있습니다. 호스트에 주의하세요: api.gamevox.com입니다.
curl -X PUT \
https://api.gamevox.com/api/users/@me/applications/123456789012345678/role-connection \
-H "Authorization: Bearer GVO.mJ4c..." \
-H "Content-Type: application/json" \
-d '{"platform_name": "RaidPlanner", "platform_username": "ShadowMage", "metadata": {}}' 사용자는 Settings > Authorized Apps에서 “Linked as ShadowMage on RaidPlanner”로 보게 됩니다. metadata 키는 앱의 역할 연결 메타데이터에 정의한 것이어야 합니다. 같은 URL에 GET하면 다시 읽을 수 있습니다.
토큰 갱신
액세스 토큰은 7일 동안 유효합니다(expires_in은 초 단위). 리프레시 토큰으로 새 쌍을 받으세요:
curl -X POST https://app-api.gamevox.com/api/oauth2/token \
-u "123456789012345678:CS.your-client-secret" \
-d grant_type=refresh_token \
-d refresh_token=GVR.9tQe... - 리프레시 토큰은 교체됩니다. 응답에 새 리프레시 토큰이 담기고, 이전 리프레시 토큰과 그 액세스 토큰은 즉시 동작을 멈추므로, 사용하기 전에 새 쌍을 저장하세요.
scope를 보내 새 토큰의 스코프를 줄일 수 있습니다. 사용자가 허용한 적 없는 스코프를 요청하면invalid_scope로 실패합니다.- 사용되지 않은 리프레시 토큰은 90일 후 만료됩니다. 사용자 한 명이 앱 하나에 대해 가질 수 있는 유효한 토큰 쌍은 최대 25개이며, 26번째를 만들면 가장 오래된 것이 삭제됩니다.
취소(로그아웃)
사용자가 앱에서 로그아웃하면 토큰을 취소하세요. 쌍 중 어느 토큰으로든 쌍 전체가 취소됩니다. 유효한 클라이언트라면 토큰의 존재 여부와 관계없이 엔드포인트는 항상 200을 반환합니다.
curl -X POST https://app-api.gamevox.com/api/oauth2/token/revoke \
-u "123456789012345678:CS.your-client-secret" \
-d token=GVO.mJ4c... PKCE
선택 사항이며, 다른 사람이 볼 수 있는 곳에서 인가 URL을 만드는 경우 권장합니다. GameVox는 S256만 지원하며, 토큰 요청에는 여전히 클라이언트 시크릿이 필요합니다. 시크릿 없는 퍼블릭 클라이언트는 없습니다.
// Before redirecting: keep the verifier with the user's pending login.
const verifier = base64url(crypto.randomBytes(32));
const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());
// authorize URL: ...&code_challenge=CHALLENGE&code_challenge_method=S256
// token request: ...&code_verifier=VERIFIER (client secret still required) 스코프
| 스코프 | 앱이 할 수 있는 것 |
|---|---|
identify | 사용자의 ID, 사용자 이름, 아바타를 읽습니다: GET /users/@me, 그리고 GET /oauth2/@me의 user. |
email | /users/@me에 사용자의 인증된 이메일을 추가합니다. |
guilds | 사용자의 서버 목록을 읽습니다: GET /users/@me/guilds. |
role_connections.write | 앱에 대한 사용자의 연결된 역할 정보를 읽고 씁니다. |
그 밖의 스코프(connections, guilds.join, bot 없는 applications.commands 포함)는 동의 화면에서 그 사실을 알리는 메시지와 함께 거부됩니다.
오류
토큰 및 취소 엔드포인트는 표준 OAuth2 형식인 {"error": "...", "error_description": "..."}로 오류를 반환하므로, OAuth 라이브러리가 그대로 읽을 수 있습니다:
| error | HTTP | 의미 |
|---|---|---|
invalid_client | 401 | 클라이언트 ID나 시크릿이 틀렸거나, 앱이 삭제되었거나, 앱 소유자의 계정이 정지되었습니다. |
invalid_grant | 400 | 코드나 리프레시 토큰을 알 수 없거나, 만료되었거나, 이미 사용되었거나, 다른 앱에 발급되었거나, redirect_uri 또는 code_verifier가 일치하지 않습니다. 사용자가 앱의 인가를 해제한 뒤에도 반환됩니다. |
invalid_request | 400 | 필수 필드가 없거나 본문이 폼 인코딩되지 않았습니다. |
invalid_scope | 400 | 갱신 요청이 원래 허용에 포함되지 않은 스코프를 요청했습니다. |
unsupported_grant_type | 400 | authorization_code와 refresh_token만 지원합니다. |
slow_down | 429 | 여러분의 IP에서 클라이언트 인증 실패가 너무 많습니다(분당 30회). 1분 기다리세요. |
잘못되었거나 만료되었거나 취소된 토큰으로 API를 호출하면 401을 받고, 필요한 스코프가 없는 토큰은 403(50001 Missing Access)을 받습니다. 응답에는 일반적인 X-RateLimit-* 헤더가 붙으며 사용자별, 앱별로 집계됩니다.
접근이 끝나는 경우
토큰은 만료 전에 동작을 멈출 수 있습니다. 다음과 같은 경우입니다:
- 사용자가 Settings > Authorized Apps에서 Deauthorize를 클릭한 경우(해당 사용자에 대해 보유한 모든 토큰이 즉시 무효화되고, 그 사용자의 연결된 역할 항목도 삭제됩니다)
- 사용자의 계정이 비활성화된 경우
- 애플리케이션이 삭제된 경우
- 개발자 계정이 정지된 경우
401이나 invalid_grant는 “로그아웃됨”으로 간주하고, 사용자를 1단계부터 다시 진행하게 하세요.
전체 예제(Node.js + Express)
state 확인, 코드 교환, 사용자 정보 읽기를 포함한 로그인 예제입니다. 내장 fetch를 쓰므로 Node 18 이상이 필요합니다.
import express from 'express';
import crypto from 'node:crypto';
const CLIENT_ID = process.env.GAMEVOX_CLIENT_ID;
const CLIENT_SECRET = process.env.GAMEVOX_CLIENT_SECRET;
const REDIRECT_URI = 'https://yourapp.example.com/auth/gamevox';
// state must be tied to THIS browser, or someone can hand a victim a callback
// URL carrying the attacker's own code and sign them in as the attacker.
// A short-lived HttpOnly cookie does that without a session store.
const STATE_COOKIE = 'gv_oauth_state';
const readCookie = (req, name) =>
(req.headers.cookie || '').split(';').map((c) => c.trim().split('='))
.find(([k]) => k === name)?.[1] || '';
const app = express();
app.get('/login', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
res.cookie(STATE_COOKIE, state, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 10 * 60 * 1000, path: '/auth/gamevox',
});
const url = new URL('https://gamevox.com/oauth2/authorize/');
url.search = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
scope: 'identify email',
redirect_uri: REDIRECT_URI,
state,
}).toString();
res.redirect(url.toString());
});
app.get('/auth/gamevox', async (req, res) => {
const { code, state, error } = req.query;
const expected = readCookie(req, STATE_COOKIE);
res.clearCookie(STATE_COOKIE, { path: '/auth/gamevox' });
if (!expected || typeof state !== 'string' || state !== expected) {
return res.status(400).send('Login expired or started in another browser');
}
if (error) return res.redirect('/?login=cancelled');
if (typeof code !== 'string') return res.status(400).send('Missing code');
const tokenRes = await fetch('https://app-api.gamevox.com/api/oauth2/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic ' + Buffer.from(CLIENT_ID + ':' + CLIENT_SECRET).toString('base64'),
},
body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI }),
});
if (!tokenRes.ok) return res.status(502).send('GameVox login failed');
const tokens = await tokenRes.json();
const me = await fetch('https://app-api.gamevox.com/api/users/@me', {
headers: { Authorization: 'Bearer ' + tokens.access_token },
}).then((r) => r.json());
// me.id is the stable GameVox user id: key your account record on it.
// Store tokens.refresh_token server-side if you need access later.
res.send('Signed in as ' + me.username);
});
app.listen(3000); Discord와의 차이
- 인가 코드 그랜트만 지원합니다. 암시적 그랜트(
response_type=token)와client_credentials는 없습니다. - 퍼블릭 클라이언트는 없습니다. PKCE 사용 여부와 관계없이 모든 토큰 요청에 클라이언트 시크릿이 필요합니다.
- 사용자 설치(
bot없는applications.commands,integration_type=1)는 아직 사용할 수 없습니다. connections,guilds.join,guilds.members.read,rpc스코프는 없습니다./users/@me/guilds의permissions는 서버를 고르기 위한 요약이며 완전한 계산 결과가 아닙니다(위 참고).