diff --git a/services/web/app/src/Features/Authentication/AuthenticationController.mjs b/services/web/app/src/Features/Authentication/AuthenticationController.mjs
index 636ac4ab95..d462150b5a 100644
--- a/services/web/app/src/Features/Authentication/AuthenticationController.mjs
+++ b/services/web/app/src/Features/Authentication/AuthenticationController.mjs
@@ -97,6 +97,7 @@ const AuthenticationController = {
analyticsId: user.analyticsId || user._id,
alphaProgram: user.alphaProgram || undefined, // only store if set
betaProgram: user.betaProgram || undefined, // only store if set
+ externalAuth: user.externalAuth || false,
}
if (user.isAdmin) {
lightUser.isAdmin = true
@@ -117,9 +118,9 @@ const AuthenticationController = {
// so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
// and send a `{redir: ""}` response on success
passport.authenticate(
- Settings.ldap?.enable ? ['custom-fail-ldapauth','local'] : ['local'],
+ 'local',
{ keepSessionInfo: true },
- async function (err, user, infoArray) {
+ async function (err, user, info) {
if (err) {
return next(err)
}
@@ -141,7 +142,6 @@ const AuthenticationController = {
return next(err)
}
} else {
- let info = infoArray[0]
if (info.redir != null) {
return res.json({ redir: info.redir })
} else {
diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs b/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs
index 6524397d6b..24d00c78a3 100644
--- a/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs
+++ b/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs
@@ -158,10 +158,6 @@ async function requestReset(req, res, next) {
return res.status(404).json({
message: req.i18n.translate('secondary_email_password_reset'),
})
- } else if (status === 'external') {
- return res.status(403).json({
- message: req.i18n.translate('password_managed_externally'),
- })
} else {
return res.status(404).json({
message: req.i18n.translate('cant_find_email'),
diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetHandler.mjs b/services/web/app/src/Features/PasswordReset/PasswordResetHandler.mjs
index b4594dba15..965bf42561 100644
--- a/services/web/app/src/Features/PasswordReset/PasswordResetHandler.mjs
+++ b/services/web/app/src/Features/PasswordReset/PasswordResetHandler.mjs
@@ -18,10 +18,6 @@ async function generateAndEmailResetToken(email) {
return null
}
- if (!user.hashedPassword) {
- return 'external'
- }
-
if (user.email !== email) {
return 'secondary'
}
@@ -76,6 +72,7 @@ async function getUserForPasswordResetToken(token) {
'overleaf.id': 1,
email: 1,
must_reconfirm: 1,
+ hashedPassword: 1,
})
await assertUserPermissions(user, ['change-password'])
diff --git a/services/web/app/src/Features/User/UserController.mjs b/services/web/app/src/Features/User/UserController.mjs
index 999a0d2ea1..ac1ca8870b 100644
--- a/services/web/app/src/Features/User/UserController.mjs
+++ b/services/web/app/src/Features/User/UserController.mjs
@@ -422,7 +422,7 @@ async function updateUserSettings(req, res, next) {
if (
newEmail == null ||
newEmail === user.email ||
- (req.externalAuthenticationSystemUsed() && !user.hashedPassword)
+ req.externalAuthenticationSystemUsed()
) {
// end here, don't update email
SessionManager.setInSessionUser(req.session, {
@@ -509,7 +509,6 @@ async function doLogout(req) {
}
async function logout(req, res, next) {
- if (req?.session.saml_extce) return res.redirect(308, '/saml/logout')
const requestedRedirect = req.body.redirect
? UrlHelper.getSafeRedirectPath(req.body.redirect)
: undefined
diff --git a/services/web/app/src/Features/User/UserPagesController.mjs b/services/web/app/src/Features/User/UserPagesController.mjs
index e25c1ca8f8..5eb5e72a06 100644
--- a/services/web/app/src/Features/User/UserPagesController.mjs
+++ b/services/web/app/src/Features/User/UserPagesController.mjs
@@ -53,10 +53,8 @@ async function settingsPage(req, res) {
const reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed'])
delete req.session.saml
let shouldAllowEditingDetails = true
- if (Settings.ldap && Settings.ldap.updateUserDetailsOnLogin) {
- shouldAllowEditingDetails = false
- }
- if (Settings.saml && Settings.saml.updateUserDetailsOnLogin) {
+ const externalAuth = req.user.externalAuth
+ if (externalAuth && Settings[externalAuth].updateUserDetailsOnLogin) {
shouldAllowEditingDetails = false
}
const oauthProviders = Settings.oauthProviders || {}
diff --git a/services/web/app/src/infrastructure/ExpressLocals.mjs b/services/web/app/src/infrastructure/ExpressLocals.mjs
index 8c9adbad27..b98624fa3a 100644
--- a/services/web/app/src/infrastructure/ExpressLocals.mjs
+++ b/services/web/app/src/infrastructure/ExpressLocals.mjs
@@ -114,9 +114,9 @@ export default async function (webRouter, privateApiRouter, publicApiRouter) {
webRouter.use(function (req, res, next) {
req.externalAuthenticationSystemUsed =
- Features.externalAuthenticationSystemUsed
+ () => !!req?.user?.externalAuth
res.locals.externalAuthenticationSystemUsed =
- Features.externalAuthenticationSystemUsed
+ () => !!req?.user?.externalAuth
req.hasFeature = res.locals.hasFeature = Features.hasFeature
next()
})
diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs
index 1c07cbb8e0..e732583845 100644
--- a/services/web/app/src/router.mjs
+++ b/services/web/app/src/router.mjs
@@ -213,6 +213,8 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
CaptchaMiddleware.canSkipCaptcha
)
+ await Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
+
webRouter.get('/login', UserPagesController.loginPage)
AuthenticationController.addEndpointToLoginWhitelist('/login')
@@ -281,8 +283,6 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
TokenAccessRouter.apply(webRouter)
HistoryRouter.apply(webRouter, privateApiRouter)
- await Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
-
if (Settings.enableSubscriptions) {
webRouter.get(
'/user/bonus',
diff --git a/services/web/app/views/user/login.pug b/services/web/app/views/user/login.pug
index 570128777e..551e9398a5 100644
--- a/services/web/app/views/user/login.pug
+++ b/services/web/app/views/user/login.pug
@@ -56,3 +56,12 @@ block content
)
span(data-ol-inflight="idle") #{settings.saml.identityServiceName}
span(hidden data-ol-inflight="pending") #{translate("logging_in")}…
+ if settings.oidc && settings.oidc.enable
+ form(data-ol-async-form, name="oidcLoginForm")
+ .actions(style='margin-top: 30px;')
+ a.btn.btn-secondary.btn-block(
+ href='/oidc/login',
+ data-ol-disabled-inflight
+ )
+ span(data-ol-inflight="idle") #{settings.oidc.identityServiceName}
+ span(hidden data-ol-inflight="pending") #{translate("logging_in")}…
diff --git a/services/web/app/views/user/passwordReset.pug b/services/web/app/views/user/passwordReset.pug
index ca2483c9a8..36afc18973 100644
--- a/services/web/app/views/user/passwordReset.pug
+++ b/services/web/app/views/user/passwordReset.pug
@@ -50,7 +50,7 @@ block content
+notification({ariaLive: 'assertive', type: 'error', className: 'mb-3', content: translate(error)})
div(data-ol-custom-form-message='no-password-allowed-due-to-sso' hidden)
- +notification({ariaLive: 'polite', type: 'error', className: 'mb-3', content: translate('you_cant_reset_password_due_to_sso', {}, [{name: 'a', attrs: {href: '/sso-login'}}])})
+ +notification({ariaLive: 'polite', type: 'error', className: 'mb-3', content: translate('you_cant_reset_password_due_to_ldap_or_sso')})
input(name='_csrf' type='hidden' value=csrfToken)
.form-group.mb-3
label.form-label(for='email') #{translate("email")}
diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug
index a07863682e..45d21c7572 100644
--- a/services/web/app/views/user/settings.pug
+++ b/services/web/app/views/user/settings.pug
@@ -11,7 +11,7 @@ block append meta
meta(
name='ol-shouldAllowEditingDetails'
data-type='boolean'
- content=shouldAllowEditingDetails || hasPassword
+ content=shouldAllowEditingDetails
)
meta(name='ol-oauthProviders' data-type='json' content=oauthProviders)
meta(name='ol-institutionLinked' data-type='json' content=institutionLinked)
@@ -34,7 +34,7 @@ block append meta
meta(
name='ol-isExternalAuthenticationSystemUsed'
data-type='boolean'
- content=externalAuthenticationSystemUsed() && !hasPassword
+ content=externalAuthenticationSystemUsed()
)
meta(name='ol-user' data-type='json' content=user)
meta(name='ol-labsExperiments' data-type='json' content=labsExperiments)
diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js
index ba56ad94f0..a232206f1d 100644
--- a/services/web/config/settings.defaults.js
+++ b/services/web/config/settings.defaults.js
@@ -1062,10 +1062,11 @@ module.exports = {
'launchpad',
'server-ce-scripts',
'user-activate',
- 'ldap-authentication',
- 'saml-authentication',
'symbol-palette',
'track-changes',
+ 'authentication/ldap',
+ 'authentication/saml',
+ 'authentication/oidc',
],
viewIncludes: {},
@@ -1101,6 +1102,20 @@ module.exports = {
|| imageName.split(':')[1],
}))
: undefined,
+
+ oauthProviders: {
+ ...(process.env.EXTERNAL_AUTH && process.env.EXTERNAL_AUTH.includes('oidc') && {
+ [process.env.OVERLEAF_OIDC_PROVIDER_ID || 'oidc']: {
+ name: process.env.OVERLEAF_OIDC_PROVIDER_NAME || 'OIDC Provider',
+ descriptionKey: process.env.OVERLEAF_OIDC_PROVIDER_DESCRIPTION,
+ descriptionOptions: { link: process.env.OVERLEAF_OIDC_PROVIDER_INFO_LINK },
+ hideWhenNotLinked: process.env.OVERLEAF_OIDC_PROVIDER_HIDE_NOT_LINKED ?
+ process.env.OVERLEAF_OIDC_PROVIDER_HIDE_NOT_LINKED.toLowerCase() === 'true' : undefined,
+ linkPath: '/oidc/login',
+ },
+ }),
+ },
+
}
module.exports.mergeWith = function (overrides) {
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 9e286c5900..807a2e2208 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -2276,6 +2276,7 @@
"you_can_select_or_invite_collaborator": "",
"you_can_select_or_invite_collaborator_plural": "",
"you_can_still_use_your_premium_features": "",
+ "you_cant_add_or_change_password_due_to_ldap_or_sso": "",
"you_cant_add_or_change_password_due_to_sso": "",
"you_cant_join_this_group_subscription": "",
"you_dont_have_any_add_ons_on_your_account": "",
diff --git a/services/web/frontend/js/features/settings/components/linking-section.tsx b/services/web/frontend/js/features/settings/components/linking-section.tsx
index cd92ab614b..e1f53ef5ca 100644
--- a/services/web/frontend/js/features/settings/components/linking-section.tsx
+++ b/services/web/frontend/js/features/settings/components/linking-section.tsx
@@ -200,7 +200,8 @@ function SSOLinkingWidgetContainer({
const { t } = useTranslation()
const { unlink } = useSSOContext()
- let description = ''
+ let description = subscription.provider.descriptionKey ||
+ `${t('login_with_service', { service: subscription.provider.name, })}.`
switch (subscription.providerId) {
case 'collabratec':
description = t('linked_collabratec_description')
diff --git a/services/web/frontend/js/features/settings/components/linking/sso-widget.tsx b/services/web/frontend/js/features/settings/components/linking/sso-widget.tsx
index c80605aefb..0769db713e 100644
--- a/services/web/frontend/js/features/settings/components/linking/sso-widget.tsx
+++ b/services/web/frontend/js/features/settings/components/linking/sso-widget.tsx
@@ -4,6 +4,7 @@ import { FetchError } from '../../../../infrastructure/fetch-json'
import IEEELogo from '../../../../shared/svgs/ieee-logo'
import GoogleLogo from '../../../../shared/svgs/google-logo'
import OrcidLogo from '../../../../shared/svgs/orcid-logo'
+import OpenIDLogo from '../../../../shared/svgs/openid-logo'
import LinkingStatus from './status'
import OLButton from '@/shared/components/ol/ol-button'
import {
@@ -18,6 +19,7 @@ const providerLogos: { readonly [p: string]: JSX.Element } = {
collabratec: