Email OTP
Passwordless email-code sign in and signup for hosted, headless, and SDK flows.
Email OTP lets users sign in or create an account with a short-lived code sent to their inbox. The same runtime works in hosted AuthPage, headless UI, and backend SDK flows. For email-link sign-in without a code entry step, see Magic Link.
SqlOS ships an Azure Communication Services sender. Set the connection string and verified sender address:
builder.AddSqlOS<AppDbContext>(options =>
{
options.AuthServer.ConfigureEmailOtp(email =>
{
email.AzureCommunicationServicesConnectionString =
builder.Configuration["SqlOS:EmailOtp:AzureCommunicationServicesConnectionString"];
email.FromAddress = builder.Configuration["SqlOS:EmailOtp:FromAddress"];
email.ApplicationName = "Acme";
});
});For local development:
SqlOS__EmailOtp__AzureCommunicationServicesConnectionString="<acs-connection-string>" \
SqlOS__EmailOtp__FromAddress="no-reply@example.com" \
dotnet runUse scripts/azure/setup-acs-email.sh to create the ACS Email resources and DNS verification records.
Open Auth Server > Security > OTP communications readiness after configuring the host. The dashboard reports whether Email OTP is enabled, whether the local sender is configured, bounded reason codes, and non-secret policy values. Connection strings remain in your deployment secret store and are never returned by the admin API.
An authenticated administrator can retrieve the same model from GET /sqlos/admin/auth/api/otp/readiness or send a bounded test with POST /sqlos/admin/auth/api/otp/test-delivery:
{ "method": "email", "destination": "operator@example.com" }The test uses the configured auth email sender, is limited to three sends per destination and 20 per operator source per hour, and records only a masked destination. It does not create a user, session, OTP challenge, or reusable login credential. A successful local readiness check does not call ACS; use the explicit test only when you intend to incur a provider request.
Seed or configure the AuthPage credential types:
options.AuthServer.EnableLocalPasswordAuth = false;
options.AuthServer.SeedAuthPage(page =>
{
page.EnabledCredentialTypes = ["email_otp"];
page.EnablePasswordSignup = false;
});With hosted AuthPage, SqlOS owns the full flow: email collection, home realm discovery, OTP delivery, verification, signup, session, org selection, and final OAuth redirect.
Drive Email OTP with @sqlos/headless (flow.emailOtp.start / verify / signupStart / signupVerify). The routes below are the HTTP wire contract; client code should use the package. See Build your own login and signup UI and Passwordless email-code onboarding.
| Endpoint | Purpose |
|---|---|
POST /sqlos/auth/headless/email-otp/start | Start an existing-user OTP sign-in |
POST /sqlos/auth/headless/email-otp/verify | Verify an existing-user OTP sign-in |
POST /sqlos/auth/headless/signup/email-otp/start | Start a new-user OTP signup |
POST /sqlos/auth/headless/signup/email-otp/verify | Verify OTP signup and issue the redirect |
await flow.emailOtp.start({ email });
await flow.emailOtp.verify({ code });
if (flow.status === "redirect" && flow.redirectUrl) {
window.location.assign(flow.redirectUrl);
}The flow holds challengeToken and, for signup, signupToken. The start view includes email and an info message containing the masked address. It does not currently expose expiry or resend timestamps; use your configured cooldown for the local button state and keep the server rate limit authoritative. Call signupStart again on resend so the flow replaces both tokens. The direct SDK start results do include MaskedEmail, ExpiresAt, and NextAllowedSendAt.
If the email belongs to an organization with required SSO, SqlOS redirects to SSO before creating an OTP challenge. Always call flow.identify({ email }) first; the initial email screen is the login view. Follow redirectUrl with window.location.assign.
Backend developers can use SqlOSAuthService directly:
var start = await authService.RequestEmailOtpSignupAsync(
new SqlOSEmailOtpSignupStartRequest(
DisplayName: "Jane Doe",
Email: "jane@example.com",
ClientId: "web",
OrganizationName: "Acme",
OrganizationId: null,
CustomFields: null),
httpContext,
ct);
var result = await authService.VerifyEmailOtpSignupAsync(
new SqlOSEmailOtpSignupVerifyRequest(
start.SignupToken,
start.ChallengeToken,
code),
httpContext,
ct);Existing-user sign-in uses RequestEmailOtpAsync(...) and VerifyEmailOtpAsync(...).
Configure the limits that make sense for your product:
options.AuthServer.ConfigureEmailOtp(email =>
{
email.MaxChallengesPerHour = 5; // per email
email.MaxChallengesPerIpPerHour = 60;
email.MaxChallengesPerClientPerHour = 200;
});SqlOS checks the limits before creating or sending a challenge and records audit events for starts, send failures, successes, failures, and rate-limit rejections.
SqlOS creates the default auth.email-otp template automatically. Edit it in Dashboard > Communications > Templates to customize the subject, HTML, and text while keeping the same runtime variables.
For built-in branding, use Email Branding. For complete control outside the template system, provide a message builder:
options.AuthServer.ConfigureEmailOtp(email =>
{
email.BuildMessage = ctx => new SqlOSAuthEmailMessage(
ctx.Email,
$"Your {ctx.ApplicationName} code",
$"<p>Your code is <strong>{ctx.Code}</strong>.</p>",
$"Your code is {ctx.Code}.");
});The builder receives the purpose (login or signup), email, masked email, code, expiry, application name, and resolved branding.