Platform Guide
Vue / Nuxt SSAFY 인증 연동
Hosted SDK를 script로 로드하고 Vue composable에서 Verify popup을 제어합니다. token 교환은 Nuxt server route나 별도 backend에서 처리합니다.
이 문서가 맞는 경우
플랫폼별 권장 흐름을 먼저 고정한 뒤, code 교환은 모두 앱 서버에서 처리합니다.
Best for- Vue 3 SPA, Nuxt 3, Vite Vue
Strategy- Hosted SDK popup + composable + backend token exchange
Callback- redirect_uri에 SDK script가 있으면 popup 결과가 원래 창으로 전달됩니다.
Support- 공식 지원: protocol, hosted page, backend exchange contract
준비물
아래 값이 준비되면 공식 지원 흐름에 맞춰 backend token exchange를 연결할 수 있습니다.
issuer
모든 플랫폼에서 canonical issuer/API base URL은 https://verify.myknow.xyz 입니다
client_id
Developer Portal에서 승인된 public 또는 confidential client id
redirect_uri
client 설정에 exact match로 등록된 callback URL 또는 deep link
scope
ssafy.verify는 필수. 기수/캠퍼스/지역은 ssafy.affiliation, 이름은 ssafy.name, 이미지는 ssafy.profile_image 추가
Mattermost id
기존 Mattermost 인증 프로젝트의 계정 매핑이 필요한 경우에만 ssafy.mattermost_id 추가
backend endpoint
앱 서버에서 code와 code_verifier를 받아 /verify/token으로 교환
client secret 금지
적용 흐름
SDK 로드
Nuxt head 또는 인증 페이지에 Hosted SDK script 추가
composable 호출
useSsafyVerify에서 ssafyVerify.verify 실행
서버 교환
code와 codeVerifier를 /api/ssafy/verify-token에 전달
상태 반영
verified 결과만 UI와 앱 세션에 반영
SDK script 추가
Vue 앱 어디에서든 window.ssafyVerify를 사용할 수 있게 Hosted SDK를 로드합니다.
붙일 위치
Nuxt app.vue, app/head.ts 또는 인증 페이지 head
확인 방법
브라우저 콘솔에서 window.ssafyVerify가 객체로 보임
app.vueHosted SDK 로드<!-- Nuxt: app.vue 또는 인증 페이지 head에 추가 -->
<script src="https://verify.myknow.xyz/sdk/ssafy-verify.js" defer></script>Vue composable 추가
Vue 컴포넌트가 사용할 수 있는 Verify 상태와 실행 함수를 제공합니다.
붙일 위치
Vue/Nuxt 앱의 composables/useSsafyVerify.ts
확인 방법
verify 호출 시 popup이 열리고 result 값이 갱신됨
composables/useSsafyVerify.tsVue Verify 흐름 상태 관리전체 코드 보기전체 코드 접기102 lines
import { ref } from "vue";
type VerifyResult =
| { ok: true; verified: true; sub: string; cohort: string | null; campus: string | null; authTime: number }
| { ok: false; errorCode: string; requestId: string | null };
type SsafySdk = {
verify(options: {
clientId: string;
redirectUri: string;
scopes: string[];
waitForCallback: true;
}): Promise<{
code: string | null;
error: string | null;
iss: string | null;
error_code: string | null;
request_id: string | null;
codeVerifier: string;
}>;
};
declare global {
interface Window {
ssafyVerify?: SsafySdk;
}
}
export function useSsafyVerify() {
const status = ref<"idle" | "working" | "verified" | "failed">("idle");
const result = ref<VerifyResult | null>(null);
async function verify() {
const sdk = window.ssafyVerify;
const expectedIssuer = import.meta.env.VITE_SSAFY_VERIFY_ISSUER ?? "https://verify.myknow.xyz";
if (!sdk) {
result.value = { ok: false, errorCode: "SDK_NOT_READY", requestId: null };
status.value = "failed";
return result.value;
}
status.value = "working";
const callback = await sdk.verify({
clientId: import.meta.env.VITE_SSAFY_VERIFY_CLIENT_ID,
redirectUri: import.meta.env.VITE_SSAFY_VERIFY_REDIRECT_URI,
scopes: ["ssafy.verify", "ssafy.affiliation", "ssafy.name"],
waitForCallback: true,
}).then((value) => value, () => null);
if (!callback) {
result.value = { ok: false, errorCode: "VERIFY_POPUP_FAILED", requestId: null };
status.value = "failed";
return result.value;
}
if (callback.error || !callback.code) {
result.value = {
ok: false,
errorCode: callback.error_code ?? "VERIFY_CANCELLED",
requestId: callback.request_id,
};
status.value = "failed";
return result.value;
}
if (callback.iss !== expectedIssuer) {
result.value = {
ok: false,
errorCode: "CALLBACK_ISSUER_MISMATCH",
requestId: callback.request_id,
};
status.value = "failed";
return result.value;
}
const response = await fetch("/api/ssafy/verify-token", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
code: callback.code,
codeVerifier: callback.codeVerifier,
redirectUri: import.meta.env.VITE_SSAFY_VERIFY_REDIRECT_URI,
iss: callback.iss,
}),
}).then((value) => value, () => null);
if (!response) {
result.value = { ok: false, errorCode: "VERIFY_NETWORK_FAILED", requestId: null };
status.value = "failed";
return result.value;
}
result.value = await response.json().then(
(value) => value as VerifyResult,
() => ({ ok: false, errorCode: "VERIFY_RESPONSE_INVALID", requestId: null }),
);
status.value = result.value.ok ? "verified" : "failed";
return result.value;
}
return { status, result, verify };
}버튼 컴포넌트 연결
일반 사용자는 SSAFY 인증하기 버튼과 성공/실패 안내만 보게 합니다.
붙일 위치
Vue/Nuxt 앱의 components/SsafyVerifyButton.vue
확인 방법
실패 시 request_id가 표시되고 민감 값은 표시되지 않음
components/SsafyVerifyButton.vue사용자용 인증 버튼<script setup lang="ts">
import { useSsafyVerify } from "@/composables/useSsafyVerify";
const { status, result, verify } = useSsafyVerify();
</script>
<template>
<section>
<button type="button" :disabled="status === 'working'" @click="verify">
{{ status === 'working' ? '인증 진행 중' : 'SSAFY 인증하기' }}
</button>
<p v-if="result?.ok">SSAFY 인증이 완료되었습니다.</p>
<p v-else-if="result && !result.ok">
인증에 실패했습니다. request_id: {{ result.requestId }}
</p>
</section>
</template>공통 서버 token exchange 계약
플랫폼이 무엇이든 최종 교환과 verification_token 검증은 앱 서버에서 수행합니다.
Client request- 앱 또는 브라우저가 자기 backend로 보내는 JSON
Backend action- backend가 /verify/token에 grant_type=verification_code로 교환
Backend response- 앱 세션에 저장해도 되는 최소 인증 결과만 반환
client-to-backend.json앱이 자기 서버에 보내는 body{
"code": "code_from_callback",
"codeVerifier": "pkce_verifier_from_client",
"redirectUri": "https://partner.example.com/ssafy",
"iss": "https://verify.myknow.xyz"
}Client request fields
code?- callback으로 받은 1회성 Verify code
codeVerifier?- PKCE 검증에 필요한 원본 verifier
redirectUri?- 외부 앱에 등록된 callback URL
iss?- callback 응답의 issuer
backend-success.json앱 서버가 클라이언트에 돌려주는 최소 결과{
"ok": true,
"verified": true,
"sub": "pairwise_subject_placeholder",
"cohort": "15",
"campus": "서울 캠퍼스",
"authTime": 1781740800
}Backend response fields
ok?- 외부 앱 backend 처리 성공 여부
verified?- SSAFY 구성원 인증 완료 여부
sub?- client별 pairwise subject
cohort?- SSAFY 기수
campus?- SSAFY 캠퍼스
authTime?- JWT auth_time NumericDate seconds
보안 체크리스트
공식 지원 범위
protocol, hosted page, backend token exchange만 공식 지원
secret 위치
client_secret은 confidential client의 서버 환경변수에만 저장
state 검증
authorize 시작 시 만든 state와 callback state를 반드시 비교
PKCE 보관
code_verifier는 callback 완료 후 서버 교환까지만 짧게 보관
token 검증
verification_token은 서버에서 iss, aud, exp, sub, client_id, verified, auth_time, amr, acr를 검증
저장 최소화
앱 DB에는 verified, sub, cohort, campus, verifiedAt 정도만 저장
Mattermost id
ssafy.mattermost_id는 기존 MM 인증 계정 매핑 목적일 때만 요청하고 저장