oidc-lib
Version:
A library for creating OIDC Service Providers
1,463 lines (1,232 loc) • 72.7 kB
JavaScript
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