Support MFA recovery tokens
This commit is contained in:
parent
a1a70e0548
commit
8680242349
@ -1,2 +1,2 @@
|
|||||||
- MFA recovery codes handling
|
|
||||||
- OAuth2 -> support refresh tokens
|
- OAuth2 -> support refresh tokens
|
||||||
|
- Localize all the things
|
||||||
|
@ -31,6 +31,7 @@ const FlitterUnits = {
|
|||||||
* Custom units that modify or add functionality that needs to be made
|
* Custom units that modify or add functionality that needs to be made
|
||||||
* available to the middleware-routing-controller stack.
|
* available to the middleware-routing-controller stack.
|
||||||
*/
|
*/
|
||||||
|
'Locale' : require('flitter-i18n/src/LocaleUnit'),
|
||||||
'Redis' : require('flitter-redis/src/RedisUnit'),
|
'Redis' : require('flitter-redis/src/RedisUnit'),
|
||||||
'Jobs' : require('flitter-jobs/src/JobsUnit'),
|
'Jobs' : require('flitter-jobs/src/JobsUnit'),
|
||||||
'Settings' : require('./app/unit/SettingsUnit'),
|
'Settings' : require('./app/unit/SettingsUnit'),
|
||||||
|
@ -26,6 +26,9 @@ const template = `
|
|||||||
<div v-if="error_message" class="error-message">{{ error_message }}</div>
|
<div v-if="error_message" class="error-message">{{ error_message }}</div>
|
||||||
<div v-if="other_message" class="other-message">{{ other_message }}</div>
|
<div v-if="other_message" class="other-message">{{ other_message }}</div>
|
||||||
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
|
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
|
||||||
|
<small
|
||||||
|
class="mr-3"
|
||||||
|
><a href="#" class="text-secondary" @click="on_do_recovery">Lost your MFA device?</a></small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@ -70,4 +73,8 @@ export default class MFAChallengePage extends Component {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async on_do_recovery(event) {
|
||||||
|
await location_service.redirect('/auth/mfa/recovery', 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
72
app/assets/app/auth/MFARecovery.component.js
Normal file
72
app/assets/app/auth/MFARecovery.component.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { Component } from '../../lib/vues6/vues6.js'
|
||||||
|
import { location_service } from '../service/Location.service.js'
|
||||||
|
import { auth_api } from '../service/AuthApi.service.js'
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<div class="coreid-auth-page col-lg-6 col-md-8 col-sm-10 col-xs-12 offset-lg-3 offset-md-2 offset-sm-1 offset-xs-0 text-left">
|
||||||
|
<div class="coreid-auth-page-inner">
|
||||||
|
<div class="coreid-header font-weight-light">{{ app_name }}</div>
|
||||||
|
<div class="coreid-message">
|
||||||
|
To recover access to your account, you can enter one of the generated MFA recovery codes:
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
placeholder="Recovery Code"
|
||||||
|
v-model="recovery_code"
|
||||||
|
@keyup="on_key_up"
|
||||||
|
name="recovery_code"
|
||||||
|
:disabled="verify_success"
|
||||||
|
ref="verify_input"
|
||||||
|
maxlength="36"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-if="error_message" class="error-message">{{ error_message }}</div>
|
||||||
|
<div v-if="other_message" class="other-message">{{ other_message }}</div>
|
||||||
|
<small
|
||||||
|
class="mr-3" v-if="!loading"
|
||||||
|
><a href="#" class="text-secondary" @click="on_do_challenge">Have a normal MFA code?</a></small>
|
||||||
|
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
export default class MFARecoveryComponent extends Component {
|
||||||
|
static get selector() { return 'coreid-mfa-recovery-page' }
|
||||||
|
static get template() { return template }
|
||||||
|
static get props() { return ['app_name'] }
|
||||||
|
|
||||||
|
verify_success = false
|
||||||
|
loading = false
|
||||||
|
recovery_code = ''
|
||||||
|
error_message = ''
|
||||||
|
other_message = ''
|
||||||
|
|
||||||
|
async vue_on_create() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.verify_input.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_key_up($event) {
|
||||||
|
if ( this.recovery_code.length === 36 ) {
|
||||||
|
this.error_message = ''
|
||||||
|
this.other_message = ''
|
||||||
|
this.loading = true
|
||||||
|
const result = await auth_api.attempt_mfa_recovery(this.recovery_code)
|
||||||
|
if ( result && result.success ) {
|
||||||
|
this.other_message = `Success! There are only ${result.remaining_codes} recovery codes remaining. Let's get you on your way...`
|
||||||
|
await location_service.redirect(result.next_destination, 5000)
|
||||||
|
} else {
|
||||||
|
this.loading = false
|
||||||
|
this.error_message = 'Hm. It doesn\'t look like that code is valid.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_do_challenge() {
|
||||||
|
await location_service.redirect('/auth/mfa/challenge', 0)
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import PasswordResetComponent from './auth/PasswordReset.component.js'
|
|||||||
import InvokeActionComponent from './InvokeAction.component.js'
|
import InvokeActionComponent from './InvokeAction.component.js'
|
||||||
import RegistrationFormComponent from './auth/register/Form.component.js'
|
import RegistrationFormComponent from './auth/register/Form.component.js'
|
||||||
import MessageContainerComponent from './dash/message/MessageContainer.component.js'
|
import MessageContainerComponent from './dash/message/MessageContainer.component.js'
|
||||||
|
import MFARecoveryComponent from './auth/MFARecovery.component.js'
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
AuthLoginForm,
|
AuthLoginForm,
|
||||||
@ -18,6 +19,7 @@ const components = {
|
|||||||
InvokeActionComponent,
|
InvokeActionComponent,
|
||||||
RegistrationFormComponent,
|
RegistrationFormComponent,
|
||||||
MessageContainerComponent,
|
MessageContainerComponent,
|
||||||
|
MFARecoveryComponent,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { components }
|
export { components }
|
||||||
|
@ -35,7 +35,7 @@ const template = `
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{{ modal.message }}
|
<span v-html="modal.message"></span>
|
||||||
<div v-if="Array.isArray(modal.inputs)" class="mt-4 mb-3">
|
<div v-if="Array.isArray(modal.inputs)" class="mt-4 mb-3">
|
||||||
<span v-for="input of modal.inputs">
|
<span v-for="input of modal.inputs">
|
||||||
<input
|
<input
|
||||||
|
@ -133,8 +133,14 @@ const template = `
|
|||||||
|
|
||||||
<h6 class="pad-top">Recovery Codes</h6>
|
<h6 class="pad-top">Recovery Codes</h6>
|
||||||
<p>Recovery codes can be used to regain access to your account in the event that you lose access to the device that generates your MFA codes.</p>
|
<p>Recovery codes can be used to regain access to your account in the event that you lose access to the device that generates your MFA codes.</p>
|
||||||
|
<span v-if="!has_mfa_recovery">
|
||||||
<p class="font-italic">No recovery codes have been generated for your account.</p>
|
<p class="font-italic">No recovery codes have been generated for your account.</p>
|
||||||
<button class="btn btn-sm btn-success">Generate Recovery Codes</button>
|
<button class="btn btn-sm btn-success" @click="on_mfa_recovery_generate">Generate Recovery Codes</button>
|
||||||
|
</span>
|
||||||
|
<span v-if="has_mfa_recovery">
|
||||||
|
<p class="font-italic">Recovery codes were generate for your account on {{ mfa_recovery_date }}. <span v-if="mfa_recovery_codes === 1">There is only 1 recovery code remaining.</span><span v-if="mfa_recovery_codes !== 1">There are {{ mfa_recovery_codes }} recovery codes remaining.</span></p>
|
||||||
|
<button class="btn btn-sm btn-success" @click="on_mfa_recovery_generate">Re-generate Recovery Codes</button>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -163,6 +169,10 @@ export default class EditProfileComponent extends Component {
|
|||||||
last_reset = ''
|
last_reset = ''
|
||||||
mfa_enable_date = ''
|
mfa_enable_date = ''
|
||||||
|
|
||||||
|
has_mfa_recovery = false
|
||||||
|
mfa_recovery_date = ''
|
||||||
|
mfa_recovery_codes = 0
|
||||||
|
|
||||||
form_message = 'No changes.'
|
form_message = 'No changes.'
|
||||||
|
|
||||||
has_mfa = false
|
has_mfa = false
|
||||||
@ -248,7 +258,16 @@ export default class EditProfileComponent extends Component {
|
|||||||
|
|
||||||
const mfa = await auth_api.has_mfa()
|
const mfa = await auth_api.has_mfa()
|
||||||
this.has_mfa = mfa && mfa.mfa_enabled
|
this.has_mfa = mfa && mfa.mfa_enabled
|
||||||
if (this.has_mfa) this.mfa_enable_date = (new Date(mfa.mfa_enable_date)).toLocaleDateString()
|
if (this.has_mfa) {
|
||||||
|
this.mfa_enable_date = (new Date(mfa.mfa_enable_date)).toLocaleDateString()
|
||||||
|
|
||||||
|
const result = await auth_api.has_mfa_recovery()
|
||||||
|
if ( result && result.has_recovery ) {
|
||||||
|
this.has_mfa_recovery = true
|
||||||
|
this.mfa_recovery_date = (new Date(result.generated)).toLocaleDateString()
|
||||||
|
this.mfa_recovery_codes = result.remaining_codes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.load_app_passwords()
|
await this.load_app_passwords()
|
||||||
}
|
}
|
||||||
@ -301,5 +320,65 @@ export default class EditProfileComponent extends Component {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async on_mfa_recovery_generate($event) {
|
||||||
|
if ( !this.has_mfa ) return
|
||||||
|
if ( !this.has_mfa_recovery ) {
|
||||||
|
await this.generate_mfa_recovery()
|
||||||
|
} else {
|
||||||
|
message_service.modal({
|
||||||
|
title: 'Are you sure?',
|
||||||
|
message: 'There are already MFA recovery codes associated with your account. If you re-generate them, you will be unable to use the old ones. Continue?',
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Cancel',
|
||||||
|
type: 'close',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Re-generate',
|
||||||
|
type: 'close',
|
||||||
|
class: ['btn', 'btn-warning'],
|
||||||
|
on_click: async () => {
|
||||||
|
await this.generate_mfa_recovery()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate_mfa_recovery() {
|
||||||
|
const codes = await auth_api.generate_mfa_recovery()
|
||||||
|
if ( codes ) {
|
||||||
|
this.display_mfa_recovery_modal(codes)
|
||||||
|
} else {
|
||||||
|
message_service.alert({
|
||||||
|
type: 'error',
|
||||||
|
message: 'An unknown error occurred while attempting to generate MFA recovery codes.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
display_mfa_recovery_modal(codes) {
|
||||||
|
const code_display = codes.map(x => `<li><code>${x}</code></li>`).join('\n')
|
||||||
|
message_service.modal({
|
||||||
|
title: 'MFA Recovery Codes',
|
||||||
|
message: `We've generated recovery codes for your account. You can use these to recover access to your account in the event that you lose your MFA device.
|
||||||
|
<br><br>
|
||||||
|
Be sure to put these somewhere safe. After you close this modal, they will disappear:
|
||||||
|
<br><br>
|
||||||
|
<ul>
|
||||||
|
${code_display}
|
||||||
|
</ul>`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Close',
|
||||||
|
type: 'close',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,21 @@ class AuthAPI {
|
|||||||
if ( result && result.data && result.data.data ) return result.data.data
|
if ( result && result.data && result.data.data ) return result.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async has_mfa_recovery() {
|
||||||
|
const result = await axios.get('/api/v1/auth/mfa/recovery')
|
||||||
|
if ( result && result.data && result.data.data ) return result.data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate_mfa_recovery() {
|
||||||
|
const result = await axios.post('/api/v1/auth/mfa/recovery')
|
||||||
|
if ( result && result.data && result.data.data && result.data.data.codes ) return result.data.data.codes
|
||||||
|
}
|
||||||
|
|
||||||
|
async attempt_mfa_recovery(code) {
|
||||||
|
const result = await axios.post('/api/v1/auth/mfa/recovery/attempt', { code })
|
||||||
|
if ( result && result.data && result.data.data ) return result.data.data
|
||||||
|
}
|
||||||
|
|
||||||
async app_passwords() {
|
async app_passwords() {
|
||||||
const result = await axios.get('/api/v1/password/app_passwords')
|
const result = await axios.get('/api/v1/password/app_passwords')
|
||||||
if ( result && result.data && Array.isArray(result.data.data) ) return result.data.data
|
if ( result && result.data && Array.isArray(result.data.data) ) return result.data.data
|
||||||
|
@ -84,6 +84,41 @@ class AuthController extends Controller {
|
|||||||
return res.api(await user.to_api())
|
return res.api(await user.to_api())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async attempt_mfa_recovery(req, res, next) {
|
||||||
|
if (
|
||||||
|
!req.user.mfa_enabled
|
||||||
|
|| !Array.isArray(req.user.mfa_token.recovery_codes)
|
||||||
|
|| req.user.mfa_token.recovery_codes.length < 1
|
||||||
|
)
|
||||||
|
return res.status(400)
|
||||||
|
.message('Your user is not configured to use MFA, or has no recovery codes.')
|
||||||
|
.api()
|
||||||
|
|
||||||
|
if ( !req.body.code )
|
||||||
|
return res.status(400)
|
||||||
|
.message('Missing required field: code')
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const success = await req.user.mfa_token.attempt_recovery(req.body.code)
|
||||||
|
|
||||||
|
if ( !success )
|
||||||
|
return res.api({ success })
|
||||||
|
|
||||||
|
if ( req.trap.has_trap('mfa_challenge') )
|
||||||
|
await req.trap.end()
|
||||||
|
|
||||||
|
let next_destination = req.session.auth.flow || this.configs.get('auth.default_login_route')
|
||||||
|
delete req.session.auth.flow
|
||||||
|
|
||||||
|
req.session.mfa_remember = true
|
||||||
|
|
||||||
|
return res.api({
|
||||||
|
success: true,
|
||||||
|
next_destination,
|
||||||
|
remaining_codes: req.user.mfa_token.recovery_codes.filter(x => !x.used).length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async validate_email(req, res, next) {
|
async validate_email(req, res, next) {
|
||||||
let is_valid = !!req.body.email
|
let is_valid = !!req.body.email
|
||||||
|
|
||||||
@ -562,6 +597,37 @@ class AuthController extends Controller {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_mfa_recovery(req, res, next) {
|
||||||
|
if ( !req.user.mfa_enabled )
|
||||||
|
return res.status(400)
|
||||||
|
.message('Your user does not have MFA enabled.')
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const token = req.user.mfa_token
|
||||||
|
if ( !Array.isArray(token.recovery_codes) || token.recovery_codes.length < 1 )
|
||||||
|
return res.api({ has_recovery: false })
|
||||||
|
|
||||||
|
return res.api({
|
||||||
|
has_recovery: true,
|
||||||
|
generated: token.recovery_codes[0].generated,
|
||||||
|
remaining_codes: token.recovery_codes.filter(x => !x.used).length,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate_mfa_recovery(req, res, next) {
|
||||||
|
if ( !req.user.mfa_enabled )
|
||||||
|
return res.status(400)
|
||||||
|
.message('Your user does not have MFA enabled.')
|
||||||
|
.api()
|
||||||
|
|
||||||
|
const token = req.user.mfa_token
|
||||||
|
const codes = await token.generate_recovery()
|
||||||
|
await req.user.save()
|
||||||
|
return res.api({
|
||||||
|
codes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async generate_mfa_key(req, res, next) {
|
async generate_mfa_key(req, res, next) {
|
||||||
if ( req.user.mfa_enabled )
|
if ( req.user.mfa_enabled )
|
||||||
return res.status(400)
|
return res.status(400)
|
||||||
|
@ -54,6 +54,23 @@ class MFAController extends Controller {
|
|||||||
...this.Vue.session(req),
|
...this.Vue.session(req),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_recovery(req, res, next) {
|
||||||
|
if (
|
||||||
|
!req.user.mfa_enabled
|
||||||
|
|| !Array.isArray(req.user.mfa_token.recovery_codes)
|
||||||
|
|| req.user.mfa_token.recovery_codes.length < 1
|
||||||
|
) return this.Vue.auth_message(res, {
|
||||||
|
message: 'Unfortunately, it looks like your account does not have any MFA recovery codes generated.',
|
||||||
|
next_destination: '/auth/mfa/challenge',
|
||||||
|
button_text: 'Go Back',
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.page('auth:mfa:recovery', {
|
||||||
|
...this.Vue.data(),
|
||||||
|
...this.Vue.session(req),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = exports = MFAController
|
module.exports = exports = MFAController
|
||||||
|
23
app/models/auth/MFARecoveryCode.model.js
Normal file
23
app/models/auth/MFARecoveryCode.model.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const { Model } = require('flitter-orm')
|
||||||
|
const bcrypt = require('bcrypt')
|
||||||
|
|
||||||
|
class MFARecoveryCodeModel extends Model {
|
||||||
|
static get schema() {
|
||||||
|
return {
|
||||||
|
code: String,
|
||||||
|
used: { type: Boolean, default: false },
|
||||||
|
generated: { type: Date, default: () => new Date },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(value) {
|
||||||
|
const code = await bcrypt.hash(value, 10)
|
||||||
|
return new this({ code })
|
||||||
|
}
|
||||||
|
|
||||||
|
async verify(code) {
|
||||||
|
return await bcrypt.compare(code, this.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = MFARecoveryCodeModel
|
@ -1,5 +1,7 @@
|
|||||||
const { Model } = require('flitter-orm')
|
const { Model } = require('flitter-orm')
|
||||||
const speakeasy = require('speakeasy')
|
const speakeasy = require('speakeasy')
|
||||||
|
const MFARecoveryCode = require('./MFARecoveryCode.model')
|
||||||
|
const uuid = require('uuid/v4')
|
||||||
|
|
||||||
class MFATokenModel extends Model {
|
class MFATokenModel extends Model {
|
||||||
static get services() {
|
static get services() {
|
||||||
@ -10,9 +12,34 @@ class MFATokenModel extends Model {
|
|||||||
return {
|
return {
|
||||||
secret: String,
|
secret: String,
|
||||||
otpauth_url: String,
|
otpauth_url: String,
|
||||||
|
recovery_codes: [MFARecoveryCode],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async attempt_recovery(code) {
|
||||||
|
for ( const token of this.recovery_codes ) {
|
||||||
|
if ( await token.verify(code) && !token.used ) {
|
||||||
|
token.used = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async generate_recovery() {
|
||||||
|
this.recovery_codes = []
|
||||||
|
const values = []
|
||||||
|
|
||||||
|
for ( let i = 0; i < 4; i++ ) {
|
||||||
|
const value = uuid()
|
||||||
|
values.push(value)
|
||||||
|
this.recovery_codes.push(await MFARecoveryCode.create(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
verify(value) {
|
verify(value) {
|
||||||
return speakeasy.totp.verify({
|
return speakeasy.totp.verify({
|
||||||
secret: this.secret,
|
secret: this.secret,
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
* routes file.
|
* routes file.
|
||||||
*/
|
*/
|
||||||
const Middleware = [
|
const Middleware = [
|
||||||
|
"i18n:Localize",
|
||||||
"auth:Utility",
|
"auth:Utility",
|
||||||
"auth:TrustTokenUtility",
|
"auth:TrustTokenUtility",
|
||||||
"Traps",
|
"Traps",
|
||||||
|
7
app/routing/middleware/i18n/Localize.middleware.js
Normal file
7
app/routing/middleware/i18n/Localize.middleware.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const Middleware = require('flitter-i18n/src/middleware/Localize')
|
||||||
|
|
||||||
|
class LocalizeMiddleware extends Middleware {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = LocalizeMiddleware
|
7
app/routing/middleware/i18n/Scope.middleware.js
Normal file
7
app/routing/middleware/i18n/Scope.middleware.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const Middleware = require('flitter-i18n/src/middleware/Scope')
|
||||||
|
|
||||||
|
class ScopeMiddleware extends Middleware {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = ScopeMiddleware
|
@ -41,6 +41,11 @@ const auth_routes = {
|
|||||||
['middleware::api:Permission', { check: 'v1:auth:groups:get' }],
|
['middleware::api:Permission', { check: 'v1:auth:groups:get' }],
|
||||||
'controller::api:v1:Auth.get_group',
|
'controller::api:v1:Auth.get_group',
|
||||||
],
|
],
|
||||||
|
'/mfa/recovery': [
|
||||||
|
'middleware::auth:APIRoute',
|
||||||
|
['middleware::api:Permission', { check: 'v1:auth:mfa:recovery:get' }],
|
||||||
|
'controller::api:v1:Auth.get_mfa_recovery',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
post: {
|
post: {
|
||||||
@ -70,6 +75,11 @@ const auth_routes = {
|
|||||||
'controller::api:v1:Auth.attempt_mfa'
|
'controller::api:v1:Auth.attempt_mfa'
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'/mfa/recovery/attempt': [
|
||||||
|
'middleware::auth:UserOnly',
|
||||||
|
'controller::api:v1:Auth.attempt_mfa_recovery'
|
||||||
|
],
|
||||||
|
|
||||||
'/mfa/enable': [
|
'/mfa/enable': [
|
||||||
'middleware::auth:UserOnly',
|
'middleware::auth:UserOnly',
|
||||||
['middleware::auth:RequireTrust', { scope: 'mfa.enable', deplete: true }],
|
['middleware::auth:RequireTrust', { scope: 'mfa.enable', deplete: true }],
|
||||||
@ -99,6 +109,11 @@ const auth_routes = {
|
|||||||
'middleware::auth:GuestOnly',
|
'middleware::auth:GuestOnly',
|
||||||
'controller::api:v1:Auth.registration',
|
'controller::api:v1:Auth.registration',
|
||||||
],
|
],
|
||||||
|
'/mfa/recovery': [
|
||||||
|
'middleware::auth:APIRoute',
|
||||||
|
['middleware::api:Permission', { check: 'v1:auth:mfa:recovery:create' }],
|
||||||
|
'controller::api:v1:Auth.generate_mfa_recovery',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
patch: {
|
patch: {
|
||||||
|
@ -20,6 +20,9 @@ const mfa_routes = {
|
|||||||
['middleware::auth:RequireTrust', { scope: 'mfa.disable' }],
|
['middleware::auth:RequireTrust', { scope: 'mfa.disable' }],
|
||||||
'controller::auth:MFA.do_disable',
|
'controller::auth:MFA.do_disable',
|
||||||
],
|
],
|
||||||
|
'/recovery': [
|
||||||
|
'controller::auth:MFA.get_recovery',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
post: {
|
post: {
|
||||||
|
7
app/views/auth/mfa/recovery.pug
Normal file
7
app/views/auth/mfa/recovery.pug
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
extends ../../theme/public/base
|
||||||
|
|
||||||
|
block append style
|
||||||
|
link(rel='stylesheet' href='/style-asset/form.css')
|
||||||
|
|
||||||
|
block vue
|
||||||
|
coreid-mfa-recovery-page(v-bind:app_name="app_name")
|
@ -11,6 +11,7 @@ const app_config = {
|
|||||||
* Can be used to generate fully-qualified links.
|
* Can be used to generate fully-qualified links.
|
||||||
*/
|
*/
|
||||||
url: env("APP_URL", "http://localhost:8000/"),
|
url: env("APP_URL", "http://localhost:8000/"),
|
||||||
|
default_locale: env('APP_DEFAULT_LOCALE', 'en_US'),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,9 +12,11 @@ const traps_config = {
|
|||||||
mfa_challenge: {
|
mfa_challenge: {
|
||||||
redirect_to: '/auth/mfa/challenge',
|
redirect_to: '/auth/mfa/challenge',
|
||||||
allowed_routes: [
|
allowed_routes: [
|
||||||
|
'/auth/mfa/recovery',
|
||||||
'/auth/mfa/challenge',
|
'/auth/mfa/challenge',
|
||||||
'/api/v1/auth/mfa/attempt',
|
'/api/v1/auth/mfa/attempt',
|
||||||
'/auth/logout',
|
'/auth/logout',
|
||||||
|
'/api/v1/auth/mfa/recovery/attempt',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
3
locale/default/common.locale.js
Normal file
3
locale/default/common.locale.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = exports = {
|
||||||
|
flitter: 'Flitter',
|
||||||
|
}
|
7
locale/en_US/common.locale.js
Normal file
7
locale/en_US/common.locale.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = exports = {
|
||||||
|
welcome: 'Welcome',
|
||||||
|
powered_by_flitter: 'powered by flitter',
|
||||||
|
new_to_flitter: 'New to Flitter?',
|
||||||
|
start_here: 'Start Here.',
|
||||||
|
log_out: 'Log out',
|
||||||
|
}
|
7
locale/es_MX/common.locale.js
Normal file
7
locale/es_MX/common.locale.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = exports = {
|
||||||
|
welcome: 'Bienvenido',
|
||||||
|
powered_by_flitter: 'impulsado por flitter',
|
||||||
|
new_to_flitter: '¿Nuevo en Flitter?',
|
||||||
|
start_here: 'Empieza aqui.',
|
||||||
|
log_out: 'Cerrar sesión',
|
||||||
|
}
|
@ -23,6 +23,7 @@
|
|||||||
"flitter-di": "^0.5.0",
|
"flitter-di": "^0.5.0",
|
||||||
"flitter-flap": "^0.5.2",
|
"flitter-flap": "^0.5.2",
|
||||||
"flitter-forms": "^0.8.1",
|
"flitter-forms": "^0.8.1",
|
||||||
|
"flitter-i18n": "^0.1.0",
|
||||||
"flitter-jobs": "^0.1.2",
|
"flitter-jobs": "^0.1.2",
|
||||||
"flitter-less": "^0.5.3",
|
"flitter-less": "^0.5.3",
|
||||||
"flitter-orm": "^0.4.0",
|
"flitter-orm": "^0.4.0",
|
||||||
@ -31,7 +32,7 @@
|
|||||||
"ioredis": "^4.17.1",
|
"ioredis": "^4.17.1",
|
||||||
"is-absolute-url": "^3.0.3",
|
"is-absolute-url": "^3.0.3",
|
||||||
"ldapjs": "^1.0.2",
|
"ldapjs": "^1.0.2",
|
||||||
"libflitter": "^0.52.1",
|
"libflitter": "^0.53.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"mongodb": "^3.5.6",
|
"mongodb": "^3.5.6",
|
||||||
"nodemailer": "^6.4.6",
|
"nodemailer": "^6.4.6",
|
||||||
|
21
yarn.lock
21
yarn.lock
@ -1888,6 +1888,14 @@ flitter-forms@^0.8.1:
|
|||||||
recursive-readdir "^2.2.2"
|
recursive-readdir "^2.2.2"
|
||||||
validator "^10.11.0"
|
validator "^10.11.0"
|
||||||
|
|
||||||
|
flitter-i18n@^0.1.0:
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/flitter-i18n/-/flitter-i18n-0.1.0.tgz#9670f9ebf5f120e794f147ebeb1fe99537ac4779"
|
||||||
|
integrity sha512-vdEgcDNtA4EEjwJ0QqIIdPwR4FI67XMr4dD5U2l/u/xrnwzUrQD2O3avc5EK4RAPXTgZpK5twotTD6julJlwoA==
|
||||||
|
dependencies:
|
||||||
|
ncp "^2.0.0"
|
||||||
|
pluralize "^8.0.0"
|
||||||
|
|
||||||
flitter-jobs@^0.1.2:
|
flitter-jobs@^0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/flitter-jobs/-/flitter-jobs-0.1.2.tgz#5536bb12be728b61f6e0940b6c18760e4f1b59d6"
|
resolved "https://registry.yarnpkg.com/flitter-jobs/-/flitter-jobs-0.1.2.tgz#5536bb12be728b61f6e0940b6c18760e4f1b59d6"
|
||||||
@ -2825,10 +2833,10 @@ leven@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
|
resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
|
||||||
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
|
integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=
|
||||||
|
|
||||||
libflitter@^0.52.1:
|
libflitter@^0.53.0:
|
||||||
version "0.52.1"
|
version "0.53.0"
|
||||||
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.52.1.tgz#3025dc97b9baaaaba00907e9c2de9a39cf41f4d8"
|
resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.53.0.tgz#f1e2250597916dd7b7b7b8f0af04118ed1f30b41"
|
||||||
integrity sha512-USoAxZFfwHZkwd39wP/DuhDN3EnXYFcSby0W2WFuJlwCOXIfFTCr0tNcPbvHJrPI/Cxgy5ngExkzpG0OZhS+yQ==
|
integrity sha512-i8otSTzNwMFJEa585bw+xfaNSuXm6c/kQBYXsyB8jzFU9PBFmvrEp/QzRqQD/lDgiQChYgjRs2TJofqDYvAOHg==
|
||||||
dependencies:
|
dependencies:
|
||||||
colors "^1.3.3"
|
colors "^1.3.3"
|
||||||
connect-mongodb-session "^2.2.0"
|
connect-mongodb-session "^2.2.0"
|
||||||
@ -3684,6 +3692,11 @@ pkg-dir@^4.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^4.0.0"
|
find-up "^4.0.0"
|
||||||
|
|
||||||
|
pluralize@^8.0.0:
|
||||||
|
version "8.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
||||||
|
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
|
||||||
|
|
||||||
pngjs@^3.3.0:
|
pngjs@^3.3.0:
|
||||||
version "3.4.0"
|
version "3.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
|
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
|
||||||
|
Loading…
Reference in New Issue
Block a user