Begin support for external auths

This commit is contained in:
Chocobozzz 2020-04-28 14:49:03 +02:00 committed by Chocobozzz
parent 98813e69bc
commit 4a8d113b9b
15 changed files with 397 additions and 175 deletions

View File

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

View File

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

View File

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

View File

@ -3,59 +3,61 @@
Login Login
</div> </div>
<div class="alert alert-info" *ngIf="signupAllowed === false" role="alert"> <ng-container *ngIf="!isAuthenticatedWithExternalAuth">
<h6 class="alert-heading" i18n> <div class="alert alert-info" *ngIf="signupAllowed === false" role="alert">
If you are looking for an account… <h6 class="alert-heading" i18n>
</h6> If you are looking for an account…
</h6>
<div i18n> <div i18n>
Currently this instance doesn't allow for user registration, but you can find an instance 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. 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>. 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>
</div>
<div *ngIf="formErrors.username" class="form-error">
{{ formErrors.username }}
</div> </div>
</div> </div>
<div class="form-group"> <div *ngIf="error" class="alert alert-danger">{{ error }}
<label i18n for="password">Password</label> <span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span>
<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 role="form" (ngSubmit)="login()" [formGroup]="form">
</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 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>
<input type="submit" i18n-value value="Login" [disabled]="!form.valid">
</form>
</ng-container>
</div> </div>
<ng-template #forgotPasswordModal> <ng-template #forgotPasswordModal>

View File

@ -22,6 +22,7 @@ export class LoginComponent extends FormReactive implements OnInit {
error: string = null error: string = null
forgotPasswordEmail = '' forgotPasswordEmail = ''
isAuthenticatedWithExternalAuth = false
private openedForgotPasswordModal: NgbModalRef private openedForgotPasswordModal: NgbModalRef
private serverConfig: ServerConfig private serverConfig: ServerConfig
@ -49,7 +50,14 @@ export class LoginComponent extends FormReactive implements OnInit {
} }
ngOnInit () { 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({ this.buildForm({
username: this.loginValidatorsService.LOGIN_USERNAME, username: this.loginValidatorsService.LOGIN_USERNAME,
@ -68,11 +76,7 @@ export class LoginComponent extends FormReactive implements OnInit {
.subscribe( .subscribe(
() => this.redirectService.redirectToPreviousRoute(), () => this.redirectService.redirectToPreviousRoute(),
err => { err => this.handleError(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
}
) )
} }
@ -99,4 +103,24 @@ export class LoginComponent extends FormReactive implements OnInit {
hideForgotPasswordModal () { hideForgotPasswordModal () {
this.openedForgotPasswordModal.close() 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
}
} }

View File

@ -1,22 +1,22 @@
import { Hooks } from '@server/lib/plugins/hooks'
import * as express from 'express' import * as express from 'express'
import { remove, writeJSON } from 'fs-extra'
import { snakeCase } from 'lodash' 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 { About } from '../../../shared/models/server/about.model'
import { CustomConfig } from '../../../shared/models/server/custom-config.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 { 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 { objectConverter } from '../../helpers/core-utils'
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
import { getServerCommit } from '../../helpers/utils'
import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config' 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 { PluginManager } from '../../lib/plugins/plugin-manager'
import { getThemeOrDefault } from '../../lib/plugins/theme-utils' 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() const configRouter = express.Router()
@ -79,7 +79,9 @@ async function getConfig (req: express.Request, res: express.Response) {
} }
}, },
plugin: { plugin: {
registered: getRegisteredPlugins() registered: getRegisteredPlugins(),
registeredExternalAuths: getExternalAuthsPlugins(),
registeredIdAndPassAuths: getIdAndPassAuthPlugins()
}, },
theme: { theme: {
registered: getRegisteredThemes(), 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 { export {

View File

@ -2,11 +2,12 @@ import * as express from 'express'
import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
import { join } from 'path' import { join } from 'path'
import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' 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 { serveThemeCSSValidator } from '../middlewares/validators/themes'
import { PluginType } from '../../shared/models/plugins/plugin.type' import { PluginType } from '../../shared/models/plugins/plugin.type'
import { isTestInstance } from '../helpers/core-utils' import { isTestInstance } from '../helpers/core-utils'
import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n' import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n'
import { logger } from '@server/helpers/logger'
const sendFileOptions = { const sendFileOptions = {
maxAge: '30 days', maxAge: '30 days',
@ -23,6 +24,12 @@ pluginsRouter.get('/plugins/translations/:locale.json',
getPluginTranslations getPluginTranslations
) )
pluginsRouter.get('/plugins/:pluginName/:pluginVersion/auth/:authName',
getPluginValidator(PluginType.PLUGIN),
getExternalAuthValidator,
handleAuthInPlugin
)
pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
getPluginValidator(PluginType.PLUGIN), getPluginValidator(PluginType.PLUGIN),
pluginStaticDirectoryValidator, pluginStaticDirectoryValidator,
@ -134,3 +141,14 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) 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)
}
}

View File

@ -1,13 +1,18 @@
import * as express from 'express' import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
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 { logger } from '@server/helpers/logger' 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 { revokeToken } from '@server/lib/oauth-model'
import { PluginManager } from '@server/lib/plugins/plugin-manager'
import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 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({ const oAuthServer = new OAuthServer({
useErrorHandler: true, useErrorHandler: true,
@ -17,15 +22,28 @@ const oAuthServer = new OAuthServer({
model: require('./oauth-model') 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) { async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
const grantType = req.body.grant_type const grantType = req.body.grant_type
if (grantType === 'password') await proxifyPasswordGrant(req, res) if (grantType === 'password') {
else if (grantType === 'refresh_token') await proxifyRefreshGrant(req, res) 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) return forwardTokenReq(req, res, next)
} }
@ -53,31 +71,60 @@ async function handleTokenRevocation (req: express.Request, res: express.Respons
return res.sendStatus(200) return res.sendStatus(200)
} }
// --------------------------------------------------------------------------- async function onExternalUserAuthenticated (options: {
npmName: string
authName: string
authResult: RegisterServerExternalAuthenticatedResult
}) {
const { npmName, authName, authResult } = options
export { if (!authResult.req || !authResult.res) {
oAuthServer, logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName)
handleIdAndPassLogin, return
onExternalAuthPlugin, }
handleTokenRevocation
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 => { return oAuthServer.token()(req, res, err => {
if (err) { if (err) {
logger.warn('Login error.', { err }) logger.warn('Login error.', { err })
return res.status(err.status) return res.status(err.status)
.json({ .json({
error: err.message, error: err.message,
code: err.name code: err.name
}) })
.end()
} }
return next() if (next) return next()
}) })
} }
@ -131,50 +178,96 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
try { try {
const loginResult = await authOptions.login(loginOptions) 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)) { if (!loginResult) continue
logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { loginResult }) if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue
continue
}
if (!loginResult.email) { logger.info(
logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { loginResult }) 'Login success with auth method %s of plugin %s for %s.',
continue authName, npmName, loginOptions.id
} )
// role is optional res.locals.bypassLogin = {
if (loginResult.role && !isUserRoleValid(loginResult.role)) { bypass: true,
logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { loginResult }) pluginName: pluginAuth.npmName,
continue authName: authOptions.authName,
} user: buildUserResult(loginResult)
// 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
} }
return
} catch (err) { } catch (err) {
logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { 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
}
}

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { logger } from '../../helpers/logger'
import { areValidationErrors } from './utils' import { areValidationErrors } from './utils'
import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
import { PluginManager } from '../../lib/plugins/plugin-manager' 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 { PluginModel } from '../../models/server/plugin'
import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
import { PluginType } from '../../../shared/models/plugins/plugin.type' 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 = [ const pluginStaticDirectoryValidator = [
param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'), param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'),
@ -175,5 +195,6 @@ export {
listAvailablePluginsValidator, listAvailablePluginsValidator,
existingPluginValidator, existingPluginValidator,
installOrUpdatePluginValidator, installOrUpdatePluginValidator,
listPluginsValidator listPluginsValidator,
getExternalAuthValidator
} }

View File

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

View File

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

View File

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

View File

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