aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json1
-rw-r--r--client/src/app/core/auth/auth.service.ts4
-rw-r--r--client/src/app/core/server/server.service.ts4
-rw-r--r--client/src/app/login/login.component.html88
-rw-r--r--client/src/app/login/login.component.ts36
-rw-r--r--server/controllers/api/config.ts56
-rw-r--r--server/controllers/plugins.ts20
-rw-r--r--server/lib/auth.ts225
-rw-r--r--server/lib/oauth-model.ts2
-rw-r--r--server/lib/plugins/register-helpers-store.ts42
-rw-r--r--server/middlewares/validators/plugins.ts25
-rw-r--r--server/typings/express.ts3
-rw-r--r--shared/models/plugins/register-server-auth.model.ts40
-rw-r--r--shared/models/plugins/register-server-setting.model.ts2
-rw-r--r--shared/models/server/server-config.model.ts16
15 files changed, 393 insertions, 171 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
index 9b578b186..e71be9bc5 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -83,6 +83,7 @@
83 "@typescript-eslint/consistent-type-definitions": "off", 83 "@typescript-eslint/consistent-type-definitions": "off",
84 "@typescript-eslint/no-misused-promises": "off", 84 "@typescript-eslint/no-misused-promises": "off",
85 "@typescript-eslint/no-namespace": "off", 85 "@typescript-eslint/no-namespace": "off",
86 "@typescript-eslint/no-empty-interface": "off",
86 "@typescript-eslint/no-extraneous-class": "off", 87 "@typescript-eslint/no-extraneous-class": "off",
87 // bugged but useful 88 // bugged but useful
88 "@typescript-eslint/restrict-plus-operands": "off" 89 "@typescript-eslint/restrict-plus-operands": "off"
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index 9ae008e39..61d755ba0 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -145,7 +145,7 @@ export class AuthService {
145 return !!this.getAccessToken() 145 return !!this.getAccessToken()
146 } 146 }
147 147
148 login (username: string, password: string) { 148 login (username: string, password: string, token?: string) {
149 // Form url encoded 149 // Form url encoded
150 const body = { 150 const body = {
151 client_id: this.clientId, 151 client_id: this.clientId,
@@ -157,6 +157,8 @@ export class AuthService {
157 password 157 password
158 } 158 }
159 159
160 if (token) Object.assign(body, { externalAuthToken: token })
161
160 const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') 162 const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
161 return this.http.post<UserLogin>(AuthService.BASE_TOKEN_URL, objectToUrlEncoded(body), { headers }) 163 return this.http.post<UserLogin>(AuthService.BASE_TOKEN_URL, objectToUrlEncoded(body), { headers })
162 .pipe( 164 .pipe(
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index da7832b32..eac8f85e4 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -54,7 +54,9 @@ export class ServerService {
54 } 54 }
55 }, 55 },
56 plugin: { 56 plugin: {
57 registered: [] 57 registered: [],
58 registeredExternalAuths: [],
59 registeredIdAndPassAuths: []
58 }, 60 },
59 theme: { 61 theme: {
60 registered: [], 62 registered: [],
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html
index 3a2d4b876..3e53e5854 100644
--- a/client/src/app/login/login.component.html
+++ b/client/src/app/login/login.component.html
@@ -3,59 +3,61 @@
3 Login 3 Login
4 </div> 4 </div>
5 5
6 <div class="alert alert-info" *ngIf="signupAllowed === false" role="alert"> 6 <ng-container *ngIf="!isAuthenticatedWithExternalAuth">
7 <h6 class="alert-heading" i18n> 7 <div class="alert alert-info" *ngIf="signupAllowed === false" role="alert">
8 If you are looking for an account… 8 <h6 class="alert-heading" i18n>
9 </h6> 9 If you are looking for an account…
10 </h6>
10 11
11 <div i18n> 12 <div i18n>
12 Currently this instance doesn't allow for user registration, but you can find an instance 13 Currently this instance doesn't allow for user registration, but you can find an instance
13 that gives you the possibility to sign up for an account and upload your videos there. 14 that gives you the possibility to sign up for an account and upload your videos there.
14 15
15 <br /> 16 <br />
16 17
17 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>. 18 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>.
18 </div>
19 </div>
20
21 <div *ngIf="error" class="alert alert-danger">{{ error }}
22 <span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span>
23 </div>
24
25 <form role="form" (ngSubmit)="login()" [formGroup]="form">
26 <div class="form-group">
27 <div>
28 <label i18n for="username">User</label>
29 <input
30 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1"
31 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #emailInput
32 >
33 <a i18n *ngIf="signupAllowed === true" routerLink="/signup" class="create-an-account">
34 or create an account
35 </a>
36 </div> 19 </div>
20 </div>
37 21
38 <div *ngIf="formErrors.username" class="form-error"> 22 <div *ngIf="error" class="alert alert-danger">{{ error }}
39 {{ formErrors.username }} 23 <span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span>
40 </div>
41 </div> 24 </div>
42 25
43 <div class="form-group"> 26 <form role="form" (ngSubmit)="login()" [formGroup]="form">
44 <label i18n for="password">Password</label> 27 <div class="form-group">
45 <div> 28 <div>
46 <input 29 <label i18n for="username">User</label>
47 type="password" name="password" id="password" i18n-placeholder placeholder="Password" required tabindex="2" autocomplete="current-password" 30 <input
48 formControlName="password" class="form-control" [ngClass]="{ 'input-error': formErrors['password'] }" 31 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1"
49 > 32 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #emailInput
50 <a i18n-title class="forgot-password-button" (click)="openForgotPasswordModal()" title="Click here to reset your password">I forgot my password</a> 33 >
34 <a i18n *ngIf="signupAllowed === true" routerLink="/signup" class="create-an-account">
35 or create an account
36 </a>
37 </div>
38
39 <div *ngIf="formErrors.username" class="form-error">
40 {{ formErrors.username }}
41 </div>
51 </div> 42 </div>
52 <div *ngIf="formErrors.password" class="form-error"> 43
53 {{ formErrors.password }} 44 <div class="form-group">
45 <label i18n for="password">Password</label>
46 <div>
47 <input
48 type="password" name="password" id="password" i18n-placeholder placeholder="Password" required tabindex="2" autocomplete="current-password"
49 formControlName="password" class="form-control" [ngClass]="{ 'input-error': formErrors['password'] }"
50 >
51 <a i18n-title class="forgot-password-button" (click)="openForgotPasswordModal()" title="Click here to reset your password">I forgot my password</a>
52 </div>
53 <div *ngIf="formErrors.password" class="form-error">
54 {{ formErrors.password }}
55 </div>
54 </div> 56 </div>
55 </div>
56 57
57 <input type="submit" i18n-value value="Login" [disabled]="!form.valid"> 58 <input type="submit" i18n-value value="Login" [disabled]="!form.valid">
58 </form> 59 </form>
60 </ng-container>
59</div> 61</div>
60 62
61<ng-template #forgotPasswordModal> 63<ng-template #forgotPasswordModal>
diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts
index 580f28822..9c8f5c52e 100644
--- a/client/src/app/login/login.component.ts
+++ b/client/src/app/login/login.component.ts
@@ -22,6 +22,7 @@ export class LoginComponent extends FormReactive implements OnInit {
22 22
23 error: string = null 23 error: string = null
24 forgotPasswordEmail = '' 24 forgotPasswordEmail = ''
25 isAuthenticatedWithExternalAuth = false
25 26
26 private openedForgotPasswordModal: NgbModalRef 27 private openedForgotPasswordModal: NgbModalRef
27 private serverConfig: ServerConfig 28 private serverConfig: ServerConfig
@@ -49,7 +50,14 @@ export class LoginComponent extends FormReactive implements OnInit {
49 } 50 }
50 51
51 ngOnInit () { 52 ngOnInit () {
52 this.serverConfig = this.route.snapshot.data.serverConfig 53 const snapshot = this.route.snapshot
54
55 this.serverConfig = snapshot.data.serverConfig
56
57 if (snapshot.queryParams.externalAuthToken) {
58 this.loadExternalAuthToken(snapshot.queryParams.username, snapshot.queryParams.externalAuthToken)
59 return
60 }
53 61
54 this.buildForm({ 62 this.buildForm({
55 username: this.loginValidatorsService.LOGIN_USERNAME, 63 username: this.loginValidatorsService.LOGIN_USERNAME,
@@ -68,11 +76,7 @@ export class LoginComponent extends FormReactive implements OnInit {
68 .subscribe( 76 .subscribe(
69 () => this.redirectService.redirectToPreviousRoute(), 77 () => this.redirectService.redirectToPreviousRoute(),
70 78
71 err => { 79 err => this.handleError(err)
72 if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.')
73 else if (err.message.indexOf('blocked') !== -1) this.error = this.i18n('You account is blocked.')
74 else this.error = err.message
75 }
76 ) 80 )
77 } 81 }
78 82
@@ -99,4 +103,24 @@ export class LoginComponent extends FormReactive implements OnInit {
99 hideForgotPasswordModal () { 103 hideForgotPasswordModal () {
100 this.openedForgotPasswordModal.close() 104 this.openedForgotPasswordModal.close()
101 } 105 }
106
107 private loadExternalAuthToken (username: string, token: string) {
108 this.isAuthenticatedWithExternalAuth = true
109
110 this.authService.login(username, null, token)
111 .subscribe(
112 () => this.redirectService.redirectToPreviousRoute(),
113
114 err => {
115 this.handleError(err)
116 this.isAuthenticatedWithExternalAuth = false
117 }
118 )
119 }
120
121 private handleError (err: any) {
122 if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.')
123 else if (err.message.indexOf('blocked') !== -1) this.error = this.i18n('You account is blocked.')
124 else this.error = err.message
125 }
102} 126}
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 06fe30371..e8941bc73 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -1,22 +1,22 @@
1import { Hooks } from '@server/lib/plugins/hooks'
1import * as express from 'express' 2import * as express from 'express'
3import { remove, writeJSON } from 'fs-extra'
2import { snakeCase } from 'lodash' 4import { snakeCase } from 'lodash'
3import { ServerConfig, UserRight } from '../../../shared' 5import validator from 'validator'
6import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig, UserRight } from '../../../shared'
4import { About } from '../../../shared/models/server/about.model' 7import { About } from '../../../shared/models/server/about.model'
5import { CustomConfig } from '../../../shared/models/server/custom-config.model' 8import { CustomConfig } from '../../../shared/models/server/custom-config.model'
6import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
7import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants'
8import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
9import { customConfigUpdateValidator } from '../../middlewares/validators/config'
10import { ClientHtml } from '../../lib/client-html'
11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' 9import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
12import { remove, writeJSON } from 'fs-extra'
13import { getServerCommit } from '../../helpers/utils'
14import validator from 'validator'
15import { objectConverter } from '../../helpers/core-utils' 10import { objectConverter } from '../../helpers/core-utils'
11import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
12import { getServerCommit } from '../../helpers/utils'
16import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config' 13import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config'
14import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants'
15import { ClientHtml } from '../../lib/client-html'
17import { PluginManager } from '../../lib/plugins/plugin-manager' 16import { PluginManager } from '../../lib/plugins/plugin-manager'
18import { getThemeOrDefault } from '../../lib/plugins/theme-utils' 17import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
19import { Hooks } from '@server/lib/plugins/hooks' 18import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
19import { customConfigUpdateValidator } from '../../middlewares/validators/config'
20 20
21const configRouter = express.Router() 21const configRouter = express.Router()
22 22
@@ -79,7 +79,9 @@ async function getConfig (req: express.Request, res: express.Response) {
79 } 79 }
80 }, 80 },
81 plugin: { 81 plugin: {
82 registered: getRegisteredPlugins() 82 registered: getRegisteredPlugins(),
83 registeredExternalAuths: getExternalAuthsPlugins(),
84 registeredIdAndPassAuths: getIdAndPassAuthPlugins()
83 }, 85 },
84 theme: { 86 theme: {
85 registered: getRegisteredThemes(), 87 registered: getRegisteredThemes(),
@@ -269,6 +271,38 @@ function getRegisteredPlugins () {
269 })) 271 }))
270} 272}
271 273
274function getIdAndPassAuthPlugins () {
275 const result: RegisteredIdAndPassAuthConfig[] = []
276
277 for (const p of PluginManager.Instance.getIdAndPassAuths()) {
278 for (const auth of p.idAndPassAuths) {
279 result.push({
280 npmName: p.npmName,
281 authName: auth.authName,
282 weight: auth.getWeight()
283 })
284 }
285 }
286
287 return result
288}
289
290function getExternalAuthsPlugins () {
291 const result: RegisteredExternalAuthConfig[] = []
292
293 for (const p of PluginManager.Instance.getExternalAuths()) {
294 for (const auth of p.externalAuths) {
295 result.push({
296 npmName: p.npmName,
297 authName: auth.authName,
298 authDisplayName: auth.authDisplayName
299 })
300 }
301 }
302
303 return result
304}
305
272// --------------------------------------------------------------------------- 306// ---------------------------------------------------------------------------
273 307
274export { 308export {
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts
index 1fc49b646..f12e1c0f5 100644
--- a/server/controllers/plugins.ts
+++ b/server/controllers/plugins.ts
@@ -2,11 +2,12 @@ import * as express from 'express'
2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' 2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
3import { join } from 'path' 3import { join } from 'path'
4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' 4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
5import { getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins' 5import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins'
6import { serveThemeCSSValidator } from '../middlewares/validators/themes' 6import { serveThemeCSSValidator } from '../middlewares/validators/themes'
7import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
8import { isTestInstance } from '../helpers/core-utils' 8import { isTestInstance } from '../helpers/core-utils'
9import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n' 9import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n'
10import { logger } from '@server/helpers/logger'
10 11
11const sendFileOptions = { 12const sendFileOptions = {
12 maxAge: '30 days', 13 maxAge: '30 days',
@@ -23,6 +24,12 @@ pluginsRouter.get('/plugins/translations/:locale.json',
23 getPluginTranslations 24 getPluginTranslations
24) 25)
25 26
27pluginsRouter.get('/plugins/:pluginName/:pluginVersion/auth/:authName',
28 getPluginValidator(PluginType.PLUGIN),
29 getExternalAuthValidator,
30 handleAuthInPlugin
31)
32
26pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', 33pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
27 getPluginValidator(PluginType.PLUGIN), 34 getPluginValidator(PluginType.PLUGIN),
28 pluginStaticDirectoryValidator, 35 pluginStaticDirectoryValidator,
@@ -134,3 +141,14 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
134 141
135 return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) 142 return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions)
136} 143}
144
145function handleAuthInPlugin (req: express.Request, res: express.Response) {
146 const authOptions = res.locals.externalAuth
147
148 try {
149 logger.debug('Forwarding auth plugin request in %s of plugin %s.', authOptions.authName, res.locals.registeredPlugin.npmName)
150 authOptions.onAuthRequest(req, res)
151 } catch (err) {
152 logger.error('Forward request error in auth %s of plugin %s.', authOptions.authName, res.locals.registeredPlugin.npmName)
153 }
154}
diff --git a/server/lib/auth.ts b/server/lib/auth.ts
index 5a6dd9dec..eaae5fdf3 100644
--- a/server/lib/auth.ts
+++ b/server/lib/auth.ts
@@ -1,13 +1,18 @@
1import * as express from 'express' 1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
2import { OAUTH_LIFETIME } from '@server/initializers/constants'
3import * as OAuthServer from 'express-oauth-server'
4import { PluginManager } from '@server/lib/plugins/plugin-manager'
5import { RegisterServerAuthPassOptions } from '@shared/models/plugins/register-server-auth.model'
6import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
7import { UserRole } from '@shared/models' 3import { generateRandomString } from '@server/helpers/utils'
4import { OAUTH_LIFETIME, WEBSERVER } from '@server/initializers/constants'
8import { revokeToken } from '@server/lib/oauth-model' 5import { revokeToken } from '@server/lib/oauth-model'
6import { PluginManager } from '@server/lib/plugins/plugin-manager'
9import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 7import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
10import { isUserUsernameValid, isUserRoleValid, isUserDisplayNameValid } from '@server/helpers/custom-validators/users' 8import { UserRole } from '@shared/models'
9import {
10 RegisterServerAuthenticatedResult,
11 RegisterServerAuthPassOptions,
12 RegisterServerExternalAuthenticatedResult
13} from '@shared/models/plugins/register-server-auth.model'
14import * as express from 'express'
15import * as OAuthServer from 'express-oauth-server'
11 16
12const oAuthServer = new OAuthServer({ 17const oAuthServer = new OAuthServer({
13 useErrorHandler: true, 18 useErrorHandler: true,
@@ -17,15 +22,28 @@ const oAuthServer = new OAuthServer({
17 model: require('./oauth-model') 22 model: require('./oauth-model')
18}) 23})
19 24
20function onExternalAuthPlugin (npmName: string, username: string, email: string) { 25// Token is the key, expiration date is the value
21 26const authBypassTokens = new Map<string, {
22} 27 expires: Date
28 user: {
29 username: string
30 email: string
31 displayName: string
32 role: UserRole
33 }
34 authName: string
35 npmName: string
36}>()
23 37
24async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) { 38async function handleIdAndPassLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
25 const grantType = req.body.grant_type 39 const grantType = req.body.grant_type
26 40
27 if (grantType === 'password') await proxifyPasswordGrant(req, res) 41 if (grantType === 'password') {
28 else if (grantType === 'refresh_token') await proxifyRefreshGrant(req, res) 42 if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res)
43 else await proxifyPasswordGrant(req, res)
44 } else if (grantType === 'refresh_token') {
45 await proxifyRefreshGrant(req, res)
46 }
29 47
30 return forwardTokenReq(req, res, next) 48 return forwardTokenReq(req, res, next)
31} 49}
@@ -53,31 +71,60 @@ async function handleTokenRevocation (req: express.Request, res: express.Respons
53 return res.sendStatus(200) 71 return res.sendStatus(200)
54} 72}
55 73
56// --------------------------------------------------------------------------- 74async function onExternalUserAuthenticated (options: {
75 npmName: string
76 authName: string
77 authResult: RegisterServerExternalAuthenticatedResult
78}) {
79 const { npmName, authName, authResult } = options
57 80
58export { 81 if (!authResult.req || !authResult.res) {
59 oAuthServer, 82 logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName)
60 handleIdAndPassLogin, 83 return
61 onExternalAuthPlugin, 84 }
62 handleTokenRevocation 85
86 if (!isAuthResultValid(npmName, authName, authResult)) return
87
88 const { res } = authResult
89
90 logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName)
91
92 const bypassToken = await generateRandomString(32)
93 const tokenLifetime = 1000 * 60 * 5 // 5 minutes
94
95 const expires = new Date()
96 expires.setTime(expires.getTime() + tokenLifetime)
97
98 const user = buildUserResult(authResult)
99 authBypassTokens.set(bypassToken, {
100 expires,
101 user,
102 npmName,
103 authName
104 })
105
106 res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
63} 107}
64 108
65// --------------------------------------------------------------------------- 109// ---------------------------------------------------------------------------
66 110
67function forwardTokenReq (req: express.Request, res: express.Response, next: express.NextFunction) { 111export { oAuthServer, handleIdAndPassLogin, onExternalUserAuthenticated, handleTokenRevocation }
112
113// ---------------------------------------------------------------------------
114
115function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) {
68 return oAuthServer.token()(req, res, err => { 116 return oAuthServer.token()(req, res, err => {
69 if (err) { 117 if (err) {
70 logger.warn('Login error.', { err }) 118 logger.warn('Login error.', { err })
71 119
72 return res.status(err.status) 120 return res.status(err.status)
73 .json({ 121 .json({
74 error: err.message, 122 error: err.message,
75 code: err.name 123 code: err.name
76 }) 124 })
77 .end()
78 } 125 }
79 126
80 return next() 127 if (next) return next()
81 }) 128 })
82} 129}
83 130
@@ -131,50 +178,96 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
131 178
132 try { 179 try {
133 const loginResult = await authOptions.login(loginOptions) 180 const loginResult = await authOptions.login(loginOptions)
134 if (loginResult) { 181
135 logger.info( 182 if (!loginResult) continue
136 'Login success with auth method %s of plugin %s for %s.', 183 if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue
137 authName, npmName, loginOptions.id 184
138 ) 185 logger.info(
139 186 'Login success with auth method %s of plugin %s for %s.',
140 if (!isUserUsernameValid(loginResult.username)) { 187 authName, npmName, loginOptions.id
141 logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { loginResult }) 188 )
142 continue 189
143 } 190 res.locals.bypassLogin = {
144 191 bypass: true,
145 if (!loginResult.email) { 192 pluginName: pluginAuth.npmName,
146 logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { loginResult }) 193 authName: authOptions.authName,
147 continue 194 user: buildUserResult(loginResult)
148 }
149
150 // role is optional
151 if (loginResult.role && !isUserRoleValid(loginResult.role)) {
152 logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { loginResult })
153 continue
154 }
155
156 // display name is optional
157 if (loginResult.displayName && !isUserDisplayNameValid(loginResult.displayName)) {
158 logger.error('Auth method %s of plugin %s did not provide a valid display name.', authName, npmName, { loginResult })
159 continue
160 }
161
162 res.locals.bypassLogin = {
163 bypass: true,
164 pluginName: pluginAuth.npmName,
165 authName: authOptions.authName,
166 user: {
167 username: loginResult.username,
168 email: loginResult.email,
169 role: loginResult.role || UserRole.USER,
170 displayName: loginResult.displayName || loginResult.username
171 }
172 }
173
174 return
175 } 195 }
196
197 return
176 } catch (err) { 198 } catch (err) {
177 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) 199 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
178 } 200 }
179 } 201 }
180} 202}
203
204function proxifyExternalAuthBypass (req: express.Request, res: express.Response) {
205 const obj = authBypassTokens.get(req.body.externalAuthToken)
206 if (!obj) {
207 logger.error('Cannot authenticate user with unknown bypass token')
208 return res.sendStatus(400)
209 }
210
211 const { expires, user, authName, npmName } = obj
212
213 const now = new Date()
214 if (now.getTime() > expires.getTime()) {
215 logger.error('Cannot authenticate user with an expired bypass token')
216 return res.sendStatus(400)
217 }
218
219 if (user.username !== req.body.username) {
220 logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username)
221 return res.sendStatus(400)
222 }
223
224 // Bypass oauth library validation
225 req.body.password = 'fake'
226
227 logger.info(
228 'Auth success with external auth method %s of plugin %s for %s.',
229 authName, npmName, user.email
230 )
231
232 res.locals.bypassLogin = {
233 bypass: true,
234 pluginName: npmName,
235 authName: authName,
236 user
237 }
238}
239
240function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) {
241 if (!isUserUsernameValid(result.username)) {
242 logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { result })
243 return false
244 }
245
246 if (!result.email) {
247 logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { result })
248 return false
249 }
250
251 // role is optional
252 if (result.role && !isUserRoleValid(result.role)) {
253 logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { result })
254 return false
255 }
256
257 // display name is optional
258 if (result.displayName && !isUserDisplayNameValid(result.displayName)) {
259 logger.error('Auth method %s of plugin %s did not provide a valid display name.', authName, npmName, { result })
260 return false
261 }
262
263 return true
264}
265
266function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
267 return {
268 username: pluginResult.username,
269 email: pluginResult.email,
270 role: pluginResult.role || UserRole.USER,
271 displayName: pluginResult.displayName || pluginResult.username
272 }
273}
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index 6eb0e4473..8b9975bb4 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -98,7 +98,7 @@ async function getRefreshToken (refreshToken: string) {
98 return tokenInfo 98 return tokenInfo
99} 99}
100 100
101async function getUser (usernameOrEmail: string, password: string) { 101async function getUser (usernameOrEmail?: string, password?: string) {
102 const res: express.Response = this.request.res 102 const res: express.Response = this.request.res
103 103
104 // Special treatment coming from a plugin 104 // Special treatment coming from a plugin
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts
index 687974ccf..277f2b687 100644
--- a/server/lib/plugins/register-helpers-store.ts
+++ b/server/lib/plugins/register-helpers-store.ts
@@ -1,31 +1,21 @@
1import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model' 1import { logger } from '@server/helpers/logger'
2import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PRIVACIES } from '@server/initializers/constants'
3import { onExternalUserAuthenticated } from '@server/lib/auth'
2import { PluginModel } from '@server/models/server/plugin' 4import { PluginModel } from '@server/models/server/plugin'
5import { RegisterServerOptions } from '@server/typings/plugins'
6import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
7import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model'
3import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model' 8import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model'
9import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model'
4import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model' 10import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model'
5import {
6 VIDEO_CATEGORIES,
7 VIDEO_LANGUAGES,
8 VIDEO_LICENCES,
9 VIDEO_PLAYLIST_PRIVACIES,
10 VIDEO_PRIVACIES
11} from '@server/initializers/constants'
12import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model' 11import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
13import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model' 12import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
14import { RegisterServerOptions } from '@server/typings/plugins' 13import { RegisterServerAuthExternalOptions, RegisterServerAuthExternalResult, RegisterServerAuthPassOptions, RegisterServerExternalAuthenticatedResult } from '@shared/models/plugins/register-server-auth.model'
15import { buildPluginHelpers } from './plugin-helpers'
16import { logger } from '@server/helpers/logger'
17import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model' 14import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
18import { serverHookObject } from '@shared/models/plugins/server-hook.model'
19import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model' 15import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
16import { serverHookObject } from '@shared/models/plugins/server-hook.model'
20import * as express from 'express' 17import * as express from 'express'
21import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model' 18import { buildPluginHelpers } from './plugin-helpers'
22import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
23import {
24 RegisterServerAuthExternalOptions,
25 RegisterServerAuthExternalResult,
26 RegisterServerAuthPassOptions
27} from '@shared/models/plugins/register-server-auth.model'
28import { onExternalAuthPlugin } from '@server/lib/auth'
29 19
30type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' 20type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
31type VideoConstant = { [key in number | string]: string } 21type VideoConstant = { [key in number | string]: string }
@@ -187,8 +177,14 @@ export class RegisterHelpersStore {
187 this.externalAuths.push(options) 177 this.externalAuths.push(options)
188 178
189 return { 179 return {
190 onAuth (options: { username: string, email: string }): void { 180 userAuthenticated (result: RegisterServerExternalAuthenticatedResult): void {
191 onExternalAuthPlugin(self.npmName, options.username, options.email) 181 onExternalUserAuthenticated({
182 npmName: self.npmName,
183 authName: options.authName,
184 authResult: result
185 }).catch(err => {
186 logger.error('Cannot execute onExternalUserAuthenticated.', { npmName: self.npmName, authName: options.authName, err })
187 })
192 } 188 }
193 } as RegisterServerAuthExternalResult 189 } as RegisterServerAuthExternalResult
194 } 190 }
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
index 65765f473..2cb49ec43 100644
--- a/server/middlewares/validators/plugins.ts
+++ b/server/middlewares/validators/plugins.ts
@@ -4,7 +4,7 @@ import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils' 4import { areValidationErrors } from './utils'
5import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' 5import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
6import { PluginManager } from '../../lib/plugins/plugin-manager' 6import { PluginManager } from '../../lib/plugins/plugin-manager'
7import { isBooleanValid, isSafePath, toBooleanOrNull } from '../../helpers/custom-validators/misc' 7import { isBooleanValid, isSafePath, toBooleanOrNull, exists } from '../../helpers/custom-validators/misc'
8import { PluginModel } from '../../models/server/plugin' 8import { PluginModel } from '../../models/server/plugin'
9import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' 9import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
10import { PluginType } from '../../../shared/models/plugins/plugin.type' 10import { PluginType } from '../../../shared/models/plugins/plugin.type'
@@ -40,6 +40,26 @@ const getPluginValidator = (pluginType: PluginType, withVersion = true) => {
40 ]) 40 ])
41} 41}
42 42
43const getExternalAuthValidator = [
44 param('authName').custom(exists).withMessage('Should have a valid auth name'),
45
46 (req: express.Request, res: express.Response, next: express.NextFunction) => {
47 logger.debug('Checking getExternalAuthValidator parameters', { parameters: req.params })
48
49 if (areValidationErrors(req, res)) return
50
51 const plugin = res.locals.registeredPlugin
52 if (!plugin.registerHelpersStore) return res.sendStatus(404)
53
54 const externalAuth = plugin.registerHelpersStore.getExternalAuths().find(a => a.authName === req.params.authName)
55 if (!externalAuth) return res.sendStatus(404)
56
57 res.locals.externalAuth = externalAuth
58
59 return next()
60 }
61]
62
43const pluginStaticDirectoryValidator = [ 63const pluginStaticDirectoryValidator = [
44 param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'), 64 param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'),
45 65
@@ -175,5 +195,6 @@ export {
175 listAvailablePluginsValidator, 195 listAvailablePluginsValidator,
176 existingPluginValidator, 196 existingPluginValidator,
177 installOrUpdatePluginValidator, 197 installOrUpdatePluginValidator,
178 listPluginsValidator 198 listPluginsValidator,
199 getExternalAuthValidator
179} 200}
diff --git a/server/typings/express.ts b/server/typings/express.ts
index e6e120403..5973496f1 100644
--- a/server/typings/express.ts
+++ b/server/typings/express.ts
@@ -29,6 +29,7 @@ import { MPlugin, MServer } from '@server/typings/models/server'
29import { MServerBlocklist } from './models/server/server-blocklist' 29import { MServerBlocklist } from './models/server/server-blocklist'
30import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token' 30import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
31import { UserRole } from '@shared/models' 31import { UserRole } from '@shared/models'
32import { RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model'
32 33
33declare module 'express' { 34declare module 'express' {
34 interface Response { 35 interface Response {
@@ -115,6 +116,8 @@ declare module 'express' {
115 116
116 registeredPlugin?: RegisteredPlugin 117 registeredPlugin?: RegisteredPlugin
117 118
119 externalAuth?: RegisterServerAuthExternalOptions
120
118 plugin?: MPlugin 121 plugin?: MPlugin
119 } 122 }
120 } 123 }
diff --git a/shared/models/plugins/register-server-auth.model.ts b/shared/models/plugins/register-server-auth.model.ts
index 403a49994..08053f017 100644
--- a/shared/models/plugins/register-server-auth.model.ts
+++ b/shared/models/plugins/register-server-auth.model.ts
@@ -1,42 +1,52 @@
1import { UserRole } from '@shared/models' 1import { UserRole } from '@shared/models'
2import { MOAuthToken } from '@server/typings/models' 2import { MOAuthToken } from '@server/typings/models'
3import * as express from 'express'
3 4
4export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions 5export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
5 6
6export interface RegisterServerAuthPassOptions { 7export interface RegisterServerAuthenticatedResult {
8 username: string
9 email: string
10 role?: UserRole
11 displayName?: string
12}
13
14export interface RegisterServerExternalAuthenticatedResult extends RegisterServerAuthenticatedResult {
15 req: express.Request
16 res: express.Response
17}
18
19interface RegisterServerAuthBase {
7 // Authentication name (a plugin can register multiple auth strategies) 20 // Authentication name (a plugin can register multiple auth strategies)
8 authName: string 21 authName: string
9 22
10 // Called by PeerTube when a user from your plugin logged out 23 // Called by PeerTube when a user from your plugin logged out
11 onLogout?(): void 24 onLogout?(): void
12 25
13 // Weight of this authentication so PeerTube tries the auth methods in DESC weight order
14 getWeight(): number
15
16 // Your plugin can hook PeerTube access/refresh token validity 26 // Your plugin can hook PeerTube access/refresh token validity
17 // So you can control for your plugin the user session lifetime 27 // So you can control for your plugin the user session lifetime
18 hookTokenValidity?(options: { token: MOAuthToken, type: 'access' | 'refresh' }): Promise<{ valid: boolean }> 28 hookTokenValidity?(options: { token: MOAuthToken, type: 'access' | 'refresh' }): Promise<{ valid: boolean }>
29}
30
31export interface RegisterServerAuthPassOptions extends RegisterServerAuthBase {
32 // Weight of this authentication so PeerTube tries the auth methods in DESC weight order
33 getWeight(): number
19 34
20 // Used by PeerTube to login a user 35 // Used by PeerTube to login a user
21 // Returns null if the login failed, or { username, email } on success 36 // Returns null if the login failed, or { username, email } on success
22 login(body: { 37 login(body: {
23 id: string 38 id: string
24 password: string 39 password: string
25 }): Promise<{ 40 }): Promise<RegisterServerAuthenticatedResult | null>
26 username: string
27 email: string
28 role?: UserRole
29 displayName?: string
30 } | null>
31} 41}
32 42
33export interface RegisterServerAuthExternalOptions { 43export interface RegisterServerAuthExternalOptions extends RegisterServerAuthBase {
34 // Authentication name (a plugin can register multiple auth strategies) 44 // Will be displayed in a block next to the login form
35 authName: string 45 authDisplayName: string
36 46
37 onLogout?: Function 47 onAuthRequest: (req: express.Request, res: express.Response) => void
38} 48}
39 49
40export interface RegisterServerAuthExternalResult { 50export interface RegisterServerAuthExternalResult {
41 onAuth (options: { username: string, email: string }): void 51 userAuthenticated (options: RegisterServerExternalAuthenticatedResult): void
42} 52}
diff --git a/shared/models/plugins/register-server-setting.model.ts b/shared/models/plugins/register-server-setting.model.ts
index ec175e9ef..920c3480f 100644
--- a/shared/models/plugins/register-server-setting.model.ts
+++ b/shared/models/plugins/register-server-setting.model.ts
@@ -9,7 +9,7 @@ export interface RegisterServerSettingOptions {
9 private: boolean 9 private: boolean
10 10
11 // Default setting value 11 // Default setting value
12 default?: string 12 default?: string | boolean
13} 13}
14 14
15export interface RegisteredServerSettings { 15export interface RegisteredServerSettings {
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index c3976a346..0ff079216 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -12,6 +12,18 @@ export interface ServerConfigTheme extends ServerConfigPlugin {
12 css: string[] 12 css: string[]
13} 13}
14 14
15export interface RegisteredExternalAuthConfig {
16 npmName: string
17 authName: string
18 authDisplayName: string
19}
20
21export interface RegisteredIdAndPassAuthConfig {
22 npmName: string
23 authName: string
24 weight: number
25}
26
15export interface ServerConfig { 27export interface ServerConfig {
16 serverVersion: string 28 serverVersion: string
17 serverCommit?: string 29 serverCommit?: string
@@ -37,6 +49,10 @@ export interface ServerConfig {
37 49
38 plugin: { 50 plugin: {
39 registered: ServerConfigPlugin[] 51 registered: ServerConfigPlugin[]
52
53 registeredExternalAuths: RegisteredExternalAuthConfig[]
54
55 registeredIdAndPassAuths: RegisteredIdAndPassAuthConfig[]
40 } 56 }
41 57
42 theme: { 58 theme: {