Begin support for external auths

pull/2737/head
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/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"

View File

@ -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(

View File

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

View File

@ -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>
</div>
<div *ngIf="formErrors.username" class="form-error">
{{ formErrors.username }}
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 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 *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>
<input type="submit" i18n-value value="Login" [disabled]="!form.valid">
</form>
<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 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>
<ng-template #forgotPasswordModal>

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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) continue
if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue
if (!loginResult.email) {
logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { loginResult })
continue
}
logger.info(
'Login success with auth method %s of plugin %s for %s.',
authName, npmName, loginOptions.id
)
// 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
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
}
}

View File

@ -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

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 { 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 * as express from 'express'
import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
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 {
RegisterServerAuthExternalOptions,
RegisterServerAuthExternalResult,
RegisterServerAuthPassOptions
} from '@shared/models/plugins/register-server-auth.model'
import { onExternalAuthPlugin } from '@server/lib/auth'
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 { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
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 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
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

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

View File

@ -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: {