Skip to content
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ MESSAGING_TWILIO_AUTH_TOKEN=
# Required when NODE_ENV=production.
# SEAMLESS_JWKS_ACTIVE_KID=main_2026_04
# SEAMLESS_JWKS_KEY_main_2026_04_PRIVATE="-----BEGIN PRIVATE KEY-----..."
# SEAMLESS_JWKS_PUBLIC_KEYS={"keys":[{"kid":"main_2026_04","pem":"-----BEGIN PUBLIC KEY-----...","createdAt":"2026-04-22T00:00:00.000Z"}]}
# JWKS_PUBLIC_KEYS={"keys":[{"kid":"main_2026_04","pem":"-----BEGIN PUBLIC KEY-----...","createdAt":"2026-04-22T00:00:00.000Z"}]}
4 changes: 2 additions & 2 deletions docs/production-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Production deployments should define:
- `OAUTH_STATE_SECRET`
- `SEAMLESS_JWKS_ACTIVE_KID`
- `SEAMLESS_JWKS_KEY_<kid>_PRIVATE`
- `SEAMLESS_JWKS_PUBLIC_KEYS`
- `JWKS_PUBLIC_KEYS`
- OAuth client-secret environment variables referenced by provider `clientSecretEnv`
- Messaging provider credentials when direct delivery is enabled

Expand All @@ -35,7 +35,7 @@ Do not store raw secrets in `system_config`.
Access tokens are signed with configured JWKS signing keys. A typical rotation is:

1. Generate a new key pair.
2. Publish the new public key in `SEAMLESS_JWKS_PUBLIC_KEYS`.
2. Publish the new public key in `JWKS_PUBLIC_KEYS`.
3. Deploy with both old and new public keys available.
4. Switch `SEAMLESS_JWKS_ACTIVE_KID` to the new key id.
5. Keep retired public keys until all tokens signed with them expire.
Expand Down
8 changes: 4 additions & 4 deletions resources/coverage-badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/controllers/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const createUser = async (req: Request, res: Response) => {

const user = await User.create({
email,
phone: phone,
phone: phone ?? null,
roles: roles ?? [],
});

Expand Down
2 changes: 1 addition & 1 deletion src/controllers/jwks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function __resetJwksCache() {
async function loadJwksFromSecrets(): Promise<JWK[]> {
logger.info('Loading JWKS from Secrets Manager');

const raw = await getSecret('SEAMLESS_JWKS_PUBLIC_KEYS');
const raw = await getSecret('JWKS_PUBLIC_KEYS');
const parsed = JSON.parse(raw);

const jwks: JWK[] = [];
Expand Down
26 changes: 12 additions & 14 deletions src/controllers/otp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export const sendPhoneOTP = async (req: Request, res: Response) => {
const authReq = req as AuthenticatedRequest;
const user = authReq.user;
const phone = user.phone;
const normalizedPhone = normalizePhoneNumber(phone);
const useExternalDelivery = await canReturnExternalDelivery(req);

if (!phone) {
Expand All @@ -73,6 +72,8 @@ export const sendPhoneOTP = async (req: Request, res: Response) => {
logger.info('Sending phone OTP');

try {
const normalizedPhone = normalizePhoneNumber(phone);

if (!isValidPhoneNumber(phone) || !normalizedPhone) {
logger.warn('Invalid phone provided');
AuthEventService.log({
Expand Down Expand Up @@ -231,7 +232,6 @@ export const verifyPhoneNumber = async (req: Request, res: Response) => {

const authReq = req as AuthenticatedRequest;
let user = authReq.user;
const email = user.email;
const phone = user.phone;

logger.info('Verifying phone number');
Expand All @@ -248,7 +248,7 @@ export const verifyPhoneNumber = async (req: Request, res: Response) => {
}

try {
if (!verificationToken || !phone || !email) {
if (!verificationToken || !phone) {
logger.warn(`Missing data from verify phone numnber request.`);
await AuthEventService.log({
userId: user.id,
Expand Down Expand Up @@ -300,7 +300,6 @@ export const verifyEmail = async (req: Request, res: Response) => {
const authReq = req as AuthenticatedRequest;
let user = authReq.user;
const email = user.email;
const phone = user.phone;

logger.info('Verifying email');

Expand All @@ -326,8 +325,8 @@ export const verifyEmail = async (req: Request, res: Response) => {
return res.status(401).json({ error: 'Invalid data' });
}

if (!email || !phone) {
logger.warn(`Missing email or phone`);
if (!email) {
logger.warn(`Missing email`);
await AuthEventService.log({
userId: user.id,
type: 'verify_otp_suspicious',
Expand All @@ -348,17 +347,17 @@ export const verifyEmail = async (req: Request, res: Response) => {
userId: user.id,
type: 'verify_otp_success',
req,
metadata: { reason: 'User verified their email number' },
metadata: { reason: 'User verified their email' },
});

if (user.phoneVerified && user.emailVerified && user.verified) {
if (user.emailVerified && user.verified) {
logger.info('User is fully verified. Logging in...');

await AuthEventService.log({
userId: user.id,
type: 'verify_otp_success',
req,
metadata: { reason: 'User completed verification of phone and email' },
metadata: { reason: 'User completed email verification' },
});

await issueSessionAndRespond({
Expand Down Expand Up @@ -488,7 +487,6 @@ export const verifyLoginEmail = async (req: Request, res: Response) => {
const authReq = req as AuthenticatedRequest;
let user = authReq.user;
const email = user.email;
const phone = user.phone;

if (await rejectDisabledLoginMethod('email_otp', req, res)) {
return;
Expand Down Expand Up @@ -522,8 +520,8 @@ export const verifyLoginEmail = async (req: Request, res: Response) => {
return res.status(401).json({ error: 'Not allowed' });
}

if (!email || !phone) {
logger.warn(`Missing email or phone`);
if (!email) {
logger.warn(`Missing email`);
await AuthEventService.log({
userId: user.id,
type: 'verify_otp_suspicious',
Expand All @@ -546,14 +544,14 @@ export const verifyLoginEmail = async (req: Request, res: Response) => {
req,
});

if (user.phoneVerified && user.emailVerified && user.verified) {
if (user.emailVerified && user.verified) {
logger.info('User is fully verified. Logging in...');

await AuthEventService.log({
userId: user.id,
type: 'verify_otp_success',
req,
metadata: { reason: 'User completed verification of phone and email' },
metadata: { reason: 'User completed email verification' },
});

await issueSessionAndRespond({
Expand Down
Loading
Loading