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