oidc-lib
Version:
A library for creating OIDC Service Providers
586 lines (503 loc) • 17.3 kB
JavaScript
module.exports = {
registerEndpoints: registerEndpoints,
processOPTokenResult: processOPTokenResult,
requestOPMetadata: requestOPMetadata,
requestValidatedDistributedClaims: requestValidatedDistributedClaims,
requestUrlContent: requestUrlContent,
requestCredentialInfo: requestCredentialInfo,
verifyIdToken: verifyIdToken,
retrieveVerifiableCredential: retrieveVerifiableCredential,
create_dpop: create_dpop,
validate_dpop: validate_dpop,
display_dpop: display_dpop,
validate_did_key: validate_did_key,
validate_urn_key: validate_urn_key,
create_key_proof: create_key_proof
}
var pk = null;
function registerEndpoints(pkSource) {
pk = pkSource;
}
function processOPTokenResult(contentObject, validationOptions){
return new Promise((resolve, reject) => {
var idpKeys;
if (Object.keys(contentObject).length === 0){
var err = pk.sts.create_claimer_error('invalid_token', 'token_is_empty');
reject(err);
return;
}
if (contentObject.error !== undefined){
var err = {
error: contentObject.error,
error_description: contentObject.error_description
}
reject(err);
return;
}
if (contentObject.id_token !== undefined){
// pk.util.log_detail('id_token', contentObject.id_token);
pk.util.last_id_token = contentObject.id_token;
// pk.util.last_id_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Iko5MjcyR1FHeVZVRWh5NER2eksxbmFsYkFScGtRbXJIeEhsWHVpUjVsVkkifQ.eyJpc3MiOiJodHRwczovL3ZpcnR1YWwuaWRlbnRpdHlleHBlcmllbmNlZnJhbWV3b3JrLmNvbS9oZWxsbyIsImF1ZCI6IlRlc3RlclBvbGljeSIsImlhdCI6MTUyMDgyMTk0OCwiZXhwIjoxNTIwODIyNTQ4LCJzdWIiOiJmZDJkYjhjYy03ODkzLTQ4MDYtYjNjMi0wMjkwMzQ1MjdjODYiLCJlbWFpbCI6ImtjYW1lcm9uQG1pY3Jvc29mdC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0.om2QxUFf4gXsiSJ9qzU9b2mnz0sif9MLc-0VUYjNB8p_mk8vHHPSHXlfSddpsOGPm6x6UjUwy0VvCVuDhUGRmrMn_LlvM37Qr--5q60CfVLtq3yX2bnuF_bZd_Q_Kr1c0zjK-AhEUFnAWdE6Qyos-WIY6_bM5CY5NLRhRf85tOMetv1w1SVzmNdMovUqKlugUpAkLRxCvg7b-USiemuqv9WnFss6NDRVcBhdyHv9W-tdDYH5TYjwmbcaChklQCQ2tSQzPzz75ywL0aYpIMVYDHGVEBuQHa0NFge3qStE5hmNDtOTP36NSV7zxxxf4miPpkk8QFBH1fHPrc5pBgOx";
var components = contentObject.id_token.split('.');
var headerString = pk.base64url.decode(components[0]);
var header = JSON.parse(headerString);
// this is an encrypted token
if (header.enc !== undefined){
resolve(header);
return;
}
var idTokenString = pk.base64url.decode(components[1]);
var idToken = JSON.parse(idTokenString);
var validationKeyPromise;
if (idToken.sub_jwk){
var keystore = pk.claimer_crypto.JWK.createKeyStore();
validationKeyPromise = keystore.add(idToken.sub_jwk);
}
else{
validationKeyPromise = requestVerificationKeys(idToken.iss);
}
validationKeyPromise.then(function(result){
idpKeys = result;
return verifyIdToken(idToken.iss, idpKeys, contentObject.id_token, idToken, validationOptions);
}, function (err){
reject(pk.sts.create_claimer_error('invalid_token', err));
return;
})
.then(function (result){
resolve(result);
}, function(err){
reject(pk.sts.create_claimer_error('access_denied', 'Error validating token: ' + err.error_description));
return;
});
}
else{
reject({
error: 'missing id token',
});
}
});
}
async function requestOPMetadata(issuer){
var options = {
url: issuer + '/.well-known/openid-configuration',
method: 'GET',
headers: [ { name: 'Accept', value: 'application/json' } ]
};
var opMetadata;
try {
opMetadata = await pk.util.jsonHttpData(options)
return opMetadata;
}
catch(err){
pk.util.log_error('ERROR OBTAINING METADATA', err);
throw(err);
}
}
async function requestUrlContent(url){
var options = {
url: url,
method: 'GET',
headers: [ { name: 'Accept', value: 'application/json' } ]
};
return await pk.util.jsonHttpData(options);
}
async function requestValidatedDistributedClaims(location_details, options, asCompactJws){
try {
var distributedClaims = {};
if (location_details.credential_format === 'w3cvc-jsonld'){
var credInfo = await requestCredentialInfo(location_details, options);
distributedClaims = credInfo;
distributedClaims.info = "This token has not been verified cryptographically."
}
else if (location_details.credential_format === 'w3cvc-jwt'){
if (location_details.JWT !== undefined){
distributedClaims = { JWT: location_details.JWT };
if (location_details.info !== undefined){
distributedClaims.info = location_details.info;
}
return distributedClaims;
}
var credInfo = await requestCredentialInfo(location_details, options);
distributedClaims = { JWT: credInfo.credential };
if (location_details.info !== undefined){
distributedClaims.info = location_details.info;
}
var contentObject = {
id_token: distributedClaims.JWT
}
var idToken = await pk.token.processOPTokenResult(contentObject);
if (asCompactJws){
return distributedClaims.JWT;
}
distributedClaims = { JWT: distributedClaims.JWT };
distributedClaims.info = 'This ADC token was fetched from the issuer during claim presentation';
}
return distributedClaims;
}
catch(err){
throw("Error in requestValidatedDistributedClaim: " + err);
}
}
async function requestCredentialInfo(location_details, options){
try{
var distributedClaims = {};
if (!options.credential_sub){
throw('requestCredentialInfo has no credential_sub');
}
if (!options.credential_id){
throw('requestCredentialInfo has no credential_id');
}
var cred_persona = pk.ptools.getPersona('id', options.credential_id);
if (location_details.credential_format === 'w3cvc-jwt'){
if (location_details.credential_endpoint === undefined){
distributedClaims = { JWT: location_details.JWT };
if (location_details.info !== undefined){
distributedClaims.info = location_details.info;
}
return distributedClaims;
}
}
var bearerToken = location_details.access_token;
var credential_key_proof = await create_key_proof(cred_persona.issuer_url, cred_persona.kid);
var postData = { request: credential_key_proof };
var credInfo = await _do_axios(location_details, postData);
return credInfo;
}
catch(err){
throw(err);
}
}
function _do_axios(claim_source, data){
return new Promise((resolve, reject) => {
axios({
url: claim_source.credential_endpoint,
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + claim_source.access_token
},
data: data
})
.then(function (response) {
resolve(response.data);
})
.catch(function (error) {
console.log(JSON.stringify(error));
})
})
}
function requestVerificationKeys(issuer){
return new Promise((resolve, reject) => {
requestOPMetadata(issuer)
.then(function(metadata){
var op_metadata = JSON.parse(metadata);
return requestUrlContent(op_metadata.jwks_uri);
}, function(err){
reject('Unable to retrieve service key metadata ' + err);
})
.then(function(keystoreString){
if(keystoreString){
var keystore = JSON.parse(keystoreString);
resolve(pk.claimer_crypto.JWK.asKeyStore(keystore));
}
}, function(err){
reject('Error retrieving keystoreString ' + err);
})
});
}
function handleKey(options, resolve, reject, response){
var responseJson = JSON.parse(response);
pk.claimer_crypto.JWK.asKeyStore(responseJson)
.then(function (result){
var idpKeys = result;
resolve(result);
});
}
async function verifyIdToken(issuer, keyset, compactRawToken, idToken, validationOptions){
var potentialError = '';
try{
if (idToken === null || idToken === undefined){
throw('No id_token present to verify');
}
if (validationOptions === undefined){
validationOptions = { enable_self_issued: false };
}
var sub;
if (idToken.sub_jwk){
if (validationOptions.enable_self_issued === false){
throw('Self-issued issuer received but wallet OP functionality not enabled');
}
var thumbprintBase64url = await pk.simple_crypto.jwkThumbprint(idToken.sub_jwk);
var thumbAsBase64 = pk.base64url.toBase64(thumbprintBase64url);
sub = pk.util.randomToURN(thumbAsBase64);
if (idToken.sub !== sub){
throw('In a self issued token the sub must map to the thumbprint of the public key');
}
}
if (!validationOptions.disable_token_expiry){
var date = new Date();
if (idToken.exp < Math.trunc(date.getTime() / 1000)){
throw('The token has expired.')
}
}
var opts = {
algorithms: ["RS256"],
format: 'compact'
};
potentialError = 'IdToken crypto verification failed';
var verified = await pk.claimer_crypto.JWS.createVerify(opts, keyset).verify(compactRawToken);
if (idToken.iss !== issuer){
throw('Invalid Token - bad issuer. iss is "' + idToken.iss + '" but configured issuer is "' + issuer + '"');
}
if (validationOptions.audience !== undefined && idToken.aud !== validationOptions.audience){
throw('Invalid Token - bad audience: ' + idToken.aud);
}
// TODO CHECK DATES!!
return idToken;
}
catch(err){
throw ('verifyIdToken failed: ' + err + '. ' + potentialError);
}
}
async function retrieveVerifiableCredential(persona, requestOptions){
const REFRESHTHRESHOLD = .8;
var distributedClaims;
var cacheId;
if (persona.kind === 'credential'){
var cachedCredential;
if (persona.claim_source.credential_type.startsWith('frozen')){
cacheId = requestOptions.credential_sub + '@' + persona.id;
var dbInfo = pk.dbs['wallet'];
try {
var credRecord = await dbInfo.provider.getDocument(dbInfo, 'vcCache', cacheId);
cachedCredential = credRecord.credential;
}
catch(err){
}
}
if (cachedCredential && persona.claim_source.credential_type === 'frozen-refresh'){
if (persona.claim_source.credential_format === 'w3cvc-jsonld'){
// TODO: check expiry of cached credential
}
else{
var components = cachedCredential.split('.');
var jwtTokenString = Buffer.from(components[1], 'base64').toString('utf8');
var jwtToken = JSON.parse(jwtTokenString);
var jwtLifetime = jwtToken.exp - jwtToken.iat;
var jwtRefreshAt = jwtToken.iat + (jwtLifetime * REFRESHTHRESHOLD);
var now = Date.now() / 1000;
if (now > jwtRefreshAt){
cachedCredential = undefined;
}
}
}
if (cachedCredential){
if (persona.claim_source.credential_format === 'w3cvc-jsonld'){
distributedClaims = {
credential: cachedCredential,
info: 'This frozen ADC token was fetched from the wallet'
}
}
else{
distributedClaims = {
JWT: cachedCredential,
info: 'This frozen ADC token was fetched from the wallet'
}
}
}
else{
distributedClaims = await pk.token.requestValidatedDistributedClaims(persona.claim_source, requestOptions, false);
if (persona.claim_source.credential_type.startsWith('frozen')){
if (persona.claim_source.credential_format === 'w3cvc-jsonld'){
var credRecord = {
id: cacheId,
credential: distributedClaims.credential
}
}
else{
var credRecord = {
id: cacheId,
credential: distributedClaims.JWT
}
}
await dbInfo.provider.createOrUpdateDocument(dbInfo, 'vcCache', credRecord);
distributedClaims.info = 'This frozen ADC token was fetched from the issuer and stored in the wallet';
}
}
}
return distributedClaims;
}
/*
{
"typ":"dpop+jwt",
"alg":"ES256",
"jwk": {
"kty":"EC",
"x":"l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
"y":"9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA",
"crv":"P-256"
}
}.{
"jti":"-BwC3ESc6acc2lTc",
"htm":"POST",
"htu":"https://server.example.com/token",
"iat":1562262616
}
*/
async function create_dpop(sub, htm, htu){
var possibleError = 'error loading key in dpop';
try{
if (sub === undefined){
throw('tokenSigningKey requested but no sub specified');
}
var loadedKey = await pk.key_management.loadSingleKey({ dictionary: {"did": sub } });
var signingKey = loadedKey.keyObject;
if (!signingKey){
throw('no signing key located for sub: ' + did);
}
var date = new Date();
var body = {
jti: await pk.simple_crypto.randomString(),
htm: htm,
htu: htu,
iat: Math.trunc(date.getTime() / 1000)
}
var headerFields = dpop_header_fields(signingKey);
possibleError = 'error signing in dpop';
var jws_compact = await pk.claimer_crypto.JWS.createSign(
{
format: 'compact',
fields: headerFields
},
signingKey).update(JSON.stringify(body)).final();
return jws_compact;
}
catch(err){
pk.util.log_error(possibleError, err);
throw(err);
}
}
function dpop_header_fields(signingKey){
var headerFields = {
typ: 'dpop+jwt',
jwk: {}
}
if (signingKey.alg){
headerFields.alg = signingKey.alg;
}
for (var key in signingKey.jwk_public){
switch (key){
case 'kty':
case 'x':
case 'y':
case 'e':
case 'n':
headerFields.jwk[key] = signingKey.jwk_public[key];
break;
case 'crv':
headerFields.jwk.crv = signingKey.jwk_public.crv;
if (signingKey.signature_alg){
headerFields.alg = signingKey.signature_alg;
}
break;
default:
break;
}
}
return headerFields;
}
async function validate_dpop(dpopCompact, sub, htm, htu){
var onerror_message = '';
try {
onerror_message = 'error splitting and parsing dpop';
var dpopArr = dpopCompact.split('.');
var dpopHeader = JSON.parse(pk.base64url.decode(dpopArr[0]));
var dpopBody = JSON.parse(pk.base64url.decode(dpopArr[1]));
onerror_message = 'error in signing fields';
// header fields must match key definition
if (sub.startsWith('did:')){
await validate_did_key(sub, dpopHeader.jwk);
}
else if (sub.startsWith('urn:uuid:')){
await validate_urn_key(sub, dpopHeader.jwk);
}
onerror_message = 'error verifying signature';
var result = await pk.simple_crypto.verify_signature(dpopCompact, dpopHeader.jwk);
if (dpopBody.htm !== htm){
throw 'dpop method does not match';
}
if (dpopBody.htu !== htu){
throw 'dpop is being presented at a different htu that it was issued';
}
// TODO: validate nonce and iat
return true;
}
catch (err){
pk.util.log_error('Error in validate_dpop: ', err);
throw('Error in validate_dpop');
}
}
async function validate_did_key(sub, jwk){
// in didFromJwk sub is just used to determine did method
var calculatedDid = await pk.did_management.didFromJwk(sub, jwk);
if (sub != calculatedDid){
throw('validate_did_key fails for ' + sub);
}
}
async function validate_urn_key(sub, jwk){
var kidAsPerSpec = await pk.simple_crypto.jwkThumbprint(jwk);
var kidInBase64 = pk.base64url.toBase64(kidAsPerSpec);
var urn = pk.util.randomToURN(kidInBase64);
// need to deal with all valid crypto identifiers rather than just urns...
if (sub !== urn){
throw 'dpop signing key not valid for sub';
}
}
function display_dpop(dpopCompact){
var dpopArr = dpopCompact.split('.');
var dpopHeader = JSON.parse(pk.base64url.decode(dpopArr[0]));
var dpopBody = JSON.parse(pk.base64url.decode(dpopArr[1]));
var display = JSON.stringify(dpopHeader, null, ' ') + '\r\n'
+ JSON.stringify(dpopBody, null, ' ');
return display;
}
async function create_key_proof(aud, kid){
try{
var stsDbInfo = pk.dbs['sts'];
var cred_key = await stsDbInfo.provider.getDocument(stsDbInfo, 'keys', kid);
if (!cred_key){
throw('process_code: cred_key not found.');
}
// TODO: accomodate key types
var did_key = 'did:key:z' + cred_key.did.replace('did:peer:z0', '');
var kid = did_key + '#' + did_key.replace('did:key:', '');
var headerFields = {
"alg": "EdDSA",
"typ": "JWT",
"kid": kid
}
var date = new Date();
var now = Math.trunc(date.getTime() / 1000);
var toBeSigned = {
"iss": pk.sts.selfIssuedIssuerIdentifier(),
"aud": aud,
"sub": did_key,
"nonce": await pk.simple_crypto.randomString(),
"auth_time": now,
"iat": now,
"exp": now + 300
}
var jws_compact = await pk.claimer_crypto.JWS.createSign(
{
format: 'compact',
fields: headerFields
},
cred_key).update(JSON.stringify(toBeSigned)).final();
return jws_compact;
}
catch(err){
console.log('Error in create_key_proof');
console.log(err);
}
}