OTPs
Make sure you're done with setup before configuring authentication methods
This authentication method has two steps:
- The user provides an email address or a phone number, to which a one-time-password (OTP) code is sent
- The user then fills out the code within your app
Your database keeps track of the issued code that was sent which allows for secure verification and expiration.
You will configure your message template, so you can send an OTP and a magic link together.
Email providers
Convex Auth implements configuration via Auth.js (opens in a new tab) "provider" configs. These JS objects define how the library sends emails.
Open the Auth.js Magic Links providers doc (opens in a new tab) to see a list of available email providers.
Choose an email provider (for example Resend) and follow the guide below.
Ignore the "database provider" configuration in Auth.js docs. Your Convex backend is your database!
Setup
Environment variables
Configure the relevant environment variables for a given email provider, as per Auth.js docs, on your Convex backend.
See setting environment variables Convex docs (opens in a new tab).
For example for Resend, after you sign up and obtain your API key, you can run (with your own value):
npx convex env set AUTH_RESEND_KEY yourresendkey
Provider configuration
Create a custom provider config for sending an OTP.
This example uses additional dependencies
- The Resend SDK (
resend
on NPM) oslo
, a handy library which is part of the Lucia project
You can install these with npm i resend@3.2.0 oslo
.
Note: resend
v3.3 and v3.4 is broken in Convex runtime, fix is coming in
v3.5
import { Email } from "@convex-dev/auth/providers/Email";
import { Resend as ResendAPI } from "resend";
import { alphabet, generateRandomString } from "oslo/crypto";
export const ResendOTP = Email({
id: "resend-otp",
apiKey: process.env.AUTH_RESEND_KEY,
maxAge: 60 * 15, // 15 minutes
async generateVerificationToken() {
return generateRandomString(8, alphabet("0-9"));
},
async sendVerificationRequest({ identifier: email, provider, token }) {
const resend = new ResendAPI(provider.apiKey);
const { error } = await resend.emails.send({
from: "My App <onboarding@resend.dev>",
to: [email],
subject: `Sign in to My App`,
text: "Your code is " + token,
});
if (error) {
throw new Error(JSON.stringify(error));
}
},
});
Then use it in convex/auth.ts
:
import { convexAuth } from "@convex-dev/auth/server";
import { ResendOTP } from "./ResendOTP";
export const { auth, signIn, signOut, store } = convexAuth({
providers: [ResendOTP],
});
Check out the
example repo (opens in a new tab)
for a more polished email template, including using React
for the email
templating (add "jsx": "react-jsx"
to your convex/tsconfig.json
).
You can find more examples of email customization in Auth.js docs (opens in a new tab).
Add sign-in form
Now you can trigger the email sending from a form submission via the signIn
function.
After the email is sent, you should show a second form for submitting the code.
When the second form is submitted, call the signIn
function again.
For example for the custom ResendOTP
provider:
import { useAuthActions } from "@convex-dev/auth/react";
export function SignIn() {
const { signIn } = useAuthActions();
const [step, setStep] = useState<"signIn" | { email: string }>("signIn");
return step === "signIn" ? (
<form
onSubmit={(event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
void signIn("resend-otp", formData).then(() =>
setStep({ email: formData.get("email") as string }),
);
}}
>
<input name="email" placeholder="Email" type="text" />
<button type="submit">Send code</button>
</form>
) : (
<form
onSubmit={(event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
void signIn("resend-otp", formData);
}}
>
<input name="code" placeholder="Code" type="text" />
<input name="email" value={step.email} type="hidden" />
<button type="submit">Continue</button>
<button type="button" onClick={() => setStep("signIn")}>
Cancel
</button>
</form>
);
}
The second call to signIn
will include the user-provided code
. Because this
is a short code, we must also provide the email
that matches the account used
for the sign-in. The library automatically rate-limits failed attempts.
Check out the example repo (opens in a new tab) for a more polished UI.
When you're done configuring your chosen authentication methods, learn how to use authentication in your frontend and backend in Authorization.
Phone providers
There are no built-in providers, but one is easy to build. The example repo has an example of using Twilio and their Node.js SDK (opens in a new tab).
The sign-in form works the same as above, but uses a phone
field instead of an
email
field:
example (opens in a new tab).
If you use a third-party service that takes care of generating and validating
OTPs, you can use a custom
ConvexCredentials
config to
implement the authentication method. The example repo shows how to do this with
the
Twilio Verify service (opens in a new tab).
When you're done configuring your chosen authentication methods, learn how to use authentication in your frontend and backend in Authorization.