From 889d2c909a82803869eefd7f71194b76bb43f871 Mon Sep 17 00:00:00 2001 From: dshuffma-ibm Date: Tue, 20 Feb 2024 14:01:15 -0500 Subject: [PATCH] fix access token generation if using api key (#633) * fix access token generation if using api key --------- Signed-off-by: David Huffman --- dictionary_blockchain.txt | 1 + packages/athena/docs/permission_apis.md | 3 - .../json_validation/ibp_openapi_v3.yaml | 162 ++++++++++++++++++ packages/athena/libs/permissions_lib.js | 111 ++++-------- packages/athena/routes/permission_apis.js | 33 ++-- 5 files changed, 220 insertions(+), 90 deletions(-) diff --git a/dictionary_blockchain.txt b/dictionary_blockchain.txt index 81d61584..48a3eaa0 100644 --- a/dictionary_blockchain.txt +++ b/dictionary_blockchain.txt @@ -356,3 +356,4 @@ openidconnect soidc channelless hlfoc +owasp diff --git a/packages/athena/docs/permission_apis.md b/packages/athena/docs/permission_apis.md index 268b3187..3815cc46 100644 --- a/packages/athena/docs/permission_apis.md +++ b/packages/athena/docs/permission_apis.md @@ -82,9 +82,6 @@ Note that the `access_token` will expire in 1 hour with the default settings. // [option 1] - JSON body - (see option 2 below...) // ------------------------------------------- { - // [required] "apikey" - set your username + colon + password - "apikey": "username:password", - // [optional] // defaults to 'urn:ibm:params:oauth:grant-type:apikey' // dsh this is currently unused but included to match the standard IAM signature diff --git a/packages/athena/json_docs/json_validation/ibp_openapi_v3.yaml b/packages/athena/json_docs/json_validation/ibp_openapi_v3.yaml index 30f13b02..69179dde 100644 --- a/packages/athena/json_docs/json_validation/ibp_openapi_v3.yaml +++ b/packages/athena/json_docs/json_validation/ibp_openapi_v3.yaml @@ -5275,6 +5275,104 @@ paths: application/json: schema: $ref: '#/components/schemas/Forbidden' + /ak/api/v3/identity/token: + post: + tags: + - Administer the console + summary: Create bearer token + description: Exchange api key for a bearer/access token. Bearer tokens have expirations and are more secure than api keys. + operationId: create_token + requestBody: + description: The settings to use to create the bearer/access token. + content: + application/json: + schema: + $ref: "#/components/schemas/CreateAccessBody" + required: true + responses: + 200: + description: Bearer/access token created successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/CreateAccessResponse" + 401: + description: Request is unauthorized (invalid credentials). + content: + application/json: + schema: + $ref: "#/components/schemas/Unauthorized" + 403: + description: Request is bad (invalid syntax). + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequest" + /ak/api/v3/identity/token/{id}: + get: + tags: + - Administer the console + summary: Get bearer token details + description: See metadata about a bearer token. + operationId: get_token + parameters: + - name: id + in: path + description: The bearer token to investigate. + required: true + schema: + type: string + responses: + 200: + description: Bearer/access token details were retreived successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/GetAccessResponse" + 401: + description: Request is unauthorized (invalid credentials). + content: + application/json: + schema: + $ref: "#/components/schemas/Unauthorized" + 403: + description: Request is bad (invalid syntax). + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequest" + delete: + tags: + - Administer the console + summary: Delete bearer token + description: Delete/invalidate a bearer token before its expiration. + operationId: delete_token + parameters: + - name: id + in: path + description: The bearer token to invalidate. + required: true + schema: + type: string + responses: + 200: + description: Bearer/access token deleted successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/DeleteAccessResponse" + 401: + description: Request is unauthorized (invalid credentials). + content: + application/json: + schema: + $ref: "#/components/schemas/Unauthorized" + 403: + description: Request is bad (invalid syntax). + content: + application/json: + schema: + $ref: "#/components/schemas/BadRequest" # # # @@ -11008,6 +11106,70 @@ components: example: roles: $ref: '#/components/schemas/api_roles' + CreateAccessBody: + type: object + properties: + roles: + $ref: '#/components/schemas/api_roles' + expiration_secs: + type: number + example: 3600 + description: After these many seconds the token will not longer be valid. + minimum: 10 + maximum: 1296000 # spam prevention limiter + CreateAccessResponse: + type: object + properties: + access_token: + type: string + example: + refresh_token: + type: string + example: + token_type: + type: string + example: Bearer + expires_in: + type: number + example: 3600 + expiration: + type: string + example: 1708445284703 + scope: + type: string + example: foc bearer + roles: + $ref: '#/components/schemas/api_roles' + message: + $ref: '#/components/schemas/message_ok' + GetAccessResponse: + type: object + properties: + access_token: + type: string + example: + roles: + $ref: '#/components/schemas/api_roles' + creation: + type: string + example: 1708442928513 + expiration: + type: string + example: 1708446528513 + time_left: + type: string + example: 57 mins + created_by: + type: string + example: + DeleteAccessResponse: + type: object + properties: + message: + $ref: '#/components/schemas/message_ok' + delete: + type: string + example: GetApiKeyResponse: type: object properties: diff --git a/packages/athena/libs/permissions_lib.js b/packages/athena/libs/permissions_lib.js index 8cbf52bd..acd45424 100644 --- a/packages/athena/libs/permissions_lib.js +++ b/packages/athena/libs/permissions_lib.js @@ -230,8 +230,7 @@ module.exports = function (logger, ev, t) { if (!email) { input_errors.push('uuid does not exist: ' + encodeURI(uuid)); } else { - if (settings_doc.access_list[email].roles.includes('manager') && !lc_roles.includes('manager')) - { + if (settings_doc.access_list[email].roles.includes('manager') && !lc_roles.includes('manager')) { let admin_count = 0; for (let id in settings_doc.access_list) { let user = settings_doc.access_list[id]; @@ -239,8 +238,7 @@ module.exports = function (logger, ev, t) { admin_count = admin_count + 1; } } - if (admin_count < 2) - { + if (admin_count < 2) { input_errors.push('[only manager] as you are the only manager for this console, you are not allowed to modify your roles'); } else { @@ -253,8 +251,7 @@ module.exports = function (logger, ev, t) { t.notifications.procrastinate(req, notice); } } - else - { + else { settings_doc.access_list[email].roles = lc_roles; // edit the user object // build a notification doc @@ -779,88 +776,56 @@ module.exports = function (logger, ev, t) { }; //-------------------------------------------------- - // Store a new access token + // Store a new access token (exchange apikey for bearer token) //-------------------------------------------------- exports.create_access_token = (req, cb) => { - let input_errors = []; let roles = (req && req.body && req.body.roles) ? req.body.roles : null; let expiration_secs = (req && req.body && !isNaN(req.body.expiration_secs)) ? req.body.expiration_secs : 3600; const parsed_auth = t.auth_header_lib.parse_auth(req); const lc_username = (parsed_auth && parsed_auth.name) ? parsed_auth.name.toLowerCase() : null; - const MAX_EXPIRATION_SECS = 60 * 60 * 24 * 15; - // init roles from user + // init roles as manager, else use the ones provided if (!Array.isArray(roles) || roles.length === 0) { - roles = ev.ACCESS_LIST[lc_username].roles; - } - - const lc_roles = validate_roles(roles); // check the input roles - if (lc_roles === false) { - input_errors.push('invalid roles for api key. valid roles:' + JSON.stringify(Object.keys(ev.ROLES_TO_ACTIONS))); - } else if (lc_roles.length === 0) { - input_errors.push('must have at least 1 role for key.'); + roles = [ev.STR.MANAGER_ROLE, ev.STR.WRITER_ROLE, ev.STR.READER_ROLE]; } - // check if user has these roles (this is overly protective, to even create a token the user needs "manager" which is the highest) - if (input_errors.length === 0) { - for (let i in roles) { - const role = roles[i]; - if (!ev.ACCESS_LIST[lc_username].roles.includes(role)) { - logger.warn('[permissions] user doe not have role. roles:', ev.ACCESS_LIST[lc_username].roles); - input_errors.push('invalid roles, cannot set a role that the creating user does not have.'); - break; - } - } - } + const access_token_doc = exports.generate_access_token(lc_username, roles, expiration_secs); - // check expiration - if (expiration_secs < 0 || expiration_secs > MAX_EXPIRATION_SECS) { - input_errors.push('invalid expiration. must be between 0 and ' + MAX_EXPIRATION_SECS + ' seconds.'); - } - - if (input_errors.length >= 1) { - logger.error('[permissions] cannot create access token. bad input:', input_errors); - cb({ statusCode: 400, msg: input_errors, }, null); - } else { - - const access_token_doc = exports.generate_access_token(lc_username, roles, expiration_secs); - - // build a notification doc - const notice = { - message: 'creating access token ' + access_token_doc._id.substring(0, 4) + '***', - }; - t.notifications.procrastinate(req, notice); - - // write the access token doc - const wr_opts = { - db_name: ev.DB_SYSTEM, - doc: access_token_doc - }; - t.otcc.createNewDoc(wr_opts, access_token_doc, (err_writeDoc, resp_writeDoc) => { - if (err_writeDoc) { - logger.error('[permissions] unable to write new access token doc', err_writeDoc); - return cb(err_writeDoc); - } else { - logger.info('[permissions] creating the access token doc - success'); - const ret = { - access_token: resp_writeDoc._id, // the id is the token! - refresh_token: 'not_supported', - token_type: 'Bearer', - expires_in: resp_writeDoc.expires_in, - expiration: resp_writeDoc.expiration, - scope: 'ibp bearer', + // build a notification doc + const notice = { + message: 'creating access token ' + access_token_doc._id.substring(0, 4) + '***', + }; + t.notifications.procrastinate(req, notice); - roles: resp_writeDoc.roles, - message: 'ok' - }; - return cb(null, ret); - } - }); - } + // write the access token doc + const wr_opts = { + db_name: ev.DB_SYSTEM, + doc: access_token_doc + }; + t.otcc.createNewDoc(wr_opts, access_token_doc, (err_writeDoc, resp_writeDoc) => { + if (err_writeDoc) { + logger.error('[permissions] unable to write new access token doc', err_writeDoc); + return cb(err_writeDoc); + } else { + logger.info('[permissions] creating the access token doc - success'); + const ret = { + access_token: resp_writeDoc._id, // the id is the token! + refresh_token: 'not_supported', + token_type: 'Bearer', + expires_in: resp_writeDoc.expires_in, + expiration: resp_writeDoc.expiration, + scope: 'foc bearer', + + roles: resp_writeDoc.roles, + message: 'ok' + }; + return cb(null, ret); + } + }); }; //-------------------------------------------------- - // Generate access token for exchange + // Generate access token for exchange - [access tokens expire] //-------------------------------------------------- exports.generate_access_token = (lc_name, roles, expires_in) => { const token = t.misc.generateRandomString(32); // this is the token, its a secret diff --git a/packages/athena/routes/permission_apis.js b/packages/athena/routes/permission_apis.js index 005caa00..3527e4b6 100644 --- a/packages/athena/routes/permission_apis.js +++ b/packages/athena/routes/permission_apis.js @@ -265,24 +265,29 @@ module.exports = function (logger, ev, t) { // Store/create a access token in the database (aka bearer token) //-------------------------------------------------- app.post('/api/v3/identity/token', t.middleware.verify_apiKey_action_session, (req, res) => { - t.permissions_lib.create_access_token(req, (err, ret) => { - if (err) { - return res.status(t.ot_misc.get_code(err)).json(err); - } else { - return res.status(200).json(ret); - } - }); + exchange_for_token(req, res); }); app.post('/ak/api/v3/identity/token', t.middleware.verify_apiKey_action_ak, (req, res) => { - t.permissions_lib.create_access_token(req, (err, ret) => { - if (err) { - return res.status(t.ot_misc.get_code(err)).json(err); - } else { - return res.status(200).json(ret); - } - }); + exchange_for_token(req, res); }); + function exchange_for_token(req, res) { + if (t.ot_misc.is_v2plus_route(req)) { + req._validate_path = '/ak/api/' + t.validate.pick_ver(req) + '/identity/token'; + logger.debug('[pre-flight] setting validate route:', req._validate_path); + } + + t.validate.request(req, res, null, () => { + t.permissions_lib.create_access_token(req, (err, ret) => { + if (err) { + return res.status(t.ot_misc.get_code(err)).json(err); + } else { + return res.status(200).json(ret); + } + }); + }); + } + //-------------------------------------------------- // Delete a access token from the database (aka bearer token) //--------------------------------------------------