Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make 2fa great again #26557

Merged
merged 25 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f9df937
misc client side 2fa improvements
zlwaterfield Nov 29, 2024
6f7114d
hook up backup codes are for 2fa
zlwaterfield Nov 29, 2024
5f703a9
Update query snapshots
github-actions[bot] Nov 29, 2024
dd20613
merge master in
zlwaterfield Dec 2, 2024
a5d0ca5
fix types
zlwaterfield Dec 2, 2024
c6d7e79
Update UI snapshots for `chromium` (1)
github-actions[bot] Dec 2, 2024
fc0fbd5
Update query snapshots
github-actions[bot] Dec 2, 2024
201b8f0
Add two factor api tests
zlwaterfield Dec 3, 2024
6322239
Delete logic.ts
zlwaterfield Dec 3, 2024
6341ea3
Add backup code tests
zlwaterfield Dec 3, 2024
f59f47c
Merge branch 'master' into zach/implement-backup-codes
zlwaterfield Dec 3, 2024
42ff021
Update query snapshots
github-actions[bot] Dec 3, 2024
796ba90
Update query snapshots
github-actions[bot] Dec 3, 2024
ef23d41
Update frontend/src/layout/GlobalModals.tsx
zlwaterfield Dec 9, 2024
196b5c6
Update frontend/src/scenes/authentication/TwoFactorSetupModal.tsx
zlwaterfield Dec 9, 2024
59d3c04
move time import
zlwaterfield Dec 9, 2024
6ef4f88
merge master in
zlwaterfield Dec 9, 2024
15048f1
Update UI snapshots for `webkit` (2)
github-actions[bot] Dec 9, 2024
afe7e7d
Update query snapshots
github-actions[bot] Dec 9, 2024
321a6b0
Update UI snapshots for `chromium` (1)
github-actions[bot] Dec 9, 2024
5f3f16e
Update query snapshots
github-actions[bot] Dec 9, 2024
1b83806
Update UI snapshots for `chromium` (2)
github-actions[bot] Dec 9, 2024
5c576e9
merge master in
zlwaterfield Dec 9, 2024
f9b3989
Update UI snapshots for `chromium` (1)
github-actions[bot] Dec 9, 2024
b1093ec
Update UI snapshots for `chromium` (2)
github-actions[bot] Dec 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified frontend/__snapshots__/scenes-other-login--second-factor--dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 10 additions & 18 deletions frontend/src/layout/GlobalModals.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { LemonModal } from '@posthog/lemon-ui'
import { actions, kea, path, reducers, useActions, useValues } from 'kea'
import { ConfirmUpgradeModal } from 'lib/components/ConfirmUpgradeModal/ConfirmUpgradeModal'
import { HedgehogBuddyWithLogic } from 'lib/components/HedgehogBuddy/HedgehogBuddyWithLogic'
import { TimeSensitiveAuthenticationModal } from 'lib/components/TimeSensitiveAuthentication/TimeSensitiveAuthentication'
import { UpgradeModal } from 'lib/components/UpgradeModal/UpgradeModal'
import { Setup2FA } from 'scenes/authentication/Setup2FA'
import { TwoFactorSetupModal } from 'scenes/authentication/TwoFactorSetupModal'
import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal'
import { membersLogic } from 'scenes/organization/membersLogic'
import { CreateEnvironmentModal } from 'scenes/project/CreateEnvironmentModal'
Expand Down Expand Up @@ -73,22 +72,15 @@ export function GlobalModals(): JSX.Element {
<SessionPlayerModal />
<PreviewingCustomCssModal />
{user && user.organization?.enforce_2fa && !user.is_2fa_enabled && (
<LemonModal title="Set up 2FA" closable={false}>
<p>
<b>Your organization requires you to set up 2FA.</b>
</p>
<p>
<b>
Use an authenticator app like Google Authenticator or 1Password to scan the QR code below.
</b>
</p>
<Setup2FA
onSuccess={() => {
userLogic.actions.loadUser()
membersLogic.actions.loadAllMembers()
}}
/>
</LemonModal>
<TwoFactorSetupModal
onSuccess={() => {
userLogic.actions.loadUser()
membersLogic.actions.loadAllMembers()
}}
forceOpen
closable={false}
required={true}
/>
)}
<HedgehogBuddyWithLogic />
</>
Expand Down
1 change: 0 additions & 1 deletion frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ export const FEATURE_FLAGS = {
EXPERIMENTS_MULTIPLE_METRICS: 'experiments-multiple-metrics', // owner: @jurajmajerik #team-experiments
WEB_ANALYTICS_WARN_CUSTOM_EVENT_NO_SESSION: 'web-analytics-warn-custom-event-no-session', // owner: @robbie-c #team-web-analytics
REMOTE_CONFIG: 'remote-config', // owner: @benjackwhite
TWO_FACTOR_UI: 'two-factor-ui', // owner: @zach
SITE_DESTINATIONS: 'site-destinations', // owner: @mariusandra #team-cdp
SITE_APP_FUNCTIONS: 'site-app-functions', // owner: @mariusandra #team-cdp
REPLAY_HOGQL_FILTERS: 'replay-hogql-filters', // owner: @pauldambra #team-replay
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/authentication/Login2FA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function Login2FA(): JSX.Element {
>
<div className="space-y-2">
<h2>Two-Factor Authentication</h2>
<p>Enter a token from your authenticator app.</p>
<p>Enter a token from your authenticator app or a backup code.</p>

<Form logic={login2FALogic} formKey="twofactortoken" enableFormOnSubmit className="space-y-4">
{generalError && <LemonBanner type="error">{generalError.detail}</LemonBanner>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import { Form } from 'kea-forms'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonField } from 'lib/lemon-ui/LemonField'

import { setup2FALogic } from './setup2FALogic'
import { twoFactorLogic } from './twoFactorLogic'

export function Setup2FA({ onSuccess }: { onSuccess: () => void }): JSX.Element | null {
const { startSetupLoading, generalError } = useValues(setup2FALogic({ onSuccess }))
export function TwoFactorSetup({ onSuccess }: { onSuccess: () => void }): JSX.Element | null {
const { startSetupLoading, generalError } = useValues(twoFactorLogic({ onSuccess }))
if (startSetupLoading) {
return null
}

return (
<>
<Form logic={setup2FALogic} formKey="token" enableFormOnSubmit className="flex flex-col space-y-4">
<Form logic={twoFactorLogic} formKey="token" enableFormOnSubmit className="flex flex-col space-y-4">
<div className="bg-white ml-auto mr-auto mt-2">
<img src="/account/two_factor/qrcode/" className="Setup2FA__image" />
</div>
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/scenes/authentication/TwoFactorSetupModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useActions, useValues } from 'kea'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonModal } from 'lib/lemon-ui/LemonModal'

import { twoFactorLogic } from './twoFactorLogic'
import { TwoFactorSetup } from './TwoFactorSetup'

interface TwoFactorSetupModalProps {
onSuccess: () => void
closable?: boolean
required?: boolean
forceOpen?: boolean
}

export function TwoFactorSetupModal({
onSuccess,
closable = true,
required = false,
forceOpen = false,
}: TwoFactorSetupModalProps): JSX.Element {
const { isTwoFactorSetupModalOpen } = useValues(twoFactorLogic)
const { toggleTwoFactorSetupModal } = useActions(twoFactorLogic)

return (
<LemonModal
title="Set up two-factor authentication"
isOpen={isTwoFactorSetupModalOpen || forceOpen}
onClose={closable ? () => toggleTwoFactorSetupModal(false) : undefined}
closable={closable}
>
<div className="max-w-md">
{required && (
<LemonBanner className="mb-4" type="warning">
Your organization requires you to set up 2FA.
</LemonBanner>
)}
<p>Use an authenticator app like Google Authenticator or 1Password to scan the QR code below.</p>
<TwoFactorSetup
onSuccess={() => {
toggleTwoFactorSetupModal(false)
if (onSuccess) {
onSuccess()
}
}}
/>
</div>
</LemonModal>
)
}
6 changes: 1 addition & 5 deletions frontend/src/scenes/authentication/login2FALogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,7 @@ export const login2FALogic = kea<login2FALogicType>([
twofactortoken: {
defaults: { token: '' } as TwoFactorForm,
errors: ({ token }) => ({
token: !token
? 'Please enter a token to continue'
: token.length !== 6 || isNaN(parseInt(token))
? 'A token must consist of 6 digits'
: null,
token: !token ? 'Please enter a token to continue' : null,
}),
submit: async ({ token }, breakpoint) => {
breakpoint()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import api from 'lib/api'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'

import type { setup2FALogicType } from './setup2FALogicType'
import type { twoFactorLogicType } from './twoFactorLogicType'

export interface TwoFactorForm {
token: number | null
Expand All @@ -18,24 +18,45 @@ export interface TwoFactorStatus {
method: string | null
}

export interface Setup2FALogicProps {
export interface TwoFactorLogicProps {
onSuccess?: () => void
}

export const setup2FALogic = kea<setup2FALogicType>([
export const twoFactorLogic = kea<twoFactorLogicType>([
path(['scenes', 'authentication', 'loginLogic']),
props({} as Setup2FALogicProps),
props({} as TwoFactorLogicProps),
connect({
values: [preflightLogic, ['preflight'], featureFlagLogic, ['featureFlags']],
}),
actions({
setGeneralError: (code: string, detail: string) => ({ code, detail }),
clearGeneralError: true,
setup: true,
loadStatus: true,
generateBackupCodes: true,
disable2FA: true,
toggleTwoFactorSetupModal: (open: boolean) => ({ open }),
toggleDisable2FAModal: (open: boolean) => ({ open }),
toggleBackupCodesModal: (open: boolean) => ({ open }),
}),
reducers({
isTwoFactorSetupModalOpen: [
false,
{
toggleTwoFactorSetupModal: (_, { open }) => open,
},
],
isDisable2FAModalOpen: [
false,
{
toggleDisable2FAModal: (_, { open }) => open,
},
],
isBackupCodesModalOpen: [
false,
{
toggleBackupCodesModal: (_, { open }) => open,
},
],
generalError: [
null as { code: string; detail: string } | null,
{
Expand Down Expand Up @@ -67,9 +88,11 @@ export const setup2FALogic = kea<setup2FALogicType>([
startSetup: [
{},
{
setup: async (_, breakpoint) => {
breakpoint()
await api.get('api/users/@me/start_2fa_setup/')
toggleTwoFactorSetupModal: async ({ open }, breakpoint) => {
if (open) {
breakpoint()
await api.get('api/users/@me/two_factor_start_setup/')
}
return { status: 'completed' }
},
},
Expand All @@ -90,20 +113,6 @@ export const setup2FALogic = kea<setup2FALogicType>([
},
},
],
disable2FA: [
false,
{
disable2FA: async () => {
try {
await api.create<any>('api/users/@me/two_factor_disable/')
return true
} catch (e) {
const { code, detail } = e as Record<string, any>
throw { code, detail }
}
},
},
],
})),
forms(({ actions }) => ({
token: {
Expand All @@ -114,7 +123,7 @@ export const setup2FALogic = kea<setup2FALogicType>([
submit: async ({ token }, breakpoint) => {
breakpoint()
try {
return await api.create<any>('api/users/@me/validate_2fa/', { token })
return await api.create<any>('api/users/@me/two_factor_validate/', { token })
} catch (e) {
const { code, detail } = e as Record<string, any>
actions.setGeneralError(code, detail)
Expand All @@ -129,16 +138,29 @@ export const setup2FALogic = kea<setup2FALogicType>([
actions.loadStatus()
props.onSuccess?.()
},
disable2FASuccess: () => {
lemonToast.success('2FA disabled successfully')
disable2FA: async () => {
try {
await api.create<any>('api/users/@me/two_factor_disable/')
lemonToast.success('2FA disabled successfully')
actions.loadStatus()
} catch (e) {
const { code, detail } = e as Record<string, any>
actions.setGeneralError(code, detail)
throw e
}
},
generateBackupCodesSuccess: () => {
lemonToast.success('Backup codes generated successfully')
},
toggleTwoFactorSetupModal: ({ open }) => {
if (!open) {
// Clear the form when closing the modal
actions.resetToken()
}
},
})),

afterMount(({ actions }) => {
actions.setup()
actions.loadStatus()
}),
])
4 changes: 2 additions & 2 deletions frontend/src/scenes/settings/SettingsMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import { HedgehogModeSettings } from './user/HedgehogModeSettings'
import { OptOutCapture } from './user/OptOutCapture'
import { PersonalAPIKeys } from './user/PersonalAPIKeys'
import { ThemeSwitcher } from './user/ThemeSwitcher'
import { TwoFactorAuthentication } from './user/TwoFactorAuthentication'
import { TwoFactorSettings } from './user/TwoFactorSettings'
import { UpdateEmailPreferences } from './user/UpdateEmailPreferences'
import { UserDetails } from './user/UserDetails'

Expand Down Expand Up @@ -473,7 +473,7 @@ export const SETTINGS_MAP: SettingSection[] = [
{
id: '2fa',
title: 'Two-factor authentication',
component: <TwoFactorAuthentication />,
component: <TwoFactorSettings />,
},
],
},
Expand Down
33 changes: 16 additions & 17 deletions frontend/src/scenes/settings/organization/Members.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LemonInput, LemonModal, LemonSwitch } from '@posthog/lemon-ui'
import { LemonInput, LemonSwitch } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini'
import { TZLabel } from 'lib/components/TZLabel'
Expand All @@ -17,8 +17,9 @@ import {
membershipLevelToName,
organizationMembershipLevelIntegers,
} from 'lib/utils/permissioning'
import { useEffect, useState } from 'react'
import { Setup2FA } from 'scenes/authentication/Setup2FA'
import { useEffect } from 'react'
import { twoFactorLogic } from 'scenes/authentication/twoFactorLogic'
import { TwoFactorSetupModal } from 'scenes/authentication/TwoFactorSetupModal'
import { membersLogic } from 'scenes/organization/membersLogic'
import { organizationLogic } from 'scenes/organizationLogic'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'
Expand Down Expand Up @@ -138,13 +139,14 @@ function ActionsComponent(_: any, member: OrganizationMemberType): JSX.Element |

export function Members(): JSX.Element | null {
const { filteredMembers, membersLoading, search } = useValues(membersLogic)
const { setSearch, ensureAllMembersLoaded, loadAllMembers } = useActions(membersLogic)
const { currentOrganization } = useValues(organizationLogic)
const { updateOrganization } = useActions(organizationLogic)
const [is2FAModalVisible, set2FAModalVisible] = useState(false)
const { preflight } = useValues(preflightLogic)
const { user } = useValues(userLogic)

const { setSearch, ensureAllMembersLoaded, loadAllMembers } = useActions(membersLogic)
const { updateOrganization } = useActions(organizationLogic)
const { toggleTwoFactorSetupModal } = useActions(twoFactorLogic)

useEffect(() => {
ensureAllMembersLoaded()
}, [])
Expand Down Expand Up @@ -210,16 +212,13 @@ export function Members(): JSX.Element | null {
render: function LevelRender(_, member) {
return (
<>
{member.user.uuid == user.uuid && is2FAModalVisible && (
<LemonModal title="Set up or manage 2FA" onClose={() => set2FAModalVisible(false)}>
<Setup2FA
onSuccess={() => {
set2FAModalVisible(false)
userLogic.actions.updateUser({})
loadAllMembers()
}}
/>
</LemonModal>
{member.user.uuid == user.uuid && (
<TwoFactorSetupModal
onSuccess={() => {
userLogic.actions.updateUser({})
loadAllMembers()
}}
/>
)}
<Tooltip
title={
Expand All @@ -231,7 +230,7 @@ export function Members(): JSX.Element | null {
<LemonTag
onClick={
member.user.uuid == user.uuid && !member.is_2fa_enabled
? () => set2FAModalVisible(true)
? () => toggleTwoFactorSetupModal(true)
: undefined
}
data-attr="2fa-enabled"
Expand Down
Loading
Loading