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 금지

브라우저와 모바일 앱에는 client_secret을 넣지 않습니다. confidential client를 쓰는 경우에도 앱 서버 환경변수에만 둡니다.

적용 흐름

SDK 로드

Nuxt head 또는 인증 페이지에 Hosted SDK script 추가

composable 호출

useSsafyVerify에서 ssafyVerify.verify 실행

서버 교환

code와 codeVerifier를 /api/ssafy/verify-token에 전달

상태 반영

verified 결과만 UI와 앱 세션에 반영

1

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>
2

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 };
}
3

버튼 컴포넌트 연결

일반 사용자는 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 인증 계정 매핑 목적일 때만 요청하고 저장