UNPKG

oidc-lib

Version:

A library for creating OIDC Service Providers

586 lines (503 loc) 17.3 kB
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); } }