Files
speckle-server/packages/server/modules/auth/services/apps.js
T

288 lines
9.1 KiB
JavaScript

'use strict'
const bcrypt = require( 'bcrypt' )
const crs = require( 'crypto-random-string' )
const appRoot = require( 'app-root-path' )
const knex = require( `${appRoot}/db/knex` )
const { createToken, createBareToken, revokeTokenById } = require( `${appRoot}/modules/core/services/tokens` )
const Users = ( ) => knex( 'users' )
const ApiTokens = ( ) => knex( 'api_tokens' )
const ServerApps = ( ) => knex( 'server_apps' )
const ServerAppsScopes = ( ) => knex( 'server_apps_scopes' )
const ServerAppsTokens = ( ) => knex( 'user_server_app_tokens' )
const Scopes = ( ) => knex( 'scopes' )
const AuthorizationCodes = ( ) => knex( 'authorization_codes' )
const RefreshTokens = ( ) => knex( 'refresh_tokens' )
module.exports = {
async getApp( { id } ) {
let allScopes = await Scopes( ).select( '*' )
let app = await ServerApps( ).select( '*' ).where( { id: id } ).first( )
if ( !app ) return null
let appScopeNames = ( await ServerAppsScopes( ).select( 'scopeName' ).where( { appId: id } ) ).map( s => s.scopeName )
app.scopes = allScopes.filter( scope => appScopeNames.indexOf( scope.name ) !== -1 )
app.author = await Users( ).select( 'id', 'name', 'avatar' ).where( { id: app.authorId } ).first( )
return app
},
async getAllPublicApps( ) {
let apps = await ServerApps( )
.select( 'server_apps.id', 'server_apps.name', 'server_apps.description', 'server_apps.logo', 'server_apps.termsAndConditionsLink', 'users.name as authorName', 'users.id as authorId' )
.where( { public: true } )
.leftJoin( 'users', 'users.id', '=', 'server_apps.authorId' )
.orderBy( 'server_apps.trustByDefault', 'DESC' )
apps.forEach( app => {
if ( app.authorName ) {
app.author = { name: app.authorName, id: app.authorId }
}
delete app.authorName
delete app.authorId
} )
return apps
},
async getAllAppsCreatedByUser( { userId } ) {
let apps = await ServerApps( )
.select( 'server_apps.id', 'server_apps.secret', 'server_apps.name', 'server_apps.description', 'server_apps.redirectUrl', 'server_apps.logo', 'server_apps.termsAndConditionsLink', 'users.name as authorName', 'users.id as authorId' )
.where( { authorId: userId } )
.leftJoin( 'users', 'users.id', '=', 'server_apps.authorId' )
apps.forEach( app => {
if ( app.authorName ) {
app.author = { name: app.authorName, id: app.authorId }
}
delete app.authorName
delete app.authorId
} )
return apps
},
async getAllAppsAuthorizedByUser( { userId } ) {
let query = knex.raw( `
SELECT DISTINCT ON (a."appId") a."appId" as id, sa."name", sa."description", sa."trustByDefault", sa."redirectUrl" as "redirectUrl", sa.logo, sa."termsAndConditionsLink", json_build_object('name', u.name, 'id', sa."authorId") as author
FROM user_server_app_tokens a
LEFT JOIN server_apps sa ON sa.id = a."appId"
LEFT JOIN users u ON sa."authorId" = u.id
WHERE a."userId" = ?
`, [ userId ] )
let { rows } = await query
return rows
},
async createApp( app ) {
app.id = crs( { length: 10 } )
app.secret = crs( { length: 10 } )
if ( !app.scopes ) {
throw new Error( 'Cannot create an app with no scopes.' )
}
let scopes = [ ...app.scopes ]
delete app.scopes
delete app.firstparty
delete app.trustByDefault
await ServerApps( ).insert( app )
await ServerAppsScopes( ).insert( scopes.map( s => ( { appId: app.id, scopeName: s } ) ) )
return { id: app.id, secret: app.secret }
},
async updateApp( { app } ) {
// any app update should nuke everything and force users to re-authorize it.
await module.exports.revokeExistingAppCredentials( { appId: app.id } )
if ( app.scopes ) {
// console.log( app.scopes, app.id )
// Flush existing app scopes
await ServerAppsScopes( ).where( { appId: app.id } ).del( )
// Update new scopes
await ServerAppsScopes( ).insert( app.scopes.map( s => ( { appId: app.id, scopeName: s } ) ) )
}
delete app.secret
delete app.scopes
let [ res ] = await ServerApps( ).returning( 'id' ).where( { id: app.id } ).update( app )
return res
},
async deleteApp( { id } ) {
await module.exports.revokeExistingAppCredentials( { appId: id } )
return await ServerApps( ).where( { id: id } ).del( )
},
async revokeRefreshToken( { tokenId } ) {
tokenId = tokenId.slice( 0, 10 )
let delCount = await RefreshTokens( ).where( { id: tokenId } ).del( )
if ( delCount === 0 )
throw new Error( 'Did not revoke token' )
return true
},
async revokeExistingAppCredentials( { appId } ) {
let resAccessCodeDelete = await AuthorizationCodes( ).where( { appId: appId } ).del( )
let resRefreshTokenDelete = await RefreshTokens( ).where( { appId: appId } ).del( )
let resApiTokenDelete = await ApiTokens( )
.whereIn( 'id', qb => {
qb.select( 'tokenId' ).from( 'user_server_app_tokens' ).where( { appId: appId } )
} )
.del( )
return resApiTokenDelete
},
async revokeExistingAppCredentialsForUser( { appId, userId } ) {
let resAccessCodeDelete = await AuthorizationCodes( ).where( { appId: appId, userId: userId } ).del( )
let resRefreshTokenDelete = await RefreshTokens( ).where( { appId: appId, userId: userId } ).del( )
let resApiTokenDelete = await ApiTokens( )
.whereIn( 'id', qb => {
qb.select( 'tokenId' ).from( 'user_server_app_tokens' ).where( { appId: appId, userId: userId } )
} )
.del( )
return resApiTokenDelete
},
async createAuthorizationCode( { appId, userId, challenge } ) {
let ac = {
id: crs( { length: 42 } ),
appId: appId,
userId: userId,
challenge: challenge
}
await AuthorizationCodes( ).insert( ac )
return ac.id
},
async createAppTokenFromAccessCode( { appId, appSecret, accessCode, challenge } ) {
let code = await AuthorizationCodes( ).select( ).where( { id: accessCode } ).first( )
if ( !code ) throw new Error( 'Access code not found.' )
if ( code.appId !== appId ) throw new Error( 'Invalid request: application id does not match.' )
await AuthorizationCodes( ).where( { id: accessCode } ).del( )
const timeDiff = Math.abs( Date.now( ) - new Date( code.createdAt ) )
if ( timeDiff > code.lifespan ) {
throw new Error( 'Access code expired' )
}
if ( code.challenge !== challenge ) throw new Error( 'Invalid request' )
let app = await ServerApps( ).select( '*' ).where( { id: appId } ).first( )
if ( !app ) throw new Error( 'Invalid app' )
if ( app.secret !== appSecret ) throw new Error( 'Invalid app credentials' )
const scopes = await ServerAppsScopes( ).select( 'scopeName' ).where( { appId: appId } )
const appScopes = scopes.map( s => s.scopeName )
const { token: appToken } = await createToken( { userId: code.userId, name: `${app.name}-token`, /* lifespan: 1.21e+9, */ scopes: appScopes } )
await ServerAppsTokens( ).insert( { userId: code.userId, tokenId: appToken.slice( 0, 10 ), appId: appId } )
let bareToken = await createBareToken( )
let refreshToken = {
id: bareToken.tokenId,
tokenDigest: bareToken.tokenHash,
appId: app.id,
userId: code.userId
}
const rtk = await RefreshTokens( ).insert( refreshToken )
return {
token: appToken,
refreshToken: bareToken.tokenId + bareToken.tokenString
}
},
async refreshAppToken( { refreshToken, appId, appSecret } ) {
let refreshTokenId = refreshToken.slice( 0, 10 )
let refreshTokenContent = refreshToken.slice( 10, 42 )
let refreshTokenDb = await RefreshTokens( ).select( '*' ).where( { id: refreshTokenId } ).first( )
if ( !refreshTokenDb )
throw new Error( 'Invalid request' )
if ( refreshTokenDb.appId !== appId )
throw new Error( 'Invalid request' )
const timeDiff = Math.abs( Date.now( ) - new Date( refreshTokenDb.createdAt ) )
if ( timeDiff > refreshTokenDb.lifespan ) {
await RefreshTokens( ).where( { id: refreshTokenId } ).del( )
throw new Error( 'Refresh token expired' )
}
let valid = await bcrypt.compare( refreshTokenContent, refreshTokenDb.tokenDigest )
if ( !valid )
throw new Error( 'Invalid token' ) // sneky hackstors
let app = await module.exports.getApp( { id: appId } )
if ( app.secret !== appSecret )
throw new Error( 'Invalid request' )
// Create the new token
const { token: appToken } = await createToken( { userId: refreshTokenDb.userId, name: `${app.name}-token`, scopes: app.scopes.map( s => s.name ) } )
await ServerAppsTokens( ).insert( { userId: refreshTokenDb.userId, tokenId: appToken.slice( 0, 10 ), appId: appId } )
// Create a new refresh token
let bareToken = await createBareToken( )
let freshRefreshToken = {
id: bareToken.tokenId,
tokenDigest: bareToken.tokenHash,
appId: appId,
userId: refreshTokenDb.userId
}
const rtk = await RefreshTokens( ).insert( freshRefreshToken )
// Finally return
return {
token: appToken,
refreshToken: bareToken.tokenId + bareToken.tokenString
}
}
}