UNPKG

oidc-lib

Version:

A library for creating OIDC Service Providers

2,204 lines 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.indexOf(response_type) < 0){
		throw_claimer_error('unsupported_response_type',  '\"' + response_type + '\" not supported as response_type.');
	}

	return response_type;
}

function check_response_type_matches(response_type, candidateArray){
	if (!response_type || !candidateArray){
		throw_claimer_error('system_configuration_error', 'Both response_type and candidateArray must be set in check_response_type_matches');
	}

	var match = false;
	var responseTypeArray = response_type.split(/[\s]+/);
	for (var i=0; i < candidateArray.length; i++){
		if (responseTypeArray.indexOf(candidateArray[i]) >= 0){
			match = true;
			break;
		}
	}

	return match;
}

function isAuthorizationCodeFlow(response_type){
	return response_type === 'code' || response_type === 'code token';
}

function verify_response_mode (response_mode) {
	if (response_mode){
		switch(response_mode){
			case 'form_post':
			case 'query':
			case 'implicit':
				return response_mode;
		}

		throw_claimer_error('invalid_request', 'Unsupported_response_mode: "' + response_mode + '" - use "form_post", "query" or "implicit"');
	}
}

function verify_scope (scope) {
	if (scope === undefined){
		throw_claimer_error('invalid_scope',  'AuthorizationRequest must specify scope');				
	}
	var scopeArray = cleanArray(scope.split(' '));
	if (scopeArray.indexOf('openid') < 0){
		throw_claimer_error('invalid_scope',  'AuthorizationRequest scope must include openid');		
	}

	for (var count=0; count < scopeArray.length; count++){
		if (scopeArray[count].length === 0){

		}
	}
	return scopeArray;
}

function cleanArray(actual) {
  var newArray = new Array();
  for (var i = 0; i < actual.length; i++) {
    if (actual[i]) {
      newArray.push(actual[i]);
    }
  }
  return newArray;
}

function verify_state (state) {
	return state;
}

function verify_code (code) {
	if (code === undefined) {
		throw_claimer_error('invalid_request', "code must be supplied but is missing");
	}

	var auth_state = pk.codeCache.get(code);
	if (auth_state === undefined){
		pk.util.log_error('verify_code', 'Code not found: ' + code);
		throw_claimer_error('invalid_grant', "authorization code is unknown: " + code);
	};

	auth_state.sts_state_bundle.code_redemptions++;
	pk.codeCache.set(code, auth_state);

	var sts_state_bundle = auth_state.sts_state_bundle;
	if (pk.util.config.content_modules[sts_state_bundle.contentModuleName].reuse_code !== true){
		if (sts_state_bundle.code_redemptions > 1){
			pk.util.log_debug('Invalidating access token: ' + sts_state_bundle.access_token);
			pk.codeCache.del(sts_state_bundle.access_token);
			throw_claimer_error('invalid_grant', "mutliple attempts to redeem authorization code");		
		}
	}

	return auth_state;
}

function verify_grant_type (grant_type) {
	if (grant_type === undefined) {
		throw_claimer_error('invalid_request', "grant_type must be supplied but is missing");
	}

	if (grant_type.toLowerCase() !== 'authorization_code') {
		throw_claimer_error('invalid_grant', "Grant type must be 'authorization_code'");
	}

	// ToDo: verify grant_type
	return grant_type;
}

function verify_nonce (nonce, isAuthorizationCodeFlow) {
	if (nonce === undefined && !isAuthorizationCodeFlow ){
		throw_claimer_error('invalid_request', "nonce must be specified in hybrid and implicit flows");		
	}
	if (nonce){
		var nonce_state = pk.nonceCache.get(nonce);
		if (nonce_state !== true){
			pk.nonceCache.set(nonce, true);
			return nonce;
		}
		else{
			throw_claimer_error('invalid_request', "nonce reused");		
		}
	}
}

function verify_claims (claimsString) {
	if (claimsString === undefined){
		return {'id_token': {}, 'userinfo': {}};
	}
	else if (typeof claimsString !== 'string'){
		throw_claimer_error('invalid_request', "claims must be received as a string");
	}

	try {
		var claims = JSON.parse(claimsString);
		if (claims.id_token === undefined){
			claims.id_token = {};
		}
		if (claims.userinfo === undefined){
			claims.userinfo = {};
		}
		for (var key in claims.id_token){
			if (key.length === 0 || key.length > 256){
				throw_claimer_error('invalid_request', "claim names must be between 1 and 256 characters");
			}
		}
		for (var key in claims.userinfo){
			if (key.length === 0 || key.length > 256){
				throw_claimer_error('invalid_request', "claim names must be between 1 and 256 characters");
			}
		}
	}
	catch(err){
		throw_claimer_error('invalid_request', "badly formed claims: " + err.error_description);		
	}

	return claims;
}

