@nullplatform/np-scope-token-validator
Version:
JWT authentication service for nginx auth_request
196 lines (163 loc) • 6.63 kB
JavaScript
#!/usr/bin/env node
/**
* Test JWK to PEM conversion with the actual key from nullplatform
*/
const crypto = require('crypto');
// The actual JWK from nullplatform
const testJwk = {
"kty": "RSA",
"n": "h9SfuSSnIxVejXwZ30T-aeP7z-YdKYSa7vI7HxG-gMzcnhaYTGevHJZLnPmV9QhIKUeOrjbQI0XidJpq57ZXLe_DEhkV4sSfziLzZBagMbV2IRMenjlQchFdLWA0QYJoCv7kpEIXvLFZuuS3gu3L9AeV8yu5EIvkaAG7ftjOnZ_xgA6tY4iwesqtHOubZMTGR4axiOdqzponAYEqK_FBXOuHYjUkbivpOgYaRq_YwjCIGSkeHIWgmWltBY5SIIh6b7R45Pti6gyNJVbaBBsV8JsLIcnDcd4NzuOcDQn3Y9FpXvJbhXkGGnMbJvYptFE-AL0KdtqrZIifpSmQEfwT-w",
"e": "AQAB",
"alg": "RS256",
"use": "sig",
"kid": "key-1"
};
console.log('Testing JWK to PEM conversion...');
console.log('JWK:', JSON.stringify(testJwk, null, 2));
console.log('Node.js version:', process.version);
function testCryptoApi() {
console.log('\n=== Testing Node.js crypto API ===');
try {
const publicKey = crypto.createPublicKey({
key: {
kty: testJwk.kty,
n: testJwk.n,
e: testJwk.e
},
format: 'jwk'
});
const pem = publicKey.export({
type: 'spki',
format: 'pem'
});
console.log('✅ Crypto API conversion successful');
console.log('PEM result:');
console.log(pem);
return pem;
} catch (error) {
console.error('❌ Crypto API failed:', error.message);
console.error('Error details:', error);
return null;
}
}
function testFallbackMethod() {
console.log('\n=== Testing fallback ASN.1 method ===');
try {
// Decode base64url encoded values
function base64urlDecode(str) {
// Add padding if needed
str += '=='.slice(0, (4 - str.length % 4) % 4);
return Buffer.from(str.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
}
const n = base64urlDecode(testJwk.n);
const e = base64urlDecode(testJwk.e);
console.log('Decoded n length:', n.length);
console.log('Decoded e length:', e.length);
console.log('n (hex preview):', n.toString('hex').substring(0, 32) + '...');
console.log('e (hex):', e.toString('hex'));
// Create ASN.1 DER structure for RSA public key
function createDerInteger(buffer) {
// If the first bit is set, prepend 0x00 to indicate positive number
if (buffer[0] & 0x80) {
const padded = Buffer.alloc(buffer.length + 1);
padded[0] = 0x00;
buffer.copy(padded, 1);
buffer = padded;
}
const length = encodeLength(buffer.length);
return Buffer.concat([Buffer.from([0x02]), length, buffer]);
}
function encodeLength(length) {
if (length < 0x80) {
return Buffer.from([length]);
}
const bytes = [];
let temp = length;
while (temp > 0) {
bytes.unshift(temp & 0xff);
temp >>= 8;
}
return Buffer.concat([Buffer.from([0x80 | bytes.length]), Buffer.from(bytes)]);
}
// RSA public key structure: SEQUENCE { n INTEGER, e INTEGER }
const nInteger = createDerInteger(n);
const eInteger = createDerInteger(e);
const rsaPublicKey = Buffer.concat([nInteger, eInteger]);
const rsaLength = encodeLength(rsaPublicKey.length);
const rsaSequence = Buffer.concat([Buffer.from([0x30]), rsaLength, rsaPublicKey]);
// Algorithm identifier for RSA
const algorithmId = Buffer.from([
0x30, 0x0d, // SEQUENCE (13 bytes)
0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // rsaEncryption OID
0x05, 0x00 // NULL
]);
// SubjectPublicKeyInfo structure
const bitString = Buffer.concat([Buffer.from([0x00]), rsaSequence]); // Prepend unused bits count
const bitStringLength = encodeLength(bitString.length);
const publicKeyBitString = Buffer.concat([Buffer.from([0x03]), bitStringLength, bitString]);
const spkiContent = Buffer.concat([algorithmId, publicKeyBitString]);
const spkiLength = encodeLength(spkiContent.length);
const spki = Buffer.concat([Buffer.from([0x30]), spkiLength, spkiContent]);
// Convert to PEM format
const base64Der = spki.toString('base64');
const pem = [
'-----BEGIN PUBLIC KEY-----',
...base64Der.match(/.{1,64}/g),
'-----END PUBLIC KEY-----'
].join('\n');
console.log('✅ Fallback conversion successful');
console.log('PEM result:');
console.log(pem);
return pem;
} catch (error) {
console.error('❌ Fallback method failed:', error.message);
console.error('Error details:', error);
return null;
}
}
function testJwtWithPem(pem) {
console.log('\n=== Testing JWT verification with PEM ===');
try {
const jwt = require('jsonwebtoken');
// Test token from the user
const testToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI3MzIxODk1NDMiLCJpYXQiOjE3NDgwMjI5NDIsImV4cCI6MTc0ODAyNDc0MiwiaXNzIjoiaHR0cHM6Ly9hcGkubnVsbHBsYXRmb3JtLmNvbS9zY29wZSIsImF1ZCI6ImgtMjAtcGF5bWVudHMtYXBpLWRldmVsb3BtZW50LXVydWd1YXkteXhieWEua3dpay1lLW1hcnQtbWFpbi5udWxsYXBwcy5pbyJ9.ceYaSi_HbEc1dNvcgOPP4bGtc3mkUGycrN_CjY9A3Ai-Tf_jSuqyeugAxOrf9F9j85CMlLwbynbaNMcQdM_paN-UALP-zK7CBphG4u8MW1c9Jj8IBTBg-OYCmJGbRxeF42cyocrJm_2xaeE6plR97zQ_qzCycxrSqmfAgs4bftJKNS3jG3un7fCxtKXNg5RAU6updPwMSuSQ-WqYB2_nMjCC6VuVObsGAVeKE99YIGO3eGhDW6zw0gHHt_6uqqYvOFs4Epb2CWKpSsWO45mYHfnssBTdIcSEGspv7s7O4fLUpxAYZHCl4qUpxtSCz423wZlmeysQUZK6onh9PnYilg";
const payload = jwt.verify(testToken, pem, {
algorithms: ['RS256'],
issuer: 'https://api.nullplatform.com/scope'
});
console.log('✅ JWT verification successful');
console.log('Payload:', payload);
return true;
} catch (error) {
console.error('❌ JWT verification failed:', error.message);
return false;
}
}
// Run all tests
async function runTests() {
console.log('Starting JWK to PEM conversion tests...\n');
// Test crypto API
const cryptoPem = testCryptoApi();
// Test fallback method
const fallbackPem = testFallbackMethod();
// Test JWT verification if we have a PEM
const pemToTest = cryptoPem || fallbackPem;
if (pemToTest) {
testJwtWithPem(pemToTest);
} else {
console.log('\n❌ No PEM available for JWT testing');
}
console.log('\n=== Summary ===');
console.log('Crypto API:', cryptoPem ? '✅ Success' : '❌ Failed');
console.log('Fallback method:', fallbackPem ? '✅ Success' : '❌ Failed');
}
if (require.main === module) {
runTests()
.then(() => {
console.log('\nTesting complete');
})
.catch((error) => {
console.error('Test error:', error);
process.exit(1);
});
}