> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# 서명 키(JWKS) 조회 | Didit Authentication API

> GET /.well-known/jwks.json은 Didit 애플리케이션의 공개 ES256 키를 반환합니다. kid 기준으로 캐시하세요. 키는 30일마다 교체되며 1일 중복 기간이 있습니다.

> [!WARNING]
>
> 인증 기능은 새로운 기능입니다. 이 엔드포인트는 `https://<name>.auth.didit.me`와 같이 애플리케이션의 발급자가 제공하며, Didit API 키는 받지 않습니다.

키는 30일마다 교체되며, 교체 전후로 하루씩 겹치는 기간이 있습니다. 따라서 5분 동안 캐시된 JWKS는 키가 교체되는 동안에도 계속 토큰을 검증합니다. 토큰에 보유하지 않은 `kid`가 명시되어 있으면 JWKS를 다시 가져오세요. 다시 가져오는 횟수는 1분에 최대 한 번입니다. 자세한 내용은 [캐싱 및 교체](https://docs.didit.me/ko/authentication/tokens-and-jwks.md#캐싱과-교체)를 참조하세요.


## OpenAPI

````yaml ko/openapi-authentication.json GET /.well-known/jwks.json
openapi: 3.0.3
info:
  title: Didit Authentication API
  version: 1.0.0
  description: >-
    애플리케이션의 최종 사용자를 위한 로그인 제품인 Didit Authentication의 서버용 endpoint입니다.
    Authentication은 **새 제품**이며 2026-11-01 릴리스에서 프로덕션에 적용됩니다.


    ## 기본 URL


    모든 애플리케이션은 자체적인 OpenID Connect 발급자(issuer)이며, 아래의 모든 endpoint는 해당 발급자의 루트에서
    제공됩니다.


    - 프로덕션 환경 애플리케이션의 경우 `https://<name>.auth.didit.me`, 샌드박스 애플리케이션의 경우
    `https://<name>.sandbox.auth.didit.me`.

    - 활성화된 커스텀 도메인. 활성화되면 기본 호스트 대신 발급자 역할을 합니다. 새 token의 `iss`에 해당 도메인이 지정되고,
    discovery가 이를 가리키며, 공개 가능 키(publishable key)가 변경됩니다. 기본 호스트에서 발급된 access
    token은 갱신될 때까지 `/session/`, `/step-up/`, `/monitoring-token/` 호출에 실패합니다.


    프로덕션과 샌드박스는 별개의 애플리케이션이므로, 별도의 키를 가진 서로 다른 발급자입니다. 공개 가능 키는 `pk_live_`(프로덕션)
    또는 `pk_test_`(샌드박스) 뒤에 발급자의 패딩 없는 base64url 인코딩(끝의 슬래시 제외)이 붙은 형태입니다. 이를
    디코딩하면 발급자를 확인할 수 있습니다. 이는 공개 식별자이며 자격 증명이 아닙니다.


    동일한 JSON endpoint는
    `https://apx.didit.me/auth/v2/authentication/{application_id}/`에서도 제공됩니다.
    token은 어느 주소에서 발급되었든 항상 위의 발급자를 지정합니다.


    ## 자격 증명


    이러한 endpoint는 Didit API 키나 콘솔 token을 절대 사용하지 않습니다.


    - Discovery, JWKS 및 `POST /token/`은 자격 증명을 요구하지 않습니다. 클라이언트는 공개 클라이언트(public
    client)입니다. 클라이언트 시크릿(client secret)이 없으며, 인가 코드(authorization code)는 PKCE
    검증자(`S256`)에 바인딩됩니다. `client_id`는 애플리케이션 ID입니다.

    - `/session/`, `/step-up/`, `/monitoring-token/`은 사용자의 access token을 사용합니다:
    `Authorization: Bearer <access_token>`. 성공한 각 호출은 세션의 활동으로 간주되므로 세션의 유휴 시간
    초과(idle timeout)가 재시작됩니다.


    ## 요청 본문


    본문을 받는 모든 endpoint는 JSON, 양식 인코딩(form-encoded) 및 multipart 본문을 허용합니다. 이 문서에는
    JSON과 OAuth 클라이언트가 전송하는 `POST /token/`용 양식 인코딩이 나열되어 있습니다. endpoint가 허용하지 않는
    필드는 `400 {"<field>": "unknown_field"}`를 반환합니다.


    ## Token


    - **Access token**: 헤더 `typ: at+jwt`를 가진 ES256 JWT로, 최대 5분 동안 유효하며 해당 세션보다
    오래 유효할 수 없습니다. 클레임: `AccessTokenClaims`.

    - **ID token**: 헤더 `typ: JWT`를 가진 ES256 JWT입니다. 클레임: `IDTokenClaims`. access
    token으로는 절대 허용되지 않습니다.

    - **Refresh token**: 사용할 때마다 순환(rotate)되는 임의의 문자열입니다. 이미 사용된 refresh token을
    다시 보내면 세션이 취소(revoke)됩니다.


    `GET /.well-known/jwks.json`의 키로 token을 검증하세요. 알고리즘은 `ES256`이어야 하고, `typ`은
    token 유형과 일치해야 하며, `iss`는 구성된 발급자와 같아야 하고, `aud`는 애플리케이션 ID여야 합니다. 오프라인
    검증에서는 access token이 만료되어야만 취소된 세션을 감지할 수 있습니다. 즉각적인 확인을 위해서는 `GET
    /session/`을 사용하세요.


    ## 오류


    | 본문 | 조건 |

    | --- | --- |

    | `{"error": "<code>"}` | `POST /token/`에서 발생하는 오류로, 모두 상태 코드 `400`이며 [RFC
    6749 섹션 5.2](https://www.rfc-editor.org/rfc/rfc6749#section-5.2) 형식의 본문을
    갖습니다: `invalid_grant`, `invalid_client` 및 Didit의 `mfa_required`. 서비스 오류:
    `temporarily_unavailable`, `monitoring_unavailable`, `monitoring_disabled`.
    |

    | `{"<field>": ["<message>"]}` 또는 `{"<field>": "<code>"}` | 요청 필드 유효성 검사에
    실패했습니다. `non_field_errors`는 여러 필드에 걸친 규칙 위반을 나타내며, `unknown_field`는
    endpoint가 허용하지 않는 필드를 나타냅니다. |

    | `{"detail": "<message>"}` | `401`(`invalid_token`),
    `403`(`origin_not_allowed`), `404`(알 수 없는 애플리케이션 또는 step-up), `429`(속도 제한).
    또한 파싱할 수 없는 본문의 경우 `400`(`JSON parse error - ...`), endpoint가 지원하지 않는 메서드의
    경우 `405`, 지원되지 않는 `Content-Type`의 경우 `415`. |


    `401` 응답에는 `WWW-Authenticate: Bearer`도 포함되며, `429` 응답에는 초 단위의 `Retry-After`가
    포함됩니다.


    두 가지 거부 응답은 JSON 형식이 아닙니다. 발급자가 아닌 주소는 이러한 endpoint에 도달하기 전에 거부됩니다(예: 사용되지
    않는 `*.auth.didit.me` 이름에 대한 빈 `404`). UUID가 아닌 `step_up_id`는 `404` 페이지를
    반환합니다.


    ## 속도 제한


    요청은 두 계층을 통과해야 합니다.


    클라이언트 IP 주소당, 애플리케이션당, endpoint당 계산되는 **Endpoint 제한**:


    - Discovery 및 JWKS: 분당 1,200건, 시간당 12,000건의 요청.

    - 기타 모든 endpoint: 분당 30건, 시간당 300건의 요청. `GET` 및 `DELETE /session/`은 동일한 카운트를
    공유합니다.


    제한 초과 시: `429 {"detail": "Too many requests from this IP address. Please
    wait before trying again. Expected available in 60 seconds."}` 또는 시간당 제한의 경우
    `3600 seconds`.


    모든 `POST` 및 `DELETE`에 적용되는 **봇 보호**. 해당 버킷은 호스팅된 로그인 단계를 포함한 애플리케이션의 모든
    `POST` 및 `DELETE` 요청이 공유하며 지속적으로 리필됩니다:


    - 클라이언트 IP 주소당 분당 60건의 요청.

    - 브라우저 및 네이티브 SDK와 같이 기기를 식별하는 요청의 경우 기기당 분당 30건의 요청.

    - 모든 IP 주소를 합산한 전체 애플리케이션 기준 분당 3,000건의 요청. token
    갱신(`grant_type=refresh_token`인 `POST /token/`)은 별도의 독립된 버킷을 사용하며 전체 애플리케이션
    기준 분당 6,000건입니다.


    버킷 초과 시: `429 {"detail": "rate_limited Expected available in N
    seconds."}`(N이 1인 경우 `1 second.`). IP별 버킷은 여러 endpoint에 걸쳐 적용되므로, 한 주소에서
    token, step-up, 로그아웃 호출을 섞어 요청하면 특정 endpoint 제한에 도달하기 전에 버킷이 소진될 수 있습니다.
    애플리케이션 버킷은 브라우저를 포함하여 해당 애플리케이션의 모든 클라이언트를 계산합니다.


    두 계층 모두 endpoint가 실행되기 전에 응답하므로, `429`를 받은 요청은 실행되지 않은 상태입니다. 갱신을 포함하여
    `Retry-After` 시간이 지난 후 다시 재시도하세요.


    ## 브라우저 요청


    `Origin` 헤더를 포함하는 요청은 애플리케이션 자체의 origin 또는 허용된 origin 중 하나에서 시작되어야 하며, 그렇지
    않으면 `403 origin_not_allowed`를 받습니다. 서버 간(server-to-server) 요청은 `Origin`을
    전송하지 않습니다.


    ## 여기에 포함되지 않는 내용


    이러한 경로는 호스팅 페이지, 브라우저 및 네이티브 SDK, AI 에이전트를 위한 경로입니다. 서버 규격(contract)에 포함되지
    않으며 서버 SDK는 이를 호출하지 않습니다:


    - 로그인 단계: `POST /authorize/`, `POST /password/`, `/email/*`, `/phone/*`,
    `/password-reset/*`, `/passkeys/*`, `/mfa/*`, `/social/*` 및
    `/protection/complete/`.

    - 호스팅 페이지 및 콜백: `/sign-in`, `/magic`, `/invite`, `/reset`, `/verify/`,
    `/sso/*`, `/wallet/callback/`, `/logo/` 및 `/.well-known/` 하위의 네이티브 앱 연결 파일.

    - 에이전트 액세스: `/agents/*` 및 `/grants/`(AI 에이전트의 access token 실시간 확인을 위한 `GET
    /agents/session/` 포함). 에이전트 액세스는 본 버전의 규격에 포함되지 않습니다.

    - 콘솔 세션을 사용하는 Authentication 콘솔 관리 API.


    Authentication 이벤트는 아직 webhook으로 전송되지 않으므로, 이 문서에서는 webhook을 정의하지 않습니다.
servers:
  - url: https://{issuer}
    description: '귀하 애플리케이션의 발급자: 로그인 오리진입니다.'
    variables:
      issuer:
        default: your-app.auth.didit.me
        description: >-
          발급자 호스트: `<name>.auth.didit.me`(프로덕션),
          `<name>.sandbox.auth.didit.me`(샌드박스) 또는 활성 커스텀 도메인입니다.
security: []
tags:
  - name: Issuer
    description: token을 검증하기 위한 디스커버리 및 서명 키입니다.
  - name: Tokens
    description: 인가 코드 및 갱신 권한 부여입니다.
  - name: Sessions
    description: 액세스 token 뒤의 세션을 확인하고 종료합니다.
  - name: Step-ups
    description: 민감한 작업 전 수행하는 인증입니다.
  - name: Monitoring
    description: 거래 모니터링 이벤트에 대한 기기 자격 증명입니다.
paths:
  /.well-known/jwks.json:
    get:
      tags:
        - Issuer
      summary: 서명 키 가져오기
      description: >-
        애플리케이션의 공개 ES256 키입니다. 모든 token은 `kid` 헤더에 해당 키의 이름을 지정합니다.


        키는 30일마다 교체됩니다. 다음 키는 서명을 시작하기 하루 전에 게시되며, 이전 키는 서명이 중단된 후 하루 동안 게시 상태를
        유지하므로 최대 5분 동안 캐시된 JWKS는 교체 중에도 계속 검증할 수 있습니다. 키를 `kid`별로 캐시하고, token이
        보유하지 않은 `kid`를 지정하는 경우 최대 1분에 한 번 JWKS를 다시 가져오십시오. Didit이 손상된 키를 철회하면 해당
        키는 이 문서에서 즉시 제거되며 `GET /session/`은 해당 키로 서명된 모든 token을 거부합니다. 교체 키는 사전
        게시 하루 없이 즉시 서명을 시작하므로, 최대 5분 전에 캐시된 JWKS는 여전히 철회된 키를 수락하고 교체 키를 아직 인식하지
        못합니다.
      operationId: authentication_get_jwks
      responses:
        '200':
          description: 게시된 키입니다.
          headers:
            Cache-Control:
              description: >-
                `public, max-age=300`: 사용자와 공유 캐시는 성공한 응답을 액세스 토큰 수명인 5분 동안 보관할
                수 있습니다.
              schema:
                type: string
                example: public, max-age=300
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JSONWebKeySet'
              examples:
                Keys:
                  summary: 서명 키 1개
                  value:
                    keys:
                      - kty: EC
                        crv: P-256
                        x: ZDN2unLMultuZwNY_ZKMvsKLRrlC_usPFZyK77cnDO8
                        'y': CfLKoaJNvhFpDWqG56H8dD_j0f1c93RdR63bhIdTh3I
                        kid: k_8sJ2dQvX0nLr5TfYb3HkPw
                        use: sig
                        alg: ES256
        '403':
          $ref: '#/components/responses/OriginNotAllowed'
        '404':
          $ref: '#/components/responses/ApplicationNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/TemporarilyUnavailable'
      security:
        - {}
components:
  schemas:
    JSONWebKeySet:
      type: object
      description: >-
        애플리케이션의 게시된 공개 서명 키입니다. 일반적으로 하나의 키이며, 교체 시점에는 다음 키가 서명 하루 전에 게시되고 이전 키가
        중단 후 하루 동안 유지되어 두 개의 키가 됩니다.
      required:
        - keys
      properties:
        keys:
          type: array
          items:
            $ref: '#/components/schemas/JSONWebKey'
    JSONWebKey:
      type: object
      description: JWK 형식(RFC 7517)의 P-256 공개 키입니다.
      required:
        - kty
        - crv
        - x
        - 'y'
        - kid
        - use
        - alg
      properties:
        kty:
          type: string
          enum:
            - EC
        crv:
          type: string
          enum:
            - P-256
        x:
          type: string
          description: 공개 포인트의 x 좌표(base64url)입니다.
          example: ZDN2unLMultuZwNY_ZKMvsKLRrlC_usPFZyK77cnDO8
        'y':
          type: string
          description: 공개 포인트의 y 좌표(base64url)입니다.
          example: CfLKoaJNvhFpDWqG56H8dD_j0f1c93RdR63bhIdTh3I
        kid:
          type: string
          description: 키 ID로, 이 키가 서명하는 모든 token의 헤더에 포함되는 `kid`입니다. 새 키 ID는 `k_`로 시작합니다.
          example: k_8sJ2dQvX0nLr5TfYb3HkPw
        use:
          type: string
          enum:
            - sig
        alg:
          type: string
          enum:
            - ES256
    Detail:
      type: object
      required:
        - detail
      properties:
        detail:
          type: string
          example: invalid_token
    OAuthError:
      title: OAuth error
      type: object
      description: OAuth 오류(RFC 6749 섹션 5.2) 또는 서비스 오류입니다.
      required:
        - error
      properties:
        error:
          type: string
          example: invalid_grant
  responses:
    OriginNotAllowed:
      description: >-
        요청에 애플리케이션 자체의 오리진도 아니고 허용된 오리진 목록에도 없는 `Origin`이 포함되어 있습니다. 서버 요청은
        `Origin`을 보내지 않습니다.
      headers:
        Cache-Control:
          description: 항상 `no-store`입니다. 이 응답은 절대 캐시하지 마십시오.
          schema:
            type: string
            example: no-store
        Pragma:
          description: 항상 `no-cache`입니다.
          schema:
            type: string
            example: no-cache
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Detail'
          examples:
            OriginNotAllowed:
              summary: 허용되지 않은 오리진(Origin)
              value:
                detail: origin_not_allowed
    ApplicationNotFound:
      description: >-
        주소가 애플리케이션에 속해 있지만 해당 애플리케이션에 인증이 활성화되어 있지 않습니다. `apx.didit.me` 주소에서는
        애플리케이션 ID를 알 수 없습니다. 어떤 애플리케이션에도 속하지 않는 주소는 endpoint에 도달하기 전에 거부되며 해당
        응답은 JSON이 아닙니다.
      headers:
        Cache-Control:
          description: 항상 `no-store`입니다. 이 응답은 절대 캐시하지 마십시오.
          schema:
            type: string
            example: no-store
        Pragma:
          description: 항상 `no-cache`입니다.
          schema:
            type: string
            example: no-cache
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Detail'
          examples:
            ApplicationNotFound:
              summary: 알 수 없는 애플리케이션
              value:
                detail: No Configuration matches the given query.
    RateLimited:
      description: >-
        이 endpoint에 대해 해당 IP 주소로부터의 요청이 너무 많거나, `POST` 또는 `DELETE` 요청 시 봇 보호 버킷이
        비어 있습니다. 아무 작업도 실행되지 않았습니다. `Retry-After`초 동안 기다린 후 다시 시도하십시오.
      headers:
        Retry-After:
          description: 재시도하기 전 대기할 시간(초)입니다.
          schema:
            type: integer
            example: 60
        Cache-Control:
          description: 항상 `no-store`입니다. 이 응답은 절대 캐시하지 마십시오.
          schema:
            type: string
            example: no-store
        Pragma:
          description: 항상 `no-cache`입니다.
          schema:
            type: string
            example: no-cache
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Detail'
          examples:
            RateLimited:
              summary: endpoint 제한
              value:
                detail: >-
                  Too many requests from this IP address. Please wait before
                  trying again. Expected available in 60 seconds.
            BotProtection:
              summary: 봇 방지 제한(POST 및 DELETE)
              value:
                detail: rate_limited Expected available in 2 seconds.
    TemporarilyUnavailable:
      description: 요청이 의존하는 서비스를 일시적으로 사용할 수 없습니다. 백오프를 적용하여 다시 시도하십시오.
      headers:
        Cache-Control:
          description: 항상 `no-store`입니다. 이 응답은 절대 캐시하지 마십시오.
          schema:
            type: string
            example: no-store
        Pragma:
          description: 항상 `no-cache`입니다.
          schema:
            type: string
            example: no-cache
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/OAuthError'
          examples:
            TemporarilyUnavailable:
              summary: 나중에 재시도
              value:
                error: temporarily_unavailable

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.