Browse Source

Begin support for external auths

Chocobozzz 4 years ago
parent
commit
4a8d113b9b

+ 1 - 0
.eslintrc.json

@@ -83,6 +83,7 @@
     "@typescript-eslint/consistent-type-definitions": "off",
     "@typescript-eslint/no-misused-promises": "off",
     "@typescript-eslint/no-namespace": "off",
+    "@typescript-eslint/no-empty-interface": "off",
     "@typescript-eslint/no-extraneous-class": "off",
     // bugged but useful
     "@typescript-eslint/restrict-plus-operands": "off"

+ 3 - 1
client/src/app/core/auth/auth.service.ts

@@ -145,7 +145,7 @@ export class AuthService {
     return !!this.getAccessToken()
   }
 
-  login (username: string, password: string) {
+  login (username: string, password: string, token?: string) {
     // Form url encoded
     const body = {
       client_id: this.clientId,
@@ -157,6 +157,8 @@ export class AuthService {
       password
     }
 
+    if (token) Object.assign(body, { externalAuthToken: token })
+
     const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
     return this.http.post<UserLogin>(AuthService.BASE_TOKEN_URL, objectToUrlEncoded(body), { headers })
                .pipe(

+ 3 - 1
client/src/app/core/server/server.service.ts

@@ -54,7 +54,9 @@ export class ServerService {
       }
     },
     plugin: {
-      registered: []
+      registered: [],
+      registeredExternalAuths: [],
+      registeredIdAndPassAuths: []
     },
     theme: {
       registered: [],

+ 45 - 43
client/src/app/login/login.component.html

@@ -3,59 +3,61 @@
     Login
   </div>
 
-  <div class="alert alert-info" *ngIf="signupAllowed === false" role="alert">
-    <h6 class="alert-heading" i18n>
-      If you are looking for an account…
-    </h6>
+  <ng-container *ngIf="!isAuthenticatedWithExternalAuth">
+    <div class="alert alert-info" *ngIf="signupAllowed === false" role="alert">
+      <h6 class="alert-heading" i18n>
+        If you are looking for an account…
+      </h6>
 
-    <div i18n>
-      Currently this instance doesn't allow for user registration, but you can find an instance
-      that gives you the possibility to sign up for an account and upload your videos there.
+      <div i18n>
+        Currently this instance doesn't allow for user registration, but you can find an instance
+        that gives you the possibility to sign up for an account and upload your videos there.
 
-      <br />
+        <br />
 
-      Find yours among multiple instances at <a class="alert-link" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer">https://joinpeertube.org/instances</a>.
-    </div>
-  </div>
-
-  <div *ngIf="error" class="alert alert-danger">{{ error }}
-    <span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span>
-  </div>
-
-  <form role="form" (ngSubmit)="login()" [formGroup]="form">
-    <div class="form-group">
-      <div>
-        <label i18n for="username">User</label>
-        <input
-          type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1"
-          formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #emailInput
-        >
-        <a i18n *ngIf="signupAllowed === true" routerLink="/signup" class="create-an-account">
-          or create an account
-        </a>
+        Find yours among multiple instances at <a class="alert-link" href="https://joinpeertube.org/instances" target="_blank" rel="noopener noreferrer">https://joinpeertube.org/instances</a>.
       </div>
+    </div>
 
-      <div *ngIf="formErrors.username" class="form-error">
-        {{ formErrors.username }}
-      </div>
+    <div *ngIf="error" class="alert alert-danger">{{ error }}
+      <span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span>
     </div>
 
-    <div class="form-group">
-      <label i18n for="password">Password</label>
-      <div>
-        <input
-          type="password" name="password" id="password" i18n-placeholder placeholder="Password" required tabindex="2" autocomplete="current-password"
-          formControlName="password" class="form-control" [ngClass]="{ 'input-error': formErrors['password'] }"
-        >
-        <a i18n-title class="forgot-password-button" (click)="openForgotPasswordModal()" title="Click here to reset your password">I forgot my password</a>
+    <form role="form" (ngSubmit)="login()" [formGroup]="form">
+      <div class="form-group">
+        <div>
+          <label i18n for="username">User</label>
+          <input
+            type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1"
+            formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #emailInput
+          >
+          <a i18n *ngIf="signupAllowed === true" routerLink="/signup" class="create-an-account">
+            or create an account
+          </a>
+        </div>
+
+        <div *ngIf="formErrors.username" class="form-error">
+          {{ formErrors.username }}
+        </div>
       </div>
-      <div *ngIf="formErrors.password" class="form-error">
-        {{ formErrors.password }}
+
+      <div class="form-group">
+        <label i18n for="password">Password</label>
+        <div>
+          <input
+            type="password" name="password" id="password" i18n-placeholder placeholder="Password" required tabindex="2" autocomplete="current-password"
+            formControlName="password" class="form-control" [ngClass]="{ 'input-error': formErrors['password'] }"
+          >
+          <a i18n-title class="forgot-password-button" (click)="openForgotPasswordModal()" title="Click here to reset your password">I forgot my password</a>
+        </div>
+        <div *ngIf="formErrors.password" class="form-error">
+          {{ formErrors.password }}
+        </div>
       </div>
-    </div>
 
-    <input type="submit" i18n-value value="Login" [disabled]="!form.valid">
-  </form>
+      <input type="submit" i18n-value value="Login" [disabled]="!form.valid">
+    </form>
+  </ng-container>
 </div>
 
 <ng-template #forgotPasswordModal>

+ 30 - 6
client/src/app/login/login.component.ts

@@ -22,6 +22,7 @@ export class LoginComponent extends FormReactive implements OnInit {
 
   error: string = null
   forgotPasswordEmail = ''
+  isAuthenticatedWithExternalAuth = false
 
   private openedForgotPasswordModal: NgbModalRef
   private serverConfig: ServerConfig
@@ -49,7 +50,14 @@ export class LoginComponent extends FormReactive implements OnInit {
   }
 
   ngOnInit () {
-    this.serverConfig = this.route.snapshot.data.serverConfig
+    const snapshot = this.route.snapshot
+
+    this.serverConfig = snapshot.data.serverConfig
+
+    if (snapshot.queryParams.externalAuthToken) {
+      this.loadExternalAuthToken(snapshot.queryParams.username, snapshot.queryParams.externalAuthToken)
+      return
+    }
 
     this.buildForm({
       username: this.loginValidatorsService.LOGIN_USERNAME,
@@ -68,11 +76,7 @@ export class LoginComponent extends FormReactive implements OnInit {
       .subscribe(
         () => this.redirectService.redirectToPreviousRoute(),
 
-        err => {
-          if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.')
-          else if (err.message.indexOf('blocked') !== -1) this.error = this.i18n('You account is blocked.')
-          else this.error = err.message
-        }
+        err => this.handleError(err)
       )
   }
 
@@ -99,4 +103,24 @@ export class LoginComponent extends FormReactive implements OnInit {
   hideForgotPasswordModal () {
     this.openedForgotPasswordModal.close()
   }
+
+  private loadExternalAuthToken (username: string, token: string) {
+    this.isAuthenticatedWithExternalAuth = true
+
+    this.authService.login(username, null, token)
+    .subscribe(
+      () => this.redirectService.redirectToPreviousRoute(),
+
+      err => {
+        this.handleError(err)
+        this.isAuthenticatedWithExternalAuth = false
+      }
+    )
+  }
+
+  private handleError (err: any) {
+    if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.')
+    else if (err.message.indexOf('blocked') !== -1) this.error = this.i18n('You account is blocked.')
+    else this.error = err.message
+  }
 }

+ 45 - 11
server/controllers/api/config.ts

@@ -1,22 +1,22 @@
+import { Hooks } from '@server/lib/plugins/hooks'
 import * as express from 'express'
+import { remove, writeJSON } from 'fs-extra'
 import { snakeCase } from 'lodash'
-import { ServerConfig, UserRight } from '../../../shared'
+import validator from 'validator'
+import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig, UserRight } from '../../../shared'
 import { About } from '../../../shared/models/server/about.model'
 import { CustomConfig } from '../../../shared/models/server/custom-config.model'
-import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
-import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants'
-import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
-import { customConfigUpdateValidator } from '../../middlewares/validators/config'
-import { ClientHtml } from '../../lib/client-html'
 import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
-import { remove, writeJSON } from 'fs-extra'
-import { getServerCommit } from '../../helpers/utils'
-import validator from 'validator'
 import { objectConverter } from '../../helpers/core-utils'
+import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
+import { getServerCommit } from '../../helpers/utils'
 import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config'
+import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants'
+import { ClientHtml } from '../../lib/client-html'
 import { PluginManager } from '../../lib/plugins/plugin-manager'
 import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
-import { Hooks } from '@server/lib/plugins/hooks'
+import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
+import { customConfigUpdateValidator } from '../../middlewares/validators/config'
 
 const configRouter = express.Router()
 
@@ -79,7 +79,9 @@ async function getConfig (req: express.Request, res: express.Response) {
       }
     },
     plugin: {
-      registered: getRegisteredPlugins()
+      registered: getRegisteredPlugins(),
+      registeredExternalAuths: getExternalAuthsPlugins(),
+      registeredIdAndPassAuths: getIdAndPassAuthPlugins()
     },
     theme: {
       registered: getRegisteredThemes(),
@@ -269,6 +271,38 @@ function getRegisteredPlugins () {
                       }))
 }
 
+function getIdAndPassAuthPlugins () {
+  const result: RegisteredIdAndPassAuthConfig[] = []
+
+  for (const p of PluginManager.Instance.getIdAndPassAuths()) {
+    for (const auth of p.idAndPassAuths) {
+      result.push({
+        npmName: p.npmName,
+        authName: auth.authName,
+        weight: auth.getWeight()
+      })
+    }
+  }
+
+  return result
+}
+
+function getExternalAuthsPlugins () {
+  const result: RegisteredExternalAuthConfig[] = []
+
+  for (const p of PluginManager.Instance.getExternalAuths()) {
+    for (const auth of p.externalAuths) {
+      result.push({
+        npmName: p.npmName,
+        authName: auth.authName,
+        authDisplayName: auth.authDisplayName
+      })
+    }
+  }
+
+  return result
+}
+
 // ---------------------------------------------------------------------------
 
 export {

+ 19 - 1
server/controllers/plugins.ts

@@ -2,11 +2,12 @@ import * as express from 'express'
 import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
 import { join } from 'path'
 import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
-import { getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
+import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins'
 import { serveThemeCSSValidator } from '../middlewares/validators/themes'
 import { PluginType } from '../../shared/models/plugins/plugin.type'
 import { isTestInstance } from '../helpers/core-utils'
 import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n'
+import { logger } from '@server/helpers/logger'
 
 const sendFileOptions = {
   maxAge: '30 days',
@@ -23,6 +24,12 @@ pluginsRouter.get('/plugins/translations/:locale.json',
   getPluginTranslations
 )
 
+pluginsRouter.get('/plugins/:pluginName/:pluginVersion/auth/:authName',
+  getPluginValidator(PluginType.PLUGIN),
+  getExternalAuthValidator,
+  handleAuthInPlugin
+)
+
 pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
   getPluginValidator(PluginType.PLUGIN),
   pluginStaticDirectoryValidator,
@@ -134,3 +141,14 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
 
   return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions)
 }
+
+function handleAuthInPlugin (req: express.Request, res: express.Response) {
+  const authOptions = res.locals.externalAuth
+
+  try {
+    logger.debug('Forwarding auth plugin request in %s of plugin %s.', authOptions.authName, res.locals.registeredPlugin.npmName)
+    authOptions.onAuthRequest(req, res)
+  } catch (err) {
+    logger.error('Forward request error in auth %s of plugin %s.', authOptions.authName, res.locals.registeredPlugin.npmName)
+  }
+}

+ 159 - 66
server/lib/auth.ts

@@ -1,13 +1,18 @@
-import * as express from 'express'
-import { OAUTH_LIFETIME } from '@server/initializers/constants'
-import * as OAuthServer from 'express-oauth-server'
-import { PluginManager } from '@server/lib/plugins/plugin-manager'
-import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model'
+import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
 import { logger } from '@server/helpers/logger'
-import { UserRole } from '@shared/models'
+import { generateRandomString } from '@server/helpers/utils'
+import { OAUTH_LIFETIME, WEBSERVER } from '@server/initializers/constants'
 import { revokeToken } from '@server/lib/oauth-model'
+import { PluginManager } from '@server/lib/plugins/plugin-manager'
 import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
-import { isUserUsernameValid, isUserRoleValid, isUserDisplayNameValid } from '@server/helpers/custom-validators/users'
+import { UserRole } from '@shared/models'
+import {
+  RegisterServerAuthenticatedResult,
+  RegisterServerAuthPassOptions,
+  RegisterServerExternalAuthenticatedResult
+} from '@shared/models/plugins/register-server-auth.model'
+import * as express from 'express'
+import * as OAuthServer from 'express-oauth-server'
 
 const oAuthServer = new OAuthServer({
   useErrorHandler: true,
@@ -17,15 +22,28 @@ const oAuthServer = new OAuthServer({
   model: require('./oauth-model')
 })
 
-function onExternalAuthPlugin (npmName: string, username: string, email: string) {
-
-}
+// Token is the key, expiration date is the value
+const authBypassTokens = new Map<string, {
+  expires: Date
+  user: {
+    username: string
+    email: string
+    displayName: string
+    role: UserRole
+  }
+  authName: string
+  npmName: string
+}>()
 
 async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
   const grantType = req.body.grant_type
 
-  if (grantType === 'password') await proxifyPasswordGrant(req, res)
-  else if (grantType === 'refresh_token') await proxifyRefreshGrant(req, res)
+  if (grantType === 'password') {
+    if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res)
+    else await proxifyPasswordGrant(req, res)
+  } else if (grantType === 'refresh_token') {
+    await proxifyRefreshGrant(req, res)
+  }
 
   return forwardTokenReq(req, res, next)
 }
@@ -53,31 +71,60 @@ async function handleTokenRevocation (req: express.Request, res: express.Respons
   return res.sendStatus(200)
 }
 
-// ---------------------------------------------------------------------------
+async function onExternalUserAuthenticated (options: {
+  npmName: string
+  authName: string
+  authResult: RegisterServerExternalAuthenticatedResult
+}) {
+  const { npmName, authName, authResult } = options
 
-export {
-  oAuthServer,
-  handleIdAndPassLogin,
-  onExternalAuthPlugin,
-  handleTokenRevocation
+  if (!authResult.req || !authResult.res) {
+    logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName)
+    return
+  }
+
+  if (!isAuthResultValid(npmName, authName, authResult)) return
+
+  const { res } = authResult
+
+  logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName)
+
+  const bypassToken = await generateRandomString(32)
+  const tokenLifetime = 1000 * 60 * 5 // 5 minutes
+
+  const expires = new Date()
+  expires.setTime(expires.getTime() + tokenLifetime)
+
+  const user = buildUserResult(authResult)
+  authBypassTokens.set(bypassToken, {
+    expires,
+    user,
+    npmName,
+    authName
+  })
+
+  res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
 }
 
 // ---------------------------------------------------------------------------
 
-function forwardTokenReq (req: express.Request, res: express.Response, next: express.NextFunction) {
+export { oAuthServer, handleIdAndPassLogin, onExternalUserAuthenticated, handleTokenRevocation }
+
+// ---------------------------------------------------------------------------
+
+function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) {
   return oAuthServer.token()(req, res, err => {
     if (err) {
       logger.warn('Login error.', { err })
 
       return res.status(err.status)
-                .json({
-                  error: err.message,
-                  code: err.name
-                })
-                .end()
+        .json({
+          error: err.message,
+          code: err.name
+        })
     }
 
-    return next()
+    if (next) return next()
   })
 }
 
@@ -131,50 +178,96 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
 
     try {
       const loginResult = await authOptions.login(loginOptions)
-      if (loginResult) {
-        logger.info(
-          'Login success with auth method %s of plugin %s for %s.',
-          authName, npmName, loginOptions.id
-        )
-
-        if (!isUserUsernameValid(loginResult.username)) {
-          logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { loginResult })
-          continue
-        }
-
-        if (!loginResult.email) {
-          logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { loginResult })
-          continue
-        }
-
-        // role is optional
-        if (loginResult.role && !isUserRoleValid(loginResult.role)) {
-          logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { loginResult })
-          continue
-        }
-
-        // display name is optional
-        if (loginResult.displayName && !isUserDisplayNameValid(loginResult.displayName)) {
-          logger.error('Auth method %s of plugin %s did not provide a valid display name.', authName, npmName, { loginResult })
-          continue
-        }
-
-        res.locals.bypassLogin = {
-          bypass: true,
-          pluginName: pluginAuth.npmName,
-          authName: authOptions.authName,
-          user: {
-            username: loginResult.username,
-            email: loginResult.email,
-            role: loginResult.role || UserRole.USER,
-            displayName: loginResult.displayName || loginResult.username
-          }
-        }
-
-        return
+
+      if (!loginResult) continue
+      if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue
+
+      logger.info(
+        'Login success with auth method %s of plugin %s for %s.',
+        authName, npmName, loginOptions.id
+      )
+
+      res.locals.bypassLogin = {
+        bypass: true,
+        pluginName: pluginAuth.npmName,
+        authName: authOptions.authName,
+        user: buildUserResult(loginResult)
       }
+
+      return
     } catch (err) {
       logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
     }
   }
 }
