UNPKG

oidc-lib

Version:

A library for creating OIDC Service Providers

1,463 lines (1,232 loc) 72.7 kB
var sts_cookie_identifier = 'sts_'; const defaultTokenTTL = 3600; const OidcClaims = ['sub', 'sub_jwk', 'acr', 'nonce', 'auth_time', 'iss', 'iat', 'exp', 'aud', 'jti', 'nbf', 'vc_constants', 'vp', 'vc']; var _metadata = {}; var viewPath = null; var pk = null; var token_response_callbacks = {}; module.exports = { registerEndpoints: registerEndpoints, applyAuthResponse: applyAuthResponse, applyConsentResponse: applyConsentResponse, submitUserinfoResponse: submitUserinfoResponse, submitCredentialResponse: submitCredentialResponse, getScopedClaims: getScopedClaims, processAuthRequest: processAuthRequest, cookie_identifier: sts_cookie_identifier, initiateAuthRequest: initiateAuthRequest, validateAuthResponse: validateAuthResponse, redirectImplicitAuthResponse: redirectImplicitAuthResponse, create_claimer_error: create_claimer_error, register_token_response_callback: register_token_response_callback, isSelfIssuedSts: isSelfIssuedSts, selfIssuedIssuerIdentifier: selfIssuedIssuerIdentifier, isScopeConsented: isScopeConsented, throw_claimer_error: throw_claimer_error }; function registerEndpoints(pkInput) { pk = pkInput; if (pk.util.usingNode()){ viewPath = __dirname + '/views/'; } else{ viewPath = '\\claimer_sts\\views\\'; } var endpoint = '/*/auth'; pk.app.options(endpoint, pk.cors()); pk.app.get(endpoint, pk.util.corsOptions(), function(req, res){ processAuthRequest(req, res, query2params(req)); }); pk.app.post(endpoint, pk.util.corsOptions(), function(req, res){ processAuthRequest(req, res, req.body); }); var endpoint = '/*/token'; pk.app.options(endpoint, pk.cors()); pk.app.get(endpoint, pk.util.corsOptions(), function(req, res){ processTokenRequest(req, res, query2params(req)); }); pk.app.options(endpoint, pk.cors()); pk.app.post(endpoint, pk.util.corsOptions(), function(req, res){ processTokenRequest(req, res, req.body); }); endpoint = '/*/userinfo'; pk.app.options(endpoint, pk.cors()); pk.app.get(endpoint, pk.util.corsOptions(), function(req, res){ processUserinfoRequest(req, res, query2params(req)); }); pk.app.post(endpoint, pk.util.corsOptions(), function(req, res){ processUserinfoRequest(req, res, req.body); }); endpoint = '/*/credential'; pk.app.options(endpoint, pk.cors({'origin': true, 'credentials': true})); pk.app.post(endpoint, pk.cors({'origin': true }), function(req, res){ processCredentialRequest(req, res, req.body); }); endpoint = '/*/.well-known/openid-configuration'; pk.app.options(endpoint, pk.cors()); pk.app.get(endpoint, pk.util.corsOptions(), function(req, res){ pk.util.log_debug('STS RECEIVED: MetadataRequest'); res.set ({ 'Cache-Control': 'no-store', 'Pragma': 'no-cache', 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.json(metadata(pk, req)); }); endpoint = '/*/jwks.json'; pk.app.options(endpoint, pk.cors()); pk.app.get('/*/jwks.json', pk.util.corsOptions(), function(req, res){ processJwksRequest(req, res); }); pk.app.post('/*/registration', function(req, res){ processRegistrationRequest(req, res); }); endpoint = '/*/issuer_resource'; pk.app.options(endpoint, pk.cors()); pk.app.get(endpoint, pk.util.corsOptions(), function(req, res){ res.set ({ 'Cache-Control': 'no-store', 'Pragma': 'no-cache', 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); processIssuerResources(req, res); }); /* pk.app.get('/walletOP', function(req, res){ processWalletOP(req, res); }); */ } async function processAuthRequest (req, res, params) { var error_handler = defaultEH; var onerror_message = ""; try{ pk.util.log_debug('--- PROCESS AUTH REQUEST ---'); var contentModuleName = pk.util.content_module_name(req); pk.util.log_debug('STS RECEIVED AuthenticationRequest for module \'' + contentModuleName + '\'\r\n'); pk.util.log_detail('Request Headers', req.headers); pk.util.log_protocol('Authentication Request', 'issuer', { name: '{OIDC_CORE}', url: '#AuthRequest' }, pk.util.inbound_wire_content(req)); if (req.headers['x-did-openid'] !== undefined){ params['did'] = req.headers['x-did-openid']; } pk.util.log_detail('Request Parameters', params); // ensure the content module implements necessary apis var cmApis = ['invokeAuthUserAgent', 'invokeConsentUserAgent']; for (var i=0; i < cmApis.length; i++){ if (!getContentModuleApi(contentModuleName, cmApis[i])){ throw('FATAL error: ' + cmApis[i] + ' is not exported from content module'); } } var id_token_hint = null; if (params.redirect_uri === undefined){ reportClientIdentityError(res, {error: 'OIDC redirect_uri param is required'}, false); return; } // make sure client_info can be submitted in request error_handler = unauthorizedClientEH; var client_info = await pk.client_info.load(params, contentModuleName); error_handler = defaultEH; try { params.client_info = client_info; } catch (err){ reportClientIdentityError(res, err, false); return; } if (params.request_uri){ try { var requestContent = await pk.util.jsonHttpData( { url: params.request_uri, headers: [ { name: 'Accept', value: 'application/json' } ] }); params.request = JSON.parse(requestContent); } catch (err){ reportClientIdentityError(res, {error: 'request_uri_not_responding'}, false); return; } } if (params.request){ var segs = params.request.split('.'); var header = JSON.parse(pk.base64url.decode(segs[0])); var requestObj = JSON.parse(pk.base64url.decode(segs[1])); if (header.alg === "none"){ for (var k in requestObj){ params[k] = requestObj[k]; } } else if (header.alg === 'RS256'){ // warning not tested var contentObject = { id_token: requestObj } // validationOptions? var result = await processOPTokenResult(contentObject); for (var k in requestObj){ if (k !== 'iss' && k !== 'aud'){ params[k] = requestObj[k]; } } } else{ reportClientIdentityError(res, {error: 'invalid_request_signature_alg'}, false); } pk.util.log_detail('Request Parameters reflecting request param', params); } try{ client_info.verify_redirect_uri(params.redirect_uri); params.response_type = verify_response_type(contentModuleName, params.response_type); params.isAuthorizationCodeFlow = isAuthorizationCodeFlow(params.response_type); } catch (err){ reportClientIdentityError(res, err, false); return; } if (isset(params.id_token_hint)){ var tokenHintKeyStore = await pk.claimer_crypto.JWK.asKeyStore( pk.key_management.contentModuleSigningKeysPublic[contentModuleName]); var opts = { algorithms: ["RS256"], format: 'compact' }; onerror_message = 'Unable to create keystore from module public keys in id_token_hint'; // var vObj = await pk.claimer_crypto.JWS.createVerify(tokenHintKeyStore, opts); var vObj = await pk.claimer_crypto.JWS.createVerify(opts, tokenHintKeyStore); onerror_message = 'Unable to create verify from keystore in id_token_hint'; var result = await vObj.verify(params.id_token_hint); pk.util.log_debug('--- PROCESS AUTH REQUEST --- ID TOKEN HINT VALIDATED'); var components = params.id_token_hint.split('.'); var payloadString = pk.base64url.decode(components[1]); onerror_message = 'error parsing payloadString'; var payload = JSON.parse(payloadString); var issuer = isSelfIssuedSts() ? selfIssuedIssuerIdentifier() : pk.util.httpsServerUrls['ISSUER_HOST'].href + contentModuleName; if (payload.iss !== issuer){ throw 'Invalid id_token_hint - bad issuer. iss is "' + payload.iss + '" but configured issuer is "' + idpIssuer + '"'; } // TODO it may not be necessary to validate audience if (payload.aud !== params.client_id){ throw 'Invalid id_token_hint - bad audience'; } // TODO add date validation error_handler = redirectEH; await processAuthRequestDetail(req, res, contentModuleName, params, payload); } else { error_handler = redirectEH; await processAuthRequestDetail(req, res, contentModuleName, params); } } catch(err){ pk.util.log_error("processAuthRequest", onerror_message); error_handler(err); } function defaultEH(err){ pk.util.log_error("defaultEH", "Unable to proces AUth Request", err); } function redirectEH(err){ var detail = err; if (err === undefined){ detail = 'invalid_id_token_hint'; } var err = create_claimer_error('invalid_request', detail); redirectOAuthError(err, res, params); } function unauthorizedClientEH(err){ var claimer_error = create_claimer_error('unauthorized_client', err); reportClientIdentityError(res, claimer_error, false); } } async function processAuthRequestDetail(req, res, contentModuleName, params, verified_id_token){ var code = await pk.simple_crypto.randomString(); var access_token = await pk.simple_crypto.randomString(); try{ var scopeInfo = { 'scopeArray': verify_scope(params.scope), 'claims': verify_claims(params.claims) }; var sts_state_bundle = { redirect_uri: params.redirect_uri, response_type: params.response_type, isAuthorizationCodeFlow: params.isAuthorizationCodeFlow, contentModuleName: contentModuleName, client_info: params.client_info, code: code, access_token: access_token, code_redemptions: 0 }; var content_module_state = { client_id: params.client_id, sub: params.client_info.sub, scopeInfo: scopeInfo }; addToObjectIfSet(sts_state_bundle, 'state', verify_state(params.state)); addToObjectIfSet(sts_state_bundle, 'prompt', params.prompt); addToObjectIfSet(sts_state_bundle, 'verified_id_token', verified_id_token); addToObjectIfSet(sts_state_bundle, 'response_mode', verify_response_mode(params.response_mode)); addToObjectIfSet(content_module_state, 'acr_values', params.acr_values); addToObjectIfSet(content_module_state, 'nonce', verify_nonce(params.nonce, params.isAuthorizationCodeFlow)); addToObjectIfSet(content_module_state, 'max_age', verify_max_age(params.max_age)); addToObjectIfSet(content_module_state, 'login_hint', params.login_hint); // iwi is non-standard parameter for systems supporting // issuer wallet identifier - a per issuer identifier for a given // wallet instance addToObjectIfSet(content_module_state, 'iwi', params.iwi); pk.util.log_detail('INITIAL AUTH sts_state_bundle', sts_state_bundle); var stsCookie = req.cookies[moduleCookieIdentifier(contentModuleName)]; var cookieLifetime = pk.util.config.content_modules[contentModuleName].cookieLifetime; if (sts_state_bundle.prompt !== 'login' && (stsCookie !== undefined) && cookieLifetime !== undefined) { await prepareAuthResponseFromCookieIfPossible(req, res, params, stsCookie, sts_state_bundle, content_module_state); return; } var auth_state = { sts_state_bundle: sts_state_bundle, content_module_state: content_module_state }; if (isset(sts_state_bundle.verified_id_token) && verifyContentModuleApiImplemented(contentModuleName, 'processVerifiedIdToken')){ getContentModuleApi(contentModuleName, 'processVerifiedIdToken')(res, verified_id_token, pk.serialize64.to64(auth_state)); return; } if (sts_state_bundle.prompt === 'none'){ var err = create_claimer_error('interaction_required', 'prompt is \'none\''); redirectOAuthError(err, res, params); return; } var date = new Date(); auth_state.sts_state_bundle.auth_time = Math.trunc(date.getTime()/1000); var auth_state_b64 = pk.serialize64.to64(auth_state); getContentModuleApi(contentModuleName, 'invokeAuthUserAgent')(req, res, params, auth_state_b64, access_token); } catch (err){ if (err.error === undefined){ var explainer = 'Unable to invoke user agent for module ' + contentModuleName; if (err.error_description !== undefined){ explainer += ' - ' + err.error_description; } else if (err.message !== undefined){ explainer += ' - ' + err.message; } err = create_claimer_error('content_provider_error', explainer); } pk.util.log_error('processAuthRequestDetail', err); redirectOAuthError(err, res, params); } } async function prepareAuthResponseFromCookieIfPossible(req, res, params, stsCookie, sts_state_bundle, content_module_state){ var authResponseSent = false; var contentModuleName = sts_state_bundle.contentModuleName; var decryptResult = await pk.claimer_crypto.JWE.createDecrypt( pk.key_management.contentModuleIntegrityKeystores[contentModuleName]).decrypt(stsCookie); try { var uncompressed = await pk.util.uncompress(decryptResult.plaintext, {asBuffer: true}); var cookie_auth_state = JSON.parse(uncompressed); /* var cookie_auth_state = JSON.parse(new Buffer(decryptResult.plaintext).toString('utf8')); */ if (cookie_auth_state.content_module_state.client_id === content_module_state.client_id && cookie_auth_state.content_module_state.login_hint === content_module_state.login_hint){ var date = new Date(); var auth_time = parseInt(cookie_auth_state.sts_state_bundle.auth_time); if ((verify_not_set(params.max_age) || auth_time + parseInt(params.max_age) > date.getTime() / 1000) && params.acr_values === cookie_auth_state.content_module_state.acr_values){ pk.util.log_debug('SENDING AuthResponse FROM COOKIE'); sts_state_bundle.auth_time = auth_time; var cookie_contentModuleState = cookie_auth_state.content_module_state; for (var cmKey in cookie_contentModuleState){ if (cmKey === 'nonce' || cmKey === 'scopeInfo'){ continue; } content_module_state[cmKey] = cookie_contentModuleState[cmKey]; } var auth_state = { sts_state_bundle: sts_state_bundle, content_module_state: content_module_state }; await applyAuthResponse(res, pk.serialize64.to64(auth_state), content_module_state); authResponseSent = true; } } if (!authResponseSent){ if (params.prompt === 'none'){ var err = create_claimer_error('interaction_required', 'prompt is \'none\''); redirectOAuthError(err, res, params); } else { var date = new Date(); sts_state_bundle.auth_time = Math.trunc(date.getTime()/1000); var auth_state = { sts_state_bundle: sts_state_bundle, content_module_state: content_module_state }; getContentModuleApi(contentModuleName, 'invokeAuthUserAgent')(req, res, params, pk.serialize64.to64(auth_state), sts_state_bundle.access_token); } } } catch(err){ // if the key database is reset, a cookie can be left over that can't be // decoded. In any case carry on as if no cookie exists var date = new Date(); sts_state_bundle.auth_time = Math.trunc(date.getTime()/1000); var auth_state = { sts_state_bundle: sts_state_bundle, content_module_state: content_module_state }; getContentModuleApi(contentModuleName, 'invokeAuthUserAgent')(req, res, params, pk.serialize64.to64(auth_state), sts_state_bundle.access_token); } } async function applyAuthResponse(res, encoded_sts_state_bundle, content_module_state_components){ var error_handler; var onerror_message = "Error in applyAuthResponse"; try{ var auth_state = pk.serialize64.from64(encoded_sts_state_bundle); var sts_state_bundle = auth_state.sts_state_bundle; var content_module_state = auth_state.content_module_state; if (content_module_state_components !== undefined){ for (var k in content_module_state_components){ content_module_state[k] = content_module_state_components[k]; } } pk.util.log_debug('--- APPLY AUTH RESPONSE --- (from ' + sts_state_bundle.contentModuleName + ')'); // pk.util.log_detail('encoded_sts_state_bundle', encoded_sts_state_bundle); pk.util.log_detail('content_module_state', content_module_state); var auth_state = { sts_state_bundle: sts_state_bundle, content_module_state: content_module_state }; if (content_module_state.sub === undefined){ throw(create_claimer_error('server_error', 'Consents storage not available because \'content_module_state.sub\' not set during authentication')); } if (content_module_state.error){ var paramObj = { redirect_uri: auth_state.sts_state_bundle.redirect_uri, state: auth_state.sts_state_bundle.state } redirectOAuthError(content_module_state.error, res, paramObj); return; } try{ var consents_id = content_module_state.client_id + '@' + sts_state_bundle.contentModuleName + '@' + content_module_state.sub; pk.util.log_debug('Getting stored consent for ' + consents_id); var doc = await pk.dbs['sts'].provider.getDocument(pk.dbs['sts'], 'consents', consents_id); pk.util.log_detail('stored consent info', doc.consentInfo); content_module_state.consentInfo = doc.consentInfo; sts_state_bundle.recorded_consentDoc = doc; // maintained to see if consent has changed } catch(err){ // if auth based on cookie, its consentInfo will be available if (sts_state_bundle.prompt === 'none'){ var err = create_claimer_error('interaction_required', 'prompt is none but consent is required'); redirectOAuthError(err, res, sts_state_bundle); } } var explicitConsent = sts_state_bundle.prompt === 'consent' || sts_state_bundle.contentModuleName === 'wallet'; var invokeConsentUserAgentFunction = getContentModuleApi(sts_state_bundle.contentModuleName, 'invokeConsentUserAgent'); if (!explicitConsent && (invokeConsentUserAgentFunction === undefined || isScopeConsented(content_module_state))){ await finalizeAuthResponse(res, auth_state, content_module_state); } else{ content_module_state.explicitConsent = sts_state_bundle.prompt === 'consent'; invokeConsentUserAgentFunction(res, content_module_state.scopeInfo, sts_state_bundle.client_info, pk.serialize64.to64(auth_state), content_module_state); } } catch (err){ throw (err); } } async function finalizeAuthResponse(res, auth_state, content_module_state){ pk.util.log_debug('--- SEND AUTH RESPONSE FINAL ---'); try{ if (content_module_state.error){ var paramObj = { redirect_uri: auth_state.sts_state_bundle.redirect_uri, state: auth_state.sts_state_bundle.state } redirectOAuthError(content_module_state.error, res, paramObj); return; } if (content_module_state.consentInfo === undefined){ throw 'AuthResponse cannot complete without consentInfo'; } // prefer new content if (content_module_state.newIdTokenContent){ content_module_state.newIdTokenContent.auth_time = auth_state.sts_state_bundle.auth_time; content_module_state.consentInfo.idTokenContent = content_module_state.newIdTokenContent; } else{ // otherwise get it from the cookie's consentInfo.idTokenContent if (!auth_state.content_module_state.consentInfo || !auth_state.content_module_state.consentInfo.idTokenContent){ throw 'AuthResponse cannot complete if both newIdTokenContent and consentInfo.idTokenContent are undefined'; } else{ content_module_state.consentInfo.idTokenContent = auth_state.content_module_state.consentInfo.idTokenContent; } } var idTokenContent = content_module_state.consentInfo.idTokenContent; if (idTokenContent.sub === undefined){ throw 'AuthResponse cannot complete if idTokenContent does not include \'sub\' '; } // trim content_module_state to remove properties not necessary for send of token and // storage of cookie / access_tokens / etc var trimmed_content_module_state = {}; var essentialContentModuleStateProperties = [ "client_id", "sub", "consentInfo", "accessTokenContent", "id", "expiry", "nonce", "scopeInfo", "acr_values", "max_age", "persist", "login_hint", "scope_claim_map" ]; for (var k in content_module_state){ if (essentialContentModuleStateProperties.indexOf(k) >= 0){ trimmed_content_module_state[k] = content_module_state[k]; } } // trim sts_state_bundle to remove properties not necessary for send of token and cookies var trimmed_sts_state_bundle = {}; var essentialStsStateBundleProperties = [ 'redirect_uri', 'contentModuleName', 'code', 'access_token', 'auth_time', 'code_redemptions', 'registration', 'state', 'response_mode', 'response_type', 'isAuthorizationCodeFlow', 'token_response_callback_info' ]; for (var k in auth_state.sts_state_bundle){ if (essentialStsStateBundleProperties.indexOf(k) >= 0){ trimmed_sts_state_bundle[k] = auth_state.sts_state_bundle[k]; } } auth_state.content_module_state = trimmed_content_module_state; auth_state.sts_state_bundle = trimmed_sts_state_bundle; var contentModuleName = auth_state.sts_state_bundle.contentModuleName; pk.util.log_detail('PrepareAuthResponseFinal auth_state', auth_state); if (content_module_state.consentInfo.scopeArray.indexOf('openid') < 0){ var err = create_claimer_error('access_denied', 'scope of openid was refused by user'); redirectOAuthError(err, res, auth_state.sts_state_bundle); return; } var code = auth_state.sts_state_bundle.code; pk.codeCache.set(code, auth_state); var accessTokenTTL = pk.util.config.content_modules[contentModuleName].accessTokenTTL; if (accessTokenTTL === undefined){ accessTokenTTL = defaultTokenTTL; } if (check_response_type_matches(auth_state.sts_state_bundle.response_type, ['code', 'token'])){ if (auth_state.content_module_state.accessTokenContent !== null){ var date = new Date(); var storageState = { id: auth_state.sts_state_bundle.access_token, expiry: Math.trunc(date.getTime() / 1000) + accessTokenTTL, sub: idTokenContent.sub }; for (var k in auth_state.content_module_state){ storageState[k] = auth_state.content_module_state[k]; } await pk.dbs['sts'].provider.createOrUpdateDocument(pk.dbs['sts'], 'access_tokens', storageState); pk.util.log_debug('Updated access_tokens') } } if (consentDelta(auth_state)){ if (auth_state.content_module_state.sub === undefined){ auth_state.content_module_state.sub = idTokenContent.sub; } var consents_id = auth_state.content_module_state.client_id + '@' + auth_state.sts_state_bundle.contentModuleName + '@' + auth_state.content_module_state.sub; var new_consentDoc = { 'id': consents_id, 'consentInfo': auth_state.content_module_state.consentInfo }; await pk.dbs['sts'].provider.createOrUpdateDocument(pk.dbs['sts'], 'consents', new_consentDoc); pk.util.log_debug('Updated consents'); } if (pk.util.config.content_modules[contentModuleName].cookieLifetime){ var compressed = await pk.util.compress(JSON.stringify(auth_state)); var result = await pk.claimer_crypto.JWE.createEncrypt({ format: 'compact' }, pk.key_management.contentModuleIntegrityKeys[contentModuleName]) .update(compressed).final(); res.cookie(moduleCookieIdentifier(contentModuleName), result, { 'maxAge': pk.util.config.content_modules[contentModuleName].cookieLifetime * 1000 }); } var sts_state_bundle = auth_state.sts_state_bundle; if (sts_state_bundle.isAuthorizationCodeFlow){ if (sts_state_bundle.response_mode === 'form_post'){ var rContent = { redirect_uri: sts_state_bundle.redirect_uri, code: sts_state_bundle.code, state: sts_state_bundle.state } if (sts_state_bundle.response_type === 'code token'){ rContent.access_token = sts_state_bundle.access_token; rContent.token_type = 'Bearer'; } res.render(viewPath + 'claimer_sts_auth_response', rContent); } else{ var location = sts_state_bundle.redirect_uri + "?code=" + encodeURIComponent(sts_state_bundle.code) + "&state=" + encodeURIComponent(sts_state_bundle.state); if (sts_state_bundle.response_type === 'code token'){ location += "&access_token=" + sts_state_bundle.access_token + "&token_type=" + "Bearer"; } pk.util.log_debug('Redirecting to ' + location); res.redirect(location); pk.util.log_protocol('Authentication Response', 'issuer', { name: '{OIDC_CORE}', url: '#AuthResponse' }, pk.util.outbound_wire_content(res)); res.end(); } } else{ generateTokenResponse(res, sts_state_bundle, auth_state.content_module_state); } } catch(err){ var claimerErr = create_claimer_error('server_error', err); redirectOAuthError(claimerErr, res, auth_state.sts_state_bundle); } } async function applyConsentResponse(res, encoded_sts_state_bundle, content_module_state){ pk.util.log_debug('--- APPLY CONSENT RESPONSE ---'); auth_state = pk.serialize64.from64(encoded_sts_state_bundle); await finalizeAuthResponse(res, auth_state, content_module_state); } async function processTokenRequest(req, res, params) { pk.util.log_debug('--- PROCESS TOKEN REQUEST ---'); var contentModuleName = pk.util.content_module_name(req); pk.util.log_debug('STS RECEIVED TokenRequest for module \'' + contentModuleName + '\''); pk.util.log_detail('Request Headers', req.headers); pk.util.log_detail('Parameters', params); var potentialError = ''; try { potentialError = 'assembling protocol pieces'; var protocolArray = [ { title: '', value: pk.util.inbound_wire_content(req) } ] pk.util.log_protocol('Token Request', 'issuer', { name: '{OIDC_CORE}', url: '#TokenRequest' }, protocolArray); params = addAuthorizationFromHeader(req, params); var client_identified = false; if (params.client_id){ var client_info = await pk.client_info.load(params, contentModuleName); try { client_info.verify_redirect_uri(params.redirect_uri); client_info.verify_client_secret(params.client_secret); client_identified = true; } catch (err){ reportClientIdentityError(res, err, false); return; } } // TODO!! FIGURE OUT WHEN UNIDENTIFIED CLIENT IS NOT OK!! // in OpenIdConnect Code token request check is that redirect_uri matches that provided in the auth request // this seems wrong -- params.response_type = verify_response_type(contentModuleName, params.response_type); potentialError = 'grant_type'; verify_grant_type(params.grant_type); potentialError = 'verifying code'; var auth_state = verify_code(params.code, contentModuleName); var htu = pk.util.httpsServerUrls['ISSUER_HOST'].href + req.path.substring(1); var sts_state_bundle = { response_type: auth_state.sts_state_bundle.response_type, auth_time: auth_state.sts_state_bundle.auth_time, contentModuleName: auth_state.sts_state_bundle.contentModuleName, code: auth_state.sts_state_bundle.code, access_token: auth_state.sts_state_bundle.access_token }; params.isAuthorizationCodeFlow = isAuthorizationCodeFlow(auth_state.sts_state_bundle.response_type); generateTokenResponse(res, sts_state_bundle, auth_state.content_module_state); } catch (err){ var msg = 'Error in processTokenRequest ' + potentialError; pk.util.log_error(msg, err); returnOAuthError(err, res); } } async function generateTokenResponse(res, sts_state_bundle, content_module_state){ var contentModuleName = sts_state_bundle.contentModuleName; pk.util.log_debug('--- SUBMIT TOKEN RESPONSE --- (from ' + contentModuleName + ')'); var issuer = isSelfIssuedSts() ? selfIssuedIssuerIdentifier() : pk.util.httpsServerUrls['ISSUER_HOST'].href + contentModuleName; try { var tokenClaims = content_module_state.consentInfo.idTokenContent; var additionalClaims = OidcClaims; // merge module and persona scope_claim_maps var maps = [ pk.feature_modules['sts'].resources.scope_claim_map, pk.feature_modules[contentModuleName].resources.scope_claim_map, content_module_state.scope_claim_map ]; var scope_claim_map = pk.util.merge_claim_maps(maps); var claimsToRelease = getScopedClaims(scope_claim_map, content_module_state.scopeInfo, content_module_state.consentInfo, tokenClaims, additionalClaims); var issuedTokenTTL = pk.util.config.content_modules[contentModuleName].issuedTokenTTL; if (!issuedTokenTTL){ issuedTokenTTL = 600; } var date = new Date(); var nowSecs = Math.trunc(date.getTime() / 1000); var idToken = { iss: issuer, aud: content_module_state.client_id, iat: nowSecs, exp: nowSecs + issuedTokenTTL }; if (isSelfIssuedSts()){ idToken.iss = selfIssuedIssuerIdentifier(); } var subClaimSet = false; for (var key in claimsToRelease){ idToken[key] = claimsToRelease[key]; } // other claims mandated by the protocol should be added // here so as not to depend on correct configuration of // claims definition controlling claimsToRelease() if (isset(content_module_state.max_age)){ idToken['auth_time'] = sts_state_bundle.auth_time; } if (isset(content_module_state.acr_values)){ idToken['acr'] = getContentModuleApi(sts_state_bundle.contentModuleName, 'get_acr')(content_module_state.acr_values); } if (isset(content_module_state.nonce)){ idToken['nonce'] = content_module_state.nonce; } if (idToken.sub === undefined){ throw_claimer_error('content_provider_error', '\'sub\' claim is not set in tokenClaims passed to generateTokenResponse'); } var contentModuleName = sts_state_bundle.contentModuleName; response_type = sts_state_bundle.response_type; var signingKey = await token_presentation_options(contentModuleName, { option: 'tokenSigningKey', // sub: idToken.sub sub: content_module_state.sub }); if (!signingKey){ signingKey = pk.key_management.contentModuleSigningKeys[contentModuleName]; } if (isSelfIssuedSts()){ idToken.sub_jwk = signingKey.jwk_private; } var c_hash = await pk.simple_crypto.oidc3136Hash(response_type, 'code', sts_state_bundle.code); if (c_hash !== null){ idToken.c_hash = c_hash; } // basic 'code' response_type without token still implies sending the tokem // so we test against "code token" to determine whether we create the at_hash var atTest = response_type; if (atTest === 'code'){ atTest = 'code token'; } var at_hash = await pk.simple_crypto.oidc3136Hash(atTest, 'token', sts_state_bundle.access_token); if (at_hash !== null){ idToken.at_hash = at_hash; } // pk.util.log_detail('idToken', idToken); var credentialTokenString = JSON.stringify(idToken); var jws_compact = await pk.claimer_crypto.JWS.createSign( { format: 'compact', fields: { typ: 'JWT', alg: pk.key_management.signatureAlg(signingKey) } }, signingKey).update(credentialTokenString).final(); if (isAuthorizationCodeFlow(sts_state_bundle.response_type)){ var tokenResult = { id_token: jws_compact, access_token: sts_state_bundle.access_token, token_type: 'Bearer', expires_in: issuedTokenTTL, state: sts_state_bundle.state }; /* var protocolArray = [ { title: '', value: tokenResult }, { title: 'Decoded id_token', value: idToken } ] pk.util.log_protocol('Token Response', 'issuer', protocolArray); */ res.set ({ 'Cache-Control': 'no-store', 'Pragma': 'no-cache', 'Access-Control-Allow-Origin': '*' }); pk.util.log_debug('STS SENDING AuthorizationCodeFlow Token Response') res.json(tokenResult); var protocolArray = [ { title: '', value: pk.util.outbound_wire_content(res, tokenResult) }, { title: 'Decoded id_token', value: idToken } ] pk.util.log_protocol('Token Response', 'issuer', [ { name: '{OIDC_CORE}', url: '#TokenResponse' }, { name: '{CRED_REQUEST}', url: '#TokenResponse' } ], protocolArray); res.end(); } else{ // hubrid or implicit flow var tokenResult = { }; var respTypeArray = sts_state_bundle.response_type.split(/[\s]+/); for (var i=0; i<respTypeArray.length; i++){ switch (respTypeArray[i]){ case "code": tokenResult.code = sts_state_bundle.code; break; case "id_token": tokenResult.id_token = jws_compact; break; case "token": tokenResult.access_token = sts_state_bundle.access_token; tokenResult.token_type = 'Bearer'; break; default: throw 'unrecognized response_type component in sendAuthToken'; } } var protocolArray = [ { title: '', value: tokenResult }, ]; if (tokenResult.id_token){ protocolArray.push( { title: 'Decoded id_token', value: idToken }, ); } pk.util.log_protocol('Token Response', 'issuer', protocolArray); tokenResult.state = sts_state_bundle.state; // pk.util.log_detail('tokenResult', tokenResult); var imposeFormPostResponseMode = await token_presentation_options(contentModuleName, { option: 'imposeFormPostResponseMode' }); var token_response_callback; if (sts_state_bundle.token_response_callback_info){ token_response_callback = token_response_callbacks[sts_state_bundle.token_response_callback_info.name]; } if (token_response_callback){ token_response_callback.function(tokenResult, token_response_callback.event); } else if (sts_state_bundle.response_mode === 'form_post' && imposeFormPostResponseMode === true){ var url = sts_state_bundle.redirect_uri; var options = { url: url, method: 'POST', headers: [ { name: 'Content-type', value: 'application/x-www-form-urlencoded' } ], // headers: [ { name: 'Content-type', value: 'application/json' } ], postData: pk.querystring.stringify(tokenResult) }; var result = await pk.util.jsonHttpData(options); pk.pmanager.managerNotification('Your credential has been submitted.', 'alert-warning', true); pk.app.renderDispatcher('get', '/wallet/manager', { page: 'personas' }); } else if (sts_state_bundle.response_mode === 'form_post'){ var url = sts_state_bundle.redirect_uri; var options = { url: url, tokenResultString: pk.querystring.stringify(tokenResult) }; res.render(viewPath + 'claimer_sts_form_post', options); res.end(); } else{ var location = sts_state_bundle.redirect_uri + '#' + pk.querystring.stringify(tokenResult); var protocolArray = [ { title: '', value: location }, { title: 'Decoded id_token', value: idToken } ]; pk.util.log_protocol('STS implicit/hybrid TokenResult', 'issuer', protocolArray); res.redirect(location); res.end(); } } } catch(err){ returnOAuthError(err, res); } } function processUserinfoRequest(req, res, params) { pk.util.log_debug('--- PROCESS USERINFO REQUEST ---'); pk.util.log_protocol('UserInfo Request', 'issuer', pk.util.inbound_wire_content(req)); var contentModuleName = pk.util.content_module_name(req); pk.util.log_debug('STS RECEIVED UserinfoRequest: ' + req.method + '\r\n'); var authorization = req.get("Authorization"); var access_token; if (authorization !== undefined){ pk.util.log_detail('Authorization via header', authorization); access_token = authorization.replace("Bearer", "").trim(); } else { access_token = params.access_token; pk.util.log_detail('Authorization via parameters', access_token); } var alternateUserinfoGeneratorFunc = pk.feature_modules[contentModuleName].code.alternateUserinfoGenerator; if ( alternateUserinfoGeneratorFunc !== undefined){ var replaceConventionalGenerator = alternateUserinfoGeneratorFunc(req, res, params, access_token, contentModuleName); if (replaceConventionalGenerator){ return; } } var content_module_state; pk.dbs['sts'].provider.getDocument(pk.dbs['sts'], 'access_tokens', access_token) .then(function(tokenContent){ content_module_state = tokenContent; try { if (content_module_state === undefined){ throw_claimer_error('access_denied', "access_token is invalid"); } // consent string was added to the Access Token by the sts when creating auth response var consentInfo = content_module_state.consentInfo; pk.util.log_detail('content_module_state', content_module_state); try { var tokenClaims = getContentModuleApi(contentModuleName, 'generateUserinfo')(res, params, consentInfo, content_module_state); } catch (err){ throw_claimer_error('content_provider_error', 'Unable to generate token for module \'' + moduleName + '\''); } } catch (err){ returnOAuthError(err, res); } }, function(readErr){ var err = create_claimer_error('access_denied', 'access_token is invalid' + readErr); returnOAuthError(err, res); return; }); } async function submitUserinfoResponse(res, contentModuleName, params, consentInfo, tokenClaims, encryptTo){ pk.util.log_debug('--- SUBMIT USERINFO RESPONSE ---'); var additionalClaims = OidcClaims; var maps = [ pk.feature_modules['sts'].resources.scope_claim_map, pk.feature_modules[contentModuleName].resources.scope_claim_map ]; var scope_claim_map = pk.util.merge_claim_maps(maps); var claimsToRelease = getScopedClaims(scope_claim_map, consentInfo, consentInfo, tokenClaims, additionalClaims); pk.util.log_detail('userinfoToken', claimsToRelease); // update claims which must be freshened var issuedTokenTTL = pk.util.config.content_modules[contentModuleName].issuedTokenTTL; var date = new Date(); claimsToRelease.iat = Math.trunc(date.getTime() / 1000); claimsToRelease.exp = Math.trunc(date.getTime() / 1000) + issuedTokenTTL; delete claimsToRelease.nonce; var clientInfoParams = { client_id: tokenClaims.aud } var signatureRequired = true; var client_info = await pk.client_info.load(clientInfoParams, contentModuleName); if (client_info.signature_info && client_info.signature_info.userinfo === false){ signatureRequired = false; } if (!signatureRequired){ res.set ({ 'Cache-Control': 'no-store', 'Pragma': 'no-cache', 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.json(claimsToRelease); pk.util.log_protocol('UserInfo Response (unsigned)', 'issuer', pk.util.outbound_wire_content(res, claimsToRelease)); return; } var credentialTokenString = JSON.stringify(claimsToRelease); var jws_compact; var signingKey = pk.key_management.contentModuleSigningKeys[contentModuleName]; pk.claimer_crypto.JWS.createSign( { format: 'compact', fields: { typ: 'JWT', alg: pk.key_management.signatureAlg(signingKey) } }, signingKey).update(credentialTokenString).final() .then(function(jws){ if (encryptTo === undefined){ return jws; } switch (encryptTo){ case 'self': break; case 'aud': throw 'encrypted tokens are not yet implemented'; default: throw 'unsupported encryptTo in submitUserinfoResponse' } return pk.claimer_crypto.JWE.createEncrypt({ format: 'compact' }, pk.key_management.contentModuleIntegrityKeys[contentModuleName]).update(jws).final(); }, function(err){ pk.util.log_detail('Signature error', err); }) .then(function(result){ jws_compact = result; res.set ({ 'Cache-Control': 'no-store', 'Pragma': 'no-cache', 'Content-Type': 'application/jwt', 'Access-Control-Allow-Origin': '*' }); pk.util.log_debug('STS SENDING Userinfo\r\n') res.send(jws_compact); },function(err){ pk.util.log_detail('Signature error', err); }) } async function processCredentialRequest(req, res, params) { var potentialError = ''; try { pk.util.log_debug('--- PROCESS CREDENTIAL REQUEST ---'); var contentModuleName = pk.util.content_module_name(req); pk.util.log_debug('STS RECEIVED Credential Request: ' + req.method); var protocolArray = [ { title: '', value: pk.util.inbound_wire_content(req)} ]; if (res.req.headers.dpop){ protocolArray.push({ title: 'Decoded dpop header for authorization token', value: pk.token.display_dpop(res.req.headers.dpop) }); } if (params.credential_sub){ if (!res.req.headers.credential_sub_dpop){ throw('Received credential_sub with no corresponding credential_sub_dpop header'); } protocolArray.push({ title:'Decoded dpop header for credential_sub', value: pk.token.display_dpop(res.req.headers.credential_sub_dpop) }); } pk.util.log_protocol('Credential Request', 'issuer', { name: '{CRED_REQUEST}', url: '#Request' }, protocolArray); var authorization = req.get("Authorization"); if (!authorization){ throw('no Authorization header present'); } pk.util.log_detail('Authorization via header', authorization); access_token = authorization.replace("Bearer", "").trim(); potentialError = 'getting access_token data'; var content_module_state = await pk.dbs['sts'].provider.getDocument(pk.dbs['sts'], 'access_tokens', access_token); if (content_module_state === undefined){ throw('access_token is undefined'); } // consent string was added to the Access Token by the sts when creating auth response var consentInfo = content_module_state.consentInfo; potentialError = 'content_provider_error'; getContentModuleApi(contentModuleName, 'generateCredentialResponse')(res, params, consentInfo, content_module_state); } catch(err){ pk.util.log_error('processCredentialRequest access_denied', err); err = create_claimer_error('access_denied', potentialError + ' ' + JSON.stringify(err)); returnOAuthError(err, res); } } async function submitCredentialResponse(res, contentModuleName, credInfo, consentInfo, credentialToken, encryptTo){ try{ pk.util.log_debug('--- SUBMIT CREDENTIAL RESPONSE ---'); if (!credInfo){ credInfo = {}; } if (credentialToken){ var additionalClaims = OidcClaims; var maps = [ pk.feature_modules['sts'].resources.scope_claim_map, pk.feature_modules[contentModuleName].resources.scope_claim_map ]; var scope_claim_map = pk.util.merge_claim_maps(maps); var claimsToRelease = getScopedClaims(scope_claim_map, consentInfo, consentInfo, credentialToken, additionalClaims); pk.util.log_detail('credentialToken claimsToRelease', claimsToRelease); var credentialTokenString = JSON.stringify(claimsToRelease); var signingKey = pk.key_management.contentModuleSigningKeys[contentModuleName]; var jws = await pk.claimer_crypto.JWS.createSign( { format: 'compact', fields: { typ: 'JWT', alg: pk.key_management.signatureAlg(signingKey) } }, signingKey).update(credentialTokenString).final(); if (encryptTo){ switch (encryptTo){ case 'self': break; case 'aud': throw 'encrypted tokens are not yet implemented'; default: throw 'unsupported encryptTo in submitUserinfoResponse' } jws = await pk.claimer_crypto.JWE.createEncrypt({ format: 'compact' }, pk.key_management.contentModuleIntegrityKeys[contentModuleName]).update(jws).final(); } credInfo.credential = jws; } res.set ({ 'Cache-Control': 'no-store', 'Pragma': 'no-cache', 'Content-Type': 'application/jwt', 'Access-Control-Allow-Origin': res.req.headers['origin'], "Access-Control-Allow-Credentials": true }); pk.util.log_debug('STS SENDING Credential\r\n') res.send(credInfo); var protocolArray = [ { title: '', value: pk.util.outbound_wire_content(res, credInfo) } ]; if (credInfo.credential){ protocolArray.push({ title: 'Decoded credential', value: claimsToRelease }); } pk.util.log_protocol('Credential Result', 'issuer', { name: '{CRED_REQUEST}', url: '#Response' }, protocolArray); } catch (err){ pk.util.log_detail('Signature error', err); } } function getScopedClaims(scope_claim_map, scopeInfo, consentInfo, tokenClaims, additionalClaims){ var tokenContent = {}; var flatTemplate = {}; flattenTemplates(flatTemplate, scope_claim_map); if (additionalClaims !== undefined){ for (var i=0; i < additionalClaims.length; i++){ var key = additionalClaims[i]; if (tokenClaims[key] !== undefined){ tokenContent[key] = tokenClaims[key]; } } } for (var key in consentInfo.claims.id_token){ if (scopeInfo.claims.id_token[key] === undefined){ continue; } var value = tokenClaims[key]; if (value === undefined || tokenContent[key] !== undefined){ continue; } tokenContent[key] = value; } for (var key in consentInfo.claims.userinfo){ if (scopeInfo.claims.userinfo[key] === undefined){ continue; } var value = tokenClaims[key]; if (value === undefined || tokenContent[key] !== undefined){ continue; } tokenContent[key] = value; } for (var count=0; count < consentInfo.scopeArray.length; count++){ var scope = consentInfo.scopeArray[count]; if (consentInfo.scopeArray.indexOf(scope) >= 0){ var scopeDetail = scope_claim_map[scope]; if (scopeDetail !== undefined && scopeDetail.content !== undefined){ for (var key in scopeDetail.content){ if (scopeDetail.content[key].input === 'password'){ continue; } var value = tokenClaims[key]; if (value === undefined || tokenContent[key] !== undefined){ continue; } tokenContent[key] = value; } } } } return tokenContent; } function metadata(pk, req) { var contentModuleName = pk.util.content_module_name(req); if (_metadata[contentModuleName] === undefined){ var host = pk.util.httpsServerUrls['ISSUER_HOST'].href + contentModuleName; var defaultMetadata = { 'issuer': host, "registration_endpoint": host + "/registration", "authorization_endpoint":host + "/auth", "token_endpoint": host + "/token", "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], "token_endpoint_auth_signing_alg_values_supported": ["RS256"], "userinfo_endpoint": host + "/userinfo", "credential_endpoint": host + "/credential", "jwks_uri": host + "/jwks.json", // "registration_endpoint": host + "/register", "scopes_supported": getScopesSupported(contentModuleName), "response_types_supported": pk.feature_modules[contentModuleName].response_types, "subject_types_supported": ["public", "pairwise"], "request_parameter_supported": true, "request_object_signing_alg_values_supported": ["none", "RS256"], "credential_signing_alg_values_supported": ["RS256"], "userinfo_signing_alg_values_supported": ["RS256"], "id_token_signing_alg_values_supported": ["RS256"], "service_documentation": host + "/service_documentation.html", "claims_supported": getClaimsSupported(contentModuleName) } // TODO: Override metadata with call to content if (pk.feature_modules[contentModuleName].code.registerMetadata !== undefined){ var overrides = pk.feature_modules[contentModuleName].code.registerMetadata(); for (var key in overrides){ defaultMetadata[key] = overrides[key]; if (overrides[key] === null){ delete defaultMetadata[key]; } } } _metadata[contentModuleName] = defaultMetadata; } return _metadata[contentModuleName]; }; function processJwksRequest(req, res){ var contentModuleName = pk.util.content_module_name(req); var jwks = pk.key_management.contentModuleSigningKeysPublic[contentModuleName]; if (jwks === undefined){ jwks = { "keys":[] }; } res.set ({ 'Cache-Control': 'no-store', 'Pragma': 'no-cache', 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.json(jwks); } /* function processWalletOP(req, res){ res.set ({ 'Cache-Control': 'no-store', 'Pragma': 'no-cache', 'Content-Type': 'application/json' }); res.render('walletOP'); } */ /////////////////////////////////////////// Verification Functions //////////////////////////////////////////// function query2params(req){ var params = req.query; for (var key in params){ if (typeof params[key] === 'string'){ params[key] = params[key].replace(/\+/g, ' '); } } return params; } function verify_response_type (contentModuleName, response_type) { if (response_type === undefined) { throw_claimer_error('invalid_request', "response_type must be supplied but is missing"); } response_type = response_type.toLowerCase(); if (pk.feature_modules[contentModuleName].response_types.inde