{
  "info": {
    "name": "Number Recycling v0.2 - JWT Bearer",
    "description": "Number Recycling v0.2 (SEP/CAMARA), JWT Bearer grant variant.\n\nClient authentication: PRIVATE_KEY_JWT only (RS256-signed client_assertion) - never client_secret.\n\nbase_url defaults to Germany staging (https://stg.api.telekom.com). Change only that one variable to target production or another country (see the base_url variable description).\n\nCurrently listed for: 🇩🇪 Germany. (Source: this app's live product-catalog grant-type configuration, staging environment - see the /documentation/postman-collections page for the current matrix.)",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "1) Get Token (JWT Bearer)",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (!pm.collectionVariables.get('client_id')) {",
              "    throw new Error('Set the client_id collection variable before running this request.');",
              "}",
              "const pk = pm.collectionVariables.get('private_key');",
              "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
              "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
              "}",
              "",
              "// 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;",
              "}",
              "",
              "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');",
              "const now = Math.floor(Date.now() / 1000);",
              "const header = { alg: 'RS256', typ: 'JWT' };",
              "if (kid) { header.kid = kid; }",
              "const payload = {",
              "    iss: clientId,",
              "    sub: clientId,",
              "    aud: tokenEndpoint,",
              "    iat: now,",
              "    exp: now + 300,",
              "    jti: pm.variables.replaceIn('{{$guid}}')",
              "};",
              "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
              "pm.collectionVariables.set('client_assertion', clientAssertion);",
              "pm.collectionVariables.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');",
              "",
              "const clientId2 = pm.collectionVariables.get('client_id');",
              "const privateKey2 = pm.collectionVariables.get('private_key');",
              "const kid2 = pm.collectionVariables.get('kid');",
              "const tokenEndpoint2 = (pm.collectionVariables.get('base_url') + '/token');",
              "const scope2 = pm.collectionVariables.get('scope');",
              "const phoneNumber2 = pm.collectionVariables.get('phone_number');",
              "const operatorToken2 = pm.collectionVariables.get('operator_token');",
              "const now2 = Math.floor(Date.now() / 1000);",
              "const header2 = { alg: 'RS256', typ: 'JWT' };",
              "if (kid2) { header2.kid = kid2; }",
              "const payload2 = {",
              "    iss: clientId2,",
              "    sub: 'tel:' + phoneNumber2,",
              "    aud: tokenEndpoint2,",
              "    iat: now2,",
              "    exp: now2 + 300,",
              "    jti: pm.variables.replaceIn('{{$guid}}'),",
              "    scope: scope2",
              "};",
              "const assertion = buildSignedJwt(header2, payload2, privateKey2);",
              "pm.collectionVariables.set('assertion', assertion);"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const json = pm.response.json();",
              "if (json.access_token) {",
              "    pm.collectionVariables.set('access_token', json.access_token);",
              "    console.log('access_token captured.');",
              "} else {",
              "    console.error('No access_token in response:', JSON.stringify(json));",
              "}",
              "pm.test('Token response has access_token', function () {",
              "    pm.expect(json.access_token, JSON.stringify(json)).to.be.a('string');",
              "});"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/x-www-form-urlencoded",
            "type": "text"
          }
        ],
        "body": {
          "mode": "urlencoded",
          "urlencoded": [
            {
              "key": "grant_type",
              "value": "urn:ietf:params:oauth:grant-type:jwt-bearer",
              "type": "text"
            },
            {
              "key": "assertion",
              "value": "{{assertion}}",
              "type": "text"
            },
            {
              "key": "scope",
              "value": "{{scope}}",
              "type": "text"
            },
            {
              "key": "client_id",
              "value": "{{client_id}}",
              "type": "text"
            },
            {
              "key": "client_assertion_type",
              "value": "{{client_assertion_type}}",
              "type": "text"
            },
            {
              "key": "client_assertion",
              "value": "{{client_assertion}}",
              "type": "text"
            }
          ]
        },
        "url": {
          "raw": "{{base_url}}/token",
          "host": [
            "{{base_url}}/token"
          ]
        },
        "description": "Exchanges a signed subscriber 'assertion' JWT (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) for an access token, authenticating the client with a private_key_jwt client_assertion. Both JWTs are built and RS256-signed in the pre-request script using pure-JS BigInt math (no Node crypto)."
      },
      "response": []
    },
    {
      "name": "Number Recycling v0.2 - Check",
      "event": [],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}",
            "type": "text"
          },
          {
            "key": "Content-Type",
            "value": "application/json",
            "type": "text"
          },
          {
            "key": "x-correlator",
            "value": "{{$guid}}",
            "type": "text"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n    \"specifiedDate\": \"{{specified_date}}\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{base_url}}/number-recycling/v0.2/check",
          "host": [
            "{{base_url}}/number-recycling/v0.2/check"
          ]
        },
        "description": "CAMARA Number Recycling v0.2 check - the subscriber is identified via the access token (sub claim); specifiedDate is the only body field, copied verbatim from CheckNumberRecyclingV02 in SEPAPIs.cs."
      },
      "response": []
    },
    {
      "name": "Health Check",
      "event": [],
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}",
            "type": "text"
          },
          {
            "key": "x-correlator",
            "value": "{{$guid}}",
            "type": "text"
          }
        ],
        "url": {
          "raw": "{{base_url}}/number-recycling/v0.2/health",
          "host": [
            "{{base_url}}/number-recycling/v0.2/health"
          ]
        },
        "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
      },
      "response": []
    }
  ],
  "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. 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 when read inside pre-request/test scripts via pm.collectionVariables.get() - only base_url itself needs to be correct."
    },
    {
      "key": "scope",
      "value": "number-recycling:check dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "Scope string used by this app for Number Recycling v0.2."
    },
    {
      "key": "client_id",
      "value": "",
      "type": "string",
      "description": "REQUIRED. Your registered OAuth client ID (PRIVATE_KEY_JWT client authentication)."
    },
    {
      "key": "private_key",
      "value": "-----BEGIN PRIVATE KEY-----\nPASTE-YOUR-PKCS8-PRIVATE-KEY-HERE\n-----END PRIVATE KEY-----",
      "type": "string",
      "description": "REQUIRED. Your own PKCS8 PEM RSA private key. Never share this key - replace the placeholder before running any request."
    },
    {
      "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": "phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "specified_date",
      "value": "2024-10-31",
      "type": "string",
      "description": "Date to check for number recycling (YYYY-MM-DD) - matches this app's StandardTestData.NumberRecyclingSpecifiedDate sample value."
    },
    {
      "key": "client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed subscriber assertion JWT. Do not edit manually."
    }
  ]
}
