{
  "info": {
    "name": "Sim Swap v1 - Authorization Code",
    "description": "Sim Swap v1 (SEP/CAMARA) - Authorization Code 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\nRequires a manual browser step: run request 1, open the printed URL in a browser, log in/consent, then paste the returned 'code' into the auth_code collection variable before running request 2.\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 Sim Swap v2 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"
  },
  "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}}'));",
              "}"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "console.log('Open this URL in a browser, log in / consent, then copy the \"code\" query parameter from the redirect URL into the auth_code collection variable:');",
              "console.log(pm.request.url.toString());"
            ]
          }
        }
      ],
      "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": "AUTHORIZATION_CODE requires an interactive browser step that Postman cannot perform on your behalf. This request only composes/displays the authorize URL (see the Console output and the request URL bar). Copy that URL, open it in a real browser, log in and consent, then copy the 'code' query parameter from the redirect back to redirect_uri into the auth_code collection variable before running the next request. Sending this request directly from Postman will NOT complete a real login."
      },
      "response": []
    },
    {
      "name": "2) Exchange Code for Token",
      "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.');",
              "}",
              "",
              "function b64urlFromBytes(bytes) {\n    let bin = '';\n    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);\n    let b64 = btoa(bin);\n    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction b64urlFromString(str) {\n    const utf8 = unescape(encodeURIComponent(str));\n    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));\n}\n\nfunction Uint8ArrayFromBinaryString(bin) {\n    const arr = new Uint8Array(bin.length);\n    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);\n    return arr;\n}\n\nfunction pemToDerBytes(pem) {\n    const b64 = pem\n        .replace(/-----BEGIN [^-]+-----/, '')\n        .replace(/-----END [^-]+-----/, '')\n        .replace(/\\s+/g, '');\n    const bin = atob(b64);\n    return Uint8ArrayFromBinaryString(bin);\n}\n\nfunction readDerTlv(bytes, offset) {\n    const tag = bytes[offset];\n    let lenByte = bytes[offset + 1];\n    let len, lenBytesUsed;\n    if ((lenByte & 0x80) === 0) {\n        len = lenByte;\n        lenBytesUsed = 1;\n    } else {\n        const numLenBytes = lenByte & 0x7f;\n        len = 0;\n        for (let i = 0; i < numLenBytes; i++) {\n            len = (len * 256) + bytes[offset + 2 + i];\n        }\n        lenBytesUsed = 1 + numLenBytes;\n    }\n    const contentStart = offset + 1 + lenBytesUsed;\n    return { tag, len, contentStart, nextOffset: contentStart + len };\n}\n\nfunction derToBigInt(bytes, start, len) {\n    let hex = '';\n    for (let i = 0; i < len; i++) {\n        const b = bytes[start + i];\n        hex += (b < 16 ? '0' : '') + b.toString(16);\n    }\n    if (hex === '') return BigInt(0);\n    return BigInt('0x' + hex);\n}\n\nfunction extractRsaPrivateKeyParams(pem) {\n    const bytes = pemToDerBytes(pem);\n    let tlv = readDerTlv(bytes, 0);\n    let cursor = tlv.contentStart;\n    let versionPeek = readDerTlv(bytes, cursor);\n    let second = readDerTlv(bytes, versionPeek.nextOffset);\n    let rsaKeyStart;\n    if (second.tag === 0x02) {\n        rsaKeyStart = cursor;\n    } else {\n        let versionTlv = readDerTlv(bytes, cursor);\n        cursor = versionTlv.nextOffset;\n        let algTlv = readDerTlv(bytes, cursor);\n        cursor = algTlv.nextOffset;\n        let octetTlv = readDerTlv(bytes, cursor);\n        let inner = readDerTlv(bytes, octetTlv.contentStart);\n        rsaKeyStart = inner.contentStart;\n    }\n    let p = rsaKeyStart;\n    let versionField = readDerTlv(bytes, p);\n    p = versionField.nextOffset;\n    let nField = readDerTlv(bytes, p);\n    const n = derToBigInt(bytes, nField.contentStart, nField.len);\n    p = nField.nextOffset;\n    let eField = readDerTlv(bytes, p);\n    p = eField.nextOffset;\n    let dField = readDerTlv(bytes, p);\n    const d = derToBigInt(bytes, dField.contentStart, dField.len);\n    const modulusByteLength = Math.ceil(n.toString(2).length / 8);\n    return { n, d, modulusByteLength };\n}\n\nfunction modPow(base, exp, mod) {\n    let result = BigInt(1);\n    base = base % mod;\n    while (exp > BigInt(0)) {\n        if (exp % BigInt(2) === BigInt(1)) {\n            result = (result * base) % mod;\n        }\n        exp = exp / BigInt(2);\n        base = (base * base) % mod;\n    }\n    return result;\n}\n\nconst SHA256_DIGEST_INFO_PREFIX_HEX =\n    '3031300d060960864801650304020105000420';\n\nfunction hexToBytes(hex) {\n    const arr = new Uint8Array(hex.length / 2);\n    for (let i = 0; i < arr.length; i++) {\n        arr[i] = parseInt(hex.substr(i * 2, 2), 16);\n    }\n    return arr;\n}\n\nfunction bytesToHex(bytes) {\n    let hex = '';\n    for (let i = 0; i < bytes.length; i++) {\n        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);\n    }\n    return hex;\n}\n\nfunction emsaPkcs1v15Encode(digestHex, k) {\n    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;\n    const digestInfo = hexToBytes(digestInfoHex);\n    const tLen = digestInfo.length;\n    if (k < tLen + 11) {\n        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');\n    }\n    const psLen = k - tLen - 3;\n    const em = new Uint8Array(k);\n    em[0] = 0x00;\n    em[1] = 0x01;\n    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;\n    em[2 + psLen] = 0x00;\n    em.set(digestInfo, 3 + psLen);\n    return em;\n}\n\nfunction bytesToBigInt(bytes) {\n    return BigInt('0x' + (bytesToHex(bytes) || '0'));\n}\n\nfunction bigIntToBytes(bi, length) {\n    let hex = bi.toString(16);\n    if (hex.length % 2 !== 0) hex = '0' + hex;\n    let bytes = hexToBytes(hex);\n    if (bytes.length < length) {\n        const padded = new Uint8Array(length);\n        padded.set(bytes, length - bytes.length);\n        bytes = padded;\n    }\n    return bytes;\n}\n\nfunction rs256Sign(signingInput, privateKeyPem) {\n    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);\n    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);\n    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);\n    const m = bytesToBigInt(em);\n    const s = modPow(m, d, n);\n    const sigBytes = bigIntToBytes(s, modulusByteLength);\n    return b64urlFromBytes(sigBytes);\n}\n\nfunction buildSignedJwt(headerObj, payloadObj, privateKeyPem) {\n    const headerB64 = b64urlFromString(JSON.stringify(headerObj));\n    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));\n    const signingInput = headerB64 + '.' + payloadB64;\n    const sig = rs256Sign(signingInput, privateKeyPem);\n    return signingInput + '.' + sig;\n}",
              "",
              "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');",
              "",
              "if (!pm.collectionVariables.get('auth_code')) {",
              "    throw new Error('Set the auth_code collection variable (from the browser redirect) before running this request.');",
              "}"
            ]
          }
        },
        {
          "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": "authorization_code",
              "type": "text"
            },
            {
              "key": "code",
              "value": "{{auth_code}}",
              "type": "text"
            },
            {
              "key": "redirect_uri",
              "value": "{{redirect_uri}}",
              "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 the manually-pasted auth_code (from request 1's browser step) for an access token, authenticating the client with a private_key_jwt client_assertion."
      },
      "response": []
    },
    {
      "name": "3) Sim Swap v1 - Check",
      "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    \"maxAge\": {{max_age}}\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{base_url}}/sim-swap/v1/check",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "sim-swap",
            "v1",
            "check"
          ]
        },
        "description": "CAMARA Sim Swap v1 check. maxAge=240 hours matches this app's SEP_APIs.razor default. The device is identified entirely via the access token (sub claim) for this grant type - including a phoneNumber/device field in the body causes a real UNNECESSARY_IDENTIFIER (422) error, so it is omitted here."
      },
      "response": []
    },
    {
      "name": "4) Sim Swap v1 - Retrieve Date",
      "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": "{}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{base_url}}/sim-swap/v1/retrieve-date",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "sim-swap",
            "v1",
            "retrieve-date"
          ]
        },
        "description": "CAMARA Sim Swap v1 retrieve-date. No maxAge field here - matches SEPAPIs.cs RetrieveSimSwapDateEndpoint, which only ever sends phoneNumber (never maxAge) in the body."
      },
      "response": []
    },
    {
      "name": "5) Sim Swap v1 - Health",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}",
            "type": "text"
          },
          {
            "key": "x-correlator",
            "value": "{{$guid}}",
            "type": "text"
          }
        ],
        "url": {
          "raw": "{{base_url}}/sim-swap/v1/health",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "sim-swap",
            "v1",
            "health"
          ]
        },
        "description": "Health check GET for Sim Swap v1 (no request body, matches SEPAPIs.cs ExecuteHealthGet)."
      },
      "response": []
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "https://stg.api.telekom.com",
      "type": "string",
      "description": "Germany staging by default. Change ONLY this variable to switch environment/country: production Germany = https://api.telekom.com; Austria staging = https://at.stg.api.telekom.com, Austria prod = https://at.api.telekom.com; Poland and Greece staging/prod hosts follow the same at.*/pl.*/gr.* pattern - see this app's /documentation/endpoints page for the full current list. 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": "client_id",
      "value": "",
      "type": "string",
      "description": "REQUIRED. Your registered OAuth client_id for the SimSwapV1 credential profile. Never commit a real value here."
    },
    {
      "key": "private_key",
      "value": "-----BEGIN PRIVATE KEY-----\nPASTE-YOUR-PKCS8-PRIVATE-KEY-HERE\n-----END PRIVATE KEY-----",
      "type": "string",
      "description": "REQUIRED. PKCS8 PEM RSA private key used to sign the private_key_jwt client_assertion (and, for the JWT Bearer variant, the subscriber assertion). Replace the placeholder text between the BEGIN/END markers with your real PKCS8 PEM key. Never commit a real private key."
    },
    {
      "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, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "max_age",
      "value": "240",
      "type": "string",
      "description": "Sim Swap maxAge in hours - 240 is the default already used by this app's SEP_APIs.razor."
    },
    {
      "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 - auto-set by the pre-request script too."
    },
    {
      "key": "client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the pre-request script on each token request. Do not edit."
    },
    {
      "key": "access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's test script. Do not edit."
    },
    {
      "key": "scope",
      "value": "openid sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid sim-swap:check sim-swap:retrieve-date dpv:FraudPreventionAndDetection - 'openid' is prefixed because SEPAPIs.cs's UsesOpenIdScope returns true for GrantType == AUTHORIZATION_CODE."
    },
    {
      "key": "redirect_uri",
      "value": "https://yourapp.com/callback",
      "type": "string",
      "description": "Must match a redirect URI registered for your OAuth client."
    },
    {
      "key": "state",
      "value": "",
      "type": "string",
      "description": "Auto-populated random state value by the pre-request script (CSRF protection). Do not edit."
    },
    {
      "key": "nonce",
      "value": "",
      "type": "string",
      "description": "Auto-populated random nonce value by the pre-request script (replay protection). Do not edit."
    },
    {
      "key": "auth_code",
      "value": "",
      "type": "string",
      "description": "REQUIRED. Paste the 'code' query parameter from the browser redirect here after completing the login/consent step in request 1."
    }
  ]
}
