Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,14 @@ router.get('/verify', hasQueryParams('user', 'otp'), async (req, res, next) => {

// GET /confirm
router.get('/confirm', confirmed({ allowLight: true }), (req, res) => {
const { ttl, userInfo: { email: user, light, api_access, prefix, product } } = req
const { ttl, userInfo: { email: user, light, api_access, prefix, product, access_expired_at } } = req
return res.json({
message: `Token confirmed for user ${user}`,
user,
light,
product,
ttl,
access_expired_at,
access: {
...api_access,
prefix,
Expand Down
45 changes: 35 additions & 10 deletions modules/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const isPrivilegedUser = (email, prefix, api_access) => {

const getUserInfo = async ({ email, product = PRODUCT_ATOM }) => {
// returns user info
const selects = ['prefix', 'jwt_uuid', 'client', 'access', 'info', PRODUCT_ATOM, PRODUCT_LOCUS]
const selects = ['prefix', 'jwt_uuid', 'client', 'access', 'info', PRODUCT_ATOM, PRODUCT_LOCUS, 'access_expired_at']
const conditions = ["active = B'1'"]
const user = await selectUser({ email, selects, conditions })

Expand All @@ -56,6 +56,20 @@ const getUserInfo = async ({ email, product = PRODUCT_ATOM }) => {
statusCode: 404,
})
}
const now = new Date()
if (user.access_expired_at && new Date(user.access_expired_at) < now) {
await updateUser({
email,
prefix: 'customers',
jwt_uuid: null,
client: null,
atom: { read: 10, write: 0 },
active: 0,
access: null,
access_expired_at: null,
})
throw new AuthorizationError(`Access for ${email} has expired. Please contact the administrator to reactivate this account.`)
}

// product access (read/write) falls back to 'atom' access if empty object
const productAccess = Object.keys(user[product] || {}).length ? user[product] : user[PRODUCT_ATOM]
Expand Down Expand Up @@ -83,12 +97,13 @@ const getUserInfo = async ({ email, product = PRODUCT_ATOM }) => {
// v2 could override per-WL policies (e.g.: { wl { cu: -1|[...], policies: [...] } } })
// v3 could override per-CU policies (e.g.: { wl: { cu: { policies: [...] } } })
},
access_expired_at: user.access_expired_at,
}
}

// Trade OTP for user access
const redeemAccess = async ({ email, otp, reset_uuid = false, product = PRODUCT_ATOM }) => {
let { prefix, api_access = {}, jwt_uuid } = await getUserInfo({ email, product })
let { prefix, api_access = {}, jwt_uuid, access_expired_at } = await getUserInfo({ email, product })

if (prefix === PREFIX_APP_REVIEWER) {
if (otp !== APP_REVIEWER_OTP) {
Expand All @@ -104,7 +119,7 @@ const redeemAccess = async ({ email, otp, reset_uuid = false, product = PRODUCT_
await updateUser({ email, jwt_uuid })
}

return { api_access, jwt_uuid, prefix }
return { api_access, jwt_uuid, prefix, access_expired_at }
}

const _resetUUID = async ({ email }) => {
Expand Down Expand Up @@ -177,7 +192,17 @@ const loginUser = async ({ user, redirect, zone='utc', product = PRODUCT_ATOM, n
})
}

const computeExpiry = (timeout, isPrivilegedUser) => {
const computeExpiry = (timeout, isPrivilegedUser, access_expired_at) => {
const now = Date.now()
if (access_expired_at) {
const expiryTime = new Date(access_expired_at).getTime()
const timeRemaining = Math.floor((expiryTime - now) / 1000)
if (timeRemaining <= 0) {
throw new AuthorizationError('Access has expired')
}
return Math.min(JWT_TTL, timeRemaining)
}

let expiry = timeout

// default timeout
Expand All @@ -199,9 +224,9 @@ const computeExpiry = (timeout, isPrivilegedUser) => {
return expiry
}

const signJWT = ({ email, api_access = {}, jwt_uuid, prefix, product }, { timeout, secret = JWT_SECRET, future_access } = {}) => {
const signJWT = ({ email, api_access = {}, jwt_uuid, prefix, product, access_expired_at = null }, { timeout, secret = JWT_SECRET, future_access } = {}) => {
// timeout in seconds
const expiresIn = computeExpiry(timeout, isPrivilegedUser(email, prefix, api_access))
const expiresIn = computeExpiry(timeout, isPrivilegedUser(email, prefix, api_access), access_expired_at)

// TODO: remove `product` from JWT when v1 `access` is stable/universal
const toSign = { email, api_access, jwt_uuid, prefix, product }
Expand All @@ -214,15 +239,15 @@ const signJWT = ({ email, api_access = {}, jwt_uuid, prefix, product }, { timeou

// verify user OTP and sign JWT on success
const verifyOTP = async ({ email, otp, reset_uuid = false, product = PRODUCT_ATOM, timeout, future_access }) => {
const { api_access, jwt_uuid, prefix } = await redeemAccess({
const { api_access, jwt_uuid, prefix, access_expired_at } = await redeemAccess({
email,
otp,
reset_uuid,
product,
})

return {
token: signJWT({ email, api_access, jwt_uuid, prefix, product }, { timeout, future_access }),
token: signJWT({ email, api_access, jwt_uuid, prefix, product, access_expired_at }, { timeout, future_access }),
api_access,
prefix,
product,
Expand Down Expand Up @@ -295,11 +320,11 @@ const getUserAccess = async ({ token, light, reset_uuid, targetProduct, forceLig

}
// confirm against DB user data and return the DB version (for v1+ `access` system)
user = await confirmUser({
const userDB = await confirmUser({
...user,
reset_uuid: ['1', 'true'].includes((reset_uuid || '').toLowerCase()),
})
return { ...user, light: false }
return { ...user, ...userDB, light: false }
}

module.exports = {
Expand Down
3 changes: 2 additions & 1 deletion modules/manage.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const _prepareConditions = ({ prefix, api_access, product = PRODUCT_ATOM, active
return conditions
}

const BASE_SELECTS = ['email', 'prefix', 'client', 'info', 'access', 'active']
const BASE_SELECTS = ['email', 'prefix', 'client', 'info', 'access', 'active', 'access_expired_at']
// list users that the given user (email) has access to
const getUsers = ({ prefix, api_access, product = PRODUCT_ATOM, active, deleted }) => {
if (api_access.version) {
Expand Down Expand Up @@ -135,6 +135,7 @@ const removeUser = ({ userInfo, prefix, api_access }) => {
atom: { read: 10, write: 0 },
active: 0,
access: null,
access_expired_at: null,
})
}

Expand Down
2 changes: 2 additions & 0 deletions sql/equsers.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ CREATE TABLE IF NOT EXISTS public.equsers (
info JSONB DEFAULT '{}'::JSONB,
otp JSONB DEFAULT '{}'::JSONB,
active BIT(1) DEFAULT B'1'::BIT(1),
access JSONB,
access_expired_at TIMESTAMPTZ,
PRIMARY KEY(email)
);

Expand Down