function verify_max_age (max_age) {
	return max_age;
}

function verify_registration(registration) {
	if (registration !== undefined && !isSelfIssuedSts()){
		throw_claimer_error('invalid_request', 'registration parameter is only valid with a self-issued OP');
	}
}

function clientGetProperty(client_id, propertyName) {
	var client = clientInfo[client_id];
	if (client === undefined){
		throw "Invalid Client";
	}

	return client[propertyName];
}

function reportClientIdentityError(res, err, using_backchannel){
	pk.util.log_error("reportClientIdentityError", "CLIENT IDENTITY ERROR - TODO:  ADD Notification");

	if (!using_backchannel){
		res.render(viewPath + 'claimer_sts_client_identity_error', {
			error: err.error,
			error_description: err.error_description
		});			
	}
}

function create_claimer_error(error, error_description){
	const errorList = {
		'invalid_request': {
			'detail': 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.'
		},
		'unauthorized_client': {
			'detail': 'The client is not authorized to request an authorization code using this method.'
		},
		'access_denied': {
			'detail': 'The resource owner or authorization server denied the request.'
		},
		'unsupported_response_type': {
			'detail': 'The authorization server does not support obtaining an authorization code using this method.'
		},
		'invalid_scope': {
           'detail': 'The requested scope is invalid, unknown, or malformed.'
		},
		'server_error': {
           'detail': 'The authorization server encountered an unexpected condition that prevented it from fulfilling the request.'
		},
		'invalid_grant': {
           'detail': 'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'
		},
		'temporarily_unavailable': {
           'detail': 'The authorization server is currently unable to handle of the server.'
		},
		'interaction_required': {
			'detail':  'Interaction is required but '
		}
	};
	var claimer_error_obj = {
	};

	if (errorList[error] === undefined){
		claimer_error_obj.error = 'server_error';
		claimer_error_obj.error_description = error_description;
	}
	else {
		claimer_error_obj.error = error;
		claimer_error_obj.error_description = errorList[error].detail + ': ' + error_description;
	}

	return claimer_error_obj;		
}

function throw_claimer_error(error, error_description){
	throw create_claimer_error(error, error_description);
}
    
function redirectOAuthError(err, res, paramsObject){
	pk.util.log_error("redirectOAuthError", err, paramsObject);
	var qs = '#';
	if (paramsObject.isAuthorizationCodeFlow){
		if (paramsObject.response_mode === 'form_post'){
			res.render(viewPath + 'claimer_sts_error_response', {
				redirect_uri: paramsObject.redirect_uri,
				error: err.error,
				error_description: err.error_description,
				state: paramsObject.state
			});
			return;
		}
		qs = '?';
	}

	var errorInfo = {};
	for (var key in err){
		errorInfo[key] = err[key];
	}

	if (paramsObject.state){
		errorInfo['state'] = paramsObject.state;
	}

	qs = pk.util.createParameterString(errorInfo, true);

	/*
	qs += 'error=' + encodeURIComponent(err.error) + '&error_description=' + encodeURIComponent(err.error_description);
	if (paramsObject.state !== undefined){			
		qs += '&state=' + encodeURIComponent(paramsObject.state);
	}
	*/

	pk.util.log_debug("location will be set to: " + paramsObject.redirect_uri + qs);
	res.redirect(paramsObject.redirect_uri + qs);
	res.end();
}

function returnOAuthError(err, res){
	pk.util.log_detail('STS RETURNING OAuthError', err);
	res.statusCode = 400;
	res.set ({
		'Cache-Control': 'no-store',
		'Pragma': 'no-cache',
		'Content-Type': 'application/json',
		'Access-Control-Allow-Origin': '*'
	});

	var errorInfo = {
		'error': err.error,
		'error_description': err.error_description
	};

	res.json(errorInfo);        	
	res.end();
}

function addAuthorizationFromHeader(req, params){
	var authorization = req.get("Authorization");
	if (authorization !== undefined){
		var credsBase64 = authorization.replace("Basic", "").trim();
		pk.util.log_debug('Authorization via header:' + credsBase64);
		// must be true base64, not base64url
		var credsArray = new Buffer(credsBase64, 'base64').toString('utf8').split(':');
		pk.util.log_detail('credsArray', credsArray);
		params['client_id'] = decodeURIComponent(credsArray[0]);
		params['client_secret'] = decodeURIComponent(credsArray[1]);
		pk.util.log_detail('authParams', params);
	}

	return params;
}

