Verify API

Verify API 요청/응답 레퍼런스

외부 앱의 로그인은 유지하면서 이번 거래에서 사용자가 SSAFY 구성원인지 확인합니다.authorize, callback, token, JWT claim까지 동일한 canonical 값 계약을 따릅니다.

Canonical issuer

문서를 어떤 배포 URL에서 읽더라도 Verify API의 issuer와 API base URL은 아래 값으로 고정합니다.

Canonical issuer?
https://verify.myknow.xyz
Canonical SDK URL?
https://verify.myknow.xyz/sdk/ssafy-verify.js
Exact match rule?
callback.iss === "https://verify.myknow.xyz"

Flow summary

Verify API는 외부 앱 로그인과 별개로 이번 거래의 SSAFY 인증 여부만 확인합니다.

1. authorize

외부 앱이 state와 PKCE challenge를 만들고 사용자를 /verify/authorize로 보냅니다.

2. consent

SSAFY Verify가 앱 이름, redirect domain, 제공 정보를 보여줍니다.

3. DM code

사용자가 Mattermost DM으로 받은 6자리 코드를 입력합니다.

4. callback

외부 앱 redirect_uri로 code, state, iss를 전달합니다.

5. token

외부 앱 backend가 /verify/token에서 verification_token으로 교환합니다.

6. verify

JWT 서명과 필수 claim을 검증하고 최소 결과만 저장합니다.

v1 공식 범위

이 페이지는 User Auth용 Verify API입니다. 프로필 조회, 디렉터리 lookup, Mattermost 알림 위임은 Server API 문서를 사용하세요. OIDC 로그인 연동은 v2 예정입니다.

Redirect URI 등록

wildcard, prefix, suffix match는 허용하지 않습니다.환경별 실제 callback URL을 등록합니다.

Production?
https://partner.example.com/auth/ssafy
Vercel Preview?
https://partner-git-preview-example.vercel.app/auth/ssafy
Local?
http://localhost:3000/auth/ssafy

Scope별 반환 claim

scope는 최소한으로 요청합니다.이름, 이미지, Mattermost id는 각각 별도 scope입니다.

ssafy.verify?
required
ssafy.affiliation?
optional
ssafy.track?
optional
ssafy.name?
optional
ssafy.profile_image?
optional
ssafy.role?
optional
ssafy.mattermost_id?
optional

Endpoint index

Authorize

/verify/authorize

사용자를 SSAFY Verify hosted page로 보내고 Mattermost DM 코드 인증을 시작합니다.

GET

Path

/verify/authorize

Authentication

public client_id와 PKCE S256 query parameter를 사용합니다.

Headers

NameTypeRequiredNullableValuesDescription
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Parameters

NameTypeRequiredNullableValuesDescription
client_idquery stringyesno-승인된 User Auth client id입니다.
redirect_uriabsolute URLyesno-Developer Portal 등록값과 exact match여야 합니다.
scopespace-separated stringyesnossafy.verify required요청 claim 범위입니다.ssafy.verify는 필수입니다.
stateopaque stringyesno-외부 앱이 생성하고 callback에서 비교하는 CSRF 방지 값입니다.
code_challengePKCE S256 challengeyesno-code_verifier의 SHA-256 base64url 값입니다.
code_challenge_methodstringyesnoS256plain은 허용하지 않습니다.
nonceopaque stringnono-Verify v1에서는 선택값입니다.v2 OIDC nonce와 구분합니다.
authorize-url.txt브라우저 이동 URL
https://verify.myknow.xyz/verify/authorize?
  client_id=client_example_public
  &redirect_uri=https%3A%2F%2Fpartner.example.com%2Fssafy%2Fcallback
  &scope=ssafy.verify%20ssafy.affiliation%20ssafy.name
  &state=random_state_from_partner
  &code_challenge=pkce_challenge
  &code_challenge_method=S256

Response Message

NameTypeRequiredNullableValuesDescription
success redirect.codestringsuccess onlyno-1회성 verification code입니다.즉시 backend token exchange로 전달합니다.
success redirect.statestringsuccess onlyno-요청 state와 exact match해야 합니다.
success redirect.issURL stringsuccess onlynohttps://verify.myknow.xyzcanonical issuer입니다.다르면 token exchange를 시작하지 않습니다.
failure redirect.error_codestringfailure onlyno-stable SSAFY Verify error code입니다.
failure redirect.request_idstringfailure onlyno-운영 문의용 request id입니다.
callback-url.txt성공 callback
https://partner.example.com/ssafy/callback?
  code=code_from_ssafy_verify
  &state=random_state_from_partner
  &iss=https%3A%2F%2Fverify.myknow.xyz

Error Spec

