React 애플리케이션에서 DataGSM OAuth 서비스를 간편하게 연동하기 위한 라이브러리입니다.
# npm
npm install @themoment-team/datagsm-oauth-react
# pnpm
pnpm add @themoment-team/datagsm-oauth-react
# yarn
yarn add @themoment-team/datagsm-oauth-reactlogin()이state, PKCEcode_verifier, (openid를 요청하면)nonce를 만들어sessionStorage에 저장하고 DataGSM authorize 페이지로 이동합니다.- 사용자가 로그인하고 동의하면 서버가
redirectUri?code=...&state=...로 돌려보냅니다. 동의를 거부하면redirectUri?error=access_denied&error_description=...&state=...로 돌려보냅니다. - 콜백 페이지에서
handleOAuthCallback()으로 state를 검증하고, 저장해 둔code_verifier와nonce를 꺼냅니다. - 앱의 백엔드가
code와code_verifier로 토큰을 발급받습니다. (토큰 교환 참고)
import { OAuthProvider } from '@themoment-team/datagsm-oauth-react';
const APP_ID = 'datagsm-application-id';
export function App() {
return (
<OAuthProvider
clientId="your-client-id"
redirectUri="https://your-app.com/oauth/callback"
scope={[`${APP_ID}:account_read`, `${APP_ID}:student_read`, 'openid']}
>
<Routes />
</OAuthProvider>
);
}| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
clientId |
string |
(필수) | DataGSM에 등록한 클라이언트 ID |
redirectUri |
string |
(필수) | 클라이언트에 등록한 redirect URI 중 하나. 정확히 일치해야 합니다. |
authMode |
'PKCE' | 'STANDARD' |
'PKCE' |
인가 코드 흐름 방식. STANDARD는 client_secret을 보관하는 백엔드가 code를 교환할 때만 사용하세요. |
scope |
string[] |
생략 시 서버 기본값 | 요청할 scope 목록 (scope 참고) |
useNonce |
boolean |
true |
openid scope를 요청할 때 nonce를 만들어 함께 보냅니다. |
issuer |
string |
https://oauth.authorization.datagsm.kr |
인가 서버 주소. ${issuer}/v1/oauth/authorize로 이동합니다. 로컬·스테이지 서버를 쓸 때 지정합니다. |
authorizationUrl |
string |
- | authorize 엔드포인트 전체 주소. issuer보다 우선합니다. |
import { OAuthLoginButton, useOAuth } from '@themoment-team/datagsm-oauth-react';
export function LoginPage() {
return <OAuthLoginButton onError={() => alert('로그인을 시작하지 못했습니다.')} />;
}
// 직접 버튼을 만들 때
export function CustomLoginButton() {
const { login } = useOAuth();
return <button onClick={() => login().catch(console.error)}>DataGSM으로 로그인</button>;
}handleOAuthCallback()은 현재 URL에서 code, state, error를 읽고 state를 검증한 뒤, 저장해 둔 code_verifier와 nonce를 돌려줍니다. 저장값은 성공과 실패에 관계없이 지웁니다.
콜백은 한 번만 소비됩니다. 같은 URL을 다시 처리하면 already_handled를 돌려주므로, React StrictMode에서 effect가 두 번 실행되거나 뒤로 가기로 콜백 URL에 돌아와도 이미 쓴 code로 토큰 교환을 다시 시도하지 않습니다. already_handled는 무시하면 되고, 처리한 뒤에는 history.replaceState로 주소창의 code를 지우는 것을 권장합니다.
import { useEffect, useState } from 'react';
import { handleOAuthCallback } from '@themoment-team/datagsm-oauth-react';
export function OAuthCallbackPage() {
const [message, setMessage] = useState('로그인 처리 중...');
useEffect(() => {
const result = handleOAuthCallback();
if (!result.ok) {
// 이미 처리한 콜백(StrictMode 재실행, 뒤로 가기)이면 첫 처리 결과를 그대로 둡니다.
if (result.error === 'already_handled') return;
if (result.error === 'access_denied') {
setMessage('권한 요청을 거부했습니다.');
} else if (result.error === 'invalid_state') {
setMessage('잘못된 로그인 요청입니다. 다시 시도해 주세요.');
} else {
setMessage(result.errorDescription ?? '로그인에 실패했습니다.');
}
return;
}
// code가 주소창과 방문 기록에 남지 않도록 지웁니다.
window.history.replaceState(null, '', window.location.pathname);
// code와 code_verifier를 앱 백엔드로 보내 토큰을 발급받습니다.
fetch('/api/auth/datagsm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: result.code,
codeVerifier: result.codeVerifier,
nonce: result.nonce,
}),
})
.then((res) => {
if (!res.ok) throw new Error('token exchange failed');
setMessage('로그인되었습니다.');
})
.catch(() => setMessage('로그인에 실패했습니다.'));
}, []);
return <p>{message}</p>;
}redirectUri가 앱 루트처럼 콜백이 아닌 방문도 받는 주소라면, OAuth 파라미터가 없는 URL에서는 not_callback이 나오고 저장값은 그대로 남습니다. URL 인자로 location.search나 /callback?code=... 같은 상대 URL도 넘길 수 있습니다.
결과 타입은 ok로 구분합니다.
| 필드 | 설명 |
|---|---|
ok: true |
code, state, codeVerifier(STANDARD 모드면 null), nonce(openid를 요청하지 않았으면 null) |
ok: false |
error, errorDescription |
error |
아래 표 참고 |
error |
의미 |
|---|---|
not_callback |
URL에 code, state, error가 없음. 콜백이 아닌 방문이며 저장값을 유지 |
already_handled |
이미 처리한 콜백 URL. 무시하면 됨 |
invalid_state |
state 불일치 또는 저장값 없음 (CSRF 의심) |
missing_code |
오류도 code도 없는 콜백 |
| 그 밖의 값 | 서버가 보낸 값 그대로 (access_denied 등) |
state 검증만 따로 하려면 verifyState(receivedState)를 사용하세요. 검증한 state는 결과와 관계없이 지워지므로 한 번만 쓸 수 있습니다. 저장값을 직접 지우려면 clearAuthorizationRequest()를 사용하세요.
PKCE로 발급된 code는 client_secret 없이 code_verifier만으로 교환할 수 있습니다. 다만 DataGSM 인가 서버의 CORS 허용 출처는 DataGSM 자체 프론트엔드로 제한되어 있어 외부 서비스의 브라우저에서 /v1/oauth/token을 직접 호출할 수 없습니다. 그래서 이 SDK는 토큰 교환 함수를 제공하지 않습니다. 콜백에서 얻은 code와 codeVerifier를 앱의 백엔드로 보내고, 백엔드가 아래처럼 교환하세요.
// 앱 백엔드 (예: Node.js)
const response = await fetch('https://oauth.authorization.datagsm.kr/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: 'your-client-id',
code,
redirect_uri: 'https://your-app.com/oauth/callback', // authorize 요청과 같은 값
code_verifier: codeVerifier,
// scope: ['datagsm-application-id:account_read'], // 선택. 배열이며, 인가된 범위 안에서 줄일 때만 사용
}),
});
if (!response.ok) {
// 실패 응답은 { "error": "invalid_grant", "error_description": "..." } 형식입니다.
throw new Error(`token exchange failed: ${response.status}`);
}
// 응답은 envelope 없이 바로 토큰 객체입니다.
const token: {
access_token: string;
token_type: 'Bearer';
expires_in: number; // 초
refresh_token: string | null;
scope: string; // 공백으로 구분된 문자열
id_token?: string | null; // openid scope를 요청했을 때만
} = await response.json();- 요청 본문은 JSON이며
scope는 배열입니다(authorize 요청의 scope는 공백 구분 문자열). openid를 요청했다면id_token의 서명을/v1/oauth/jwks로 검증한 뒤,nonce클레임이 콜백에서 받은nonce와 같은지 확인하세요.STANDARD모드로 받은 code는client_secret이 반드시 필요합니다.
scope는 {appId}:{scopeName} 형식입니다. 요청한 scope는 클라이언트에 허용된 범위 안에 있어야 합니다.
| scope | 설명 |
|---|---|
{appId}:account_read |
계정 정보 조회 |
{appId}:student_read |
학생 정보 조회 |
{appId}:club_read |
동아리 정보 조회 |
{appId}:project_read |
프로젝트 정보 조회 |
openid |
id_token 발급 요청 (접두어 없음) |
scope를 생략하면 서버가{appId}:account_read,{appId}:student_read를 기본으로 부여합니다.club_read,project_read는 명시적으로 요청해야 합니다.openid만 요청해도 기본 scope가 함께 부여됩니다.
- 기본
authMode가STANDARD에서PKCE로 바뀌었습니다. 기존처럼 백엔드가client_secret으로 code를 교환한다면authMode="STANDARD"를 명시하세요. PKCE로 바꾸면 토큰 요청에code_verifier를 함께 보내야 합니다. - PKCE는 HTTPS 또는 localhost에서만 동작합니다. challenge를 만드는 Web Crypto(
crypto.subtle)는 보안 컨텍스트에서만 제공되므로,http://192.168.x.x:5173같은 HTTP 개발 서버나 HTTP 배포에서는login()이 실패합니다. 이런 환경이라면 HTTPS로 바꾸거나authMode="STANDARD"를 명시하세요. - 모든 authorize 요청에
state가 붙고, 모드와 관계없이sessionStorage에 저장합니다. 사이트 데이터를 차단한 브라우저나 스토리지가 막힌 서드파티 iframe에서는login()이 실패하며, 이때 중간까지 저장한 값은 지워집니다. - 콜백 페이지에서는
handleOAuthCallback()으로 state 검증과 저장값 정리를 한 번에 처리하는 것을 권장합니다.getCodeVerifier(),clearVerifier()는 그대로 쓸 수 있지만, 이 조합을 쓴다면verifyState()로 state를 함께 검증하세요(검증한 state는 지워집니다).
자세한 사용법은 기술문서를 참고하십시오.