298d8d6e52
- the database expects challenge to be not null, so we should guard against this early before consuming database resources
321 lines
9.1 KiB
JavaScript
321 lines
9.1 KiB
JavaScript
'use strict'
|
|
const bcrypt = require('bcrypt')
|
|
const crs = require('crypto-random-string')
|
|
const knex = require(`@/db/knex`)
|
|
|
|
const { createBareToken, createAppToken } = require(`@/modules/core/services/tokens`)
|
|
const { logger } = require('@/logging/logging')
|
|
const { getDefaultApp } = require('@/modules/auth/defaultApps')
|
|
const Users = () => knex('users')
|
|
const ApiTokens = () => knex('api_tokens')
|
|
const ServerApps = () => knex('server_apps')
|
|
const ServerAppsScopes = () => knex('server_apps_scopes')
|
|
const Scopes = () => knex('scopes')
|
|
|
|
const AuthorizationCodes = () => knex('authorization_codes')
|
|
const RefreshTokens = () => knex('refresh_tokens')
|
|
|
|
const addDefaultAppOverrides = (app) => {
|
|
const defaultApp = getDefaultApp({ id: app.id })
|
|
if (defaultApp) app.redirectUrl = defaultApp.redirectUrl
|
|
return app
|
|
}
|
|
|
|
module.exports = {
|
|
async getApp({ id }) {
|
|
const allScopes = await Scopes().select('*')
|
|
|
|
const app = await ServerApps().select('*').where({ id }).first()
|
|
if (!app) return null
|
|
|
|
const 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 addDefaultAppOverrides(app)
|
|
},
|
|
|
|
async getAllPublicApps() {
|
|
const apps = await ServerApps()
|
|
.select(
|
|
'server_apps.id',
|
|
'server_apps.name',
|
|
'server_apps.description',
|
|
'server_apps.trustByDefault',
|
|
'server_apps.redirectUrl',
|
|
'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) => {
|
|
app = addDefaultAppOverrides(app)
|
|
|
|
if (app.authorName && app.authorId) {
|
|
app.author = { name: app.authorName, id: app.authorId }
|
|
}
|
|
delete app.authorName
|
|
delete app.authorId
|
|
})
|
|
|
|
return apps
|
|
},
|
|
|
|
async getAllAppsCreatedByUser({ userId }) {
|
|
const 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 }) {
|
|
const 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]
|
|
)
|
|
|
|
const { rows } = await query
|
|
return rows.map((r) => {
|
|
const app = {
|
|
...r,
|
|
author: r.author?.id ? r.author : null
|
|
}
|
|
return addDefaultAppOverrides(app)
|
|
})
|
|
},
|
|
|
|
async createApp(app) {
|
|
app.id = crs({ length: 10 })
|
|
app.secret = crs({ length: 10 })
|
|
|
|
const scopes = (app.scopes || []).filter((s) => !!s?.length)
|
|
|
|
if (!scopes.length) {
|
|
throw new Error('Cannot create an app with no 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) {
|
|
logger.debug(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
|
|
|
|
const [{ id }] = await ServerApps()
|
|
.returning('id')
|
|
.where({ id: app.id })
|
|
.update(app)
|
|
|
|
return id
|
|
},
|
|
|
|
async deleteApp({ id }) {
|
|
await module.exports.revokeExistingAppCredentials({ appId: id })
|
|
|
|
return await ServerApps().where({ id }).del()
|
|
},
|
|
|
|
async revokeRefreshToken({ tokenId }) {
|
|
tokenId = tokenId.slice(0, 10)
|
|
await RefreshTokens().where({ id: tokenId }).del()
|
|
return true
|
|
},
|
|
|
|
async revokeExistingAppCredentials({ appId }) {
|
|
await AuthorizationCodes().where({ appId }).del()
|
|
await RefreshTokens().where({ appId }).del()
|
|
|
|
const resApiTokenDelete = await ApiTokens()
|
|
.whereIn('id', (qb) => {
|
|
qb.select('tokenId').from('user_server_app_tokens').where({ appId })
|
|
})
|
|
.del()
|
|
|
|
return resApiTokenDelete
|
|
},
|
|
|
|
async revokeExistingAppCredentialsForUser({ appId, userId }) {
|
|
await AuthorizationCodes().where({ appId, userId }).del()
|
|
await RefreshTokens().where({ appId, userId }).del()
|
|
const resApiTokenDelete = await ApiTokens()
|
|
.whereIn('id', (qb) => {
|
|
qb.select('tokenId').from('user_server_app_tokens').where({ appId, userId })
|
|
})
|
|
.del()
|
|
|
|
return resApiTokenDelete
|
|
},
|
|
|
|
async createAuthorizationCode({ appId, userId, challenge }) {
|
|
if (!challenge) throw new Error('Please provide a valid challenge.')
|
|
|
|
const ac = {
|
|
id: crs({ length: 42 }),
|
|
appId,
|
|
userId,
|
|
challenge
|
|
}
|
|
|
|
await AuthorizationCodes().insert(ac)
|
|
return ac.id
|
|
},
|
|
|
|
async createAppTokenFromAccessCode({ appId, appSecret, accessCode, challenge }) {
|
|
const 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')
|
|
|
|
const 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 })
|
|
|
|
const appScopes = scopes.map((s) => s.scopeName)
|
|
|
|
const appToken = await createAppToken({
|
|
userId: code.userId,
|
|
name: `${app.name}-token`,
|
|
scopes: appScopes,
|
|
appId
|
|
})
|
|
|
|
const bareToken = await createBareToken()
|
|
|
|
const refreshToken = {
|
|
id: bareToken.tokenId,
|
|
tokenDigest: bareToken.tokenHash,
|
|
appId: app.id,
|
|
userId: code.userId
|
|
}
|
|
|
|
await RefreshTokens().insert(refreshToken)
|
|
|
|
return {
|
|
token: appToken,
|
|
refreshToken: bareToken.tokenId + bareToken.tokenString
|
|
}
|
|
},
|
|
|
|
async refreshAppToken({ refreshToken, appId, appSecret }) {
|
|
const refreshTokenId = refreshToken.slice(0, 10)
|
|
const refreshTokenContent = refreshToken.slice(10, 42)
|
|
|
|
const 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')
|
|
}
|
|
|
|
const valid = await bcrypt.compare(refreshTokenContent, refreshTokenDb.tokenDigest)
|
|
if (!valid) throw new Error('Invalid token') // sneky hackstors
|
|
|
|
const app = await module.exports.getApp({ id: appId })
|
|
if (app.secret !== appSecret) throw new Error('Invalid request')
|
|
|
|
// Create the new token
|
|
const appToken = await createAppToken({
|
|
userId: refreshTokenDb.userId,
|
|
name: `${app.name}-token`,
|
|
scopes: app.scopes.map((s) => s.name),
|
|
appId
|
|
})
|
|
|
|
// Create a new refresh token
|
|
const bareToken = await createBareToken()
|
|
|
|
const freshRefreshToken = {
|
|
id: bareToken.tokenId,
|
|
tokenDigest: bareToken.tokenHash,
|
|
appId,
|
|
userId: refreshTokenDb.userId
|
|
}
|
|
|
|
await RefreshTokens().insert(freshRefreshToken)
|
|
|
|
// Finally return
|
|
return {
|
|
token: appToken,
|
|
refreshToken: bareToken.tokenId + bareToken.tokenString
|
|
}
|
|
}
|
|
}
|