Error CodeHTTPCauseAction
INVALID_REQUEST400필수 parameter 또는 form field 누락문서의 Request Parameters와 Body를 확인합니다.
CLIENT_NOT_FOUND401등록되지 않은 client_idDeveloper Portal의 client_id와 배포 환경변수를 확인합니다.
CLIENT_PENDING403client가 아직 승인되지 않음관리자 승인 후 다시 요청합니다.
CLIENT_DISABLED403client가 disabled 또는 rejected 상태운영자에게 request_id와 client_id를 전달합니다.
REDIRECT_URI_MISMATCH400redirect_uri가 등록값과 exact match가 아님Local, Preview, Production URL을 각각 등록합니다.
SCOPE_NOT_ALLOWED400요청 scope가 승인되지 않았거나 ssafy.verify가 없음승인 scope와 요청 scope를 맞춥니다.
RATE_LIMITED429token 또는 인증 요청 rate limit 초과잠시 뒤 재시도합니다.

Security Notes

  • redirect_uri는 exact match만 허용합니다.
  • state와 PKCE S256을 항상 사용합니다.
  • callback code는 URL, 로그, client storage에 저장하지 않습니다.

Callback fields

{redirect_uri}?code=...&state=...&iss=...

인증 성공 또는 실패 후 외부 앱 callback URL로 전달되는 field 계약입니다.

REDIRECT

Path

{redirect_uri}?code=...&state=...&iss=...

Authentication

외부 앱이 state와 iss를 로컬에서 검증합니다.

Headers

NameTypeRequiredNullableValuesDescription

Response Message

NameTypeRequiredNullableValuesDescription
codestringsuccess onlyno-성공 시 전달되는 1회성 code입니다.
statestringyesno-요청 시작 값과 비교합니다.
issURL stringsuccess onlynohttps://verify.myknow.xyzcanonical issuer입니다.
errorstringfailure onlynoaccess_denied | invalid_request | server_errorOAuth-style public error입니다.
error_codestringfailure onlyno-SSAFY Verify stable error code입니다.
request_idstringfailure onlyno-운영 추적용 request id입니다.
callback-error-url.txt실패 callback
https://partner.example.com/ssafy/callback?
  error=access_denied
  &error_code=CONSENT_DENIED
  &request_id=req_placeholder
  &state=random_state_from_partner

Error Spec

Error CodeHTTPCauseAction
INVALID_REQUEST400필수 parameter 또는 form field 누락문서의 Request Parameters와 Body를 확인합니다.
CLIENT_NOT_FOUND401등록되지 않은 client_idDeveloper Portal의 client_id와 배포 환경변수를 확인합니다.
CLIENT_PENDING403client가 아직 승인되지 않음관리자 승인 후 다시 요청합니다.
CLIENT_DISABLED403client가 disabled 또는 rejected 상태운영자에게 request_id와 client_id를 전달합니다.
REDIRECT_URI_MISMATCH400redirect_uri가 등록값과 exact match가 아님Local, Preview, Production URL을 각각 등록합니다.
SCOPE_NOT_ALLOWED400요청 scope가 승인되지 않았거나 ssafy.verify가 없음승인 scope와 요청 scope를 맞춥니다.
RATE_LIMITED429token 또는 인증 요청 rate limit 초과잠시 뒤 재시도합니다.

Security Notes

  • 성공 callback만 backend token exchange로 전달합니다.
  • iss가 canonical issuer와 다르면 로컬 오류로 처리합니다.
  • request_id는 사용자 문의 화면에 작게 표시할 수 있습니다.

Hosted SDK local errors

/sdk/ssafy-verify.js

popup, preflight, callback timeout에서 SDK가 Promise reject로 반환하는 로컬 오류 계약입니다.

LOCAL

Path

/sdk/ssafy-verify.js

Authentication

브라우저 origin과 redirectUri origin을 로컬에서 검증합니다.

Headers

NameTypeRequiredNullableValuesDescription

Response Message

NameTypeRequiredNullableValuesDescription
errorstringyesno-SDK local error type입니다.
error_codestringyesnoSSAFY_VERIFY_REDIRECT_ORIGIN_MISMATCH | SSAFY_VERIFY_POPUP_CLOSED | SSAFY_VERIFY_CALLBACK_TIMEOUT | SSAFY_VERIFY_STATE_MISMATCHbrowser-local stable code입니다.SSAFY Verify HTTP response가 아닐 수 있습니다.
request_idstringyesyes-authorize 응답이 도착한 경우에만 포함됩니다.
phasestringyesnopreflight | popup | callback | authorize실패 단계입니다.
statestringnoyes-가능한 경우 요청 state를 보존합니다.

CODE / Mean

