Skip to content

Commit

Permalink
fix access token generation if using api key (#633)
Browse files Browse the repository at this point in the history
* fix access token generation if using api key
---------
Signed-off-by: David Huffman <[email protected]>
  • Loading branch information
dshuffma-ibm authored Feb 20, 2024
1 parent 3cd0ddf commit 889d2c9
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 90 deletions.
1 change: 1 addition & 0 deletions dictionary_blockchain.txt
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,4 @@ openidconnect
soidc
channelless
hlfoc
owasp
3 changes: 0 additions & 3 deletions packages/athena/docs/permission_apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
162 changes: 162 additions & 0 deletions packages/athena/json_docs/json_validation/ibp_openapi_v3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
#
#
#
Expand Down Expand Up @@ -11008,6 +11106,70 @@ components:
example: <api_secret here>
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: <bearer token here>
refresh_token:
type: string
example: <refresh token here>
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: <bearer token here>
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: <username here>
DeleteAccessResponse:
type: object
properties:
message:
$ref: '#/components/schemas/message_ok'
delete:
type: string
example: <bearer token here>
GetApiKeyResponse:
type: object
properties:
Expand Down
111 changes: 38 additions & 73 deletions packages/athena/libs/permissions_lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,15 @@ 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];
if (user && user.roles && user.roles.includes('manager')) {
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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
33 changes: 19 additions & 14 deletions packages/athena/routes/permission_apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
//--------------------------------------------------
Expand Down

0 comments on commit 889d2c9

Please sign in to comment.