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 공식 범위
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
Callback fields
{redirect_uri}?code=...&state=...&iss=...
인증 성공 또는 실패 후 외부 앱 callback URL로 전달되는 field 계약입니다.
Path
{redirect_uri}?code=...&state=...&iss=...Authentication
외부 앱이 state와 iss를 로컬에서 검증합니다.
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
code | string | success only | no | - | 성공 시 전달되는 1회성 code입니다. |
state | string | yes | no | - | 요청 시작 값과 비교합니다. |
iss | URL string | success only | no | https://verify.myknow.xyz | canonical issuer입니다. |
error | string | failure only | no | access_denied | invalid_request | server_error | OAuth-style public error입니다. |
error_code | string | failure only | no | - | SSAFY Verify stable error code입니다. |
request_id | string | failure only | no | - | 운영 추적용 request id입니다. |
callback-error-url.txt실패 callbackhttps://partner.example.com/ssafy/callback?
error=access_denied
&error_code=CONSENT_DENIED
&request_id=req_placeholder
&state=random_state_from_partnerError Spec
| Error Code | HTTP | Cause | Action |
|---|---|---|---|
INVALID_REQUEST | 400 | 필수 parameter 또는 form field 누락 | 문서의 Request Parameters와 Body를 확인합니다. |
CLIENT_NOT_FOUND | 401 | 등록되지 않은 client_id | Developer Portal의 client_id와 배포 환경변수를 확인합니다. |
CLIENT_PENDING | 403 | client가 아직 승인되지 않음 | 관리자 승인 후 다시 요청합니다. |
CLIENT_DISABLED | 403 | client가 disabled 또는 rejected 상태 | 운영자에게 request_id와 client_id를 전달합니다. |
REDIRECT_URI_MISMATCH | 400 | redirect_uri가 등록값과 exact match가 아님 | Local, Preview, Production URL을 각각 등록합니다. |
SCOPE_NOT_ALLOWED | 400 | 요청 scope가 승인되지 않았거나 ssafy.verify가 없음 | 승인 scope와 요청 scope를 맞춥니다. |
RATE_LIMITED | 429 | token 또는 인증 요청 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로 반환하는 로컬 오류 계약입니다.
Path
/sdk/ssafy-verify.jsAuthentication
브라우저 origin과 redirectUri origin을 로컬에서 검증합니다.
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
error | string | yes | no | - | SDK local error type입니다. |
error_code | string | yes | no | SSAFY_VERIFY_REDIRECT_ORIGIN_MISMATCH | SSAFY_VERIFY_POPUP_CLOSED | SSAFY_VERIFY_CALLBACK_TIMEOUT | SSAFY_VERIFY_STATE_MISMATCH | browser-local stable code입니다.SSAFY Verify HTTP response가 아닐 수 있습니다. |
request_id | string | yes | yes | - | authorize 응답이 도착한 경우에만 포함됩니다. |
phase | string | yes | no | preflight | popup | callback | authorize | 실패 단계입니다. |
state | string | no | yes | - | 가능한 경우 요청 state를 보존합니다. |
CODE / Mean
| CODE | Mean |
|---|---|
SSAFY_VERIFY_REDIRECT_ORIGIN_MISMATCH | redirectUri origin과 opener origin 불일치 |
SSAFY_VERIFY_POPUP_CLOSED | callback 도착 전 popup 닫힘 |
SSAFY_VERIFY_CALLBACK_TIMEOUT | timeoutMs 안에 callback message 없음 |
SSAFY_VERIFY_STATE_MISMATCH | callback 또는 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 Code | HTTP | Cause | Action |
|---|---|---|---|
SSAFY_VERIFY_REDIRECT_ORIGIN_MISMATCH | local | redirectUri origin과 현재 window origin 불일치 | 현재 origin에서 callback URL을 계산하고 Developer Portal에 exact 등록합니다. |
SSAFY_VERIFY_CALLBACK_TIMEOUT | local | popup 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로 교환합니다.
Path
/verify/tokenAuthentication
public client는 PKCE만, confidential client는 client_secret도 함께 검증합니다.
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
content-type | string | yes | no | application/x-www-form-urlencoded | form body endpoint에서 사용합니다. |
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Request Body
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
grant_type | string | yes | no | verification_code | Verify API 전용 grant type입니다. |
code | string | yes | no | - | callback으로 받은 1회성 code입니다. |
client_id | string | yes | no | - | authorize 요청의 client_id와 같아야 합니다. |
client_secret | string | confidential only | no | - | confidential client backend에서만 전송합니다. |
code_verifier | PKCE verifier | yes | no | - | 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_VERIFIERResponse Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
verification_token | JWT string | yes | no | - | RS256 signed JWT입니다.JWKS로 검증하고 원문 저장은 피합니다. |
token_type | string | yes | no | Bearer | 고정값입니다. |
expires_in | number | yes | no | 300 | 초 단위 만료 시간입니다. |
scope | space-separated string | yes | no | - | 실제로 승인되어 반영된 scope입니다. |
result.verification_id | string | yes | no | - | 이번 Verify 거래 id입니다. |
result.verified | boolean | yes | no | true | true일 때만 인증 완료로 처리합니다. |
result.sub | string | yes | no | - | client별 pairwise subject입니다. |
result.auth_time | ISO 8601 string | yes | no | - | 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 Code | HTTP | Cause | Action |
|---|---|---|---|
INVALID_REQUEST | 400 | 필수 parameter 또는 form field 누락 | 문서의 Request Parameters와 Body를 확인합니다. |
CLIENT_NOT_FOUND | 401 | 등록되지 않은 client_id | Developer Portal의 client_id와 배포 환경변수를 확인합니다. |
CLIENT_PENDING | 403 | client가 아직 승인되지 않음 | 관리자 승인 후 다시 요청합니다. |
CLIENT_DISABLED | 403 | client가 disabled 또는 rejected 상태 | 운영자에게 request_id와 client_id를 전달합니다. |
REDIRECT_URI_MISMATCH | 400 | redirect_uri가 등록값과 exact match가 아님 | Local, Preview, Production URL을 각각 등록합니다. |
SCOPE_NOT_ALLOWED | 400 | 요청 scope가 승인되지 않았거나 ssafy.verify가 없음 | 승인 scope와 요청 scope를 맞춥니다. |
RATE_LIMITED | 429 | token 또는 인증 요청 rate limit 초과 | 잠시 뒤 재시도합니다. |
CLIENT_SECRET_INVALID | 401 | confidential client secret 불일치 | backend 환경변수의 secret과 재발급 이력을 확인합니다. |
VERIFY_TRANSACTION_NOT_FOUND | 400 | code가 없거나 다른 client의 code이거나 만료됨 | Verify authorize부터 다시 시작합니다. |
PKCE_VERIFICATION_FAILED | 400 | code_verifier가 authorize 당시 challenge와 맞지 않음 | PKCE verifier 보관과 전달 경로를 확인합니다. |
USER_BLOCKED | 403 | 사용자가 차단되었거나 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을 조회합니다.
Path
/verify/jwksAuthentication
none
Headers
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
x-request-id | string | no | no | 1-128 chars | 문의와 로그 추적용 id입니다.없으면 서버가 안전한 request_id를 생성합니다. |
Response Message
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
keys[] | array<JWK> | yes | no | - | RS256 public key 목록입니다. |
keys[].kid | string | yes | no | - | JWT header kid와 매칭합니다. |
keys[].alg | string | yes | no | RS256 | 서명 알고리즘입니다. |
verify-jwks-response.jsonJWKS 응답 형태{
"keys": [
{
"kty": "RSA",
"kid": "ssafy-verify-main",
"use": "sig",
"alg": "RS256",
"n": "public_modulus_placeholder",
"e": "AQAB"
}
]
}Error Spec
| Error Code | HTTP | Cause | Action |
|---|---|---|---|
CONFIGURATION_MISSING | 500 | 서명 키 설정 오류 | 운영 환경변수와 request_id를 확인합니다. |
Security Notes
- JWKS에는 private key가 포함되지 않습니다.
- 응답은 public cache max-age 300으로 제공됩니다.
- verification_token 원문을 public 로그에 남기지 않습니다.
Verification token claims
외부 앱 backend는 verification_token의 서명과 아래 claim을 검증한 뒤 최소 결과만 저장합니다.
| Name | Type | Required | Nullable | Values | Description |
|---|---|---|---|---|---|
iss | URL string | yes | no | https://verify.myknow.xyz | canonical issuer입니다. |
aud | string | yes | no | - | partner client_id와 같아야 합니다. |
sub | string | yes | no | - | client별 pairwise subject입니다. |
client_id | string | yes | no | - | token을 발급받은 client id입니다. |
verified | boolean | yes | no | true | true가 아니면 인증 성공으로 처리하지 않습니다. |
auth_time | NumericDate seconds | yes | no | - | JWT claim에서는 seconds number입니다. |
amr | array<string> | yes | no | includes mattermost_dm | Mattermost DM 코드 인증 방식입니다. |
acr | string | yes | no | urn:ssafy:verify:assurance:mattermost-team-dm:v1 | Verify v1 보증 수준입니다. |
ssafy_verified | boolean | scope-gated | no | true | false | JWT claim.`ssafy.verify` scope가 승인된 경우 포함됩니다. |
ssafy_mattermost_user_id | string | scope-gated | no | - | JWT claim.`ssafy.mattermost_id` scope가 승인되고 Mattermost identity가 있을 때 포함됩니다. |
name | string | scope-gated | no | - | JWT claim.`ssafy.name` scope가 승인된 경우 포함되는 표시 이름입니다. |
picture | URL string | scope-gated | yes | - | JWT claim.`ssafy.profile_image` scope가 승인된 경우 포함됩니다.가능하면 absolute HTTPS URL이며, relative URL이면 client는 issuer 기준으로 resolve합니다. |
ssafy_cohort | string | scope-gated | yes | 0 | 14 | 15 | 16 | null | JWT claim.`ssafy.affiliation` scope가 승인된 경우 포함됩니다.public 응답 경계에서 canonical 값으로 정규화됩니다. |
ssafy_campus | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | JWT claim.`ssafy.affiliation` scope가 승인된 경우 포함됩니다.`서울 캠퍼스`, `서울캠퍼스` 같은 legacy 값은 `서울`로 반환됩니다. |
ssafy_region | string | scope-gated | yes | 서울 | 대전 | 광주 | 구미 | 부울경 | 창업 | null | JWT claim.`ssafy.affiliation` scope가 승인된 경우 포함됩니다.현재 public vocabulary는 campus와 같습니다. |
ssafy_track | string | scope-gated | yes | canonical slug | null | JWT claim.`ssafy.track` scope가 승인된 경우 포함됩니다.affiliation scope에는 포함되지 않습니다. |
ssafy_track_name | string | scope-gated | yes | free-form display label | null | JWT claim.`ssafy.track` scope가 승인된 경우 포함되는 track 표시 이름입니다. |
ssafy_role | string | scope-gated | no | member | owner | admin | auditor | JWT claim.`ssafy.role` scope가 승인된 경우 포함되는 권한 role입니다. |
ssafy_role_name | string | scope-gated | yes | free-form display label | JWT claim.표시용 역할명입니다.알려진 예시는 교육생, 운영진, 팀원, 팀장, 강사, 프로, 실습코치, 컨설턴트, 트랙대표, 사무국, 연구팀입니다. |
ssafy_is_staff | boolean | scope-gated | no | true | false | JWT claim.운영진/스태프성 역할 여부입니다. |
auth_time 타입
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를 요청합니다.