+
+function proxifyExternalAuthBypass (req: express.Request, res: express.Response) {
+  const obj = authBypassTokens.get(req.body.externalAuthToken)
+  if (!obj) {
+    logger.error('Cannot authenticate user with unknown bypass token')
+    return res.sendStatus(400)
+  }
+
+  const { expires, user, authName, npmName } = obj
+
+  const now = new Date()
+  if (now.getTime() > expires.getTime()) {
+    logger.error('Cannot authenticate user with an expired bypass token')
+    return res.sendStatus(400)
+  }
+
+  if (user.username !== req.body.username) {
+    logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username)
+    return res.sendStatus(400)
+  }
+
+  // Bypass oauth library validation
+  req.body.password = 'fake'
+
+  logger.info(
+    'Auth success with external auth method %s of plugin %s for %s.',
+    authName, npmName, user.email
+  )
+
+  res.locals.bypassLogin = {
+    bypass: true,
+    pluginName: npmName,
+    authName: authName,
+    user
+  }
+}
+
+function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) {
+  if (!isUserUsernameValid(result.username)) {
+    logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { result })
+    return false
+  }
+
+  if (!result.email) {
+    logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { result })
+    return false
+  }
+
+  // role is optional
+  if (result.role && !isUserRoleValid(result.role)) {
+    logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { result })
+    return false
+  }
+
+  // display name is optional
+  if (result.displayName && !isUserDisplayNameValid(result.displayName)) {
+    logger.error('Auth method %s of plugin %s did not provide a valid display name.', authName, npmName, { result })
+    return false
+  }
+
+  return true
+}
+
+function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
+  return {
+    username: pluginResult.username,
+    email: pluginResult.email,
+    role: pluginResult.role || UserRole.USER,
+    displayName: pluginResult.displayName || pluginResult.username
+  }
+}

