auth.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
  2. import { logger } from '@server/helpers/logger'
  3. import { generateRandomString } from '@server/helpers/utils'
  4. import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
  5. import { revokeToken } from '@server/lib/oauth-model'
  6. import { PluginManager } from '@server/lib/plugins/plugin-manager'
  7. import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
  8. import { UserRole } from '@shared/models'
  9. import {
  10. RegisterServerAuthenticatedResult,
  11. RegisterServerAuthPassOptions,
  12. RegisterServerExternalAuthenticatedResult
  13. } from '@server/types/plugins/register-server-auth.model'
  14. import * as express from 'express'
  15. import * as OAuthServer from 'express-oauth-server'
  16. import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
  17. const oAuthServer = new OAuthServer({
  18. useErrorHandler: true,
  19. accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
  20. refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
  21. continueMiddleware: true,
  22. model: require('./oauth-model')
  23. })
  24. // Token is the key, expiration date is the value
  25. const authBypassTokens = new Map<string, {
  26. expires: Date
  27. user: {
  28. username: string
  29. email: string
  30. displayName: string
  31. role: UserRole
  32. }
  33. authName: string
  34. npmName: string
  35. }>()
  36. async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
  37. const grantType = req.body.grant_type
  38. if (grantType === 'password') {
  39. if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res)
  40. else await proxifyPasswordGrant(req, res)
  41. } else if (grantType === 'refresh_token') {
  42. await proxifyRefreshGrant(req, res)
  43. }
  44. return forwardTokenReq(req, res, next)
  45. }
  46. async function handleTokenRevocation (req: express.Request, res: express.Response) {
  47. const token = res.locals.oauth.token
  48. res.locals.explicitLogout = true
  49. const result = await revokeToken(token)
  50. // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
  51. // oAuthServer.revoke(req, res, err => {
  52. // if (err) {
  53. // logger.warn('Error in revoke token handler.', { err })
  54. //
  55. // return res.status(err.status)
  56. // .json({
  57. // error: err.message,
  58. // code: err.name
  59. // })
  60. // .end()
  61. // }
  62. // })
  63. return res.json(result)
  64. }
  65. async function onExternalUserAuthenticated (options: {
  66. npmName: string
  67. authName: string
  68. authResult: RegisterServerExternalAuthenticatedResult
  69. }) {
  70. const { npmName, authName, authResult } = options
  71. if (!authResult.req || !authResult.res) {
  72. logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName)
  73. return
  74. }
  75. const { res } = authResult
  76. if (!isAuthResultValid(npmName, authName, authResult)) {
  77. res.redirect('/login?externalAuthError=true')
  78. return
  79. }
  80. logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName)
  81. const bypassToken = await generateRandomString(32)
  82. const expires = new Date()
  83. expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME)
  84. const user = buildUserResult(authResult)
  85. authBypassTokens.set(bypassToken, {
  86. expires,
  87. user,
  88. npmName,
  89. authName
  90. })
  91. // Cleanup
  92. const now = new Date()
  93. for (const [ key, value ] of authBypassTokens) {
  94. if (value.expires.getTime() < now.getTime()) {
  95. authBypassTokens.delete(key)
  96. }
  97. }
  98. res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
  99. }
  100. // ---------------------------------------------------------------------------
  101. export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
  102. // ---------------------------------------------------------------------------
  103. function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) {
  104. return oAuthServer.token()(req, res, err => {
  105. if (err) {
  106. logger.warn('Login error.', { err })
  107. return res.status(err.status)
  108. .json({
  109. error: err.message,
  110. code: err.name
  111. })
  112. }
  113. if (next) return next()
  114. })
  115. }
  116. async function proxifyRefreshGrant (req: express.Request, res: express.Response) {
  117. const refreshToken = req.body.refresh_token
  118. if (!refreshToken) return
  119. const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken)
  120. if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName
  121. }
  122. async function proxifyPasswordGrant (req: express.Request, res: express.Response) {
  123. const plugins = PluginManager.Instance.getIdAndPassAuths()
  124. const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
  125. for (const plugin of plugins) {
  126. const auths = plugin.idAndPassAuths
  127. for (const auth of auths) {
  128. pluginAuths.push({
  129. npmName: plugin.npmName,
  130. registerAuthOptions: auth
  131. })
  132. }
  133. }
  134. pluginAuths.sort((a, b) => {
  135. const aWeight = a.registerAuthOptions.getWeight()
  136. const bWeight = b.registerAuthOptions.getWeight()
  137. // DESC weight order
  138. if (aWeight === bWeight) return 0
  139. if (aWeight < bWeight) return 1
  140. return -1
  141. })
  142. const loginOptions = {
  143. id: req.body.username,
  144. password: req.body.password
  145. }
  146. for (const pluginAuth of pluginAuths) {
  147. const authOptions = pluginAuth.registerAuthOptions
  148. const authName = authOptions.authName
  149. const npmName = pluginAuth.npmName
  150. logger.debug(
  151. 'Using auth method %s of plugin %s to login %s with weight %d.',
  152. authName, npmName, loginOptions.id, authOptions.getWeight()
  153. )
  154. try {
  155. const loginResult = await authOptions.login(loginOptions)
  156. if (!loginResult) continue
  157. if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue
  158. logger.info(
  159. 'Login success with auth method %s of plugin %s for %s.',
  160. authName, npmName, loginOptions.id
  161. )
  162. res.locals.bypassLogin = {
  163. bypass: true,
  164. pluginName: pluginAuth.npmName,
  165. authName: authOptions.authName,
  166. user: buildUserResult(loginResult)
  167. }
  168. return
  169. } catch (err) {
  170. logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
  171. }
  172. }
  173. }
  174. function proxifyExternalAuthBypass (req: express.Request, res: express.Response) {
  175. const obj = authBypassTokens.get(req.body.externalAuthToken)
  176. if (!obj) {
  177. logger.error('Cannot authenticate user with unknown bypass token')
  178. return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
  179. }
  180. const { expires, user, authName, npmName } = obj
  181. const now = new Date()
  182. if (now.getTime() > expires.getTime()) {
  183. logger.error('Cannot authenticate user with an expired external auth token')
  184. return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
  185. }
  186. if (user.username !== req.body.username) {
  187. logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username)
  188. return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
  189. }
  190. // Bypass oauth library validation
  191. req.body.password = 'fake'
  192. logger.info(
  193. 'Auth success with external auth method %s of plugin %s for %s.',
  194. authName, npmName, user.email
  195. )
  196. res.locals.bypassLogin = {
  197. bypass: true,
  198. pluginName: npmName,
  199. authName: authName,
  200. user
  201. }
  202. }
  203. function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) {
  204. if (!isUserUsernameValid(result.username)) {
  205. logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username })
  206. return false
  207. }
  208. if (!result.email) {
  209. logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email })
  210. return false
  211. }
  212. // role is optional
  213. if (result.role && !isUserRoleValid(result.role)) {
  214. logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role })
  215. return false
  216. }
  217. // display name is optional
  218. if (result.displayName && !isUserDisplayNameValid(result.displayName)) {
  219. logger.error(
  220. 'Auth method %s of plugin %s did not provide a valid display name.',
  221. authName, npmName, { displayName: result.displayName }
  222. )
  223. return false
  224. }
  225. return true
  226. }
  227. function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
  228. return {
  229. username: pluginResult.username,
  230. email: pluginResult.email,
  231. role: pluginResult.role ?? UserRole.USER,
  232. displayName: pluginResult.displayName || pluginResult.username
  233. }
  234. }