Not able to get data using access token generate by client credentials using DPoP

I am using a service Application in Okta. I have used public-key / private key as client authentication and enabled DPop (Require Demonstrating Proof of Possession (DPoP) header in token requests).

I have generated an RSA key converted it into JWK and uploaded it to my service Application in Okta.

I have generated a DPoP proof and client assertion. Then I hit access token API But got an error
From the error header, I got the Dpop nonce, Again I generated DPoP proof and Hit access token API. Now I have the access token.

Now I want to hit any API like Get groups But I got this error: Request failed with status code 400
I have decoded the access token it has all the necessary information.

Can someone tell me How to get an access token in an DPoP enabled service Application by which I can hit any API and get the data?

I am providing the code:

const crypto = require(‘crypto’);
const sshpk = require(‘sshpk’);
const jwt = require(‘jsonwebtoken’);
const axios = require(‘axios’);
const { v4: uuidv4 } = require(‘uuid’);
const fs = require(‘fs’);
const forge = require(‘node-forge’);
const username = ‘vikalp.khandelwal@robomq.io’
const tokenURL = ‘https://dev-97298997.okta.com/oauth2/v1/token
const groupsURL = ‘https://dev-97298997.okta.com/api/v1/groups

// Step 1: Generate RSA key pair
const generateRsaKey = async (username) => {
const { privateKey, publicKey } = crypto.generateKeyPairSync(‘rsa’, {
modulusLength: 2048,
publicKeyEncoding: {
type: ‘pkcs1’,
format: ‘pem’,
},
privateKeyEncoding: {
type: ‘pkcs1’,
format: ‘pem’,
},
});

// SSH Key Manipulation
const publickey = sshpk.parseKey(publicKey, 'pem');
const pubKey = publickey.toString('ssh');
let usernameRSA = username.replace(/[&\/\\#,+()$~%.'":*?<>{}]/g, '_');
usernameRSA = usernameRSA.substr(0, usernameRSA.lastIndexOf('_')); 
usernameRSA = checkUsername(usernameRSA);
let sshRsa = pubKey.replace('(unnamed)', usernameRSA);
let privatekey = privateKey.replace(/\n/g, " ");

const keyPairs = {
    user_publickey: sshRsa,
    user_privatekey: privatekey,
    user_certificate: generateCertificate(sshRsa, privateKey, username),
};

fs.writeFileSync('pubkey.txt', keyPairs.user_publickey, function (err) {
    if (err) throw err;
});
fs.writeFileSync('pvtKey.txt', keyPairs.user_privatekey, function (err) {
    if (err) throw err;
});
fs.writeFileSync('certificate.txt', keyPairs.user_certificate, function (err) {
    if (err) throw err;
});

}

function checkUsername(usernameRSA) {
if (usernameRSA.length > 32) {
const reglastintial = (/[A-Za-z0-9_]+_[A-Za-z0-9]/)
const domainname = usernameRSA.substr(usernameRSA.lastIndexOf(‘@’), usernameRSA.length - 1)
usernameRSA = usernameRSA.substr(0, usernameRSA.lastIndexOf(‘@’))
let intialusername = usernameRSA

  usernameRSA = ((reglastintial).test(usernameRSA) ? usernameRSA.match(reglastintial)[0] : usernameRSA)

  let last_intital = null
  if ((usernameRSA + domainname).length > 32) {
    if ((/_[A-Za-z0-9]+$/).test(intialusername)) {
      last_intital = intialusername.match(/_[A-Za-z0-9]+$/)[0]
    }
    usernameRSA = ((/[A-Za-z0-9]/).test(intialusername.charAt(0)) ? intialusername.match(/[A-Za-z0-9]/)[0] : intialusername.match(/_[A-Za-z0-9]/)[0])
    if (last_intital != null) {
      usernameRSA += last_intital
    }
  }

  if ((usernameRSA + domainname).length > 32) {
    usernameRSA = ((reglastintial).test(usernameRSA) ? usernameRSA.match(reglastintial)[0] : usernameRSA)
  }
  usernameRSA = usernameRSA + domainname
  usernameRSA = usernameRSA.replace(/[&\/\\#,+()$~%.'":*?<>{}@]/g, '_')
}
else {
  usernameRSA = usernameRSA.replace(/[&\/\\#,+()$~%.'":*?<>{}@]/g, '_')
}
return usernameRSA

}

const getAsymmetricPrivateKey = async (privateKey) => {
try {
const regex = /-----BEGIN RSA PRIVATE KEY-----(.*?)-----END RSA PRIVATE KEY-----/s;
let privatePEMKey = privateKey.replace(regex, (_, content) => {
const replacedContent = content.split(’ ‘).join(’\n’);
return -----BEGIN RSA PRIVATE KEY-----${replacedContent}-----END RSA PRIVATE KEY-----;
});
return crypto.createPrivateKey({ key: privatePEMKey, format: ‘pem’ });
} catch (error) {
throw error;
}
};

const generateCertificate = (publicKey, privateKey, username) => {
const key = sshpk.parseKey(publicKey, ‘ssh’);
publicKey = key.toString(‘pem’);

let keys = {
    publicKey: forge.pki.publicKeyFromPem(publicKey),
    privateKey: forge.pki.privateKeyFromPem(privateKey),
};
  
let cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 20);

let attrs = [{
    name: 'commonName',
    value: username,
}];

cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.sign(keys.privateKey);
return forge.pki.certificateToPem(cert);

};

const getFingerprint = async (cert) => {
try {
const certificate = forge.pki.certificateFromPem(cert);
const md = forge.md.sha1.create();
md.update(forge.asn1.toDer(forge.pki.certificateToAsn1(certificate)).getBytes());
const thumbprint = md.digest().toHex();
const binaryFingerprint = Buffer.from(thumbprint, ‘hex’);
return binaryFingerprint.toString(‘base64’);
} catch (error) {
console.error(error);
throw error;
}
};

// Step 3: Convert Public Key to JWK format
const convertJWK = async (pubKey, certFingerprint) => {
const sshKey = sshpk.parseKey(pubKey, ‘ssh’);
if (sshKey.parts.length >= 2) {
const exponent = sshKey.parts[0].data.toString(‘base64’);
const modulus = sshKey.parts[1].data.toString(‘base64’);
const jwk = {
kty: ‘RSA’,
e: exponent,
n: modulus,
alg: ‘RS256’,
use: ‘sig’,
kid: certFingerprint
};

    console.log(JSON.stringify(jwk, null, 2));
    fs.writeFileSync('jwk.txt', JSON.stringify(jwk, null, 2), function (err) {
        if (err) throw err;
    });
}

};

// Step 4 & 7: Regenerate DPoP proof with nonce and retry
const generateDpop = async (jwk, pvtKey, url, method, nonce) => {
const header = {
alg: ‘RS256’,
typ: ‘dpop+jwt’,
jwk: JSON.parse(jwk) // Ensure this is parsed correctly
};
const proofPayload = {
jti: uuidv4(),
htu: url, // URI of the resource (token endpoint or API endpoint)
htm: method, // HTTP method
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300, // Expire in 5 minutes
};

// Include nonce if present
if (nonce) proofPayload.nonce = nonce;
else proofPayload.nonce = uuidv4()


const dpopProof = jwt.sign(proofPayload, pvtKey, {
    algorithm: 'RS256',
    header: header,
});
// Log for debugging
console.log('Generated DPoP Proof:', dpopProof)
return dpopProof;

};

// Step 5: Get Client assertion
const generateClientAssertion = async (pKey, certFingerprint) => {
const payload = {
iss: ‘0oajycc0vu4uIxLvT5d7’,
sub: ‘0oajycc0vu4uIxLvT5d7’,
aud: tokenURL,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 * 60),
};

// JWT Header
const header = {
    alg: 'RS256',
    typ: 'JWT',
    kid: certFingerprint
}

// Signing client assertion JWT
return jwt.sign(payload, pKey, { algorithm: 'RS256', header })

}

// Step 6 & 8: Get Access Token
const getAccessToken = async (clientAssertion, dpopProof) => {
try {
const response = await axios({
method: ‘POST’,
url: tokenURL,
headers: {
‘Accept’: ‘application/json’,
‘Content-Type’: ‘application/x-www-form-urlencoded’,
‘DPoP’: dpopProof // Include DPoP proof in headers
},
data: new URLSearchParams({
‘grant_type’: ‘client_credentials’,
‘client_id’: ‘0oajycc0vu4uIxLvT5d7’,
‘client_assertion_type’: ‘urn:ietf:params:oauth:client-assertion-type:jwt-bearer’,
‘client_assertion’: clientAssertion,
‘scope’: ‘okta.users.manage okta.groups.manage okta.schemas.read okta.groups.read’ ,
}).toString(),
});
console.log(‘access_token>>’, response.data.access_token)
return response.data.access_token;
} catch (error) {
if (error.response && error.response.headers[‘dpop-nonce’]) {
return error.response.headers[‘dpop-nonce’]; // Return nonce for retry
} else {
throw new Error(Error fetching access token: ${error.message});
}
}
};

// Step 9: Convert base64 hash
const convertHash = (access_token) => {
const boundAccessToken = access_token
const hash = crypto.createHash(‘sha256’).update(boundAccessToken).digest(‘base64’);
return hash;
}

// Step 10: Use DPoP-bound token to access groups
const generateModifiedDpop = async (jwk, pvtKey, url, method, hash) => {
var claims = {
htm: method,
htu: url,
ath: hash
}
dpopJWKForResource = jwt.sign(claims, pvtKey,
{
algorithm: ‘RS256’,
header:
{
typ: ‘dpop+jwt’,
jwk: jwk
}
}
);
console.log(‘dpopJWKForResource>>>’, dpopJWKForResource);
return dpopJWKForResource;
}

// Step 11: Get Groups
const getGroups = async (dpopProof, accessToken) => {
try {
const response = await axios.get(groupsURL, {
headers: {
Authorization: DPoP ${accessToken},
DPoP: dpopProof
}
});
console.log(response.data);
} catch (error) {
console.error(‘Error fetching groups:’, error.message);
}
};

// Final Function to execute the flow
const executeFlow = async (username) => {
try {
// Step 1: Generate RSA key pair
await generateRsaKey(username);

    // Step 2: Load keys from files
    const pubKey = fs.readFileSync('pubkey.txt', 'utf8');
    const pvtKey = fs.readFileSync('pvtKey.txt', 'utf8');
    const pKey = await getAsymmetricPrivateKey(pvtKey);

    const certificate = fs.readFileSync('certificate.txt', 'utf8');  // Assume you already generated this certificate
    const certFingerprint = await getFingerprint(certificate);

    // Step 3: Convert public key to JWK and calculate certificate fingerprint
    await convertJWK(pubKey, certFingerprint);
    const jwk = fs.readFileSync('jwk.txt', 'utf8')

    // Step 4: Generate initial DPoP proof
    let dpopProof = await generateDpop(jwk, pKey, tokenURL, 'POST');
    let access_token

    // Step 5: Get Client assertion
    const clientAssertion  = await generateClientAssertion(pKey, certFingerprint)

    // Step 6: Get nonce using access Token API
    let nonce = await getAccessToken(clientAssertion, dpopProof)  // Handle nonce case
    if (nonce) {
        // Step 7: Regenerate DPoP proof with nonce and retry
        dpopProof = await generateDpop(jwk, pKey, tokenURL, 'POST', nonce);
        // Step 8: Get access Token
        access_token = await getAccessToken(clientAssertion, dpopProof);
        // Step 9: Convert base64 hash
        const hash = convertHash(access_token)
        // Step 10: Use DPoP-bound token to access groups
        const dpopJWKForResource = await generateModifiedDpop(jwk, pKey, groupsURL, 'GET', hash);
        // Step 11: Get Groups
        await getGroups(dpopJWKForResource, access_token);
    }
} catch (error) {
    console.error('Error executing flow:', error.message);
}

};

// Trigger the flow
executeFlow(username); Not able to get data using access token generate by client credentials using DPoP

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.