CODEMean
SSAFY_VERIFY_REDIRECT_ORIGIN_MISMATCHredirectUri origin과 opener origin 불일치
SSAFY_VERIFY_POPUP_CLOSEDcallback 도착 전 popup 닫힘
SSAFY_VERIFY_CALLBACK_TIMEOUTtimeoutMs 안에 callback message 없음
SSAFY_VERIFY_STATE_MISMATCHcallback 또는 authorize message state 불일치
sdk-error.jsonSDK local error
{
  "error": "timeout",
  "error_code": "SSAFY_VERIFY_CALLBACK_TIMEOUT",
  "message": "SSAFY Verify popup callback timed out.",
  "request_id": null,
  "phase": "callback",
  "state": "state_placeholder"
}

Error Spec

Error CodeHTTPCauseAction
SSAFY_VERIFY_REDIRECT_ORIGIN_MISMATCHlocalredirectUri origin과 현재 window origin 불일치현재 origin에서 callback URL을 계산하고 Developer Portal에 exact 등록합니다.
SSAFY_VERIFY_CALLBACK_TIMEOUTlocalpopup callback postMessage가 timeoutMs 내 도착하지 않음callback page SDK 로드와 preview URL 등록 상태를 확인합니다.

Security Notes

  • local error는 SSAFY Verify HTTP response와 구분합니다.
  • state mismatch는 backend exchange로 넘기지 않습니다.
  • frontend 환경변수에 production redirect URI를 고정하지 않습니다.

Token exchange

/verify/token

callback code와 PKCE verifier를 verification_token JWT로 교환합니다.

POST

Path

/verify/token

Authentication

public client는 PKCE만, confidential client는 client_secret도 함께 검증합니다.

Headers

NameTypeRequiredNullableValuesDescription
content-typestringyesnoapplication/x-www-form-urlencodedform body endpoint에서 사용합니다.
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Request Body

NameTypeRequiredNullableValuesDescription
grant_typestringyesnoverification_codeVerify API 전용 grant type입니다.
codestringyesno-callback으로 받은 1회성 code입니다.
client_idstringyesno-authorize 요청의 client_id와 같아야 합니다.
client_secretstringconfidential onlyno-confidential client backend에서만 전송합니다.
code_verifierPKCE verifieryesno-authorize 시작 시 만든 원본 verifier입니다.
verify-token.shtoken exchange 요청
curl -X POST https://verify.myknow.xyz/verify/token \
  -H "content-type: application/x-www-form-urlencoded" \
  --data-urlencode grant_type=verification_code \
  --data-urlencode client_id=client_example_public \
  --data-urlencode code=code_from_callback \
  --data-urlencode code_verifier=$PKCE_CODE_VERIFIER

Response Message

NameTypeRequiredNullableValuesDescription
verification_tokenJWT stringyesno-RS256 signed JWT입니다.JWKS로 검증하고 원문 저장은 피합니다.
token_typestringyesnoBearer고정값입니다.
expires_innumberyesno300초 단위 만료 시간입니다.
scopespace-separated stringyesno-실제로 승인되어 반영된 scope입니다.
result.verification_idstringyesno-이번 Verify 거래 id입니다.
result.verifiedbooleanyesnotruetrue일 때만 인증 완료로 처리합니다.
result.substringyesno-client별 pairwise subject입니다.
result.auth_timeISO 8601 stringyesno-JSON 응답의 인증 완료 시각입니다.JWT claim은 NumericDate seconds입니다.
verify-token-response.jsontoken exchange 성공 응답
{
  "verification_token": "jwt_placeholder",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "ssafy.verify ssafy.affiliation ssafy.name",
  "result": {
    "verification_id": "verification_id_placeholder",
    "verified": true,
    "sub": "pairwise_subject_placeholder",
    "auth_time": "2026-06-18T00:00:00.000Z"
  }
}

Error Spec

Error CodeHTTPCauseAction
INVALID_REQUEST400필수 parameter 또는 form field 누락문서의 Request Parameters와 Body를 확인합니다.
CLIENT_NOT_FOUND401등록되지 않은 client_idDeveloper Portal의 client_id와 배포 환경변수를 확인합니다.
CLIENT_PENDING403client가 아직 승인되지 않음관리자 승인 후 다시 요청합니다.
CLIENT_DISABLED403client가 disabled 또는 rejected 상태운영자에게 request_id와 client_id를 전달합니다.
REDIRECT_URI_MISMATCH400redirect_uri가 등록값과 exact match가 아님Local, Preview, Production URL을 각각 등록합니다.
SCOPE_NOT_ALLOWED400요청 scope가 승인되지 않았거나 ssafy.verify가 없음승인 scope와 요청 scope를 맞춥니다.
RATE_LIMITED429token 또는 인증 요청 rate limit 초과잠시 뒤 재시도합니다.
CLIENT_SECRET_INVALID401confidential client secret 불일치backend 환경변수의 secret과 재발급 이력을 확인합니다.
VERIFY_TRANSACTION_NOT_FOUND400code가 없거나 다른 client의 code이거나 만료됨Verify authorize부터 다시 시작합니다.
PKCE_VERIFICATION_FAILED400code_verifier가 authorize 당시 challenge와 맞지 않음PKCE verifier 보관과 전달 경로를 확인합니다.
USER_BLOCKED403사용자가 차단되었거나 profile을 사용할 수 없음request_id와 사용자 식별자를 운영자에게 전달합니다.

