Skip to content

Commit

Permalink
Merge pull request #60 from themkvz/main
Browse files Browse the repository at this point in the history
[ Feat ]: Pass a pre-read FormData object via context.
  • Loading branch information
mw10013 committed May 10, 2024
2 parents 184a8eb + 8a60d31 commit d596754
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 11 deletions.
36 changes: 30 additions & 6 deletions docs/README.md
Expand Up @@ -93,7 +93,7 @@ Create a file called `auth.server.ts` wherever you want. <br />
> A random 64-character hexadecimal string is required to generate the TOTP codes. This string should be stored securely and not shared with anyone.
> You can use a site like https://www.grc.com/passwords.htm to generate a strong secret.
Implement the following code and replace the `secret` property with a string containing exactly 64 random hexadecimal characters (0-9 and A-F) into your `.env` file. An example is `928F416BAFC49B969E62052F00450B6E974B03E86DC6984D1FA787B7EA533227`.
Implement the following code and replace the `secret` property with a string containing exactly 64 random hexadecimal characters (0-9 and A-F) into your `.env` file. An example is `928F416BAFC49B969E62052F00450B6E974B03E86DC6984D1FA787B7EA533227`.

```ts
// app/modules/auth/auth.server.ts
Expand Down Expand Up @@ -225,11 +225,11 @@ export default function Login() {
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{/* Login Form. */}
<Form method="POST">
<label htmlFor="email">Email</label>
<input type="email" name="email" placeholder="Insert email .." required />
<button type="submit">Send Code</button>
</Form>
<Form method="POST">
<label htmlFor="email">Email</label>
<input type="email" name="email" placeholder="Insert email .." required />
<button type="submit">Send Code</button>
</Form>

{/* Login Errors Handling. */}
<span>{authError?.message}</span>
Expand Down Expand Up @@ -362,8 +362,32 @@ export async function action({ request }: ActionFunctionArgs) {

Done! 🎉 Feel free to check the [Starter Example](https://github.com/dev-xo/totp-starter-example) for a detailed implementation.

## Passing a pre-read FormData object

Because you may want to do validations or read values from the FormData before calling `authenticate`, `remix-auth-totp` allows you to pass a FormData object as part of the optional context.

```ts
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData()
await authenticator.authenticate(type, request, {
// use formData here
successRedirect: formData.get('redirectTo'),
failureRedirect: '/login',
context: { formData }, // pass pre-read formData here
})
}
```

This way, you don't need to clone the request yourself.

See https://github.com/sergiodxa/remix-auth-form?tab=readme-ov-file#passing-a-pre-read-formdata-object

## [Options and Customization](https://github.com/dev-xo/remix-auth-totp/blob/main/docs/customization.md)

The Strategy includes a few options that can be customized.

You can find a detailed list of all the available options in the [customization](https://github.com/dev-xo/remix-auth-totp/blob/main/docs/customization.md) documentation.

```
```
14 changes: 13 additions & 1 deletion src/index.ts
Expand Up @@ -350,7 +350,7 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
const user: User | null = session.get(options.sessionKey) ?? null
if (user) return this.success(user, request, sessionStorage, options)

const formData = request.method === 'POST' ? await request.formData() : new FormData()
const formData = await this._readFormData(request, options)
const formDataEmail = coerceToOptionalNonEmptyString(formData.get(this.emailFieldKey))
const formDataCode = coerceToOptionalNonEmptyString(formData.get(this.codeFieldKey))
const sessionEmail = coerceToOptionalString(session.get(this.sessionEmailKey))
Expand Down Expand Up @@ -522,4 +522,16 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
})
}
}

private async _readFormData(request: Request, options: AuthenticateOptions) {
if (request.method !== 'POST') {
return new FormData()
}

if (options.context?.formData instanceof FormData) {
return options.context.formData
}

return await request.formData()
}
}
71 changes: 67 additions & 4 deletions test/index.spec.ts
Expand Up @@ -152,6 +152,69 @@ describe('[ Basics ]', () => {
expect(sendTOTP).toHaveBeenCalledTimes(1)
expect(verify).toHaveBeenCalledTimes(1)
})

test('Should use pre-read Form Data in context.', async () => {
let sendTOTPOptions: SendTOTPOptions | undefined
sendTOTP.mockImplementation(async (options: SendTOTPOptions) => {
sendTOTPOptions = options
})

const strategy = new TOTPStrategy(TOTP_STRATEGY_OPTIONS, verify)
let formData = new FormData()
formData.append(FORM_FIELDS.EMAIL, DEFAULT_EMAIL)
formData.append('type', 'remix-auth-totp')
let request = new Request(`${HOST_URL}/login`, {
method: 'POST',
body: formData,
})
let preReadFormData = await request.formData()

let session: Session | undefined
await strategy
.authenticate(request, sessionStorage, {
...AUTH_OPTIONS,
successRedirect: '/verify',
failureRedirect: '/login',
context: { formData: preReadFormData },
})
.catch(async (reason) => {
if (reason instanceof Response) {
session = await sessionStorage.getSession(
reason.headers.get('set-cookie') ?? '',
)
} else throw reason
})

expect(sendTOTPOptions).not.toBeUndefined()
expect(session).not.toBeUndefined()
expect(sendTOTP).toHaveBeenCalledTimes(1)
expect(verify).toHaveBeenCalledTimes(0)

formData = new FormData()
formData.append(FORM_FIELDS.CODE, sendTOTPOptions?.code || '')
request = new Request(`${HOST_URL}/verify`, {
method: 'POST',
headers: {
cookie: (session && (await sessionStorage.commitSession(session))) || '',
},
body: formData,
})
preReadFormData = await request.formData()
await strategy
.authenticate(request, sessionStorage, {
...AUTH_OPTIONS,
successRedirect: '/',
failureRedirect: '/login',
context: { formData: preReadFormData },
})
.catch(async (reason) => {
if (reason instanceof Response) {
} else throw reason
})

expect(sendTOTP).toHaveBeenCalledTimes(1)
expect(verify).toHaveBeenCalledTimes(1)
})
})

describe('[ TOTP ]', () => {
Expand Down Expand Up @@ -1077,13 +1140,13 @@ describe('[ Utils ]', () => {

test('Should throw an error on invalid secret.', async () => {
const secrets = [
"b2FE35059924CDBF5B52A84765B8B010F5291993A9BC39410139D4F5110060",
"b2FE35059924CDBF5B52A84765B8B010F5291993A9BC39410139D4F511006034a",
"b2FE35059924CDBF5B52A84765B8B010F5291993A9BC39410139D4F51100603#"
'b2FE35059924CDBF5B52A84765B8B010F5291993A9BC39410139D4F5110060',
'b2FE35059924CDBF5B52A84765B8B010F5291993A9BC39410139D4F511006034a',
'b2FE35059924CDBF5B52A84765B8B010F5291993A9BC39410139D4F51100603#',
]

for (const secret of secrets) {
expect(() => asJweKey(secret)).toThrow()
expect(() => asJweKey(secret)).toThrow()
}
})
})

0 comments on commit d596754

Please sign in to comment.