ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

231 lines
7.2 KiB

  1. import {
  2. isUserAdminFlagsValid,
  3. isUserDisplayNameValid,
  4. isUserRoleValid,
  5. isUserUsernameValid,
  6. isUserVideoQuotaDailyValid,
  7. isUserVideoQuotaValid
  8. } from '@server/helpers/custom-validators/users.js'
  9. import { logger } from '@server/helpers/logger.js'
  10. import { generateRandomString } from '@server/helpers/utils.js'
  11. import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants.js'
  12. import { PluginManager } from '@server/lib/plugins/plugin-manager.js'
  13. import { OAuthTokenModel } from '@server/models/oauth/oauth-token.js'
  14. import { MUser } from '@server/types/models/index.js'
  15. import {
  16. RegisterServerAuthenticatedResult,
  17. RegisterServerAuthPassOptions,
  18. RegisterServerExternalAuthenticatedResult
  19. } from '@server/types/plugins/register-server-auth.model.js'
  20. import { UserAdminFlag, UserRole } from '@peertube/peertube-models'
  21. import { BypassLogin } from './oauth-model.js'
  22. export type ExternalUser =
  23. Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> &
  24. { displayName: string }
  25. // Token is the key, expiration date is the value
  26. const authBypassTokens = new Map<string, {
  27. expires: Date
  28. user: ExternalUser
  29. userUpdater: RegisterServerAuthenticatedResult['userUpdater']
  30. authName: string
  31. npmName: string
  32. }>()
  33. async function onExternalUserAuthenticated (options: {
  34. npmName: string
  35. authName: string
  36. authResult: RegisterServerExternalAuthenticatedResult
  37. }) {
  38. const { npmName, authName, authResult } = options
  39. if (!authResult.req || !authResult.res) {
  40. logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName)
  41. return
  42. }
  43. const { res } = authResult
  44. if (!isAuthResultValid(npmName, authName, authResult)) {
  45. res.redirect('/login?externalAuthError=true')
  46. return
  47. }
  48. logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName)
  49. const bypassToken = await generateRandomString(32)
  50. const expires = new Date()
  51. expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME)
  52. const user = buildUserResult(authResult)
  53. authBypassTokens.set(bypassToken, {
  54. expires,
  55. user,
  56. npmName,
  57. authName,
  58. userUpdater: authResult.userUpdater
  59. })
  60. // Cleanup expired tokens
  61. const now = new Date()
  62. for (const [ key, value ] of authBypassTokens) {
  63. if (value.expires.getTime() < now.getTime()) {
  64. authBypassTokens.delete(key)
  65. }
  66. }
  67. res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
  68. }
  69. async function getAuthNameFromRefreshGrant (refreshToken?: string) {
  70. if (!refreshToken) return undefined
  71. const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken)
  72. return tokenModel?.authName
  73. }
  74. async function getBypassFromPasswordGrant (username: string, password: string): Promise<BypassLogin> {
  75. const plugins = PluginManager.Instance.getIdAndPassAuths()
  76. const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
  77. for (const plugin of plugins) {
  78. const auths = plugin.idAndPassAuths
  79. for (const auth of auths) {
  80. pluginAuths.push({
  81. npmName: plugin.npmName,
  82. registerAuthOptions: auth
  83. })
  84. }
  85. }
  86. pluginAuths.sort((a, b) => {
  87. const aWeight = a.registerAuthOptions.getWeight()
  88. const bWeight = b.registerAuthOptions.getWeight()
  89. // DESC weight order
  90. if (aWeight === bWeight) return 0
  91. if (aWeight < bWeight) return 1
  92. return -1
  93. })
  94. const loginOptions = {
  95. id: username,
  96. password
  97. }
  98. for (const pluginAuth of pluginAuths) {
  99. const authOptions = pluginAuth.registerAuthOptions
  100. const authName = authOptions.authName
  101. const npmName = pluginAuth.npmName
  102. logger.debug(
  103. 'Using auth method %s of plugin %s to login %s with weight %d.',
  104. authName, npmName, loginOptions.id, authOptions.getWeight()
  105. )
  106. try {
  107. const loginResult = await authOptions.login(loginOptions)
  108. if (!loginResult) continue
  109. if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue
  110. logger.info(
  111. 'Login success with auth method %s of plugin %s for %s.',
  112. authName, npmName, loginOptions.id
  113. )
  114. return {
  115. bypass: true,
  116. pluginName: pluginAuth.npmName,
  117. authName: authOptions.authName,
  118. user: buildUserResult(loginResult),
  119. userUpdater: loginResult.userUpdater
  120. }
  121. } catch (err) {
  122. logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
  123. }
  124. }
  125. return undefined
  126. }
  127. function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin {
  128. const obj = authBypassTokens.get(externalAuthToken)
  129. if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
  130. const { expires, user, authName, npmName } = obj
  131. const now = new Date()
  132. if (now.getTime() > expires.getTime()) {
  133. throw new Error('Cannot authenticate user with an expired external auth token')
  134. }
  135. if (user.username !== username) {
  136. throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`)
  137. }
  138. logger.info(
  139. 'Auth success with external auth method %s of plugin %s for %s.',
  140. authName, npmName, user.email
  141. )
  142. return {
  143. bypass: true,
  144. pluginName: npmName,
  145. authName,
  146. userUpdater: obj.userUpdater,
  147. user
  148. }
  149. }
  150. function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) {
  151. const returnError = (field: string) => {
  152. logger.error('Auth method %s of plugin %s did not provide a valid %s.', authName, npmName, field, { [field]: result[field] })
  153. return false
  154. }
  155. if (!isUserUsernameValid(result.username)) return returnError('username')
  156. if (!result.email) return returnError('email')
  157. // Following fields are optional
  158. if (result.role && !isUserRoleValid(result.role)) return returnError('role')
  159. if (result.displayName && !isUserDisplayNameValid(result.displayName)) return returnError('displayName')
  160. if (result.adminFlags && !isUserAdminFlagsValid(result.adminFlags)) return returnError('adminFlags')
  161. if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota')
  162. if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily')
  163. if (result.userUpdater && typeof result.userUpdater !== 'function') {
  164. logger.error('Auth method %s of plugin %s did not provide a valid user updater function.', authName, npmName)
  165. return false
  166. }
  167. return true
  168. }
  169. function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
  170. return {
  171. username: pluginResult.username,
  172. email: pluginResult.email,
  173. role: pluginResult.role ?? UserRole.USER,
  174. displayName: pluginResult.displayName || pluginResult.username,
  175. adminFlags: pluginResult.adminFlags ?? UserAdminFlag.NONE,
  176. videoQuota: pluginResult.videoQuota,
  177. videoQuotaDaily: pluginResult.videoQuotaDaily
  178. }
  179. }
  180. // ---------------------------------------------------------------------------
  181. export {
  182. onExternalUserAuthenticated,
  183. getBypassFromExternalAuth,
  184. getAuthNameFromRefreshGrant,
  185. getBypassFromPasswordGrant
  186. }