Security Notes

  • code와 code_verifier는 token exchange 직후 폐기합니다.
  • verification_token은 서버에서만 검증합니다.
  • JWT의 iss, aud, exp, sub, client_id, verified, auth_time, amr, acr를 모두 검증합니다.

JWKS

/verify/jwks

verification_token 서명 검증용 public JWK Set을 조회합니다.

GET

Path

/verify/jwks

Authentication

none

Headers

NameTypeRequiredNullableValuesDescription
x-request-idstringnono1-128 chars문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다.

Response Message

NameTypeRequiredNullableValuesDescription
keys[]array<JWK>yesno-RS256 public key 목록입니다.
keys[].kidstringyesno-JWT header kid와 매칭합니다.
keys[].algstringyesnoRS256서명 알고리즘입니다.
verify-jwks-response.jsonJWKS 응답 형태
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "ssafy-verify-main",
      "use": "sig",
      "alg": "RS256",
      "n": "public_modulus_placeholder",
      "e": "AQAB"
    }
  ]
}

Error Spec

Error CodeHTTPCauseAction
CONFIGURATION_MISSING500서명 키 설정 오류운영 환경변수와 request_id를 확인합니다.

Security Notes

  • JWKS에는 private key가 포함되지 않습니다.
  • 응답은 public cache max-age 300으로 제공됩니다.
  • verification_token 원문을 public 로그에 남기지 않습니다.

Verification token claims

외부 앱 backend는 verification_token의 서명과 아래 claim을 검증한 뒤 최소 결과만 저장합니다.

NameTypeRequiredNullableValuesDescription
issURL stringyesnohttps://verify.myknow.xyzcanonical issuer입니다.
audstringyesno-partner client_id와 같아야 합니다.
substringyesno-client별 pairwise subject입니다.
client_idstringyesno-token을 발급받은 client id입니다.
verifiedbooleanyesnotruetrue가 아니면 인증 성공으로 처리하지 않습니다.
auth_timeNumericDate secondsyesno-JWT claim에서는 seconds number입니다.
amrarray<string>yesnoincludes mattermost_dmMattermost DM 코드 인증 방식입니다.
acrstringyesnourn:ssafy:verify:assurance:mattermost-team-dm:v1Verify v1 보증 수준입니다.
ssafy_verifiedbooleanscope-gatednotrue | falseJWT claim.`ssafy.verify` scope가 승인된 경우 포함됩니다.
ssafy_mattermost_user_idstringscope-gatedno-JWT claim.`ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다.
namestringscope-gatedno-JWT claim.`ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다.
pictureURL stringscope-gatedyes-JWT claim.`ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다.
ssafy_cohortstringscope-gatedyes0 | 14 | 15 | 16 | nullJWT claim.`ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다.
ssafy_campusstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | nullJWT claim.`ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다.
ssafy_regionstringscope-gatedyes서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | nullJWT claim.`ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다.
ssafy_trackstringscope-gatedyescanonical slug | nullJWT claim.`ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다.
ssafy_track_namestringscope-gatedyesfree-form display label | nullJWT claim.`ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다.
ssafy_rolestringscope-gatednomember | owner | admin | auditorJWT claim.`ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다.
ssafy_role_namestringscope-gatedyesfree-form display labelJWT claim.표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다.
ssafy_is_staffbooleanscope-gatednotrue | falseJWT claim.운영진/스태프성 역할 여부입니다.

auth_time 타입

`/verify/token` JSON의 `result.auth_time`은 ISO 8601 문자열이고, verification_token JWT claim의 `auth_time`은 NumericDate seconds입니다.

Security notes

  • callback code와 verification_token 원문은 로그에 남기지 않습니다.
  • redirect URI는 등록값과 exact match여야 합니다.
  • callback iss가 canonical issuer와 다르면 token exchange를 시작하지 않습니다.
  • PKCE verifier는 token exchange 직후 폐기합니다.
  • JWT는 iss, aud, exp, sub, client_id, verified, auth_time, amr, acr를 모두 검증합니다.
  • 기존 Mattermost 인증 계정 매핑이 필요할 때만 ssafy.mattermost_id를 요청합니다.