Begin support for external auths
This commit is contained in:
parent
98813e69bc
commit
4a8d113b9b
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -54,7 +54,9 @@ export class ServerService {
|
|||
}
|
||||
},
|
||||
plugin: {
|
||||
registered: []
|
||||
registered: [],
|
||||
registeredExternalAuths: [],
|
||||
registeredIdAndPassAuths: []
|
||||
},
|
||||
theme: {
|
||||
registered: [],
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export interface RegisterServerSettingOptions {
|
|||
private: boolean
|
||||
|
||||
// Default setting value
|
||||
default?: string
|
||||
default?: string | boolean
|
||||
}
|
||||
|
||||
export interface RegisteredServerSettings {
|
||||
|
|
|
@ -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…
Reference in New Issue