2020-02-06 16:57:33 +01:00
'use strict' ;
exports = module . exports = {
2021-03-15 12:47:57 -07:00
add ,
get ,
update ,
del ,
2021-06-04 09:28:40 -07:00
delByAccessToken ,
delExpired ,
2021-09-17 14:32:13 -07:00
delByUserIdAndType ,
2021-06-04 09:28:40 -07:00
listByUserId ,
2021-03-15 12:47:57 -07:00
getByAccessToken ,
2020-02-06 16:57:33 +01:00
2022-09-23 12:57:13 +02:00
hasScope ,
2025-03-07 11:53:03 +01:00
isIpAllowedSync ,
2022-09-23 12:57:13 +02:00
SCOPES : [ '*' ] //, 'apps', 'domains'],
2020-02-06 16:57:33 +01:00
} ;
2025-03-07 11:53:03 +01:00
const TOKENS _FIELDS = [ 'id' , 'accessToken' , 'identifier' , 'clientId' , 'scopeJson' , 'expires' , 'name' , 'lastUsedTime' , 'allowedIpRanges' ] . join ( ',' ) ;
2021-06-04 09:28:40 -07:00
2025-08-14 11:17:38 +05:30
const assert = require ( 'node:assert' ) ,
2020-02-06 16:57:33 +01:00
BoxError = require ( './boxerror.js' ) ,
2025-08-14 11:17:38 +05:30
crypto = require ( 'node:crypto' ) ,
2021-06-04 09:28:40 -07:00
database = require ( './database.js' ) ,
2020-02-06 16:57:33 +01:00
hat = require ( './hat.js' ) ,
2025-05-06 16:16:33 +02:00
ipaddr = require ( './ipaddr.js' ) ,
2025-07-28 12:53:27 +02:00
safe = require ( 'safetydance' ) ;
2020-02-06 16:57:33 +01:00
2025-03-07 11:53:03 +01:00
const gParsedRangesCache = new Map ( ) ; // indexed by token.id
2022-09-22 21:58:56 +02:00
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
result . scope = safe . JSON . parse ( result . scopeJson ) || { } ;
2022-09-24 17:29:42 +02:00
delete result . scopeJson ;
2022-09-22 21:58:56 +02:00
return result ;
}
2020-02-06 16:57:33 +01:00
function validateTokenName ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
2022-02-07 13:19:59 -08:00
if ( name . length > 64 ) return new BoxError ( BoxError . BAD _FIELD , 'name too long' ) ;
2020-02-06 16:57:33 +01:00
return null ;
}
2022-09-22 21:58:56 +02:00
function validateScope ( scope ) {
assert . strictEqual ( typeof scope , 'object' ) ;
for ( const key in scope ) {
2022-09-24 17:29:42 +02:00
if ( exports . SCOPES . indexOf ( key ) === - 1 ) return new BoxError ( BoxError . BAD _FIELD , ` Unkown token scope ${ key } . Valid scopes are ${ exports . SCOPES . join ( ',' ) } ` ) ;
if ( scope [ key ] !== 'rw' && scope [ key ] !== 'r' ) return new BoxError ( BoxError . BAD _FIELD , ` Unkown token scope value ${ scope [ key ] } for ${ key } . Valid values are 'rw' or 'r' ` ) ;
2022-09-22 21:58:56 +02:00
}
return null ;
}
2022-09-23 12:57:13 +02:00
function hasScope ( token , method , path ) {
assert . strictEqual ( typeof token , 'object' ) ;
assert . strictEqual ( typeof method , 'string' ) ;
assert . strictEqual ( typeof path , 'string' ) ;
// TODO path checking in the future
const matchAll = token . scope [ '*' ] ;
if ( matchAll === 'rw' ) {
return true ;
} else if ( matchAll === 'r' && ( method === 'GET' || method === 'HEAD' || method === 'OPTIONS' ) ) {
return true ;
} else {
return false ;
}
}
2025-03-07 11:53:03 +01:00
function parseIpRanges ( ipRanges ) {
assert . strictEqual ( typeof ipRanges , 'string' ) ;
if ( ipRanges . length === 0 ) return [ '0.0.0.0/0' , '::/0' ] ;
const result = [ ] ;
for ( const line of ipRanges . split ( '\n' ) ) {
if ( ! line || line . startsWith ( '#' ) ) continue ;
// each line can have comma separated list. this complexity is because we changed the UI to take a line input instead of textarea
for ( const entry of line . split ( ',' ) ) {
const rangeOrIP = entry . trim ( ) ;
2025-09-08 18:59:47 +02:00
if ( ! ipaddr . isValid ( rangeOrIP ) && ! ipaddr . isValidCIDR ( rangeOrIP ) ) throw new BoxError ( BoxError . BAD _FIELD , ` ' ${ rangeOrIP } ' is not a valid IP or range ` ) ;
2025-03-07 11:53:03 +01:00
result . push ( rangeOrIP ) ;
}
}
return result ;
}
2021-06-04 09:28:40 -07:00
async function add ( token ) {
assert . strictEqual ( typeof token , 'object' ) ;
2025-03-07 11:53:03 +01:00
const { clientId , identifier , expires , allowedIpRanges } = token ;
2021-06-04 09:28:40 -07:00
const name = token . name || '' ;
2022-09-22 21:58:56 +02:00
const scope = token . scope || { '*' : 'rw' } ;
let error = validateTokenName ( name ) ;
if ( error ) throw error ;
error = validateScope ( scope ) ;
2021-06-04 09:28:40 -07:00
if ( error ) throw error ;
2025-03-07 11:53:03 +01:00
parseIpRanges ( allowedIpRanges ) ; // validate
2025-07-28 12:53:27 +02:00
const id = 'tid-' + crypto . randomUUID ( ) ;
2023-06-02 20:47:36 +02:00
const accessToken = token . accessToken || hat ( 8 * 32 ) ;
2021-06-04 09:28:40 -07:00
2025-03-07 11:53:03 +01:00
await database . query ( 'INSERT INTO tokens (id, accessToken, identifier, clientId, expires, scopeJson, name, allowedIpRanges) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' , [ id , accessToken , identifier , clientId , expires , JSON . stringify ( scope ) , name , allowedIpRanges ] ) ;
2021-06-04 09:28:40 -07:00
2025-03-07 11:53:03 +01:00
return { id , accessToken , scope , clientId , identifier , expires , name , allowedIpRanges } ;
2020-02-06 16:57:33 +01:00
}
2020-02-07 16:20:05 +01:00
2021-06-04 09:28:40 -07:00
async function get ( id ) {
2020-02-07 16:20:05 +01:00
assert . strictEqual ( typeof id , 'string' ) ;
2021-06-04 09:28:40 -07:00
const result = await database . query ( ` SELECT ${ TOKENS _FIELDS } FROM tokens WHERE id = ? ` , [ id ] ) ;
if ( result . length === 0 ) return null ;
2020-02-07 16:20:05 +01:00
2022-09-22 21:58:56 +02:00
return postProcess ( result [ 0 ] ) ;
2020-02-07 16:20:05 +01:00
}
2021-06-04 09:28:40 -07:00
async function del ( id ) {
2020-02-07 16:20:05 +01:00
assert . strictEqual ( typeof id , 'string' ) ;
2021-06-04 09:28:40 -07:00
const result = await database . query ( 'DELETE FROM tokens WHERE id = ?' , [ id ] ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Token not found' ) ;
2020-02-07 16:20:05 +01:00
}
2021-06-04 09:28:40 -07:00
async function listByUserId ( userId ) {
2020-02-07 16:20:05 +01:00
assert . strictEqual ( typeof userId , 'string' ) ;
2022-09-22 21:58:56 +02:00
const results = await database . query ( ` SELECT ${ TOKENS _FIELDS } FROM tokens WHERE identifier = ? ` , [ userId ] ) ;
results . forEach ( postProcess ) ;
return results ;
2021-06-04 09:28:40 -07:00
}
async function getByAccessToken ( accessToken ) {
assert . strictEqual ( typeof accessToken , 'string' ) ;
2020-02-07 16:20:05 +01:00
2025-03-07 11:53:03 +01:00
const result = await database . query ( ` SELECT ${ TOKENS _FIELDS } FROM tokens WHERE accessToken = ? AND expires > ? ` , [ accessToken , Date . now ( ) ] ) ;
2021-06-04 09:28:40 -07:00
if ( result . length === 0 ) return null ;
2022-09-22 21:58:56 +02:00
return postProcess ( result [ 0 ] ) ;
2020-02-07 16:20:05 +01:00
}
2021-03-15 12:47:57 -07:00
2021-06-04 09:28:40 -07:00
async function delByAccessToken ( accessToken ) {
assert . strictEqual ( typeof accessToken , 'string' ) ;
2021-03-15 12:47:57 -07:00
2021-06-04 09:28:40 -07:00
const result = await database . query ( 'DELETE FROM tokens WHERE accessToken = ?' , [ accessToken ] ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Token not found' ) ;
}
2021-03-15 12:47:57 -07:00
2021-06-04 09:28:40 -07:00
async function delExpired ( ) {
const result = await database . query ( 'DELETE FROM tokens WHERE expires <= ?' , [ Date . now ( ) ] ) ;
return result . affectedRows ;
2021-03-15 12:47:57 -07:00
}
2021-09-17 14:32:13 -07:00
async function delByUserIdAndType ( userId , type ) {
assert . strictEqual ( typeof userId , 'string' ) ;
assert . strictEqual ( typeof type , 'string' ) ;
const result = await database . query ( 'DELETE FROM tokens WHERE identifier=? AND clientId=?' , [ userId , type ] ) ;
return result . affectedRows ;
}
2021-06-04 09:28:40 -07:00
async function update ( id , values ) {
2021-03-15 12:47:57 -07:00
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof values , 'object' ) ;
2025-02-02 11:45:48 +01:00
const args = [ ] ;
const fields = [ ] ;
for ( const k in values ) {
2021-06-04 09:28:40 -07:00
fields . push ( k + ' = ?' ) ;
args . push ( values [ k ] ) ;
}
args . push ( id ) ;
2025-03-07 11:53:03 +01:00
const result = await database . query ( ` UPDATE tokens SET ${ fields . join ( ', ' ) } WHERE id = ? ` , args ) ;
2021-06-04 09:28:40 -07:00
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Token not found' ) ;
}
2025-03-07 11:53:03 +01:00
function isIpAllowedSync ( token , ip ) {
assert . strictEqual ( typeof token , 'object' ) ;
assert . strictEqual ( typeof ip , 'string' ) ;
let allowedIpRanges = gParsedRangesCache . get ( token . id ) ; // returns undefined if not found
if ( ! allowedIpRanges ) {
allowedIpRanges = parseIpRanges ( token . allowedIpRanges || '' ) ;
gParsedRangesCache . set ( token . id , allowedIpRanges ) ;
}
for ( const ipOrRange of allowedIpRanges ) {
if ( ! ipOrRange . includes ( '/' ) ) {
2025-05-06 16:16:33 +02:00
if ( ipaddr . isEqual ( ipOrRange , ip ) ) return true ;
2025-03-07 11:53:03 +01:00
} else {
2025-05-06 16:16:33 +02:00
if ( ipaddr . includes ( ipOrRange , ip ) ) return true ;
2025-03-07 11:53:03 +01:00
}
}
return false ;
}