function verifyContentModuleApiImplemented(contentModuleName, api){
	if (pk.feature_modules[contentModuleName] === undefined ||
		pk.feature_modules[contentModuleName].code === undefined){

		throw_claimer_error('system_configuration_error', 'Module ' + contentModuleName + ' does not define the mandatory generateUserinfo function needed by STS');
	}

	var retVal = false;
	if (pk.feature_modules[contentModuleName].code[api] !== undefined){
		retVal = true;
	}

	return retVal;
}

function getContentModuleApi(contentModuleName, api){
	if (pk.feature_modules[contentModuleName] === undefined || 
		pk.feature_modules[contentModuleName].code === undefined){

		throw_claimer_error('system_configuration_error', 'Module ' + contentModuleName + ' does not define the mandatory generateUserinfo function needed by STS');
	}

	if (pk.feature_modules[contentModuleName].code[api] !== undefined){
		return pk.feature_modules[contentModuleName].code[api];
	}

	switch(api){
		case 'invokeConsentUserAgent':
		case 'generateUserinfo':
		case 'invokeAuthUserAgent':
			throw_claimer_error('system_configuration_error', 'Module \'' + contentModuleName + '\' does not define the mandatory \'' + api + '\' function needed by STS');

		case 'get_acr':
			return defaultModule_get_acr;

		// optional apis
		case 'invokeConsentUserAgent':
		case 'processLoginHint':
			break;

		default:
			throw_claimer_error('system_configuration_error', 'Default api \'' + api + '\' function needed by STS is not implemented');
	}
}

async function getContentModuleApiAsync(contentModuleName, api){
	if (pk.feature_modules[contentModuleName] === undefined ||
		pk.feature_modules[contentModuleName].code === undefined){

		throw_claimer_error('system_configuration_error', 'Module ' + contentModuleName + ' does not define the mandatory generateUserinfo function needed by STS');
	}

	if (pk.feature_modules[contentModuleName].code[api] !== undefined){
		return await pk.feature_modules[contentModuleName].code[api];
	}

	switch(api){
		case 'invokeConsentUserAgent':
		case 'generateUserinfo':
		case 'invokeAuthUserAgent':
			throw_claimer_error('system_configuration_error', 'Module \'' + contentModuleName + '\' does not define the mandatory \'' + api + '\' function needed by STS');

		case 'get_acr':
			return defaultModule_get_acr;

		// optional apis
		case 'invokeConsentUserAgent':
		case 'processLoginHint':
			break;

		default:
			throw_claimer_error('system_configuration_error', 'Default api \'' + api + '\' function needed by STS is not implemented');
	}
}

function moduleCookieIdentifier(contentModuleName){
	return(sts_cookie_identifier + contentModuleName);
}

function verify_not_set(param){
	return ! isset(param);
}

function isset(param){
	if (typeof param !== 'object'){
		if (!param)
			return false;
	}
	return true;
}

function isSelfIssuedSts(){
	return pk.util.config.sts.wallet_sts === true;
}

function selfIssuedIssuerIdentifier(){
	return window.location.origin + pk.util.WALLET_ENDPOINT;
}

function addToObjectIfSet(obj, name, value){
	if (isset(value)){
		obj[name] = value;
	}
}

function getClaimsSupported(contentModuleName){
	var scope_claim_map = pk.feature_modules[contentModuleName].resources.scope_claim_map;
	var claimsSupportedArray = [];
	for(var scopeKey in scope_claim_map){
		var scope = scope_claim_map[scopeKey];
		for (var claimKey in scope.content){
			if (claimsSupportedArray.indexOf(claimKey) < 0){
				claimsSupportedArray.push(claimKey);
			}	
		}
	}

	return claimsSupportedArray;
}

function getScopesSupported(contentModuleName){
	var scope_claim_map = pk.feature_modules[contentModuleName].resources.scope_claim_map;
	var scopesSupportedArray = [ 'openid' ];
	for(var key in scope_claim_map){
		if (!scopesSupportedArray.includes(key)){
			scopesSupportedArray.push(key);
		}
	}

	return scopesSupportedArray;
}

function consentDelta(auth_state){
	if (auth_state.sts_state_bundle.recorded_consentDoc === undefined){
		return true;
	}

	var consentInfo = auth_state.content_module_state.consentInfo;
	var recorded_consentInfo = auth_state.sts_state_bundle.recorded_consentDoc.consentInfo;

	if (JSON.stringify(consentInfo) !== JSON.stringify(recorded_consentInfo)){
		return true;
	}
	return false;
}

