From b16559beaa2df5a4413a1c5c427e52664701e7d5 Mon Sep 17 00:00:00 2001 From: Antoine Jaussoin Date: Sat, 2 Apr 2022 22:45:36 +0100 Subject: [PATCH] Optionally disable password accounts (#381) --- .env.example | 2 + backend/src/admin/router.ts | 2 + backend/src/auth/passport.ts | 6 + backend/src/common/payloads.ts | 2 + backend/src/config.ts | 5 + backend/src/index.ts | 4 + backend/src/types.ts | 2 + docs/docs/self-hosting/authentication.md | 37 ++++++ docs/docs/self-hosting/optionals.md | 2 + frontend/src/auth/modal/LoginModal.tsx | 128 ++++++++++++------- frontend/src/auth/modal/account/Register.tsx | 11 ++ frontend/src/common/payloads.ts | 2 + frontend/src/global/state.ts | 2 + self-hosting/docker-compose.full.yml | 2 + 14 files changed, 161 insertions(+), 46 deletions(-) create mode 100644 docs/docs/self-hosting/authentication.md diff --git a/.env.example b/.env.example index c7d27b974..132a5d134 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,8 @@ SELF_HOSTED_ADMIN=admin@admin.org SENTRY_URL= SESSION_SECRET=changeme DISABLE_ANONYMOUS_LOGIN=false +DISABLE_PASSWORD_LOGIN=false +DISABLE_PASSWORD_REGISTRATION=false TWITTER_KEY= TWITTER_SECRET= GOOGLE_KEY= diff --git a/backend/src/admin/router.ts b/backend/src/admin/router.ts index e110da923..5ad6a45c2 100644 --- a/backend/src/admin/router.ts +++ b/backend/src/admin/router.ts @@ -25,6 +25,8 @@ router.get('/self-hosting', async (_, res) => { !!config.SENDGRID_VERIFICATION_EMAIL_TID && !!config.SENDGRID_SENDER, disableAnonymous: config.DISABLE_ANONYMOUS_LOGIN, + disablePasswords: config.DISABLE_PASSWORD_LOGIN, + disablePasswordRegistration: config.DISABLE_PASSWORD_REGISTRATION, oAuth: { google: !!config.GOOGLE_KEY && !!config.GOOGLE_SECRET, github: !!config.GITHUB_KEY && !!config.GITHUB_SECRET, diff --git a/backend/src/auth/passport.ts b/backend/src/auth/passport.ts index cf878cccc..d79d69c3b 100644 --- a/backend/src/auth/passport.ts +++ b/backend/src/auth/passport.ts @@ -260,6 +260,12 @@ export default () => { ); } else { // Regular account login + + // Checking if they are allowed in the first place + if (config.DISABLE_PASSWORD_LOGIN) { + return done('Password accounts are disabled', undefined); + } + const identity = await loginUser(username, password); done( !identity ? 'User cannot log in' : null, diff --git a/backend/src/common/payloads.ts b/backend/src/common/payloads.ts index f533390c4..053206fa8 100644 --- a/backend/src/common/payloads.ts +++ b/backend/src/common/payloads.ts @@ -58,6 +58,8 @@ export interface BackendCapabilities { licenced: boolean; oAuth: OAuthAvailabilities; disableAnonymous: boolean; + disablePasswords: boolean; + disablePasswordRegistration: boolean; } export interface OAuthAvailabilities { diff --git a/backend/src/config.ts b/backend/src/config.ts index a81b117e8..0ff839027 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -65,6 +65,11 @@ const config: BackendConfig = { BASE_URL: defaults('BASE_URL', 'http://localhost:80'), SECURE_COOKIES: defaultsBool('SECURE_COOKIES', false), DISABLE_ANONYMOUS_LOGIN: defaultsBool('DISABLE_ANONYMOUS_LOGIN', false), + DISABLE_PASSWORD_LOGIN: defaultsBool('DISABLE_PASSWORD_LOGIN', false), + DISABLE_PASSWORD_REGISTRATION: defaultsBool( + 'DISABLE_PASSWORD_REGISTRATION', + false + ), TWITTER_KEY: defaults('TWITTER_KEY', ''), TWITTER_SECRET: defaults('TWITTER_SECRET', ''), GOOGLE_KEY: defaults('GOOGLE_KEY', ''), diff --git a/backend/src/index.ts b/backend/src/index.ts index 203358bd0..602daf4a4 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -396,6 +396,10 @@ db().then(() => { res.status(500).send('You are already logged in'); return; } + if (config.DISABLE_PASSWORD_REGISTRATION) { + res.status(403).send('Password accounts registration is disabled.'); + return; + } const registerPayload = req.body as RegisterPayload; if ( (await getIdentityByUsername('password', registerPayload.username)) !== diff --git a/backend/src/types.ts b/backend/src/types.ts index e49cba114..c6e98eeed 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -18,6 +18,8 @@ export interface BackendConfig { SECURE_COOKIES: boolean; SENTRY_URL: string; DISABLE_ANONYMOUS_LOGIN: boolean; + DISABLE_PASSWORD_LOGIN: boolean; + DISABLE_PASSWORD_REGISTRATION: boolean; TWITTER_KEY: string; TWITTER_SECRET: string; GOOGLE_KEY: string; diff --git a/docs/docs/self-hosting/authentication.md b/docs/docs/self-hosting/authentication.md new file mode 100644 index 000000000..3742f48c8 --- /dev/null +++ b/docs/docs/self-hosting/authentication.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 5 +--- + +# 🎫 Authentication + +When running a self-hosted instance, you can customise how your users will authenticate to the system. + +You essentially have 3 authentication mechanisms: + +- OAuth (aka Social) authentication (Google, Okta, GitHub etc.) +- Password accounts (the good old email / password combination) +- Anonymous accounts (where the user cannot retrieve his posts and sessions on a different machine) + +By default, OAuth is disabled (because it is not configured), and the other two are enabled. + + + +## OAuth + +This is a subject of a [dedicated page here](oauth). + +## Password accounts + +You have two optional settings for password accounts. + +You can disable them entirely, by setting the environment variable `DISABLE_PASSWORD_LOGIN` to `true`. + +You can also only disable registration, by setting the environment variable `DISABLE_PASSWORD_REGISTRATION` to `true`. + +If you want more details on how to set these environment variables, [read this page](optionals). + +If you disabled registration, it means the administrator will need to create the accounts manually, by using the [admin screen](admin). + +## Anonymous accounts + +If you do not wish your users to use anonymous accounts, and force them to authenticate properly, they can be disabled by setting the environment variable `DISABLE_ANONYMOUS_LOGIN` to `true`. \ No newline at end of file diff --git a/docs/docs/self-hosting/optionals.md b/docs/docs/self-hosting/optionals.md index 331738d49..015838bb8 100644 --- a/docs/docs/self-hosting/optionals.md +++ b/docs/docs/self-hosting/optionals.md @@ -40,6 +40,8 @@ services: BASE_URL: http://localhost:80 # This must be the URL of the frontend app once deployed. Only useful if you need OAuth, SendGrid or Stripe SECURE_COOKIES: 'false' # You can set this to true if you are using HTTPS. This is more secure. DISABLE_ANONYMOUS_LOGIN: 'false' # Set to true to disable anonymous accounts + DISABLE_PASSWORD_LOGIN: 'false' # Set to true to disable password accounts (email accounts). + DISABLE_PASSWORD_REGISTRATION: 'false' # Set to true to disable password accounts registration (but not login!) # -- OAuth: Set these to enable OAuth authentication for one or more provider. This is optional. -- TWITTER_KEY: diff --git a/frontend/src/auth/modal/LoginModal.tsx b/frontend/src/auth/modal/LoginModal.tsx index f40d15b63..c40be20a4 100644 --- a/frontend/src/auth/modal/LoginModal.tsx +++ b/frontend/src/auth/modal/LoginModal.tsx @@ -12,20 +12,25 @@ import AnonAuth from './AnonAuth'; import AccountAuth from './AccountAuth'; import useOAuthAvailabilities from '../../global/useOAuthAvailabilities'; import useBackendCapabilities from '../../global/useBackendCapabilities'; +import { Alert } from '@mui/material'; interface LoginModalProps { onClose: () => void; } +type TabType = 'account' | 'social' | 'anon' | null; + const Login = ({ onClose }: LoginModalProps) => { const { any } = useOAuthAvailabilities(); - const { disableAnonymous } = useBackendCapabilities(); + const { disableAnonymous, disablePasswords } = useBackendCapabilities(); const hasNoSocialMediaAuth = !any; + const hasNoWayOfLoggingIn = + hasNoSocialMediaAuth && disableAnonymous && disablePasswords; const translations = useTranslations(); const fullScreen = useMediaQuery('(max-width:600px)'); const { setUser } = useContext(UserContext); - const [currentTab, setCurrentTab] = useState( - hasNoSocialMediaAuth ? 'account' : 'social' + const [currentTab, setCurrentTab] = useState( + getDefaultMode(any, !disablePasswords, !disableAnonymous) ); const handleClose = useCallback(() => { @@ -34,7 +39,7 @@ const Login = ({ onClose }: LoginModalProps) => { } }, [onClose]); const handleTab = useCallback((_: React.ChangeEvent<{}>, value: string) => { - setCurrentTab(value); + setCurrentTab(value as TabType); }, []); return ( { open onClose={handleClose} > - - - {!hasNoSocialMediaAuth ? ( - - ) : null} - - {!disableAnonymous ? ( - - ) : null} - - - - {currentTab === 'social' ? ( - - ) : null} - {currentTab === 'account' ? ( - - ) : null} - {currentTab === 'anon' ? ( - - ) : null} - + {hasNoWayOfLoggingIn ? ( + + Your administrator disabled all login possibilities (OAuth, password, + anonymous). Ask your administrator to re-enable at least one. + + ) : ( + <> + + + {!hasNoSocialMediaAuth ? ( + + ) : null} + {!disablePasswords ? ( + + ) : null} + {!disableAnonymous ? ( + + ) : null} + + + + {currentTab === 'social' ? ( + + ) : null} + {currentTab === 'account' ? ( + + ) : null} + {currentTab === 'anon' ? ( + + ) : null} + + + )} ); }; +function getDefaultMode( + oauth: boolean, + password: boolean, + anon: boolean +): TabType { + if (oauth) { + return 'social'; + } + + if (password) { + return 'account'; + } + + if (anon) { + return 'anon'; + } + + return null; +} + export default Login; diff --git a/frontend/src/auth/modal/account/Register.tsx b/frontend/src/auth/modal/account/Register.tsx index c9176572b..eb6c06870 100644 --- a/frontend/src/auth/modal/account/Register.tsx +++ b/frontend/src/auth/modal/account/Register.tsx @@ -15,6 +15,7 @@ import { Person, Email, VpnKey } from '@mui/icons-material'; import { register } from '../../../api'; import { validate } from 'isemail'; import UserContext from '../../Context'; +import useBackendCapabilities from 'global/useBackendCapabilities'; type RegisterProps = { onClose: () => void; @@ -38,6 +39,7 @@ const Register = ({ onClose }: RegisterProps) => { const [generalError, setGeneralError] = useState(null); const [isSuccessful, setIsSuccessful] = useState(false); const { setUser } = useContext(UserContext); + const { disablePasswordRegistration } = useBackendCapabilities(); const validEmail = useMemo(() => { return validate(registerEmail); @@ -78,6 +80,15 @@ const Register = ({ onClose }: RegisterProps) => { onClose, ]); + if (disablePasswordRegistration) { + return ( + + Registration is disabled by your administrator. Ask your administrator + to create an account for you. + + ); + } + return ( ({ licenced: true, selfHosted: false, disableAnonymous: false, + disablePasswords: false, + disablePasswordRegistration: false, oAuth: { google: false, github: false, diff --git a/self-hosting/docker-compose.full.yml b/self-hosting/docker-compose.full.yml index ad21440a9..3fc140d82 100644 --- a/self-hosting/docker-compose.full.yml +++ b/self-hosting/docker-compose.full.yml @@ -68,6 +68,8 @@ services: BASE_URL: http://localhost:80 # This must be the URL of the frontend app once deployed. Only useful if you need OAuth, SendGrid or Stripe SECURE_COOKIES: 'false' # You can set this to true if you are using HTTPS. This is more secure. DISABLE_ANONYMOUS_LOGIN: 'false' # Set to true to disable anonymous accounts + DISABLE_PASSWORD_LOGIN: 'false' # Set to true to disable password accounts (email accounts). + DISABLE_PASSWORD_REGISTRATION: 'false' # Set to true to disable password accounts registration (but not login!) # -- OAuth: Set these to enable OAuth authentication for one or more provider. This is optional. -- TWITTER_KEY: