Skip to content

Commit

Permalink
Support reset_mfa policy (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
byn9826 authored Nov 29, 2024
1 parent f246aff commit a71ae7d
Show file tree
Hide file tree
Showing 17 changed files with 492 additions and 36 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
- sign_in_or_sign_up
- change_password
- change_email
- reset_mfa
- <b>Mailer Option</b>:
- SendGrid
- Mailgun
Expand Down
11 changes: 10 additions & 1 deletion admin-panel/app/[lang]/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,23 @@ const Page = () => {
})
}

const handleResetMfa = () => {
loginRedirect({
locale: locale || undefined, policy: 'reset_mfa',
})
}

return (
<section className='flex flex-col gap-4 w-32'>
<section className='flex flex-col gap-4 w-40'>
<Button onClick={handleChangePassword}>
{t('account.changePassword')}
</Button>
<Button onClick={handleChangeEmail}>
{t('account.changeEmail')}
</Button>
<Button onClick={handleResetMfa}>
{t('account.resetMfa')}
</Button>
</section>
)
}
Expand Down
3 changes: 2 additions & 1 deletion admin-panel/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
},
"account": {
"changePassword": "Change Password",
"changeEmail": "Change Email"
"changeEmail": "Change Email",
"resetMfa": "Reset MFA"
},
"common": {
"enable": "Enable",
Expand Down
3 changes: 2 additions & 1 deletion admin-panel/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
},
"account": {
"changePassword": "Changer le mot de passe",
"changeEmail": "Changer l'email"
"changeEmail": "Changer l'email",
"resetMfa": "Réinitialiser MFA"
},
"common": {
"enable": "Activer",
Expand Down
242 changes: 238 additions & 4 deletions server/src/__tests__/normal/identity-policy.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import {
} from 'tests/identity'
import { jwtService } from 'services'
import { cryptoUtil } from 'utils'
import {
enrollEmailMfa, enrollOtpMfa, enrollSmsMfa,
} from 'tests/util'

let db: Database

Expand Down Expand Up @@ -149,7 +152,7 @@ describe(
)

test(
'should redirect if use wrong auth code',
'should throw 400 if use wrong auth code',
async () => {
await insertUsers(
db,
Expand All @@ -169,8 +172,8 @@ describe(
},
mock(db),
)
expect(res.status).toBe(302)
expect(res.headers.get('Location')).toBe(`${routeConfig.IdentityRoute.AuthCodeExpired}?locale=en`)
expect(res.status).toBe(400)
expect(await res.text()).toBe(localeConfig.Error.WrongAuthCode)
},
)
},
Expand Down Expand Up @@ -422,7 +425,7 @@ describe(
)

test(
'should redirect if use wrong auth code',
'should throw 400 if use wrong auth code',
async () => {
await insertUsers(
db,
Expand All @@ -443,9 +446,240 @@ describe(
},
mock(db),
)
expect(res.status).toBe(400)
expect(await res.text()).toBe(localeConfig.Error.WrongAuthCode)
},
)
},
)

describe(
'get /reset-mfa',
() => {
test(
'should show reset mfa page',
async () => {
await insertUsers(
db,
false,
)
const params = await prepareFollowUpParams(db)

const res = await app.request(
`${routeConfig.IdentityRoute.ResetMfa}${params}`,
{},
mock(db),
)

const html = await res.text()
const dom = new JSDOM(html)
const document = dom.window.document
expect(document.getElementsByTagName('button').length).toBe(1)
expect(document.getElementsByTagName('select').length).toBe(1)
},
)

test(
'should redirect if use wrong auth code',
async () => {
await insertUsers(
db,
false,
)
await prepareFollowUpParams(db)

const res = await app.request(
`${routeConfig.IdentityRoute.ResetMfa}?locale=en&code=abc`,
{},
mock(db),
)
expect(res.status).toBe(302)
expect(res.headers.get('Location')).toBe(`${routeConfig.IdentityRoute.AuthCodeExpired}?locale=en`)
},
)

test(
'could disable locale selector',
async () => {
global.process.env.ENABLE_LOCALE_SELECTOR = false as unknown as string
await insertUsers(
db,
false,
)
const params = await prepareFollowUpParams(db)

const res = await app.request(
`${routeConfig.IdentityRoute.ChangePassword}${params}`,
{},
mock(db),
)

const html = await res.text()
const dom = new JSDOM(html)
const document = dom.window.document
expect(document.getElementsByTagName('select').length).toBe(0)
global.process.env.ENABLE_LOCALE_SELECTOR = true as unknown as string
},
)
},
)

describe(
'post /reset-mfa',
() => {
test(
'should reset email mfa',
async () => {
process.env.ENABLE_USER_APP_CONSENT = false as unknown as string

await insertUsers(
db,
false,
)
enrollEmailMfa(db)
const body = await prepareFollowUpBody(db)

const res = await app.request(
routeConfig.IdentityRoute.ResetMfa,
{
method: 'POST',
body: JSON.stringify({ ...body }),
},
mock(db),
)
const json = await res.json()
expect(json).toStrictEqual({ success: true })

const appRecord = await getApp(db)
const reLoginRes = await postSignInRequest(
db,
appRecord,
{ password: 'Password1!' },
)
const loginResJson = await reLoginRes.json() as { code: string }
expect(loginResJson).toStrictEqual({
code: expect.any(String),
redirectUri: 'http://localhost:3000/en/dashboard',
state: '123',
scopes: ['profile', 'openid', 'offline_access'],
nextPage: routeConfig.IdentityRoute.AuthorizeMfaEnroll,
})

process.env.ENABLE_USER_APP_CONSENT = true as unknown as string
},
)

test(
'should reset sms mfa',
async () => {
process.env.ENABLE_USER_APP_CONSENT = false as unknown as string

await insertUsers(
db,
false,
)
await enrollSmsMfa(db)
await db.prepare('update "user" set "smsPhoneNumber" = ?, "smsPhoneNumberVerified" = ?').run(
'+16471231234',
1,
)
const body = await prepareFollowUpBody(db)

const res = await app.request(
routeConfig.IdentityRoute.ResetMfa,
{
method: 'POST',
body: JSON.stringify({ ...body }),
},
mock(db),
)
const json = await res.json()
expect(json).toStrictEqual({ success: true })

const appRecord = await getApp(db)
const reLoginRes = await postSignInRequest(
db,
appRecord,
{ password: 'Password1!' },
)
const loginResJson = await reLoginRes.json() as { code: string }
expect(loginResJson).toStrictEqual({
code: expect.any(String),
redirectUri: 'http://localhost:3000/en/dashboard',
state: '123',
scopes: ['profile', 'openid', 'offline_access'],
nextPage: routeConfig.IdentityRoute.AuthorizeMfaEnroll,
})

process.env.ENABLE_USER_APP_CONSENT = true as unknown as string
},
)

test(
'should reset otp mfa',
async () => {
process.env.ENABLE_USER_APP_CONSENT = false as unknown as string

await insertUsers(
db,
false,
)
await enrollOtpMfa(db)
const body = await prepareFollowUpBody(db)

const res = await app.request(
routeConfig.IdentityRoute.ResetMfa,
{
method: 'POST',
body: JSON.stringify({ ...body }),
},
mock(db),
)
const json = await res.json()
expect(json).toStrictEqual({ success: true })

const appRecord = await getApp(db)
const reLoginRes = await postSignInRequest(
db,
appRecord,
{ password: 'Password1!' },
)
const loginResJson = await reLoginRes.json() as { code: string }
expect(loginResJson).toStrictEqual({
code: expect.any(String),
redirectUri: 'http://localhost:3000/en/dashboard',
state: '123',
scopes: ['profile', 'openid', 'offline_access'],
nextPage: routeConfig.IdentityRoute.AuthorizeMfaEnroll,
})

process.env.ENABLE_USER_APP_CONSENT = true as unknown as string
},
)

test(
'should redirect if use wrong auth code',
async () => {
await insertUsers(
db,
false,
)
await prepareFollowUpBody(db)

const res = await app.request(
routeConfig.IdentityRoute.ResetMfa,
{
method: 'POST',
body: JSON.stringify({
locale: 'en',
code: 'abc',
}),
},
mock(db),
)
expect(res.status).toBe(400)
expect(await res.text()).toBe(localeConfig.Error.WrongAuthCode)
},
)
},
)
27 changes: 27 additions & 0 deletions server/src/__tests__/normal/oauth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,33 @@ describe(
},
)

test(
'could redirect to reset mfa through session',
async () => {
global.process.env.ENFORCE_ONE_MFA_ENROLLMENT = [] as unknown as string
const appRecord = await getApp(db)
await insertUsers(db)
await postSignInRequest(
db,
appRecord,
)

const url = routeConfig.OauthRoute.Authorize
const res = await getSignInRequest(
db,
url,
appRecord,
'&policy=reset_mfa',
)
expect(res.status).toBe(302)
const path = res.headers.get('Location')
expect(path).toContain(`${routeConfig.IdentityRoute.ResetMfa}`)
expect(path).toContain('&code=')

global.process.env.ENFORCE_ONE_MFA_ENROLLMENT = ['email', 'otp'] as unknown as string
},
)

test(
'could redirect to change email through session',
async () => {
Expand Down
23 changes: 23 additions & 0 deletions server/src/configs/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,29 @@ export const verifyEmail = Object.freeze({
},
})

export const resetMfa = Object.freeze({
title: {
en: 'Reset your MFA',
fr: 'Réinitialisez votre MFA',
},
success: {
en: 'Reset success!',
fr: 'Réinitialisation réussie!',
},
desc: {
en: 'Your current Multi-Factor Authentication (MFA) method will be reset. After this reset, you will need to set up MFA again to ensure continued secure access to your account.',
fr: "Votre méthode actuelle d'authentification multifactorielle (MFA) sera réinitialisée. Après cette réinitialisation, vous devrez configurer à nouveau votre MFA pour garantir un accès sécurisé continu à votre compte.",
},
confirm: {
en: 'Confirm',
fr: 'Confirmer',
},
redirect: {
en: 'Redirect back',
fr: 'Rediriger en arrière',
},
})

export const emailVerificationEmail = Object.freeze({
subject: {
en: 'Welcome to Melody Auth! Please verify your email address',
Expand Down
Loading

0 comments on commit a71ae7d

Please sign in to comment.