Skip to content

[6.x] Two Factor Authentication #11664

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

Draft
wants to merge 45 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
2e4606e
Bring Mity Digital's 2FA addon into Core
duncanmcclean Apr 2, 2025
78baa37
Hide "Two Factor" fieldtype in field selector
duncanmcclean Apr 2, 2025
7649f76
Make the two factor field listable
duncanmcclean Apr 2, 2025
2efbc0f
Hide two factor field in user creation wizard
duncanmcclean Apr 2, 2025
0d804d0
Translations.
duncanmcclean Apr 2, 2025
0b1cc73
Code formatting
duncanmcclean Apr 2, 2025
5cfdd2b
simplify recovery codes notification
duncanmcclean Apr 2, 2025
dcbe25d
wip
duncanmcclean Apr 2, 2025
8e7be9b
Prevent two_factor field being returned by GraphQL
duncanmcclean Apr 2, 2025
daaa0e5
Wire up the event listener
duncanmcclean Apr 4, 2025
5b8021d
Copy *most* of the tests from Mity's 2FA addon
duncanmcclean Apr 4, 2025
7be3350
Filter out two factor field when outputting user form fields
duncanmcclean Apr 7, 2025
6bc3bed
auth/auth
duncanmcclean Apr 7, 2025
ef4102c
Finish copying over tests from two factor addon
duncanmcclean Apr 8, 2025
d4443e3
Code formatting
duncanmcclean Apr 8, 2025
7c945d3
Remove blueprint option, we can hard code it.
duncanmcclean Apr 8, 2025
a76d82c
Two Factor should always be enabled.
duncanmcclean Apr 8, 2025
113a65f
Refactor how we determine if 2FA is required for a user
duncanmcclean Apr 8, 2025
bca0990
Refactor fieldtype preload
duncanmcclean Apr 9, 2025
4f4ed42
Rework the setup process on the user publish form
duncanmcclean Apr 9, 2025
b056f7f
Rework how you view recovery codes in the CP
duncanmcclean Apr 9, 2025
294f9d7
Rework the disable process
duncanmcclean Apr 11, 2025
82e61a8
Fix the index fieldtype
duncanmcclean Apr 14, 2025
c4e40a4
Improve empty state when viewing 2FA status of other users
duncanmcclean Apr 14, 2025
6f82d7d
You shouldn't be able to cancel out of setup page anymore.
duncanmcclean Apr 14, 2025
3a1f67f
Improve the index fieldtype
duncanmcclean Apr 14, 2025
7af9013
Remove event listener
duncanmcclean Apr 14, 2025
78ba11b
Removed locked feature
duncanmcclean Apr 14, 2025
406b2c4
Actually remove `enabled` option from config
duncanmcclean Apr 14, 2025
0293fe8
rename environment variable
duncanmcclean Apr 14, 2025
8b413de
Move methods from `StatamicTwoFactorUser` to user classes
duncanmcclean Apr 14, 2025
03b0aaf
wip
duncanmcclean Apr 14, 2025
f4e8a15
Persist `confirmed_at` and `completed` as timestamps
duncanmcclean Apr 14, 2025
69ec982
Fix index fieldtype
duncanmcclean Apr 14, 2025
d52dd7f
wip
duncanmcclean Apr 14, 2025
1380b91
wip
duncanmcclean Apr 16, 2025
828f50f
fieldtype improvements
duncanmcclean Apr 16, 2025
b95388e
Users should complete the challenge before being logged in
duncanmcclean Apr 17, 2025
da473b9
Various refactorings
duncanmcclean Apr 17, 2025
815f0c5
Throw auth failed event when user can't be found
duncanmcclean Apr 17, 2025
99d3a3f
We'll handle this in a middleware shortly.
duncanmcclean Apr 17, 2025
89eafd2
Re-implement validity check
duncanmcclean Apr 17, 2025
df85319
Remove the middleware
duncanmcclean Apr 17, 2025
d5a6eda
wip
duncanmcclean Apr 17, 2025
84840f8
fix tests
duncanmcclean Apr 17, 2025
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
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"require": {
"ext-json": "*",
"ajthinking/archetype": "^1.0.3 || ^2.0",
"bacon/bacon-qr-code": "^3.0",
"composer/semver": "^3.4",
"guzzlehttp/guzzle": "^6.3 || ^7.0",
"james-heinrich/getid3": "^1.9.21",
Expand All @@ -23,6 +24,7 @@
"michelf/php-smartypants": "^1.8.1",
"nesbot/carbon": "^3.0",
"pixelfear/composer-dist-plugin": "^0.1.4",
"pragmarx/google2fa": "^8.0",
"rebing/graphql-laravel": "^9.8",
"rhukster/dom-sanitizer": "^1.0.6",
"spatie/blink": "^1.3",
Expand Down
17 changes: 17 additions & 0 deletions config/users.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,23 @@
'redirect' => env('STATAMIC_IMPERSONATE_REDIRECT', null),
],

'two_factor' => [

/*
|--------------------------------------------------------------------------
| Required for...
|--------------------------------------------------------------------------
|
| Determines the roles required to have two factor authentication setup.
| You can require two-factor for all users by setting this to ['*'].
| You can also require two-factor for super users by adding 'super_users'.
|
*/

'enforced_roles' => [],

],

/*
|--------------------------------------------------------------------------
| Default Sorting
Expand Down
2 changes: 2 additions & 0 deletions resources/js/bootstrap/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import GlobalSearch from '../components/GlobalSearch.vue';
import GlobalSiteSelector from '../components/GlobalSiteSelector.vue';
import DarkModeToggle from '../components/DarkModeToggle.vue';
import Login from '../components/login/Login.vue';
import TwoFactorChallenge from '../components/login/TwoFactorChallenge.vue';
import BaseEntryCreateForm from '../components/entries/BaseCreateForm.vue';
import BaseTermCreateForm from '../components/terms/BaseCreateForm.vue';
import CreateTermButton from '../components/terms/CreateTermButton.vue';
Expand Down Expand Up @@ -51,6 +52,7 @@ export default {
GlobalSiteSelector,
DarkModeToggle,
Login,
TwoFactorChallenge,
BaseEntryCreateForm,
BaseTermCreateForm,
CreateTermButton,
Expand Down
4 changes: 4 additions & 0 deletions resources/js/bootstrap/fieldtypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ import TagsIndexFieldtype from '../components/fieldtypes/TagsIndexFieldtype.vue'
import TemplateFolderFieldtype from '../components/fieldtypes/TemplateFolderFieldtype.vue';
import ToggleFieldtype from '../components/fieldtypes/ToggleFieldtype.vue';
import ToggleIndexFieldtype from '../components/fieldtypes/ToggleIndexFieldtype.vue';
import TwoFactorFieldtype from '../components/fieldtypes/TwoFactorFieldtype.vue';
import TwoFactorIndexFieldtype from '../components/fieldtypes/TwoFactorIndexFieldtype.vue';
import WidthFieldtype from '../components/fieldtypes/WidthFieldtype.vue';
import VideoFieldtype from '../components/fieldtypes/VideoFieldtype.vue';
import SetPicker from '../components/fieldtypes/replicator/SetPicker.vue';
Expand Down Expand Up @@ -138,6 +140,8 @@ export default function registerFieldtypes(app) {
);
app.component('toggle-fieldtype', ToggleFieldtype);
app.component('toggle-fieldtype-index', ToggleIndexFieldtype);
app.component('two_factor-fieldtype', TwoFactorFieldtype);
app.component('two_factor-fieldtype-index', TwoFactorIndexFieldtype);
app.component('width-fieldtype', WidthFieldtype);
app.component('video-fieldtype', VideoFieldtype);
app.component(
Expand Down
103 changes: 103 additions & 0 deletions resources/js/components/fieldtypes/TwoFactorFieldtype.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<template>
<div>
<template v-if="isCurrentUser && !isSetup">
<div>
<p class="mb-4 text-sm text-gray">{{ __('statamic::messages.two_factor_enable_introduction') }}</p>

<div class="flex space-x-2">
<button class="btn" @click="setupModalOpen = true">
{{ __('Enable two factor authentication') }}
</button>
</div>
</div>
</template>

<template v-else-if="!isCurrentUser && !isSetup">
<p class="text-sm text-gray">{{ __('statamic::messages.two_factor_not_setup') }}</p>
</template>

<template v-else>
<p class="mb-4 text-sm text-gray">{{ __('statamic::messages.two_factor_enabled') }}</p>

<div class="flex items-center space-x-4">
<button v-if="isCurrentUser" class="btn" @click="recoveryCodesModalOpen = true">
{{ __('Show recovery codes') }}
</button>

<DisableTwoFactor
:url="meta.routes.disable"
:is-current-user="isCurrentUser"
:is-enforced="isEnforced"
@reset-complete="resetComplete"
v-slot="{ confirm }"
>
<button class="btn-danger" @click="confirm">{{ __('Disable two factor authentication') }}</button>
</DisableTwoFactor>
</div>
</template>

<TwoFactorSetup
v-if="setupModalOpen"
:setup-url="meta.routes.setup"
:recovery-code-urls="meta.routes.recovery_codes"
@close="setupModalOpen = false"
@setup-complete="setupComplete"
/>

<TwoFactorRecoveryCodesModal
v-if="recoveryCodesModalOpen"
:recovery-codes-url="meta.routes.recovery_codes.show"
:generate-url="meta.routes.recovery_codes.generate"
:download-url="meta.routes.recovery_codes.download"
@close="recoveryCodesModalOpen = false"
/>
</div>
</template>

<script>
import Fieldtype from './Fieldtype.vue';
import DisableTwoFactor from './two-factor/Disable.vue';
import TwoFactorSetup from './two-factor/Setup.vue';
import TwoFactorRecoveryCodesModal from './two-factor/RecoveryCodesModal.vue';

export default {
mixins: [Fieldtype],

components: {
DisableTwoFactor,
TwoFactorSetup,
TwoFactorRecoveryCodesModal,
},

data() {
return {
isLocked: this.meta.is_locked,
isSetup: this.meta.is_setup,
recoveryCodesModalOpen: false,
setupModalOpen: false,
};
},

computed: {
isCurrentUser() {
return this.meta.is_current_user;
},

isEnforced() {
return this.meta.is_enforced;
},
},

methods: {
setupComplete() {
this.isSetup = true;
this.setupModalOpen = false;
},

resetComplete() {
this.isSetup = false;
this.isLocked = false;
},
},
};
</script>
21 changes: 21 additions & 0 deletions resources/js/components/fieldtypes/TwoFactorIndexFieldtype.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<div class="flex items-center space-x-2">
<template v-if="value.setup">
<svg-icon name="light/check" class="w-3 text-green-600" />
<div>{{ __('Set up') }}</div>
</template>

<template v-else>
<svg-icon name="light/close" class="w-3 text-gray-500" />
<div>{{ __('Not set up') }}</div>
</template>
</div>
</template>

<script>
import IndexFieldtype from './IndexFieldtype.vue';

export default {
mixins: [IndexFieldtype],
};
</script>
82 changes: 82 additions & 0 deletions resources/js/components/fieldtypes/two-factor/Disable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<template>
<div>
<slot :confirm="confirm" />

<confirmation-modal
v-if="confirming"
:title="__('Are you sure?')"
:danger="true"
@confirm="disable"
@cancel="confirming = false"
>
<p class="mb-2" v-html="__('statamic::messages.disable_two_factor_authentication')"></p>

<p
v-if="isCurrentUser && isEnforced"
v-html="__('statamic::messages.disable_two_factor_authentication_current_user_enforced')"
></p>
<p
v-if="isCurrentUser && !isEnforced"
v-html="__('statamic::messages.disable_two_factor_authentication_current_user_optional')"
></p>
<p
v-if="!isCurrentUser && isEnforced"
v-html="__('statamic::messages.disable_two_factor_authentication_other_user_enforced')"
></p>
<p
v-if="!isCurrentUser && !isEnforced"
v-html="__('statamic::messages.disable_two_factor_authentication_other_user_optional')"
></p>
</confirmation-modal>
</div>
</template>

<script>
export default {
props: {
url: String,
isCurrentUser: Boolean,
isEnforced: Boolean,
},

data() {
return {
loading: false,
confirming: false,
};
},

watch: {
loading(loading) {
this.$progress.loading(loading);
},
},

methods: {
confirm() {
this.confirming = true;
},

disable() {
this.loading = true;

this.$axios
.delete(this.url)
.then((response) => {
this.$toast.success(__('Disabled two factor authentication'));

this.$emit('reset-complete');

if (response.data.redirect) {
window.location = response.data.redirect;
}
})
.catch((error) => this.$toast.error(error.message))
.finally(() => {
this.loading = false;
this.confirming = false;
});
},
},
};
</script>
Loading
Loading