function isScopeConsented(content_module_state){
	var consentInfo = content_module_state.consentInfo;
	if (consentInfo === undefined){
		return false;
	}

	var newScopeLength = content_module_state.scopeInfo.scopeArray.length;
	for (var count=0; count < newScopeLength; count++){
		if (consentInfo.scopeArray.indexOf(content_module_state.scopeInfo.scopeArray[count]) < 0){
			return false;
		}
	}

	for (var key in content_module_state.scopeInfo.claims.id_token){
		if (consentInfo.claims.id_token[key] === undefined){
			return false;
		}
	}

	for (var key in content_module_state.scopeInfo.claims.userinfo){
		if (consentInfo.claims.userinfo[key] === undefined){
			return false;
		}
	}

	return true;
}

function areClaimsContainedIn(newClaims, establishedClaims){
	return true;
}

/////////////////////////////////// DEFAULT MODULESAPIs ////////////////////////////////////////

function defaultModule_get_acr(acr){
	pk.util.log_debug('DEFAULT MODULE get_acr');
	var acrArray = acr.split(' ');
	return acrArray[0];
}

function flattenTemplates(result, template)
{
	var definition;
	for (var parameter in template){
		definition = template[parameter];
		if (definition.input === 'fieldset'){
			flattenTemplates(result, definition.content);
		}
		else{
			result[parameter] = definition;
		}
	}
}

