Browse Source

Begin support for external auths

tags/v2.2.0-rc.1
Chocobozzz Chocobozzz 7 months ago
parent
commit
4a8d113b9b
15 changed files with 393 additions and 171 deletions
  1. +1
    -0
      .eslintrc.json
  2. +3
    -1
      client/src/app/core/auth/auth.service.ts
  3. +3
    -1
      client/src/app/core/server/server.service.ts
  4. +45
    -43
      client/src/app/login/login.component.html
  5. +30
    -6
      client/src/app/login/login.component.ts
  6. +45
    -11
      server/controllers/api/config.ts
  7. +19
    -1
      server/controllers/plugins.ts
  8. +159
    -66
      server/lib/auth.ts
  9. +1
    -1
      server/lib/oauth-model.ts
  10. +19
    -23
      server/lib/plugins/register-helpers-store.ts
  11. +23
    -2
      server/middlewares/validators/plugins.ts
  12. +3
    -0
      server/typings/express.ts
  13. +25
    -15
      shared/models/plugins/register-server-auth.model.ts
  14. +1
    -1
      shared/models/plugins/register-server-setting.model.ts
  15. +16
    -0
      shared/models/server/server-config.model.ts

+ 1
- 0
.eslintrc.json 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"


+ 3
- 1
client/src/app/core/auth/auth.service.ts 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(


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

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


+ 45
- 43
client/src/app/login/login.component.html 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>
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 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
}
}

+ 45
- 11
server/controllers/api/config.ts 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 {


+ 19
- 1
server/controllers/plugins.ts 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)
}
}

+ 159
- 66
server/lib/auth.ts 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.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 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


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

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

+ 3
- 0
server/typings/express.ts 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
}
}


+ 25
- 15
shared/models/plugins/register-server-auth.model.ts 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
}

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

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


Loading…
Cancel
Save