+ 1 - 1
server/lib/oauth-model.ts

@@ -98,7 +98,7 @@ async function getRefreshToken (refreshToken: string) {
   return tokenInfo
 }
 
-async function getUser (usernameOrEmail: string, password: string) {
+async function getUser (usernameOrEmail?: string, password?: string) {
   const res: express.Response = this.request.res
 
   // Special treatment coming from a plugin

+ 19 - 23
server/lib/plugins/register-helpers-store.ts

@@ -1,31 +1,21 @@
-import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model'
+import { logger } from '@server/helpers/logger'
+import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PRIVACIES } from '@server/initializers/constants'
+import { onExternalUserAuthenticated } from '@server/lib/auth'
 import { PluginModel } from '@server/models/server/plugin'
+import { RegisterServerOptions } from '@server/typings/plugins'
+import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
+import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model'
 import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model'
+import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model'
 import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model'
-import {
-  VIDEO_CATEGORIES,
-  VIDEO_LANGUAGES,
-  VIDEO_LICENCES,
-  VIDEO_PLAYLIST_PRIVACIES,
-  VIDEO_PRIVACIES
-} from '@server/initializers/constants'
 import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
-import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model'
-import { RegisterServerOptions } from '@server/typings/plugins'
-import { buildPluginHelpers } from './plugin-helpers'
-import { logger } from '@server/helpers/logger'
+import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
+import { RegisterServerAuthExternalOptions, RegisterServerAuthExternalResult, RegisterServerAuthPassOptions, RegisterServerExternalAuthenticatedResult } from '@shared/models/plugins/register-server-auth.model'
 import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
-import { serverHookObject } from '@shared/models/plugins/server-hook.model'
 import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
+import { serverHookObject } from '@shared/models/plugins/server-hook.model'
 import * as express from 'express'
-import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
-import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
-import {
-  RegisterServerAuthExternalOptions,
-  RegisterServerAuthExternalResult,
-  RegisterServerAuthPassOptions
-} from '@shared/models/plugins/register-server-auth.model'
-import { onExternalAuthPlugin } from '@server/lib/auth'
+import { buildPluginHelpers } from './plugin-helpers'
 
 type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
 type VideoConstant = { [key in number | string]: string }
@@ -187,8 +177,14 @@ export class RegisterHelpersStore {
       this.externalAuths.push(options)
 
       return {
-        onAuth (options: { username: string, email: string }): void {
-          onExternalAuthPlugin(self.npmName, options.username, options.email)
+        userAuthenticated (result: RegisterServerExternalAuthenticatedResult): void {
+          onExternalUserAuthenticated({
+            npmName: self.npmName,
+            authName: options.authName,
+            authResult: result
+          }).catch(err => {
+            logger.error('Cannot execute onExternalUserAuthenticated.', { npmName: self.npmName, authName: options.authName, err })
+          })
         }
       } as RegisterServerAuthExternalResult
     }

+ 23 - 2
server/middlewares/validators/plugins.ts

@@ -4,7 +4,7 @@ import { logger } from '../../helpers/logger'
 import { areValidationErrors } from './utils'
 import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
 import { PluginManager } from '../../lib/plugins/plugin-manager'
-import { isBooleanValid, isSafePath, toBooleanOrNull } from '../../helpers/custom-validators/misc'
+import { isBooleanValid, isSafePath, toBooleanOrNull, exists } from '../../helpers/custom-validators/misc'
 import { PluginModel } from '../../models/server/plugin'
 import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
 import { PluginType } from '../../../shared/models/plugins/plugin.type'
@@ -40,6 +40,26 @@ const getPluginValidator = (pluginType: PluginType, withVersion = true) => {
   ])
 }
 
