diff --git a/node-packages/commons/src/api.ts b/node-packages/commons/src/api.ts index f88ec4517e..a85c462891 100644 --- a/node-packages/commons/src/api.ts +++ b/node-packages/commons/src/api.ts @@ -113,24 +113,7 @@ let transportOptions: { timeout: 60000 }; -if (!envHasConfig('JWTSECRET') || !envHasConfig('JWTAUDIENCE')) { - logger.error( - 'Unable to create api token due to missing `JWTSECRET`/`JWTAUDIENCE` environment variables' - ); -} else { - const apiAdminToken = createJWTWithoutUserId({ - payload: { - role: 'admin', - iss: 'lagoon-commons', - aud: getConfigFromEnv('JWTAUDIENCE') - }, - jwtSecret: getConfigFromEnv('JWTSECRET') - }); - - transportOptions.headers.Authorization = `Bearer ${apiAdminToken}`; -} - -const transport = new Transport(`${getConfigFromEnv('API_HOST', 'http://api:3000')}/graphql`, transportOptions); +const transport = new Transport(`${getConfigFromEnv('API_HOST', 'http://api:3000')}/graphql`, {transportOptions}); export const graphqlapi = new Lokka({ transport }); diff --git a/node-packages/commons/src/lokka-transport-http-retry.ts b/node-packages/commons/src/lokka-transport-http-retry.ts index 210377486e..9315406a77 100644 --- a/node-packages/commons/src/lokka-transport-http-retry.ts +++ b/node-packages/commons/src/lokka-transport-http-retry.ts @@ -1,13 +1,41 @@ import { Transport as LokkaTransportHttp } from '@lagoon/lokka-transport-http'; import fetchUrl from 'node-fetch'; +import { createJWTWithoutUserId } from './jwt'; +import { logger } from './logs/local-logger'; +import { envHasConfig, getConfigFromEnv } from './util/config'; + +// generate a fresh token for each operation +const generateToken = () => { + if (!envHasConfig('JWTSECRET') || !envHasConfig('JWTAUDIENCE')) { + logger.error( + 'Unable to create api token due to missing `JWTSECRET`/`JWTAUDIENCE` environment variables' + ); + } else { + const apiAdminToken = createJWTWithoutUserId({ + payload: { + role: 'admin', + iss: 'lagoon-internal', + aud: getConfigFromEnv('JWTAUDIENCE'), + // set a 60s expiry on the token + exp: Math.floor(Date.now() / 1000) + 60 + }, + jwtSecret: getConfigFromEnv('JWTSECRET') + }); + + return `Bearer ${apiAdminToken}`; + } + return "" +} class NetworkError extends Error {} class ApiError extends Error {} // Retries the fetch if operational/network errors occur const retryFetch = (endpoint, options, retriesLeft = 5, interval = 1000) => - new Promise((resolve, reject) => - fetchUrl(endpoint, options) + new Promise((resolve, reject) => { + // get a fresh token for every request + options.headers.Authorization = generateToken() + return fetchUrl(endpoint, options) .then(response => { if (response.status !== 200 && response.status !== 400) { throw new NetworkError(`Invalid status code: ${response.status}`); @@ -38,6 +66,7 @@ const retryFetch = (endpoint, options, retriesLeft = 5, interval = 1000) => retryFetch(endpoint, options, retriesLeft - 1).then(resolve, reject); }, interval); }) + } ); export class Transport extends LokkaTransportHttp { diff --git a/services/api/src/util/auth.ts b/services/api/src/util/auth.ts index 656d7d3ed2..666c84e119 100644 --- a/services/api/src/util/auth.ts +++ b/services/api/src/util/auth.ts @@ -13,6 +13,7 @@ import { saveRedisKeycloakCache } from '../clients/redisClient'; interface ILegacyToken { iat: string; + exp: string; iss: string; sub: string; aud: string; @@ -105,7 +106,26 @@ export const getCredentialsForLegacyToken = async token => { throw new Error('Decoding token resulted in "null" or "undefined".'); } - const { role = 'none', aud, sub, iss, iat } = decoded; + const { role = 'none', aud, sub, iss, iat, exp } = decoded; + + // check the expiration on legacy tokens, reject them if necessary + const maxExpiry = getConfigFromEnv('LEGACY_EXPIRY_MAX', '3600') // 1hour default + const rejectLegacyExpiry = getConfigFromEnv('LEGACY_EXPIRY_REJECT', 'false') // don't reject intially, just log + if (exp) { + if ((parseInt(exp)-parseInt(iat)) > parseInt(maxExpiry)) { + const msg = `Legacy token (sub:${sub}; iss:${iss}) expiry ${(parseInt(exp)-parseInt(iat))} is greater than ${parseInt(maxExpiry)}` + logger.warn(msg); + if (rejectLegacyExpiry == "true") { + throw new Error(msg); + } + } + } else { + const msg = `Legacy token (sub:${sub}; iss:${iss}) has no expiry` + logger.warn(msg); + if (rejectLegacyExpiry == "true") { + throw new Error(msg); + } + } if (aud !== getConfigFromEnv('JWTAUDIENCE')) { throw new Error('Token audience mismatch.');