Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[ Refactor ] Add customizable TOTP_NOT_FOUND error message and remove hostUrl from MagicLinkGenerationOptions. #34

Merged
merged 3 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 6 additions & 7 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ authenticator.use(
The Magic Link is optional and enabled by default. You can decide to opt-out by setting the `enabled` option to `false`.

Furthermore, the Magic Link can be customized via the `magicLinkGeneration` object in the TOTPStrategy Instance.
The URL link generated will be in the format of `{hostURL}{callbackPath}?{codeField}=<magic-link-code>`.
The URL link generated will be in the format of `{request url origin}{callbackPath}?{codeField}=<magic-link-code>`.

```ts
export interface MagicLinkGenerationOptions {
Expand All @@ -84,12 +84,6 @@ export interface MagicLinkGenerationOptions {
* @default true
*/
enabled?: boolean
/**
* The host URL for the Magic Link.
* If omitted, it will be inferred from the request.
* @default undefined
*/
hostUrl?: string
/**
* The callback path for the Magic Link.
* @default '/magic-link'
Expand Down Expand Up @@ -122,6 +116,11 @@ export interface CustomErrorsOptions {
* The inactive TOTP error message.
*/
inactiveTotp?: string
/**
* The TOTP not found error message.
*/
totpNotFound?: string

}

authenticator.use(
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ export const ERRORS = {
INVALID_EMAIL: 'Email is not valid.',
INVALID_TOTP: 'Code is not valid.',
INACTIVE_TOTP: 'Code is no longer active.',
TOTP_NOT_FOUND: 'Database TOTP not found.',

// Miscellaneous errors.
REQUIRED_ENV_SECRET: 'Missing required .env secret.',
REQUIRED_SUCCESS_REDIRECT_URL: 'Missing required successRedirect URL.',

USER_NOT_FOUND: 'User not found.',
TOTP_NOT_FOUND: 'Database TOTP not found.',

INVALID_MAGIC_LINK_PATH: 'Invalid magic-link expected path.',
INVALID_JWT: 'Invalid JWT.',
Expand Down
19 changes: 8 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,6 @@
*/
enabled?: boolean

/**
* The host URL for the Magic Link.
* If omitted, it will be inferred from the Request.
*
* @default undefined
*/
hostUrl?: string

/**
* The callback URL path for the Magic Link.
* @default '/magic-link'
Expand Down Expand Up @@ -209,6 +201,11 @@
* The inactive TOTP error message.
*/
inactiveTotp?: string

/**
* The TOTP not found error message.
*/
totpNotFound?: string
}

/**
Expand Down Expand Up @@ -344,14 +341,14 @@
} satisfies TOTPGenerationOptions
private readonly _magicLinkGenerationDefaults = {
enabled: true,
hostUrl: undefined,
callbackPath: '/magic-link',
} satisfies MagicLinkGenerationOptions
private readonly _customErrorsDefaults = {
requiredEmail: ERRORS.REQUIRED_EMAIL,
invalidEmail: ERRORS.INVALID_EMAIL,
invalidTotp: ERRORS.INVALID_TOTP,
inactiveTotp: ERRORS.INACTIVE_TOTP,
totpNotFound: ERRORS.TOTP_NOT_FOUND,
} satisfies CustomErrorsOptions

constructor(
Expand Down Expand Up @@ -547,7 +544,7 @@
if (error instanceof Error) {
if (error.message === ERRORS.INVALID_JWT) {
const dbTOTP = await this.handleTOTP(sessionTotp)
if (!dbTOTP || !dbTOTP.hash) throw new Error(ERRORS.TOTP_NOT_FOUND)
if (!dbTOTP || !dbTOTP.hash) throw new Error(this.customErrors.totpNotFound)

await this.handleTOTP(sessionTotp, { active: false })

Expand Down Expand Up @@ -601,7 +598,7 @@
private async _validateTOTP(sessionTotp: string, otp: string) {
// Retrieve encrypted TOTP from database.
const dbTOTP = await this.handleTOTP(sessionTotp)
if (!dbTOTP || !dbTOTP.hash) throw new Error(ERRORS.TOTP_NOT_FOUND)
if (!dbTOTP || !dbTOTP.hash) throw new Error(this.customErrors.totpNotFound)

if (dbTOTP.active !== true) {
throw new Error(this.customErrors.inactiveTotp)
Expand All @@ -616,7 +613,7 @@
}

// Decryption and Verification.
const { iat, exp, ...totp } = (await verifyJWT({

Check warning on line 616 in src/index.ts

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'iat' is assigned a value but never used

Check warning on line 616 in src/index.ts

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'exp' is assigned a value but never used
jwt: sessionTotp,
secretKey: this.secret,
})) as Required<TOTPGenerationOptions> & { iat: number; exp: number }
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

const url = new URL(
options.callbackPath ?? '/',
options.hostUrl ?? new URL(options.request.url).origin,
new URL(options.request.url).origin,
)
url.searchParams.set(options.param, options.code)

Expand All @@ -44,7 +44,7 @@
* JSON Web Token (JWT).
*/
type SignJWTOptions = {
payload: { [key: string]: any }

Check warning on line 47 in src/utils.ts

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

Unexpected any. Specify a different type
expiresIn: number
secretKey: string
}
Expand Down
37 changes: 36 additions & 1 deletion test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,41 @@ describe('[ TOTP ]', () => {
expect(result).toEqual(new AuthorizationError(ERRORS.TOTP_NOT_FOUND))
})

test('Should throw a custom Error message on missing TOTP from database.', async () => {
const CUSTOM_ERROR = 'Custom error message.'

const totp = generateTOTP(TOTP_GENERATION_DEFAULTS)
const formData = new FormData()
formData.append(FORM_FIELDS.TOTP, totp.otp)

const request = new Request(`${HOST_URL}`, {
method: 'POST',
body: formData,
})

const strategy = new TOTPStrategy(
{
secret: SECRET_ENV,
storeTOTP,
sendTOTP,
handleTOTP,
customErrors: {
totpNotFound: CUSTOM_ERROR,
},
},
verify,
)
const result = (await strategy
.authenticate(request, sessionStorage, {
...AUTH_OPTIONS,
throwOnError: true,
successRedirect: '/',
})
.catch((error) => error)) as Response

expect(result).toEqual(new AuthorizationError(CUSTOM_ERROR))
})

test('Should throw an Error on inactive TOTP.', async () => {
handleTOTP.mockImplementation(() =>
Promise.resolve({ hash: signedTotp, attempts: 0, active: false }),
Expand Down Expand Up @@ -691,7 +726,7 @@ describe('[ TOTP ]', () => {
})

describe('[ Utils ]', () => {
test('Should use the origin from the request for the magic-link if hostUrl is not provided.', async () => {
test('Should use the origin from the request for the magic-link.', async () => {
const samples: Array<[string, string]> = [
['http://localhost/login', 'http://localhost/magic-link?code=U2N2EY'],
['http://localhost:3000/login', 'http://localhost:3000/magic-link?code=U2N2EY'],
Expand Down
1 change: 0 additions & 1 deletion test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export const TOTP_GENERATION_DEFAULTS = {

export const MAGIC_LINK_GENERATION_DEFAULTS = {
enabled: true,
hostUrl: undefined,
callbackPath: '/magic-link',
} satisfies MagicLinkGenerationOptions

Expand Down