DataGSM의 OAuth Flow

DataGSM OAuth는 무엇인가?
OAuth는 사용자가 비밀번호를 직접 노출하지 않고도, 다른 웹사이트나 앱에 자신의 정보를 안전하게 제공하기 위한 개방형 표준 인증/인가 프레임워크입니다.
보통 구글 로그인 그리고 카카오톡 로그인이 대표적인 OAuth의 예시입니다.
그런데 교내 프로젝트들의 입장으로 본다면, 기존 외부 OAuth를 이용한다면 학번, 기숙사 호실 등 교내 데이터를 가져올 수 없기에, 사용자가 직접 입력해야하는 번거로움이 존재합니다.
그래서 DataGSM의 OAuth는 교내 프로젝트들을 위해 기존 OAuth 플렛폼들의 한계를 극복하기 위해 개발된 기능입니다. DataGSM에는 아래와 같은 데이터들을 추가적으로 제공받을 수 있습니다.
- 계정 데이터 : 학교 이메일, 학생/선생님 여부, 소속 동아리, 참여 프로젝트 등
- 학생 데이터 : 이름, 학번, 성별, 기숙사 호실, 역할(학생회, 기숙사자치위원회 등), 깃허브 아이디 등
이 덕분에 DataGSM을 사용하여 교내 프로젝트를 개발하는 인원들은 데이터 관리에 시간 자원을 덜 들여도 되고, 사용자들은 서비스마다 정보를 일일이 입력할 필요가 없어져 여러 편리함을 가져다줍니다.
DataGSM의 OAuth Flow
DataGSM의 OAuth 기능을 이용하기 위해서는 어떻게 작동이 되는지, 간단하게 흐름정도는 파악이 필요합니다. 그래서 DataGSM 기술 문서 내용을 바탕으로 DataGSM의 OAuth Flow의 각 스텝을 세부적으로 정리해보았습니다.
0단계 - OAuth Flow가 시작되기 전
DataGSM에서 OAuth Flow가 시작되기 전, DataGSM 웹사이트의 Client(클라이언트) 기능을 이용하여 `Client ID` , `Client Secret` 이 발급되어야 합니다.
Client 발급 시 생성되는 정보들
{
"clientId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"clientSecret": "9f8e7d6c-5b4a-3210-9876-fedcba098765",
"clientName": "EveryGSM v2",
"serviceName": "EveryGSM v2",
"redirectUrls": ["https://every.datagsm.kr/callback"],
"scopes": ["datagsm:self_read"]
}
0.5단계 - PKCE 값 생성
로그인 요청을 시작하기 전, 매 로그인 시도마다 프론트엔드는 브라우저에서 PKCE 쌍을 생성합니다.
// code_verifier: 43~128자의 랜덤 문자열
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
// code_challenge: code_verifier를 SHA-256 해시 후 Base64url 인코딩
async function generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(digest));
}
code_verifier는 4단계(토큰 교환)까지 프론트엔드가 sessionStorage에 저장합니다.
code_verifier, code_challenge가 무엇인지는 뒤에서 설명하도록 하겠습니다.
1단계 - 인증 시작
외부 서비스에서 사용자를 아래 URL로 리다이렉트시킵니다.
GET /v1/oauth/authorize?client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890&redirect_uri=https%3A%2F%2Fevery.datagsm.kr%2Fcallback&response_type=code&state=xyz789&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&scope=datagsm%3Aself_read
포함되는 파라미터들
| 파라미터 | 필수 여부 | 설명 |
|---|---|---|
client_id |
O | 0단계에서 발급받은 client_id |
redirect_uri |
O | 로그인 완료 후 돌아갈 주소, 0단계에 등록한 값과 정확하게 일치해야 합니다. |
response_type |
O | code 외 다른 값이라면 상태코드 400을 반환합니다. |
state |
권장 | CSRF 방지용 임의 문자열로 3단계 리다이렉트 시 그대로 되돌아옵니다. |
code_challenge |
PKCE 사용 시 필수 | 0.5단계에서 만든 해시값입니다. |
code_challenge_method |
PKCE 사용 시 필수 | S256 또는 plain |
scope |
선택 | 공백으로 구분된 scope 목록으로, 생략 시 서버가 지정한 기본 scope가 적용됩니다. |
이 과정을 통과한다면 state, code_challenge 등을 백엔드 저장소인 Redis에 임시 저장하고, 이 데이터를 찾기 위한 열쇠인 토큰을 발급해줍니다.
그리고 브라우저는 아래 응답을 받고, Location의 URL로 이동하게 됩니다.
HTTP/1.1 302 Found
Location: https://oauth.authorization.datagsm.kr/oauth/authorize?token=6f9619ff-8b86-d011-b42d-00cf4fc964ff
2단계 - 로그인 화면이 세션 정보를 조회
DataGSM 로그인 페이지는 1단계 마지막에서 리다이렉트된 URL에 token으로 화면에 "OO 서비스로 로그인 합니다." 문구와 요청 권한 목록을 보여주기 위해 아래 주소로 요청을 보냅니다.
GET /v1/oauth/sessions/6f9619ff-8b86-d011-b42d-00cf4fc964ff
그러면 서버에서는 아래와 같이 응답을 보내주게 되고, 해당 값들을 바탕으로 동의 화면을 구성하여 사용자에게 보여주게 됩니다.
// 서버 응답 예시
{
"serviceName": "EveryGSM v2",
"expiresAt": 1757308800000,
"requestedScopes": [
{ "scope": "datagsm:self_read", "description": "내 정보 조회", "applicationName": "DataGSM" }
]
}
왜 한번 더 조회하나요? 그냥 토큰 없이 URL 파라미터에 평문으로 다 적으면 안되나요?
1단계 URL 쿼리에 service_name=예시앱 처럼 평문으로 넣으면, 공격자가 피싱 페이지에서 이름만 도용해 사용자를 속일 수 있습니다. 서버가 Redis에 저장된 진짜 클라이언트 정보를 조회해서 내려주면 위조가 불가능해집니다.
3단계 - 로그인 처리 및 Authorization Code 발급
사용자가 이메일/비밀번호를 입력하고 로그인 버튼을 누르면, 프론트엔드는 이 값들과 1단계에서 받은 token을 묶어 서버로 전송합니다.
// 서버 요청 예시
{
"email": "대충 계정 이메일",
"password": "대충 계정 비밀번호",
"token": "6f9619ff-8b86-d011-b42d-00cf4fc964ff"
}
그러면 서버는 응답으로 외부 서비스의 redirect_uri로 리다이렉트 시킵니다.
이때 외부 서비스에서는 콜백받은 아래 URL에서 code와 state를 파싱합니다. (이때 code가 Authorization Code 역할을 합니다.)
(state는 1단계에서 CSRF를 막기 위해 선택적으로 넣는 값입니다.)
HTTP/1.1 302 Found
Location: https://every.datagsm.kr/callback?code=k3JqZ9xP2mN8wYtR7bQ4sL1fV6cX0dA5&state=abc123
3.5단계 - (분기, 중요!) 정보 수정 요청 조회
관리자 페이지에서 학번, 전공/창체 동아리, 기숙사 호실을 다시 입력하라고 요청을 보낼 수 있습니다.
만약 이러한 요청이 들어온 사용자가 로그인을 시도한다면 서버로부터 아래 응답을 받습니다. 그러면 프론트엔드에서는 Authorization Code를 아직은 발급받지 못하고, 정보 수정 페이지 분기로 넘어가게 됩니다.
프론트엔드에서 POST /v1/oauth/authorize/data-edit-requirements 주소를 통해 수정이 필요한 값들에 대해 화면을 통해 보여줍니다. 이때 입력된 값들은 3단계의 요청 예시에 포함되어 아래와 같이 요청을 보냅니다. 그리고 Authorization Code를 발급받게 됩니다.
// 서버 요청 예시 (학번을 수정해달라고 요청받은 상황일때)
{
"email": "대충 계정 이메일",
"password": "대충 계정 비밀번호",
"token": "6f9619ff-8b86-d011-b42d-00cf4fc964ff",
"studentGrade": 2,
"studentClass": 2,
"studentNumber": 7
}
관리자가 요청한 값들과 일치하지 않다면(예. 관리자는 학번, 전공동아리 수정을 요청했지만 서버 요청에는 학번 데이터만 있을 때) Authorization Code를 지급하지 않습니다.
4단계 - Authorization Code를 토큰으로 교환
Authorization Code를 가지고 있는 외부 서비스가 DataGSM의 백엔드 서버로 직접 요청을 보내 토큰을 받아오는 과정입니다.
외부 서비스의 백엔드 서버에서는 아래 요청 바디를 담아 DataGSM 백엔드 서버로 요청을 보냅니다.
{
"grant_type": "authorization_code",
"client_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"client_secret": "9f8e7d6c-5b4a-3210-9876-fedcba098765",
"code": "k3JqZ9xP2mN8wYtR7bQ4sL1fV6cX0dA5",
"redirect_uri": "https://every.datagsm.kr/callback",
"code_verifier": "dBjftJeSSVPxgS31dKTHlEpQMZlzvvMpqHN0KT9LM5E"
}
| 필드 | 설명 |
|---|---|
grant_type |
필수이고, "authorization_code"를 보냅니다. |
client_id |
필수입니다. |
client_secret |
PKCE 코드면 생략이 가능하고, 아니라면 필수입니다. |
code |
3단계에서 받은 Authorization Code 입니다. |
redirect_uri |
코드 발급 시 저장된 값과 일치해야 합니다. |
code_verifier |
PKCE 사용 시 필수로, 0.5단계 값을 그대로 사용합니다. |
요청을 보내고 통과했다면, DataGSM의 백엔드 서버에서 JWT Access Token + Refresh Token을 응답으로 보내줍니다.
( 여기서 PKCE를 사용하여 code_verifier를 보냈다면, SHA-256 해시가 적용된 code_challenge와 일치하는지 확인 후 토큰을 제공합니다. )
// DataGSM 백엔드 서버에서 보내는 응답
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzMjUwMTJAZ3NtLmhzLmtyIiwi...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "datagsm:self_read"
}
5단계 - 발급받은 토큰으로 유저 리소스 접근
외부 서비스에서는 Authorization: Bearer {access_token} 헤더를 실어 DataGSM의 리소스 API(GET /v1/userinfo)를 호출합니다.
마치며
DataGSM의 OAuth Flow는 더 세부적으로 보안을 지키기 위해 정교한 로직들로 구성이 되어 있습니다. 이번 기술 블로그에서는 사용자들의 이해를 위한 대략적인 흐름만 기술하였고, 자세한 흐름은 아래 사진으로 표현되어 있습니다.