+const getExternalAuthValidator = [
+  param('authName').custom(exists).withMessage('Should have a valid auth name'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking getExternalAuthValidator parameters', { parameters: req.params })
+
+    if (areValidationErrors(req, res)) return
+
+    const plugin = res.locals.registeredPlugin
+    if (!plugin.registerHelpersStore) return res.sendStatus(404)
+
+    const externalAuth = plugin.registerHelpersStore.getExternalAuths().find(a => a.authName === req.params.authName)
+    if (!externalAuth) return res.sendStatus(404)
+
+    res.locals.externalAuth = externalAuth
+
+    return next()
+  }
+]
+
 const pluginStaticDirectoryValidator = [
   param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'),
 
@@ -175,5 +195,6 @@ export {
   listAvailablePluginsValidator,
   existingPluginValidator,
   installOrUpdatePluginValidator,
-  listPluginsValidator
+  listPluginsValidator,
+  getExternalAuthValidator
 }

+ 3 - 0
server/typings/express.ts

@@ -29,6 +29,7 @@ import { MPlugin, MServer } from '@server/typings/models/server'
 import { MServerBlocklist } from './models/server/server-blocklist'
 import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
 import { UserRole } from '@shared/models'
+import { RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model'
 
 declare module 'express' {
   interface Response {
@@ -115,6 +116,8 @@ declare module 'express' {
 
       registeredPlugin?: RegisteredPlugin
 
+      externalAuth?: RegisterServerAuthExternalOptions
+
       plugin?: MPlugin
     }
   }

+ 25 - 15
shared/models/plugins/register-server-auth.model.ts

@@ -1,42 +1,52 @@
 import { UserRole } from '@shared/models'
 import { MOAuthToken } from '@server/typings/models'
+import * as express from 'express'
 
 export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
 
-export interface RegisterServerAuthPassOptions {
+export interface RegisterServerAuthenticatedResult {
+  username: string
+  email: string
+  role?: UserRole
+  displayName?: string
+}
+
+export interface RegisterServerExternalAuthenticatedResult extends RegisterServerAuthenticatedResult {
+  req: express.Request
+  res: express.Response
+}
+
+interface RegisterServerAuthBase {
   // Authentication name (a plugin can register multiple auth strategies)
   authName: string
 
   // Called by PeerTube when a user from your plugin logged out
   onLogout?(): void
 
-  // Weight of this authentication so PeerTube tries the auth methods in DESC weight order
-  getWeight(): number
-
   // Your plugin can hook PeerTube access/refresh token validity
   // So you can control for your plugin the user session lifetime
   hookTokenValidity?(options: { token: MOAuthToken, type: 'access' | 'refresh' }): Promise<{ valid: boolean }>
+}
+
+export interface RegisterServerAuthPassOptions extends RegisterServerAuthBase {
+  // Weight of this authentication so PeerTube tries the auth methods in DESC weight order
+  getWeight(): number
 
   // Used by PeerTube to login a user
   // Returns null if the login failed, or { username, email } on success
   login(body: {
     id: string
     password: string
-  }): Promise<{
-    username: string
-    email: string
-    role?: UserRole
-    displayName?: string
-  } | null>
+  }): Promise<RegisterServerAuthenticatedResult | null>
 }
 
-export interface RegisterServerAuthExternalOptions {
-  // Authentication name (a plugin can register multiple auth strategies)
-  authName: string
+export interface RegisterServerAuthExternalOptions extends RegisterServerAuthBase {
+  // Will be displayed in a block next to the login form
+  authDisplayName: string
 
-  onLogout?: Function
+  onAuthRequest: (req: express.Request, res: express.Response) => void
 }
 
 export interface RegisterServerAuthExternalResult {
-  onAuth (options: { username: string, email: string }): void
+  userAuthenticated (options: RegisterServerExternalAuthenticatedResult): void
 }

+ 1 - 1
shared/models/plugins/register-server-setting.model.ts

@@ -9,7 +9,7 @@ export interface RegisterServerSettingOptions {
   private: boolean
 
   // Default setting value
-  default?: string
+  default?: string | boolean
 }
 
 export interface RegisteredServerSettings {

+ 16 - 0
shared/models/server/server-config.model.ts

@@ -12,6 +12,18 @@ export interface ServerConfigTheme extends ServerConfigPlugin {
   css: string[]
 }
 
+export interface RegisteredExternalAuthConfig {
+  npmName: string
+  authName: string
+  authDisplayName: string
+}
+
+export interface RegisteredIdAndPassAuthConfig {
+  npmName: string
+  authName: string
+  weight: number
+}
+
 export interface ServerConfig {
   serverVersion: string
   serverCommit?: string
@@ -37,6 +49,10 @@ export interface ServerConfig {
 
   plugin: {
     registered: ServerConfigPlugin[]
+
+    registeredExternalAuths: RegisteredExternalAuthConfig[]
+
+    registeredIdAndPassAuths: RegisteredIdAndPassAuthConfig[]
   }
 
   theme: {