{
  "info": {
    "name": "KYC Match v0.2 - Authorization Code",
    "_postman_id": "kycmatch-v02-de-staging-authcode",
    "description": "KYC Match v0.2 (kyc-match/v0.2/match) using the Authorization Code grant. This grant requires an interactive browser step outside Postman (opening the authorize URL, logging in/consenting, capturing the redirected code) - request 1 builds the authorize URL, request 2 exchanges the manually-pasted code for a token, request 3 calls KYC Match. Client authentication is PRIVATE_KEY_JWT only (RS256-signed client_assertion) - never client_secret. See the base_url variable description for the default environment and how to switch country/environment.\n\nScope is the exact string used by CheckKycMatchEndpoint in SEPAPIs.cs: \"dpv:FraudPreventionAndDetection kyc-match:match\". This app only prefixes scopes with \"openid\" when its UsesOpenIdScope setting is active for a given credential profile/environment (see SEPAPIs.cs ResolveScope calls) - that is environment-specific, not fixed for KYC Match, so this collection ships the base scope as-is. If your real OAuth server requires an \"openid\" scope for CIBA or Authorization Code token issuance, prepend it yourself in the `scope` variable (e.g. \"openid dpv:FraudPreventionAndDetection kyc-match:match\").\n\nNot currently listed for any country in the product catalog. (Source: this app's live product-catalog grant-type configuration, staging environment - see the /documentation/postman-collections page for the current matrix.)\n\nIMPORTANT: only one version of this API family can be ordered onto a single TMF Application - if your client_id/private_key were provisioned for KYC Match v0.3 instead of this one (e.g. you ordered Sim Swap v1 but not v2, or vice versa), authentication will fail here with a 401 UNAUTHENTICATED/InvalidClaim error even though your credentials are otherwise valid and correctly entered. You need a client_id/private_key from an Application that specifically ordered THIS version to use this collection.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://stg.api.telekom.com",
      "type": "string",
      "description": "Single variable driving every request in this collection. Default is Germany STAGING (https://stg.api.telekom.com). Change ONLY this variable to switch environment/country:\n- Germany production: https://api.telekom.com\n- Austria staging: https://at.stg.api.telekom.com | production: https://at.api.telekom.com\n- Poland staging: https://pl.stg.api.telekom.com | production: https://pl.api.telekom.com\n- Greece staging: https://gr.stg.api.telekom.com | production: https://gr.api.telekom.com\nSee the /documentation/endpoints page in the MBAPI app for the full country/environment reference table. Every request and script in this collection builds the full endpoint path directly from base_url (e.g. {{base_url}}/token) rather than through a separate derived variable, because Postman does not resolve nested variable references (a variable whose value itself contains {{...}}) when read inside pre-request/test scripts via pm.collectionVariables.get() - only base_url itself needs to be correct. "
    },
    {
      "key": "scope",
      "value": "dpv:FraudPreventionAndDetection kyc-match:match",
      "type": "string",
      "description": "Scope is the exact string used by CheckKycMatchEndpoint in SEPAPIs.cs: \"dpv:FraudPreventionAndDetection kyc-match:match\". This app only prefixes scopes with \"openid\" when its UsesOpenIdScope setting is active for a given credential profile/environment (see SEPAPIs.cs ResolveScope calls) - that is environment-specific, not fixed for KYC Match, so this collection ships the base scope as-is. If your real OAuth server requires an \"openid\" scope for CIBA or Authorization Code token issuance, prepend it yourself in the `scope` variable (e.g. \"openid dpv:FraudPreventionAndDetection kyc-match:match\")."
    },
    {
      "key": "phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "client_id",
      "value": "",
      "type": "string",
      "description": "Your registered OAuth client ID (PRIVATE_KEY_JWT client authentication). Required - never ships with a real value."
    },
    {
      "key": "private_key",
      "value": "-----BEGIN PRIVATE KEY-----\nPASTE-YOUR-PKCS8-PRIVATE-KEY-HERE\n-----END PRIVATE KEY-----",
      "type": "string",
      "description": "Your PKCS8 PEM RSA private key (matching the public key/JWK registered for your client). Replace the placeholder body between the BEGIN/END markers with your real key. Never commit or share this value."
    },
    {
      "key": "kid",
      "value": "",
      "type": "string",
      "description": "Optional key ID matching a key in your JWKS - leave blank if your JWKS has only one key."
    },
    {
      "key": "client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by pre-request scripts - the signed RS256 client_assertion JWT. Do not edit manually."
    },
    {
      "key": "access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's test script once a token is obtained."
    },
    {
      "key": "redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Your registered OAuth redirect URI. Change to match your client registration."
    },
    {
      "key": "state",
      "value": "",
      "type": "string",
      "description": "Auto-populated random state value (CSRF protection) by request 1's pre-request script."
    },
    {
      "key": "nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated random nonce value by request 1's pre-request script."
    },
    {
      "key": "auth_code",
      "value": "",
      "type": "string",
      "description": "Paste the authorization code you receive from the browser redirect after completing request 1 in this variable, then run request 2."
    }
  ],
  "item": [
    {
      "name": "1. Build Authorize URL (open in browser)",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (!pm.collectionVariables.get('state')) {",
              "    pm.collectionVariables.set('state', pm.variables.replaceIn('{{$guid}}'));",
              "}",
              "if (!pm.collectionVariables.get('nonce')) {",
              "    pm.collectionVariables.set('nonce', pm.variables.replaceIn('{{$guid}}'));",
              "}"
            ]
          }
        }
      ],
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{redirect_uri}}&scope={{scope}}&state={{state}}&nonce={{nonce}}",
          "host": [
            "{{base_url}}/authorize"
          ],
          "query": [
            {
              "key": "response_type",
              "value": "code"
            },
            {
              "key": "client_id",
              "value": "{{client_id}}"
            },
            {
              "key": "redirect_uri",
              "value": "{{redirect_uri}}"
            },
            {
              "key": "scope",
              "value": "{{scope}}"
            },
            {
              "key": "state",
              "value": "{{state}}"
            },
            {
              "key": "nonce",
              "value": "{{nonce}}"
            }
          ]
        },
        "description": "MANUAL STEP REQUIRED: Authorization Code is an interactive browser flow and cannot be completed inside Postman. Click 'Code' / copy the generated request URL (Postman's preview, or the Send button then check the browser-blocked redirect), open it in a real browser, log in and consent, then copy the `code` query parameter from the redirect back to redirect_uri and paste it into the `auth_code` collection variable. Then run request 2."
      },
      "response": []
    },
    {
      "name": "2. Exchange auth_code for Token",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
              "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
              "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
              "",
              "function b64urlFromBytes(bytes) {",
              "    let bin = '';",
              "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
              "    let b64 = btoa(bin);",
              "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
              "}",
              "",
              "function b64urlFromString(str) {",
              "    // str is a JS string containing only ASCII/UTF-8 JSON text",
              "    const utf8 = unescape(encodeURIComponent(str));",
              "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
              "}",
              "",
              "function Uint8ArrayFromBinaryString(bin) {",
              "    const arr = new Uint8Array(bin.length);",
              "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
              "    return arr;",
              "}",
              "",
              "function pemToDerBytes(pem) {",
              "    const b64 = pem",
              "        .replace(/-----BEGIN [^-]+-----/, '')",
              "        .replace(/-----END [^-]+-----/, '')",
              "        .replace(/\\s+/g, '');",
              "    const bin = atob(b64);",
              "    return Uint8ArrayFromBinaryString(bin);",
              "}",
              "",
              "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
              "function readDerTlv(bytes, offset) {",
              "    const tag = bytes[offset];",
              "    let lenByte = bytes[offset + 1];",
              "    let len, lenBytesUsed;",
              "    if ((lenByte & 0x80) === 0) {",
              "        len = lenByte;",
              "        lenBytesUsed = 1;",
              "    } else {",
              "        const numLenBytes = lenByte & 0x7f;",
              "        len = 0;",
              "        for (let i = 0; i < numLenBytes; i++) {",
              "            len = (len * 256) + bytes[offset + 2 + i];",
              "        }",
              "        lenBytesUsed = 1 + numLenBytes;",
              "    }",
              "    const contentStart = offset + 1 + lenBytesUsed;",
              "    return { tag, len, contentStart, nextOffset: contentStart + len };",
              "}",
              "",
              "function derToBigInt(bytes, start, len) {",
              "    let hex = '';",
              "    for (let i = 0; i < len; i++) {",
              "        const b = bytes[start + i];",
              "        hex += (b < 16 ? '0' : '') + b.toString(16);",
              "    }",
              "    if (hex === '') return BigInt(0);",
              "    return BigInt('0x' + hex);",
              "}",
              "",
              "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
              "function extractRsaPrivateKeyParams(pem) {",
              "    const bytes = pemToDerBytes(pem);",
              "    // Top-level SEQUENCE",
              "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
              "    let cursor = tlv.contentStart;",
              "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
              "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
              "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
              "    let versionPeek = readDerTlv(bytes, cursor);",
              "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
              "    let rsaKeyStart;",
              "    if (second.tag === 0x02) {",
              "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
              "        rsaKeyStart = cursor;",
              "    } else {",
              "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
              "        // skip version INTEGER",
              "        let versionTlv = readDerTlv(bytes, cursor);",
              "        cursor = versionTlv.nextOffset;",
              "        // skip AlgorithmIdentifier SEQUENCE",
              "        let algTlv = readDerTlv(bytes, cursor);",
              "        cursor = algTlv.nextOffset;",
              "        // OCTET STRING wrapping the PKCS#1 key",
              "        let octetTlv = readDerTlv(bytes, cursor);",
              "        // Inside the octet string is the PKCS#1 SEQUENCE",
              "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
              "        rsaKeyStart = inner.contentStart;",
              "    }",
              "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
              "    let p = rsaKeyStart;",
              "    let versionField = readDerTlv(bytes, p);",
              "    p = versionField.nextOffset;",
              "    let nField = readDerTlv(bytes, p);",
              "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
              "    p = nField.nextOffset;",
              "    let eField = readDerTlv(bytes, p);",
              "    p = eField.nextOffset;",
              "    let dField = readDerTlv(bytes, p);",
              "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
              "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
              "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
              "    // exclude that padding byte, so derive it from n's actual bit length.",
              "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
              "    return { n, d, modulusByteLength };",
              "}",
              "",
              "// Modular exponentiation: base^exp mod m using BigInt",
              "function modPow(base, exp, mod) {",
              "    let result = BigInt(1);",
              "    base = base % mod;",
              "    while (exp > BigInt(0)) {",
              "        if (exp % BigInt(2) === BigInt(1)) {",
              "            result = (result * base) % mod;",
              "        }",
              "        exp = exp / BigInt(2);",
              "        base = (base * base) % mod;",
              "    }",
              "    return result;",
              "}",
              "",
              "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
              "const SHA256_DIGEST_INFO_PREFIX_HEX =",
              "    '3031300d060960864801650304020105000420';",
              "",
              "function hexToBytes(hex) {",
              "    const arr = new Uint8Array(hex.length / 2);",
              "    for (let i = 0; i < arr.length; i++) {",
              "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
              "    }",
              "    return arr;",
              "}",
              "",
              "function bytesToHex(bytes) {",
              "    let hex = '';",
              "    for (let i = 0; i < bytes.length; i++) {",
              "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
              "    }",
              "    return hex;",
              "}",
              "",
              "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
              "function emsaPkcs1v15Encode(digestHex, k) {",
              "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
              "    const digestInfo = hexToBytes(digestInfoHex);",
              "    const tLen = digestInfo.length;",
              "    if (k < tLen + 11) {",
              "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
              "    }",
              "    const psLen = k - tLen - 3;",
              "    const em = new Uint8Array(k);",
              "    em[0] = 0x00;",
              "    em[1] = 0x01;",
              "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
              "    em[2 + psLen] = 0x00;",
              "    em.set(digestInfo, 3 + psLen);",
              "    return em;",
              "}",
              "",
              "function bytesToBigInt(bytes) {",
              "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
              "}",
              "",
              "function bigIntToBytes(bi, length) {",
              "    let hex = bi.toString(16);",
              "    if (hex.length % 2 !== 0) hex = '0' + hex;",
              "    let bytes = hexToBytes(hex);",
              "    if (bytes.length < length) {",
              "        const padded = new Uint8Array(length);",
              "        padded.set(bytes, length - bytes.length);",
              "        bytes = padded;",
              "    }",
              "    return bytes;",
              "}",
              "",
              "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
              "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
              "function rs256Sign(signingInput, privateKeyPem) {",
              "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
              "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
              "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
              "    const m = bytesToBigInt(em);",
              "    const s = modPow(m, d, n);",
              "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
              "    return b64urlFromBytes(sigBytes);",
              "}",
              "",
              "// Build and sign a JWT given header/payload objects and a PEM private key.",
              "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
              "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
              "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
              "    const signingInput = headerB64 + '.' + payloadB64;",
              "    const sig = rs256Sign(signingInput, privateKeyPem);",
              "    return signingInput + '.' + sig;",
              "}",
              "",
              "function checkPrivateKeyConfigured(pk) {",
              "    if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
              "        throw new Error('private_key collection variable is still the placeholder - paste your real PKCS8 PEM RSA private key before running requests.');",
              "    }",
              "}",
              "",
              "function buildClientAssertionJwt() {",
              "    const clientId = pm.collectionVariables.get('client_id');",
              "    const privateKey = pm.collectionVariables.get('private_key');",
              "    const kid = pm.collectionVariables.get('kid');",
              "    const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
              "",
              "    if (!clientId) {",
              "        throw new Error('client_id collection variable is empty - set it before running requests.');",
              "    }",
              "    checkPrivateKeyConfigured(privateKey);",
              "",
              "    const header = { alg: 'RS256', typ: 'JWT' };",
              "    if (kid) {",
              "        header.kid = kid;",
              "    }",
              "",
              "    const nowSeconds = Math.floor(Date.now() / 1000);",
              "    const payload = {",
              "        iss: clientId,",
              "        sub: clientId,",
              "        aud: tokenEndpoint,",
              "        iat: nowSeconds,",
              "        exp: nowSeconds + 300,",
              "        jti: pm.variables.replaceIn('{{$guid}}')",
              "    };",
              "",
              "    const jwt = buildSignedJwt(header, payload, privateKey);",
              "    pm.collectionVariables.set('client_assertion', jwt);",
              "    return jwt;",
              "}",
              "",
              "buildClientAssertionJwt();"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const jsonData = pm.response.json();",
              "if (jsonData.access_token) {",
              "    pm.collectionVariables.set('access_token', jsonData.access_token);",
              "    console.log('access_token captured.');",
              "} else {",
              "    console.error('No access_token in response:', pm.response.text());",
              "}",
              "pm.test('Token response has access_token', function () {",
              "    pm.expect(jsonData.access_token).to.be.a('string').and.not.empty;",
              "});"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/x-www-form-urlencoded"
          }
        ],
        "body": {
          "mode": "urlencoded",
          "urlencoded": [
            {
              "key": "grant_type",
              "value": "authorization_code",
              "type": "text"
            },
            {
              "key": "code",
              "value": "{{auth_code}}",
              "type": "text",
              "description": "Paste the code you received from the browser redirect after completing request 1."
            },
            {
              "key": "redirect_uri",
              "value": "{{redirect_uri}}",
              "type": "text"
            },
            {
              "key": "client_id",
              "value": "{{client_id}}",
              "type": "text"
            },
            {
              "key": "client_assertion_type",
              "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
              "type": "text"
            },
            {
              "key": "client_assertion",
              "value": "{{client_assertion}}",
              "type": "text",
              "description": "Signed PRIVATE_KEY_JWT client authentication assertion built by the pre-request script."
            }
          ]
        },
        "url": {
          "raw": "{{base_url}}/token",
          "host": [
            "{{base_url}}/token"
          ]
        },
        "description": "Exchanges the manually-pasted auth_code for an access_token, authenticating the client with PRIVATE_KEY_JWT (client_assertion), never client_secret."
      },
      "response": []
    },
    {
      "name": "POST KYC Match v0.2",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// No signing needed here - just reuses {{access_token}} captured by the token request(s) above."
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "x-correlator",
            "value": "{{$guid}}",
            "description": "Unique correlation ID for tracing this request through the API gateway/backend."
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"phoneNumber\": \"{{phone_number}}\",\n  \"givenName\": \"Max\",\n  \"familyName\": \"Mustermann\",\n  \"address\": \"Musterstraße 1\",\n  \"streetName\": \"Musterstraße\",\n  \"streetNumber\": \"1\",\n  \"postalCode\": \"10115\",\n  \"locality\": \"Berlin\",\n  \"country\": \"DE\",\n  \"birthdate\": \"1980-01-01\",\n  \"email\": \"max.mustermann@example.de\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{base_url}}/kyc-match/v0.2/match",
          "host": [
            "{{base_url}}/kyc-match/v0.2/match"
          ]
        },
        "description": "Calls the KYC Match v0.2 resource (kyc-match/v0.2/match) with sample subscriber data (see the phone_number variable for the live DE staging standard-config MSISDN). Field names copied verbatim from CheckKycMatchEndpoint in SEPAPIs.cs."
      },
      "response": []
    },
    {
      "name": "GET KYC Match Health v0.2",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}"
          },
          {
            "key": "x-correlator",
            "value": "{{$guid}}",
            "description": "Unique correlation ID for tracing this request through the API gateway/backend."
          }
        ],
        "url": {
          "raw": "{{base_url}}/kyc-match/v0.2/health",
          "host": [
            "{{base_url}}/kyc-match/v0.2/health"
          ]
        },
        "description": "Health check for the KYC Match v0.2 service. Run after obtaining access_token from any of the token requests in this collection."
      },
      "response": []
    }
  ]
}
