diff --git a/services/web/app/src/Features/Authentication/AuthenticationController.js b/services/web/app/src/Features/Authentication/AuthenticationController.js
index 983526006e..baba8aacee 100644
--- a/services/web/app/src/Features/Authentication/AuthenticationController.js
+++ b/services/web/app/src/Features/Authentication/AuthenticationController.js
@@ -82,6 +82,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
@@ -102,9 +103,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)
}
@@ -126,7 +127,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 e3167592ff..02c9fbb2c0 100644
--- a/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs
+++ b/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs
@@ -119,7 +119,7 @@ async function requestReset(req, res, next) {
OError.tag(err, 'failed to generate and email password reset token', {
email,
})
- if (err.message === 'user does not have permission for change-password') {
+ if (err.message === 'user does not have one or more permissions within change-password') {
return res.status(403).json({
message: {
key: 'no-password-allowed-due-to-sso',
@@ -137,10 +137,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 0ac203222c..2c1aefe6a6 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.js b/services/web/app/src/Features/User/UserController.js
index bbf433cccb..e4186d39a8 100644
--- a/services/web/app/src/Features/User/UserController.js
+++ b/services/web/app/src/Features/User/UserController.js
@@ -401,7 +401,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, {
@@ -478,7 +478,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 1c6ce7b82c..d8b1bf1943 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.js b/services/web/app/src/infrastructure/ExpressLocals.js
index 002c342eef..4453f88144 100644
--- a/services/web/app/src/infrastructure/ExpressLocals.js
+++ b/services/web/app/src/infrastructure/ExpressLocals.js
@@ -106,9 +106,9 @@ module.exports = 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 bb1bfdd23e..fe803de4a8 100644
--- a/services/web/app/src/router.mjs
+++ b/services/web/app/src/router.mjs
@@ -216,6 +216,8 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
CaptchaMiddleware.canSkipCaptcha
)
+ await Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter)
+
webRouter.get('/login', UserPagesController.loginPage)
AuthenticationController.addEndpointToLoginWhitelist('/login')
@@ -284,8 +286,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 a7df5f092c..cb102edd4b 100644
--- a/services/web/app/views/user/login.pug
+++ b/services/web/app/views/user/login.pug
@@ -59,3 +59,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 410e79fbb2..1d019b65fc 100644
--- a/services/web/app/views/user/passwordReset.pug
+++ b/services/web/app/views/user/passwordReset.pug
@@ -52,7 +52,7 @@ block content
.notification-content-and-cta
.notification-content
p
- | !{translate("you_cant_reset_password_due_to_sso", {}, [{name: 'a', attrs: {href: '/sso-login'}}])}
+ | !{translate("you_cant_reset_password_due_to_ldap_or_sso")}
input(type="hidden", name="_csrf", value=csrfToken)
.form-group.mb-3
diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug
index 90217effbf..45d6ebeb90 100644
--- a/services/web/app/views/user/settings.pug
+++ b/services/web/app/views/user/settings.pug
@@ -9,7 +9,7 @@ block vars
block append meta
meta(name="ol-hasPassword" data-type="boolean" content=hasPassword)
- meta(name="ol-shouldAllowEditingDetails" data-type="boolean" content=shouldAllowEditingDetails || hasPassword)
+ meta(name="ol-shouldAllowEditingDetails" data-type="boolean" content=shouldAllowEditingDetails)
meta(name="ol-oauthProviders", data-type="json", content=oauthProviders)
meta(name="ol-institutionLinked", data-type="json", content=institutionLinked)
meta(name="ol-samlError", data-type="json", content=samlError)
@@ -21,7 +21,7 @@ block append meta
meta(name="ol-ssoErrorMessage", content=ssoErrorMessage)
meta(name="ol-thirdPartyIds", data-type="json", content=thirdPartyIds || {})
meta(name="ol-passwordStrengthOptions", data-type="json", content=settings.passwordStrengthOptions || {})
- meta(name="ol-isExternalAuthenticationSystemUsed" data-type="boolean" content=externalAuthenticationSystemUsed() && !hasPassword)
+ meta(name="ol-isExternalAuthenticationSystemUsed" data-type="boolean" content=externalAuthenticationSystemUsed())
meta(name="ol-user" data-type="json" content=user)
meta(name="ol-dropbox" data-type="json" content=dropbox)
meta(name="ol-github" data-type="json" content=github)
diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js
index 9cad1a373e..ae57a47adf 100644
--- a/services/web/config/settings.defaults.js
+++ b/services/web/config/settings.defaults.js
@@ -1006,8 +1006,9 @@ module.exports = {
'user-activate',
'symbol-palette',
'track-changes',
- 'ldap-authentication',
- 'saml-authentication',
+ 'authentication/ldap',
+ 'authentication/saml',
+ 'authentication/oidc',
],
viewIncludes: {},
@@ -1043,6 +1044,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 b826f386fb..e7cf26d6f1 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -2037,6 +2037,7 @@
"you_can_select_or_invite": "",
"you_can_select_or_invite_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 69d2dc268e..7171a81d3e 100644
--- a/services/web/frontend/js/features/settings/components/linking-section.tsx
+++ b/services/web/frontend/js/features/settings/components/linking-section.tsx
@@ -212,7 +212,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 8d5bc169d1..9722f41329 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 '@/features/ui/components/ol/ol-button'
import OLModal, {
@@ -17,6 +18,7 @@ const providerLogos: { readonly [p: string]: JSX.Element } = {
collabratec: