aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHJ <30-hj@users.noreply.git.pleroma.social>2019-06-12 20:16:55 +0000
committerHJ <30-hj@users.noreply.git.pleroma.social>2019-06-12 20:16:55 +0000
commite53f11c30fb2eedd0a437d3cfe9592c555de6a95 (patch)
tree560ecc4d370fda07c5da8cde5c60c679fa9c7862 /src
parent9df99c5205b1cb560bb25c0dd81cc90acbde4d7f (diff)
parent77eceedbf7a5b53948d7d91b3d228aa303c02081 (diff)
Merge branch 'feature/2fa' into 'develop'
adds 2FA/two_factor_authentication support See merge request pleroma/pleroma-fe!556
Diffstat (limited to 'src')
-rw-r--r--src/boot/after_store.js2
-rw-r--r--src/boot/routes.js4
-rw-r--r--src/components/auth_form/auth_form.js26
-rw-r--r--src/components/login_form/login_form.js63
-rw-r--r--src/components/login_form/login_form.vue68
-rw-r--r--src/components/mfa_form/recovery_form.js41
-rw-r--r--src/components/mfa_form/recovery_form.vue42
-rw-r--r--src/components/mfa_form/totp_form.js40
-rw-r--r--src/components/mfa_form/totp_form.vue45
-rw-r--r--src/components/user_panel/user_panel.js8
-rw-r--r--src/components/user_panel/user_panel.vue11
-rw-r--r--src/components/user_settings/confirm.js9
-rw-r--r--src/components/user_settings/confirm.vue14
-rw-r--r--src/components/user_settings/mfa.js152
-rw-r--r--src/components/user_settings/mfa.vue121
-rw-r--r--src/components/user_settings/mfa_backup_codes.js17
-rw-r--r--src/components/user_settings/mfa_backup_codes.vue22
-rw-r--r--src/components/user_settings/mfa_totp.js49
-rw-r--r--src/components/user_settings/mfa_totp.vue23
-rw-r--r--src/components/user_settings/user_settings.js4
-rw-r--r--src/components/user_settings/user_settings.vue2
-rw-r--r--src/i18n/en.json39
-rw-r--r--src/i18n/ru.json38
-rw-r--r--src/main.js2
-rw-r--r--src/modules/auth_flow.js89
-rw-r--r--src/modules/instance.js1
-rw-r--r--src/services/api/api.service.js57
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js11
-rw-r--r--src/services/new_api/mfa.js38
-rw-r--r--src/services/new_api/oauth.js35
30 files changed, 1004 insertions, 69 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 603de348..7510e562 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -92,6 +92,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
? 0
: config.logoMargin
})
+ store.commit('authFlow/setInitialStrategy', config.loginMethod)
copyInstanceOption('redirectRootNoLogin')
copyInstanceOption('redirectRootLogin')
@@ -100,7 +101,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('formattingOptionsEnabled')
copyInstanceOption('hideMutedPosts')
copyInstanceOption('collapseMessageWithSubject')
- copyInstanceOption('loginMethod')
copyInstanceOption('scopeCopy')
copyInstanceOption('subjectLineBehavior')
copyInstanceOption('postContentType')
diff --git a/src/boot/routes.js b/src/boot/routes.js
index 1a179099..055a0aea 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -13,7 +13,7 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue'
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
import UserSearch from 'components/user_search/user_search.vue'
import Notifications from 'components/notifications/notifications.vue'
-import LoginForm from 'components/login_form/login_form.vue'
+import AuthForm from 'components/auth_form/auth_form.js'
import ChatPanel from 'components/chat_panel/chat_panel.vue'
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
import About from 'components/about/about.vue'
@@ -42,7 +42,7 @@ export default (store) => {
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
{ name: 'user-settings', path: '/user-settings', component: UserSettings },
{ name: 'notifications', path: '/:username/notifications', component: Notifications },
- { name: 'login', path: '/login', component: LoginForm },
+ { name: 'login', path: '/login', component: AuthForm },
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
{ name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
diff --git a/src/components/auth_form/auth_form.js b/src/components/auth_form/auth_form.js
new file mode 100644
index 00000000..e9a6e2d5
--- /dev/null
+++ b/src/components/auth_form/auth_form.js
@@ -0,0 +1,26 @@
+import LoginForm from '../login_form/login_form.vue'
+import MFARecoveryForm from '../mfa_form/recovery_form.vue'
+import MFATOTPForm from '../mfa_form/totp_form.vue'
+import { mapGetters } from 'vuex'
+
+const AuthForm = {
+ name: 'AuthForm',
+ render (createElement) {
+ return createElement('component', { is: this.authForm })
+ },
+ computed: {
+ authForm () {
+ if (this.requiredTOTP) { return 'MFATOTPForm' }
+ if (this.requiredRecovery) { return 'MFARecoveryForm' }
+ return 'LoginForm'
+ },
+ ...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
+ },
+ components: {
+ MFARecoveryForm,
+ MFATOTPForm,
+ LoginForm
+ }
+}
+
+export default AuthForm
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index dc917e47..1119754e 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -1,28 +1,44 @@
+import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
import oauthApi from '../../services/new_api/oauth.js'
+
const LoginForm = {
data: () => ({
user: {},
- authError: false
+ error: false
}),
computed: {
- loginMethod () { return this.$store.state.instance.loginMethod },
- loggingIn () { return this.$store.state.users.loggingIn },
- registrationOpen () { return this.$store.state.instance.registrationOpen }
+ isPasswordAuth () { return this.requiredPassword },
+ isTokenAuth () { return this.requiredToken },
+ ...mapState({
+ registrationOpen: state => state.instance.registrationOpen,
+ instance: state => state.instance,
+ loggingIn: state => state.users.loggingIn,
+ oauth: state => state.oauth
+ }),
+ ...mapGetters(
+ 'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA']
+ )
},
methods: {
- oAuthLogin () {
+ ...mapMutations('authFlow', ['requireMFA']),
+ ...mapActions({ login: 'authFlow/login' }),
+ submit () {
+ this.isTokenMethod ? this.submitToken() : this.submitPassword()
+ },
+ submitToken () {
oauthApi.login({
- oauth: this.$store.state.oauth,
- instance: this.$store.state.instance.server,
+ oauth: this.oauth,
+ instance: this.instance.server,
commit: this.$store.commit
})
},
- submit () {
+ submitPassword () {
const data = {
- oauth: this.$store.state.oauth,
- instance: this.$store.state.instance.server
+ oauth: this.oauth,
+ instance: this.instance.server
}
- this.clearError()
+ this.error = false
+
oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials(
{
@@ -31,24 +47,27 @@ const LoginForm = {
username: this.user.username,
password: this.user.password
}
- ).then(async (result) => {
+ ).then((result) => {
if (result.error) {
- this.authError = result.error
- this.user.password = ''
+ if (result.error === 'mfa_required') {
+ this.requireMFA({app: app, settings: result})
+ } else {
+ this.error = result.error
+ this.focusOnPasswordInput()
+ }
return
}
- this.$store.commit('setToken', result.access_token)
- try {
- await this.$store.dispatch('loginUser', result.access_token)
+ this.login(result).then(() => {
this.$router.push({name: 'friends'})
- } catch (e) {
- console.log(e)
- }
+ })
})
})
},
- clearError () {
- this.authError = false
+ clearError () { this.error = false },
+ focusOnPasswordInput () {
+ let passwordInput = this.$refs.passwordInput
+ passwordInput.focus()
+ passwordInput.setSelectionRange(0, passwordInput.value.length)
}
}
}
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index c6be2e00..a2c5cf8f 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -1,47 +1,53 @@
<template>
- <div class="login panel panel-default">
- <!-- Default panel contents -->
- <div class="panel-heading">
- {{$t('login.login')}}
- </div>
- <div class="panel-body">
- <form v-if="loginMethod == 'password'" v-on:submit.prevent='submit(user)' class='login-form'>
+<div class="login panel panel-default">
+ <!-- Default panel contents -->
+
+ <div class="panel-heading">{{$t('login.login')}}</div>
+
+ <div class="panel-body">
+ <form class='login-form' @submit.prevent='submit'>
+ <template v-if="isPasswordAuth">
<div class='form-group'>
<label for='username'>{{$t('login.username')}}</label>
- <input :disabled="loggingIn" v-model='user.username' class='form-control' id='username' v-bind:placeholder="$t('login.placeholder')">
+ <input :disabled="loggingIn" v-model='user.username'
+ class='form-control' id='username'
+ :placeholder="$t('login.placeholder')">
</div>
<div class='form-group'>
<label for='password'>{{$t('login.password')}}</label>
- <input :disabled="loggingIn" v-model='user.password' class='form-control' id='password' type='password'>
+ <input :disabled="loggingIn" v-model='user.password'
+ ref='passwordInput' class='form-control' id='password' type='password'>
</div>
- <div class='form-group'>
- <div class='login-bottom'>
- <div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
- <button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
- </div>
- </div>
- </form>
+ </template>
- <form v-if="loginMethod == 'token'" v-on:submit.prevent='oAuthLogin' class="login-form">
- <div class="form-group">
- <p>{{$t('login.description')}}</p>
- </div>
- <div class='form-group'>
- <div class='login-bottom'>
- <div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
- <button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
+ <div class="form-group" v-if="isTokenAuth">
+ <p>{{$t('login.description')}}</p>
+ </div>
+
+ <div class='form-group'>
+ <div class='login-bottom'>
+ <div>
+ <router-link :to="{name: 'registration'}"
+ v-if='registrationOpen'
+ class='register'>
+ {{$t('login.register')}}
+ </router-link>
</div>
- </div>
- </form>
-
- <div v-if="authError" class='form-group'>
- <div class='alert error'>
- {{authError}}
- <i class="button-icon icon-cancel" @click="clearError"></i>
+ <button :disabled="loggingIn" type='submit' class='btn btn-default'>
+ {{$t('login.login')}}
+ </button>
</div>
</div>
+ </form>
+ </div>
+
+ <div v-if="error" class='form-group'>
+ <div class='alert error'>
+ {{error}}
+ <i class="button-icon icon-cancel" @click="clearError"></i>
</div>
</div>
+</div>
</template>
<script src="./login_form.js" ></script>
diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js
new file mode 100644
index 00000000..fbe9b437
--- /dev/null
+++ b/src/components/mfa_form/recovery_form.js
@@ -0,0 +1,41 @@
+import mfaApi from '../../services/new_api/mfa.js'
+import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
+
+export default {
+ data: () => ({
+ code: null,
+ error: false
+ }),
+ computed: {
+ ...mapGetters({
+ authApp: 'authFlow/app',
+ authSettings: 'authFlow/settings'
+ }),
+ ...mapState({ instance: 'instance' })
+ },
+ methods: {
+ ...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
+ ...mapActions({ login: 'authFlow/login' }),
+ clearError () { this.error = false },
+ submit () {
+ const data = {
+ app: this.authApp,
+ instance: this.instance.server,
+ mfaToken: this.authSettings.mfa_token,
+ code: this.code
+ }
+
+ mfaApi.verifyRecoveryCode(data).then((result) => {
+ if (result.error) {
+ this.error = result.error
+ this.code = null
+ return
+ }
+
+ this.login(result).then(() => {
+ this.$router.push({name: 'friends'})
+ })
+ })
+ }
+ }
+}
diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue
new file mode 100644
index 00000000..e0e2d65b
--- /dev/null
+++ b/src/components/mfa_form/recovery_form.vue
@@ -0,0 +1,42 @@
+<template>
+<div class="login panel panel-default">
+ <!-- Default panel contents -->
+
+ <div class="panel-heading">{{$t('login.heading.recovery')}}</div>
+
+ <div class="panel-body">
+ <form class='login-form' @submit.prevent='submit'>
+ <div class='form-group'>
+ <label for='code'>{{$t('login.recovery_code')}}</label>
+ <input v-model='code' class='form-control' id='code'>
+ </div>
+
+ <div class='form-group'>
+ <div class='login-bottom'>
+ <div>
+ <a href="#" @click.prevent="requireTOTP">
+ {{$t('login.enter_two_factor_code')}}
+ </a>
+ <br />
+ <a href="#" @click.prevent="abortMFA">
+ {{$t('general.cancel')}}
+ </a>
+ </div>
+ <button type='submit' class='btn btn-default'>
+ {{$t('general.verify')}}
+ </button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+
+ <div v-if="error" class='form-group'>
+ <div class='alert error'>
+ {{error}}
+ <i class="button-icon icon-cancel" @click="clearError"></i>
+ </div>
+ </div>
+</div>
+</template>
+<script src="./recovery_form.js" ></script>
diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js
new file mode 100644
index 00000000..6c94fe52
--- /dev/null
+++ b/src/components/mfa_form/totp_form.js
@@ -0,0 +1,40 @@
+import mfaApi from '../../services/new_api/mfa.js'
+import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
+export default {
+ data: () => ({
+ code: null,
+ error: false
+ }),
+ computed: {
+ ...mapGetters({
+ authApp: 'authFlow/app',
+ authSettings: 'authFlow/settings'
+ }),
+ ...mapState({ instance: 'instance' })
+ },
+ methods: {
+ ...mapMutations('authFlow', ['requireRecovery', 'abortMFA']),
+ ...mapActions({ login: 'authFlow/login' }),
+ clearError () { this.error = false },
+ submit () {
+ const data = {
+ app: this.authApp,
+ instance: this.instance.server,
+ mfaToken: this.authSettings.mfa_token,
+ code: this.code
+ }
+
+ mfaApi.verifyOTPCode(data).then((result) => {
+ if (result.error) {
+ this.error = result.error
+ this.code = null
+ return
+ }
+
+ this.login(result).then(() => {
+ this.$router.push({name: 'friends'})
+ })
+ })
+ }
+ }
+}
diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue
new file mode 100644
index 00000000..c547785e
--- /dev/null
+++ b/src/components/mfa_form/totp_form.vue
@@ -0,0 +1,45 @@
+<template>
+<div class="login panel panel-default">
+ <!-- Default panel contents -->
+
+ <div class="panel-heading">
+ {{$t('login.heading.totp')}}
+ </div>
+
+ <div class="panel-body">
+ <form class='login-form' @submit.prevent='submit'>
+ <div class='form-group'>
+ <label for='code'>
+ {{$t('login.authentication_code')}}
+ </label>
+ <input v-model='code' class='form-control' id='code'>
+ </div>
+
+ <div class='form-group'>
+ <div class='login-bottom'>
+ <div>
+ <a href="#" @click.prevent="requireRecovery">
+ {{$t('login.enter_recovery_code')}}
+ </a>
+ <br />
+ <a href="#" @click.prevent="abortMFA">
+ {{$t('general.cancel')}}
+ </a>
+ </div>
+ <button type='submit' class='btn btn-default'>
+ {{$t('general.verify')}}
+ </button>
+ </div>
+ </div>
+ </form>
+ </div>
+
+ <div v-if="error" class='form-group'>
+ <div class='alert error'>
+ {{error}}
+ <i class="button-icon icon-cancel" @click="clearError"></i>
+ </div>
+ </div>
+</div>
+</template>
+<script src="./totp_form.js"></script>
diff --git a/src/components/user_panel/user_panel.js b/src/components/user_panel/user_panel.js
index d4478290..c2f51eb6 100644
--- a/src/components/user_panel/user_panel.js
+++ b/src/components/user_panel/user_panel.js
@@ -1,13 +1,15 @@
-import LoginForm from '../login_form/login_form.vue'
+import AuthForm from '../auth_form/auth_form.js'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCard from '../user_card/user_card.vue'
+import { mapState } from 'vuex'
const UserPanel = {
computed: {
- user () { return this.$store.state.users.currentUser }
+ signedIn () { return this.user },
+ ...mapState({ user: state => state.users.currentUser })
},
components: {
- LoginForm,
+ AuthForm,
PostStatusForm,
UserCard
}
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 8310f30e..37e28ca5 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -1,13 +1,20 @@
<template>
<div class="user-panel">
- <div v-if='user' class="panel panel-default" style="overflow: visible;">
+
+ <div v-if="signedIn" key="user-panel" class="panel panel-default signed-in">
<UserCard :user="user" :hideBio="true" rounded="top"/>
<div class="panel-footer">
<post-status-form v-if='user'></post-status-form>
</div>
</div>
- <login-form v-if='!user'></login-form>
+ <auth-form v-else key="user-panel"/>
</div>
</template>
<script src="./user_panel.js"></script>
+
+<style lang="scss">
+.user-panel .signed-in {
+ overflow: visible;
+}
+</style>
diff --git a/src/components/user_settings/confirm.js b/src/components/user_settings/confirm.js
new file mode 100644
index 00000000..0f4ddfc9
--- /dev/null
+++ b/src/components/user_settings/confirm.js
@@ -0,0 +1,9 @@
+const Confirm = {
+ props: ['disabled'],
+ data: () => ({}),
+ methods: {
+ confirm () { this.$emit('confirm') },
+ cancel () { this.$emit('cancel') }
+ }
+}
+export default Confirm
diff --git a/src/components/user_settings/confirm.vue b/src/components/user_settings/confirm.vue
new file mode 100644
index 00000000..46a42e38
--- /dev/null
+++ b/src/components/user_settings/confirm.vue
@@ -0,0 +1,14 @@
+<template>
+<div>
+ <slot></slot>
+ <button class="btn btn-default" @click="confirm" :disabled="disabled">
+ {{$t('general.confirm')}}
+ </button>
+ <button class="btn btn-default" @click="cancel" :disabled="disabled">
+ {{$t('general.cancel')}}
+ </button>
+</div>
+</template>
+
+<script src="./confirm.js">
+</script>
diff --git a/src/components/user_settings/mfa.js b/src/components/user_settings/mfa.js
new file mode 100644
index 00000000..2acee862
--- /dev/null
+++ b/src/components/user_settings/mfa.js
@@ -0,0 +1,152 @@
+import RecoveryCodes from './mfa_backup_codes.vue'
+import TOTP from './mfa_totp.vue'
+import Confirm from './confirm.vue'
+import VueQrcode from '@chenfengyuan/vue-qrcode'
+import { mapState } from 'vuex'
+
+const Mfa = {
+ data: () => ({
+ settings: { // current settings of MFA
+ enabled: false,
+ totp: false
+ },
+ setupState: { // setup mfa
+ state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
+ setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
+ },
+ backupCodes: {
+ getNewCodes: false,
+ inProgress: false, // progress of fetch codes
+ codes: []
+ },
+ otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
+ provisioning_uri: '',
+ key: ''
+ },
+ currentPassword: null,
+ otpConfirmToken: null,
+ error: null,
+ readyInit: false
+ }),
+ components: {
+ 'recovery-codes': RecoveryCodes,
+ 'totp-item': TOTP,
+ 'qrcode': VueQrcode,
+ 'confirm': Confirm
+ },
+ computed: {
+ canSetupOTP () {
+ return (
+ (this.setupInProgress && this.backupCodesPrepared) ||
+ this.settings.enabled
+ ) && !this.settings.totp && !this.setupOTPInProgress
+ },
+ setupInProgress () {
+ return this.setupState.state !== '' && this.setupState.state !== 'complete'
+ },
+ setupOTPInProgress () {
+ return this.setupState.state === 'setupOTP' && !this.completedOTP
+ },
+ prepareOTP () {
+ return this.setupState.setupOTPState === 'prepare'
+ },
+ confirmOTP () {
+ return this.setupState.setupOTPState === 'confirm'
+ },
+ completedOTP () {
+ return this.setupState.setupOTPState === 'completed'
+ },
+ backupCodesPrepared () {
+ return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
+ },
+ confirmNewBackupCodes () {
+ return this.backupCodes.getNewCodes
+ },
+ ...mapState({
+ backendInteractor: (state) => state.api.backendInteractor
+ })
+ },
+
+ methods: {
+ activateOTP () {
+ if (!this.settings.enabled) {
+ this.setupState.state = 'getBackupcodes'
+ this.fetchBackupCodes()
+ }
+ },
+ fetchBackupCodes () {
+ this.backupCodes.inProgress = true
+ this.backupCodes.codes = []
+
+ return this.backendInteractor.generateMfaBackupCodes()
+ .then((res) => {
+ this.backupCodes.codes = res.codes
+ this.backupCodes.inProgress = false
+ })
+ },
+ getBackupCodes () { // get a new backup codes
+ this.backupCodes.getNewCodes = true
+ },
+ confirmBackupCodes () { // confirm getting new backup codes
+ this.fetchBackupCodes().then((res) => {
+ this.backupCodes.getNewCodes = false
+ })
+ },
+ cancelBackupCodes () { // cancel confirm form of new backup codes
+ this.backupCodes.getNewCodes = false
+ },
+
+ // Setup OTP
+ setupOTP () { // prepare setup OTP
+ this.setupState.state = 'setupOTP'
+ this.setupState.setupOTPState = 'prepare'
+ this.backendInteractor.mfaSetupOTP()
+ .then((res) => {
+ this.otpSettings = res
+ this.setupState.setupOTPState = 'confirm'
+ })
+ },
+ doConfirmOTP () { // handler confirm enable OTP
+ this.error = null
+ this.backendInteractor.mfaConfirmOTP({
+ token: this.otpConfirmToken,
+ password: this.currentPassword
+ })
+ .then((res) => {
+ if (res.error) {
+ this.error = res.error
+ return
+ }
+ this.completeSetup()
+ })
+ },
+
+ completeSetup () {
+ this.setupState.setupOTPState = 'complete'
+ this.setupState.state = 'complete'
+ this.currentPassword = null
+ this.error = null
+ this.fetchSettings()
+ },
+ cancelSetup () { // cancel setup
+ this.setupState.setupOTPState = ''
+ this.setupState.state = ''
+ this.currentPassword = null
+ this.error = null
+ },
+ // end Setup OTP
+
+ // fetch settings from server
+ async fetchSettings () {
+ let result = await this.backendInteractor.fetchSettingsMFA()
+ this.settings = result.settings
+ return result
+ }
+ },
+ mounted () {
+ this.fetchSettings().then(() => {
+ this.readyInit = true
+ })
+ }
+}
+export default Mfa
diff --git a/src/components/user_settings/mfa.vue b/src/components/user_settings/mfa.vue
new file mode 100644
index 00000000..ded426dd
--- /dev/null
+++ b/src/components/user_settings/mfa.vue
@@ -0,0 +1,121 @@
+<template>
+<div class="setting-item mfa-settings" v-if="readyInit">
+
+ <div class="mfa-heading">
+ <h2>{{$t('settings.mfa.title')}}</h2>
+ </div>
+
+ <div>
+ <div class="setting-item" v-if="!setupInProgress">
+ <!-- Enabled methods -->
+ <h3>{{$t('settings.mfa.authentication_methods')}}</h3>
+ <totp-item :settings="settings" @deactivate="fetchSettings" @activate="activateOTP"/>
+ <br />
+
+ <div v-if="settings.enabled"> <!-- backup codes block-->
+ <recovery-codes :backup-codes="backupCodes" v-if="!confirmNewBackupCodes" />
+ <button class="btn btn-default" @click="getBackupCodes" v-if="!confirmNewBackupCodes">
+ {{$t('settings.mfa.generate_new_recovery_codes')}}
+ </button>
+
+ <div v-if="confirmNewBackupCodes">
+ <confirm @confirm="confirmBackupCodes" @cancel="cancelBackupCodes"
+ :disabled="backupCodes.inProgress">
+ <p class="warning">{{$t('settings.mfa.warning_of_generate_new_codes')}}</p>
+ </confirm>
+ </div>
+ </div>
+ </div>
+
+ <div v-if="setupInProgress"> <!-- setup block-->
+
+ <h3>{{$t('settings.mfa.setup_otp')}}</h3>
+
+ <recovery-codes :backup-codes="backupCodes" v-if="!setupOTPInProgress"/>
+
+
+ <button class="btn btn-default" @click="cancelSetup" v-if="canSetupOTP">
+ {{$t('general.cancel')}}
+ </button>
+
+ <button class="btn btn-default" v-if="canSetupOTP" @click="setupOTP">
+ {{$t('settings.mfa.setup_otp')}}
+ </button>
+
+ <template v-if="setupOTPInProgress">
+ <i v-if="prepareOTP">{{$t('settings.mfa.wait_pre_setup_otp')}}</i>
+
+ <div v-if="confirmOTP">
+ <div class="setup-otp">
+ <div class="qr-code">
+ <h4>{{$t('settings.mfa.scan.title')}}</h4>
+ <p>{{$t('settings.mfa.scan.desc')}}</p>
+ <qrcode :value="otpSettings.provisioning_uri" :options="{ width: 200 }"></qrcode>
+ <p>
+ {{$t('settings.mfa.scan.secret_code')}}:
+ {{otpSettings.key}}
+ </p>
+ </div>
+
+ <div class="verify">
+ <h4>{{$t('general.verify')}}</h4>
+ <p>{{$t('settings.mfa.verify.desc')}}</p>
+ <input type="text" v-model="otpConfirmToken">
+
+ <p>{{$t('settings.enter_current_password_to_confirm')}}:</p>
+ <input type="password" v-model="currentPassword">
+ <div class="confirm-otp-actions">
+ <button class="btn btn-default" @click="doConfirmOTP">
+ {{$t('settings.mfa.confirm_and_enable')}}
+ </button>
+ <button class="btn btn-default" @click="cancelSetup">
+ {{$t('general.cancel')}}
+ </button>
+ </div>
+ <div class="alert error" v-if="error">{{error}}</div>
+ </div>
+ </div>
+ </div>
+ </template>
+ </div>
+
+ </div>
+</div>
+</template>
+
+<script src="./mfa.js"></script>
+<style lang="scss">
+@import '../../_variables.scss';
+.warning {
+ color: $fallback--cOrange;
+ color: var(--cOrange, $fallback--cOrange);
+}
+.mfa-settings {
+ .mfa-heading, .method-item {
+ overflow: hidden;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: baseline;
+ }
+
+ .setup-otp {
+ display: flex;
+ justify-content: center;
+ flex-wrap: wrap;
+ .qr-code {
+ flex: 1;
+ padding-right: 10px;
+ }
+ .verify { flex: 1; }
+ .error { margin: 4px 0 0 0; }
+ .confirm-otp-actions {
+ button {
+ width: 15em;
+ margin-top: 5px;
+ }
+
+ }
+ }
+}
+</style>
diff --git a/src/components/user_settings/mfa_backup_codes.js b/src/components/user_settings/mfa_backup_codes.js
new file mode 100644
index 00000000..f0a984ec
--- /dev/null
+++ b/src/components/user_settings/mfa_backup_codes.js
@@ -0,0 +1,17 @@
+export default {
+ props: {
+ backupCodes: {
+ type: Object,
+ default: () => ({
+ inProgress: false,
+ codes: []
+ })
+ }
+ },
+ data: () => ({}),
+ computed: {
+ inProgress () { return this.backupCodes.inProgress },
+ ready () { return this.backupCodes.codes.length > 0 },
+ displayTitle () { return this.inProgress || this.ready }
+ }
+}
diff --git a/src/components/user_settings/mfa_backup_codes.vue b/src/components/user_settings/mfa_backup_codes.vue
new file mode 100644
index 00000000..c275bd63
--- /dev/null
+++ b/src/components/user_settings/mfa_backup_codes.vue
@@ -0,0 +1,22 @@
+<template>
+<div>
+ <h4 v-if="displayTitle">{{$t('settings.mfa.recovery_codes')}}</h4>
+ <i v-if="inProgress">{{$t('settings.mfa.waiting_a_recovery_codes')}}</i>
+ <template v-if="ready">
+ <p class="alert warning">{{$t('settings.mfa.recovery_codes_warning')}}</p>
+ <ul class="backup-codes"><li v-for="code in backupCodes.codes">{{code}}</li></ul>
+ </template>
+</div>
+</template>
+<script src="./mfa_backup_codes.js"></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+.warning {
+ color: $fallback--cOrange;
+ color: var(--cOrange, $fallback--cOrange);
+}
+.backup-codes {
+ font-family: var(--postCodeFont, monospace);
+}
+</style>
diff --git a/src/components/user_settings/mfa_totp.js b/src/components/user_settings/mfa_totp.js
new file mode 100644
index 00000000..8408d8e9
--- /dev/null
+++ b/src/components/user_settings/mfa_totp.js
@@ -0,0 +1,49 @@
+import Confirm from './confirm.vue'
+import { mapState } from 'vuex'
+
+export default {
+ props: ['settings'],
+ data: () => ({
+ error: false,
+ currentPassword: '',
+ deactivate: false,
+ inProgress: false // progress peform request to disable otp method
+ }),
+ components: {
+ 'confirm': Confirm
+ },
+ computed: {
+ isActivated () {
+ return this.settings.totp
+ },
+ ...mapState({
+ backendInteractor: (state) => state.api.backendInteractor
+ })
+ },
+ methods: {
+ doActivate () {
+ this.$emit('activate')
+ },
+ cancelDeactivate () { this.deactivate = false },
+ doDeactivate () {
+ this.error = null
+ this.deactivate = true
+ },
+ confirmDeactivate () { // confirm deactivate TOTP method
+ this.error = null
+ this.inProgress = true
+ this.backendInteractor.mfaDisableOTP({
+ password: this.currentPassword
+ })
+ .then((res) => {
+ this.inProgress = false
+ if (res.error) {
+ this.error = res.error
+ return
+ }
+ this.deactivate = false
+ this.$emit('deactivate')
+ })
+ }
+ }
+}
diff --git a/src/components/user_settings/mfa_totp.vue b/src/components/user_settings/mfa_totp.vue
new file mode 100644
index 00000000..6b73c8f4
--- /dev/null
+++ b/src/components/user_settings/mfa_totp.vue
@@ -0,0 +1,23 @@
+<template>
+<div>
+ <div class="method-item">
+ <strong>{{$t('settings.mfa.otp')}}</strong>
+ <button class="btn btn-default" v-if="!isActivated" @click="doActivate">
+ {{$t('general.enable')}}
+ </button>
+
+ <button class="btn btn-default" :disabled="deactivate" @click="doDeactivate"
+ v-if="isActivated">
+ {{$t('general.disable')}}
+ </button>
+ </div>
+
+ <confirm @confirm="confirmDeactivate" @cancel="cancelDeactivate"
+ :disabled="inProgress" v-if="deactivate">
+ {{$t('settings.enter_current_password_to_confirm')}}:
+ <input type="password" v-model="currentPassword">
+ </confirm>
+ <div class="alert error" v-if="error">{{error}}</div>
+</div>
+</template>
+<script src="./mfa_totp.js"></script>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index ae36e5e8..69505806 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -17,6 +17,7 @@ import Importer from '../importer/importer.vue'
import Exporter from '../exporter/exporter.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription'
import userSearchApi from '../../services/new_api/user_search.js'
+import Mfa from './mfa.vue'
const BlockList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
@@ -75,7 +76,8 @@ const UserSettings = {
MuteCard,
ProgressButton,
Importer,
- Exporter
+ Exporter,
+ Mfa
},
computed: {
user () {
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 15a5428c..bbe41f11 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -152,7 +152,7 @@
</tbody>
</table>
</div>
-
+ <mfa />
<div class="setting-item">
<h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 2b5c5468..a29f394b 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -27,7 +27,11 @@
"optional": "optional",
"show_more": "Show more",
"show_less": "Show less",
- "cancel": "Cancel"
+ "cancel": "Cancel",
+ "disable": "Disable",
+ "enable": "Enable",
+ "confirm": "Confirm",
+ "verify": "Verify"
},
"image_cropper": {
"crop_picture": "Crop picture",
@@ -48,7 +52,15 @@
"placeholder": "e.g. lain",
"register": "Register",
"username": "Username",
- "hint": "Log in to join the discussion"
+ "hint": "Log in to join the discussion",
+ "authentication_code": "Authentication code",
+ "enter_recovery_code": "Enter a recovery code",
+ "enter_two_factor_code": "Enter a two-factor code",
+ "recovery_code": "Recovery code",
+ "heading" : {
+ "totp" : "Two-factor authentication",
+ "recovery" : "Two-factor recovery"
+ }
},
"media_modal": {
"previous": "Previous",
@@ -138,6 +150,29 @@
},
"settings": {
"app_name": "App name",
+ "security": "Security",
+ "enter_current_password_to_confirm": "Enter your current password to confirm your identity",
+ "mfa": {
+ "otp" : "OTP",
+ "setup_otp" : "Setup OTP",
+ "wait_pre_setup_otp" : "presetting OTP",
+ "confirm_and_enable" : "Confirm & enable OTP",
+ "title": "Two-factor Authentication",
+ "generate_new_recovery_codes" : "Generate new recovery codes",
+ "warning_of_generate_new_codes" : "When you generate new recovery codes, your old codes won’t work anymore.",
+ "recovery_codes" : "Recovery codes.",
+ "waiting_a_recovery_codes": "Receiving backup codes...",
+ "recovery_codes_warning" : "Write the codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.",
+ "authentication_methods" : "Authentication methods",
+ "scan": {
+ "title": "Scan",
+ "desc": "Using your two-factor app, scan this QR code or enter text key:",
+ "secret_code": "Key"
+ },
+ "verify": {
+ "desc": "To enable two-factor authentication, enter the code from your two-factor app:"
+ }
+ },
"attachmentRadius": "Attachments",
"attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 7a22194a..d24ef0cb 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -9,7 +9,11 @@
"general": {
"apply": "Применить",
"submit": "Отправить",
- "cancel": "Отмена"
+ "cancel": "Отмена",
+ "disable": "Оключить",
+ "enable": "Включить",
+ "confirm": "Подтвердить",
+ "verify": "Проверить"
},
"login": {
"login": "Войти",
@@ -17,7 +21,15 @@
"password": "Пароль",
"placeholder": "e.c. lain",
"register": "Зарегистрироваться",
- "username": "Имя пользователя"
+ "username": "Имя пользователя",
+ "authentication_code": "Код аутентификации",
+ "enter_recovery_code": "Ввести код восстановления",
+ "enter_two_factor_code": "Ввести код аутентификации",
+ "recovery_code": "Код восстановления",
+ "heading" : {
+ "TotpForm" : "Двухфакторная аутентификация",
+ "RecoveryForm" : "Two-factor recovery"
+ }
},
"nav": {
"back": "Назад",
@@ -79,6 +91,28 @@
}
},
"settings": {
+ "enter_current_password_to_confirm": "Введите свой текущий пароль",
+ "mfa": {
+ "otp" : "OTP",
+ "setup_otp" : "Настройка OTP",
+ "wait_pre_setup_otp" : "предварительная настройка OTP",
+ "confirm_and_enable" : "Подтвердить и включить OTP",
+ "title": "Двухфакторная аутентификация",
+ "generate_new_recovery_codes" : "Получить новые коды востановления",
+ "warning_of_generate_new_codes" : "После получения новых кодов восстановления, старые больше не будут работать.",
+ "recovery_codes" : "Коды восстановления.",
+ "waiting_a_recovery_codes": "Получение кодов восстановления ...",
+ "recovery_codes_warning" : "Запишите эти коды и держите в безопасном месте - иначе вы их больше не увидите. Если вы потеряете доступ к OTP приложению - без резервных кодов вы больше не сможете залогиниться.",
+ "authentication_methods" : "Методы аутентификации",
+ "scan": {
+ "title": "Сканирование",
+ "desc": "Используйте приложение для двухэтапной аутентификации для сканирования этого QR-код или введите текстовый ключ:",
+ "secret_code": "Ключ"
+ },
+ "verify": {
+ "desc": "Чтобы включить двухэтапную аутентификации, введите код из вашего приложение для двухэтапной аутентификации:"
+ }
+ },
"attachmentRadius": "Прикреплённые файлы",
"attachments": "Вложения",
"autoload": "Включить автоматическую загрузку при прокрутке вниз",
diff --git a/src/main.js b/src/main.js
index e12db6a3..5758c7bd 100644
--- a/src/main.js
+++ b/src/main.js
@@ -10,6 +10,7 @@ import apiModule from './modules/api.js'
import configModule from './modules/config.js'
import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js'
+import authFlowModule from './modules/auth_flow.js'
import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js'
import reportsModule from './modules/reports.js'
@@ -77,6 +78,7 @@ const persistedStateOptions = {
config: configModule,
chat: chatModule,
oauth: oauthModule,
+ authFlow: authFlowModule,
mediaViewer: mediaViewerModule,
oauthTokens: oauthTokensModule,
reports: reportsModule
diff --git a/src/modules/auth_flow.js b/src/modules/auth_flow.js
new file mode 100644
index 00000000..86328cf3
--- /dev/null
+++ b/src/modules/auth_flow.js
@@ -0,0 +1,89 @@
+const PASSWORD_STRATEGY = 'password'
+const TOKEN_STRATEGY = 'token'
+
+// MFA strategies
+const TOTP_STRATEGY = 'totp'
+const RECOVERY_STRATEGY = 'recovery'
+
+// initial state
+const state = {
+ app: null,
+ settings: {},
+ strategy: PASSWORD_STRATEGY,
+ initStrategy: PASSWORD_STRATEGY // default strategy from config
+}
+
+const resetState = (state) => {
+ state.strategy = state.initStrategy
+ state.settings = {}
+ state.app = null
+}
+
+// getters
+const getters = {
+ app: (state, getters) => {
+ return state.app
+ },
+ settings: (state, getters) => {
+ return state.settings
+ },
+ requiredPassword: (state, getters, rootState) => {
+ return state.strategy === PASSWORD_STRATEGY
+ },
+ requiredToken: (state, getters, rootState) => {
+ return state.strategy === TOKEN_STRATEGY
+ },
+ requiredTOTP: (state, getters, rootState) => {
+ return state.strategy === TOTP_STRATEGY
+ },
+ requiredRecovery: (state, getters, rootState) => {
+ return state.strategy === RECOVERY_STRATEGY
+ }
+}
+
+// mutations
+const mutations = {
+ setInitialStrategy (state, strategy) {
+ if (strategy) {
+ state.initStrategy = strategy
+ state.strategy = strategy
+ }
+ },
+ requirePassword (state) {
+ state.strategy = PASSWORD_STRATEGY
+ },
+ requireToken (state) {
+ state.strategy = TOKEN_STRATEGY
+ },
+ requireMFA (state, {app, settings}) {
+ state.settings = settings
+ state.app = app
+ state.strategy = TOTP_STRATEGY // default strategy of MFA
+ },
+ requireRecovery (state) {
+ state.strategy = RECOVERY_STRATEGY
+ },
+ requireTOTP (state) {
+ state.strategy = TOTP_STRATEGY
+ },
+ abortMFA (state) {
+ resetState(state)
+ }
+}
+
+// actions
+const actions = {
+ async login ({state, dispatch, commit}, {access_token}) {
+ commit('setToken', access_token, { root: true })
+ await dispatch('loginUser', access_token, { root: true })
+ resetState(state)
+ }
+}
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ mutations,
+ actions
+}
diff --git a/src/modules/instance.js b/src/modules/instance.js
index d4185f6a..fc4578ed 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -27,7 +27,6 @@ const defaultState = {
scopeCopy: true,
subjectLineBehavior: 'email',
postContentType: 'text/plain',
- loginMethod: 'password',
nsfwCensorImage: undefined,
vapidPublicKey: undefined,
noAttachmentLinks: false,
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 04135a48..ac151a0b 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -17,6 +17,13 @@ const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
+const MFA_SETTINGS_URL = '/api/pleroma/profile/mfa'
+const MFA_BACKUP_CODES_URL = '/api/pleroma/profile/mfa/backup_codes'
+
+const MFA_SETUP_OTP_URL = '/api/pleroma/profile/mfa/setup/totp'
+const MFA_CONFIRM_OTP_URL = '/api/pleroma/profile/mfa/confirm/totp'
+const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/mfa/totp'
+
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
@@ -649,6 +656,51 @@ const changePassword = ({credentials, password, newPassword, newPasswordConfirma
.then((response) => response.json())
}
+const settingsMFA = ({credentials}) => {
+ return fetch(MFA_SETTINGS_URL, {
+ headers: authHeaders(credentials),
+ method: 'GET'
+ }).then((data) => data.json())
+}
+
+const mfaDisableOTP = ({credentials, password}) => {
+ const form = new FormData()
+
+ form.append('password', password)
+
+ return fetch(MFA_DISABLE_OTP_URL, {
+ body: form,
+ method: 'DELETE',
+ headers: authHeaders(credentials)
+ })
+ .then((response) => response.json())
+}
+
+const mfaConfirmOTP = ({credentials, password, token}) => {
+ const form = new FormData()
+
+ form.append('password', password)
+ form.append('code', token)
+
+ return fetch(MFA_CONFIRM_OTP_URL, {
+ body: form,
+ headers: authHeaders(credentials),
+ method: 'POST'
+ }).then((data) => data.json())
+}
+const mfaSetupOTP = ({credentials}) => {
+ return fetch(MFA_SETUP_OTP_URL, {
+ headers: authHeaders(credentials),
+ method: 'GET'
+ }).then((data) => data.json())
+}
+const generateMfaBackupCodes = ({credentials}) => {
+ return fetch(MFA_BACKUP_CODES_URL, {
+ headers: authHeaders(credentials),
+ method: 'GET'
+ }).then((data) => data.json())
+}
+
const fetchMutes = ({credentials}) => {
return promisedRequest({ url: MASTODON_USER_MUTES_URL, credentials })
.then((users) => users.map(parseUser))
@@ -776,6 +828,11 @@ const apiService = {
importFollows,
deleteAccount,
changePassword,
+ settingsMFA,
+ mfaDisableOTP,
+ generateMfaBackupCodes,
+ mfaSetupOTP,
+ mfaConfirmOTP,
fetchFollowRequests,
approveUser,
denyUser,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 36152429..674b7516 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -116,6 +116,12 @@ const backendInteractorService = (credentials) => {
const deleteAccount = ({password}) => apiService.deleteAccount({credentials, password})
const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation})
+ const fetchSettingsMFA = () => apiService.settingsMFA({credentials})
+ const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({credentials})
+ const mfaSetupOTP = () => apiService.mfaSetupOTP({credentials})
+ const mfaConfirmOTP = ({password, token}) => apiService.mfaConfirmOTP({credentials, password, token})
+ const mfaDisableOTP = ({password}) => apiService.mfaDisableOTP({credentials, password})
+
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({id})
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({id})
const reportUser = (params) => apiService.reportUser({credentials, ...params})
@@ -166,6 +172,11 @@ const backendInteractorService = (credentials) => {
importFollows,
deleteAccount,
changePassword,
+ fetchSettingsMFA,
+ generateMfaBackupCodes,
+ mfaSetupOTP,
+ mfaConfirmOTP,
+ mfaDisableOTP,
fetchFollowRequests,
approveUser,
denyUser,
diff --git a/src/services/new_api/mfa.js b/src/services/new_api/mfa.js
new file mode 100644
index 00000000..ddf90e6b
--- /dev/null
+++ b/src/services/new_api/mfa.js
@@ -0,0 +1,38 @@
+const verifyOTPCode = ({app, instance, mfaToken, code}) => {
+ const url = `${instance}/oauth/mfa/challenge`
+ const form = new window.FormData()
+
+ form.append('client_id', app.client_id)
+ form.append('client_secret', app.client_secret)
+ form.append('mfa_token', mfaToken)
+ form.append('code', code)
+ form.append('challenge_type', 'totp')
+
+ return window.fetch(url, {
+ method: 'POST',
+ body: form
+ }).then((data) => data.json())
+}
+
+const verifyRecoveryCode = ({app, instance, mfaToken, code}) => {
+ const url = `${instance}/oauth/mfa/challenge`
+ const form = new window.FormData()
+
+ form.append('client_id', app.client_id)
+ form.append('client_secret', app.client_secret)
+ form.append('mfa_token', mfaToken)
+ form.append('code', code)
+ form.append('challenge_type', 'recovery')
+
+ return window.fetch(url, {
+ method: 'POST',
+ body: form
+ }).then((data) => data.json())
+}
+
+const mfa = {
+ verifyOTPCode,
+ verifyRecoveryCode
+}
+
+export default mfa
diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js
index 9e656507..35c9c236 100644
--- a/src/services/new_api/oauth.js
+++ b/src/services/new_api/oauth.js
@@ -71,12 +71,45 @@ const getToken = ({app, instance, code}) => {
body: form
}).then((data) => data.json())
}
+const verifyOTPCode = ({app, instance, mfaToken, code}) => {
+ const url = `${instance}/oauth/mfa/challenge`
+ const form = new window.FormData()
+
+ form.append('client_id', app.client_id)
+ form.append('client_secret', app.client_secret)
+ form.append('mfa_token', mfaToken)
+ form.append('code', code)
+ form.append('challenge_type', 'totp')
+
+ return window.fetch(url, {
+ method: 'POST',
+ body: form
+ }).then((data) => data.json())
+}
+
+const verifyRecoveryCode = ({app, instance, mfaToken, code}) => {
+ const url = `${instance}/oauth/mfa/challenge`
+ const form = new window.FormData()
+
+ form.append('client_id', app.client_id)
+ form.append('client_secret', app.client_secret)
+ form.append('mfa_token', mfaToken)
+ form.append('code', code)
+ form.append('challenge_type', 'recovery')
+
+ return window.fetch(url, {
+ method: 'POST',
+ body: form
+ }).then((data) => data.json())
+}
const oauth = {
login,
getToken,
getTokenWithCredentials,
- getOrCreateApp
+ getOrCreateApp,
+ verifyOTPCode,
+ verifyRecoveryCode
}
export default oauth