async function processRegistrationRequest(req, res){
	var response = {};

	var contentModuleName = pk.util.content_module_name(req);
	var registrationConf = pk.util.config.content_modules[contentModuleName];
	if (!registrationConf.dynamicClientRegistrationSupported){
		response.error = 'access_denied';
		response.error_description = 'this service does not support dynamic client registration';
		sendRegistrationResponse(res, response);
		return;
	}

	var requireBasicInfo = true;
	if (registrationConf.requireBasicInfo === false){
		requireBasicInfo = false;
	}
	
	var json = req.body;

	if (json.redirect_uris === undefined || json.redirect_uris.length === undefined
			|| json.redirect_uris.length < 1){
		response.error = 'invalid_redirect_uri';
		sendRegistrationResponse(res, response);
		return;
	}

	if (json.client_name === undefined || json.client_name.length < 1){
		if (requireBasicInfo){
			response.error = 'invalid_client_metadata';
			response.error_description = 'client_name must be specified';
			sendRegistrationResponse(res, response);
			return;
		}

		var urlObj = new pk.util.url(json.redirect_uris[0]);
		json.client_name = urlObj.hostname;
	}

	var punctuationless = json.client_name.replace(/[.,\/#!$%\^&\*;:{}=\-_`~() @\t\r\n]/g,"");
	var id_prefix = punctuationless.replace(/\s{2,}/g," ") + '@';

	if (json.client_uri === undefined || json.client_uri.length < 1){
		if (requireBasicInfo ){
			response.error = 'invalid_client_metadata';httpsServerUrls['ISSUER_HOST']
			response.error_description = 'client_uri must be specified';
			sendRegistrationResponse(res, response);
			return;
		}
	}

	try {
		var id = await pk.simple_crypto.randomString();
		json.client_id = id_prefix + id.substring(0, 10);
		json.id = json.client_id;

		for(var ruri=0; ruri < json.redirect_uris.length; ruri++){
			if (json.redirect_uris[ruri].startsWith(pk.util.httpsServerUrls['ISSUER_HOST'].href)){
				var offset = json.redirect_uris[ruri].indexOf('/tester');
				if (offset >= 0){
					json.redirect_uris[ruri] = json.redirect_uris[ruri].substring(0, offset);
					json.redirect_uris[ruri] += '/tester/authresp/' + json.client_id;
				}
			}
		}

		var secret = await pk.simple_crypto.randomString();
		json.client_secret = secret.substring(0, 10);;
		await pk.dbs['sts'].provider.createOrUpdateDocument(pk.dbs['sts'], 
			'clients', json);

		pk.util.log_debug('Created new client registration record.')
		json.client_secret_expires_at = 0;		
		json.status = "OK";
		await sendRegistrationResponse(res, json);
	}
	catch(err){
		response.status = 'client_creation_error';
		response.result = err;
		pk.util.log_error('processRegistrationRequest', 'ERROR creating client registration record'); 
		await sendRegistrationResponse(res, response);
	}
}

function sendRegistrationResponse(res, response){
	res.set ({
		'Cache-Control': 'no-store',
		'Pragma': 'no-cache',
		'Access-Control-Allow-Origin': '*'
	});

	if (response.error !== undefined){
		return returnOAuthError(response, res);
	}

	pk.util.log_detail('STS SENDING Registration Response', response);
	res.json(response);
	res.end();	
}

function initiateAuthRequest(res, contentModuleName, url, oidcRequest, stateObj){
	pk.util.log_debug('--- STS INITIATE AUTH REQUEST ---');
	pk.util.log_debug('Module: \'' + contentModuleName + '\'\r\n');
	var	state = JSON.stringify(stateObj);
	pk.claimer_crypto.JWE.createEncrypt({ format: 'compact' }, 
			pk.key_management.contentModuleIntegrityKeys[contentModuleName]).update(state).final()
    .then(function(stateSecret) {
    	if (stateSecret){
	    	oidcRequest.state = stateSecret;
			var paramString = pk.util.createParameterString(oidcRequest)
			var loginRedirect = url + paramString;
			pk.util.log_debug("Redirecting to: ", loginRedirect);
			res.redirect(loginRedirect);
			res.end();
    	}
    }, function(err){
    	pk.util.log_error('initiateAuthRequest', err);
    });
}

async function validateAuthResponse(req, res, contentModuleName, contentObject){
	var error_description;
	try{
		var verifiedIdToken = await pk.token.processOPTokenResult(contentObject);
		if (verifiedIdToken === null || verifiedIdToken === undefined){
			throw(create_claimer_error('invalid_token', 'token payload is missing'));
		}

		error_description = 'Unable to load auth response decryption key';
		var stateEncObj = await pk.claimer_crypto.JWE.createDecrypt(pk.key_management.contentModuleIntegrityKeystores[contentModuleName]).decrypt(contentObject.state);

		error_description = 'Error parsing decrypted contentObject state';
		var decodedState = new Buffer(stateEncObj.plaintext).toString('utf8');
		stateObj = JSON.parse(decodedState);

		if (stateObj.params.nonce !== verifiedIdToken.nonce || 
				stateObj.verifier_redirect_uri !== verifiedIdToken.aud){
			error_description = undefined;
			throw(create_claimer_error('token_error', 'invalid nonce, or aud'));
		}

		return stateObj['userToken'] = verifiedIdToken;
	}
	catch (err){
		if (typeof err === "object" && error_description){
			var current = err.error_description;
			err.error_description = error_description + (current ?  current : '');
		}

		throw(err);
	}
}

function redirectImplicitAuthResponse(req, res, redirect_uri){
	// payload has been sent in the reg fashion
	res.set ({
		'Cache-Control': 'no-store',
		'Pragma': 'no-cache'
	});

    res.render(viewPath + 'claimer_sts_implicit_redirect', {
    	redirect_uri: redirect_uri
    });
}

function token_presentation_options(contentModuleName, params){
	const mod = 'token_presentation_options';
	if (verifyContentModuleApiImplemented(contentModuleName, mod)){
		return getContentModuleApi(contentModuleName, mod)(params);
	} 
}

function register_token_response_callback(name, parameters){
	token_response_callbacks[name] = parameters;
}

function processIssuerResources(req, res){
	var contentDataPath = process.cwd() + '\\' + req.params[0] + '\\data\\';
	if (pk.util.isUnixFilesystem()){
		contentDataPath = pk.util.forwardSlash(contentDataPath);
	}

	var resourceReq = req.query.resources;
	var resourceList = resourceReq.split(' ');
	var resources = {};

	for (var i=0; i<resourceList.length; i++){
		var requestedResource = resourceList[i];
		var resource;

		try{
		 	switch (requestedResource){
				case 'card_design':	
					var resource_path = contentDataPath + 'issuerCard_350.jpg';
					var resourceBuffer = pk.fs.readFileSync(resource_path);
					resource = 'data:image/jpeg;base64,' + resourceBuffer.toString('base64');
					break;

				case 'scope_claim_map':	
					resource = pk.fs.readFileSync(contentDataPath + 'scope_claim_map.json', {encoding: 'utf8'});
					break;

				case 'wallet_render':
					resource = pk.fs.readFileSync(contentDataPath + 'wallet_render.html', {encoding: 'utf8'});
					break;

				case 'display_render':
					resource = pk.fs.readFileSync(contentDataPath + 'display_render.js', {encoding: 'utf8'});
					break;

				case 'recommended_apps':
					resource = pk.fs.readFileSync(contentDataPath + 'recommended_apps.json', {encoding: 'utf8'});
					break;

				default:
					pk.util.log_error('getResource', 'No support for resource type: ' + requestedResource);
					resource = '';
			}
		}
		catch(err){
			resource = '';
		}

		resources[requestedResource] = resource;
	}
	
	res.json(resources);
}