Wait for Magic
July 31, 2026
Subscribe

Cloud Knowledge

Your Go-To Hub for Cloud Solutions & Insights

Field Notes: Building a Phone-First Account Recovery Flow in Okta
Field Notes: Identity & Access Engineering

When Users Forget Everything Except Their Phone: Redesigning Okta Account Recovery for Cloud Knowledge

A first-person account of how our identity engineering team scoped, designed, prototyped, and stress-tested a phone-first, low-friction account recovery experience in Okta and the hard conversation we had to have about password as a recovery value.

This is the story of a support ticket that became an architecture review, and an architecture review that became a three-week proof of concept. It's written the way it actually happened including the parts where our first idea was, on reflection, a bad one.

01 . The Ask

It started as a one-line Slack message

Every engagement has an origin story, and ours is unglamorous. It was a Tuesday, and the message came from our account lead who works directly with Cloud Knowledge's IT Service Management team:

"Hey: can we do something in Okta where users recover their account with their password instead of email? Apparently half our reset tickets are people who don't remember what email address they registered with. They all remember their phone number though. Email should be optional. Client wants this yesterday." Slack, #cloudknowledge-iam, 9:14 AM

If you've worked in workforce or customer identity for more than a few months, you already felt two things reading that message. First, sympathy: this is an extremely common, extremely real problem. Second, a small alarm bell, because "recover your account with your password" is a sentence that doesn't parse the way it's meant to. You can't recover access to an account using the very credential you've lost. That's not pedantry; it's the whole reason recovery flows exist.

But we've learned not to take a one-line feature request at face value, good or bad. The words a client uses in a Slack message are rarely the actual requirement: they're a compressed, slightly garbled transmission of a real, specific pain that somebody on their helpdesk is feeling five times a day. Our job in the first 48 hours of any request like this is decompression: figure out what they actually need, not what they typed.

So before we touched a single toggle in the Okta Admin Console, we did what we always do: we asked for the ticket data.

What the helpdesk logs actually said

Cloud Knowledge is a mid-size professional services organization: roughly 6,200 employees, a large percentage of them field-based consultants, technicians, and site staff who are not sitting at a laptop all day. Many of them were provisioned Okta accounts as part of a broader HR and device modernization project eighteen months prior. Their primary device, for a large chunk of this population, is a company or personal mobile phone. Some of them touch a corporate email inbox once a week, if that.

We pulled three months of ServiceNow tickets tagged account-recovery and password-reset, and a pattern jumped out immediately:

  • 41% of recovery tickets were closed with the note "user did not know registered email/username."
  • 27% were standard "forgot password, reset link worked fine": the boring, already-solved case.
  • 18% involved a user who had access to their phone number but not to the email inbox tied to their Okta profile (often a personal email set up years ago, migrated jobs, or an alias they never use).
  • 14% were miscellaneous: locked accounts, MFA device lost, factor enrollment issues.

So the underlying, real requirement was not "let people type their password to get back into their password." It was: a meaningful chunk of this user population can reliably identify themselves by phone number, and struggles to identify themselves by email or username. The recovery experience needed to be re-anchored around a factor people actually remember.

Translation exercise

"Give users the option to give password as a value to recover their account, email optional" became, after triage: "Allow self-service account recovery using a verified mobile phone number as the primary identifier, with email as a secondary/optional identifier, without weakening the security of the reset itself." That's the brief we actually built against, and it's the brief this article is about.

We want to be upfront about that translation, because it's the most important decision we made in the entire engagement, and it happened before we opened Okta at all. A lot of what follows: the architecture, the POC, the failure modes we hit: only makes sense in light of that reframing. So let's walk through how we got there, and then how we built it.

02 . The Hard Conversation

Why "recover with your password" can't be the literal design

Before we get into architecture, it's worth spending real time on why we pushed back on the literal ask, because we think this is the part other teams skip, and it's the part that determines whether the resulting solution is a security incident waiting to happen or a genuinely good user experience.

The circular logic problem

A password reset or account recovery flow exists for exactly one scenario: the user cannot authenticate with their normal credential. If a user has forgotten, lost, or been locked out of their password, then by definition they cannot supply that password as proof of identity during recovery: there's nothing to check it against that isn't itself the thing they've lost access to. Any flow that appears to let someone "recover with their password" is actually doing one of a few things under the hood, and each of them has very different security implications:

What it looks likeWhat's actually happeningSecurity implication
User types their old/current password to "recover" the account They already know the password: this isn't recovery, it's a login, possibly followed by a forced factor re-enrollment Fine, but it's not solving the "forgot my identifier" problem at all
User types any password and the system accepts it as a recovery factor The system is not actually verifying a secret: it's accepting arbitrary input as if it were proof of identity Critical vulnerability: this is account takeover as a feature
User is prompted to set a new password as part of proving who they are Setting a new secret isn't authentication: anyone, including an attacker, can "set a new password" Also account takeover, just with extra steps

We've seen variations of that second and third pattern shipped by well-meaning teams before, usually because a legacy on-prem system did something similar and nobody has re-examined it since. It's a classic "it's always been like this" vulnerability. We were not going to build Cloud Knowledge a new one, no matter how the original request was phrased.

Why we flagged this explicitly, in writing, to the client

Any flow where possession of a secret is not actually verified: where the system just accepts a claimed value without checking it against something the legitimate user alone could produce: collapses the entire security model of the identity provider. Okta, like every credible IdP, is built around the idea that recovery must re-establish identity through a factor independent of the one that was lost. We were not willing to design around that principle, and we said so on the call, with the ticket data as backup for a better alternative.

The conversation that actually mattered

We scheduled a 30-minute call with Cloud Knowledge's IT Service Delivery Manager and one architect from their security team. We didn't lead with "no." We led with the ticket breakdown from section one, and then asked a single question: "When your users say they can't remember their email, is it the address itself they've forgotten, or is it that they've never had reliable access to that inbox from the device they're standing in front of?"

The answer, after some back and forth, was mostly the latter. Many of their field staff had been set up with a corporate email alias during onboarding, used it exactly once (to accept the offer letter and set an initial password), and never touched it again because their actual day-to-day identity, from their own point of view, was "the phone number I gave HR."

That reframed the whole ask for the client too. Nobody in that call actually wanted an insecure password-bypass. What they wanted was for phone number to become a first-class recovery identifier, on par with (or ahead of) email, because that's the credential their workforce actually retains. Once we had that alignment, the rest of the design fell into place much faster than we expected.

What we agreed to build instead

  • Phone number (SMS, and voice as a fallback for areas with poor SMS delivery) becomes a primary recovery identifier, verified via a one-time passcode: a real, possession-based factor.
  • Email remains supported but becomes optional at the identifier stage: users can look up their account by phone number alone, and the system resolves it to the correct Okta profile.
  • Under no circumstance does the flow accept a password, a "security answer," or any user-supplied secret as sole proof of identity during recovery. Recovery is always gated behind possession of a registered, verified device (or, as a fallback, a live identity-verification step).
  • The end of a successful recovery flow is always a forced password reset and, where policy requires it, re-enrollment of MFA: recovery proves who you are; it does not skip the "now set a new secret" step.

With that agreed, we moved into design. The next section walks through the architecture options we actually put on the whiteboard, including the ones we threw out.

03 . Requirements

Turning a Slack message into a requirements document

With the reframed brief in hand, we ran a proper requirements workshop: ninety minutes, three teams in the room (virtually): Cloud Knowledge IT Service Delivery, Cloud Knowledge Security/GRC, and our own IAM engineering pod. We used a fairly standard MoSCoW pass (Must / Should / Could / Won't) because it forces prioritization arguments to happen in the workshop rather than three weeks into the build.

Must haveShould haveCould haveWon't have (v1)
PriorityRequirementNotes from the room
MustRecover account using verified phone number only, no email required at lookup stageThis is the entire point of the engagement
MustOTP delivered via SMS, with voice call as a fallback channelSeveral regional offices reported poor SMS delivery on certain MVNOs
MustNo secret (password, security question, employee ID) is ever accepted as sole proof of identity in recoveryNon-negotiable, security team required this in writing
MustRecovery always ends in a forced password resetStandard Okta behavior, but had to be explicit for the design doc
MustFull audit trail of recovery attempts, including failed lookups, in Okta System LogRequired by Cloud Knowledge's SOC 2 auditor
ShouldRate limiting / lockout after repeated failed recovery attempts per phone number and per IPBrute-force and enumeration protection
ShouldUsers can self-register/verify a phone number proactively, not just during a recovery eventYou can't recover with a phone number nobody set up in advance
ShouldGraceful fallback to email-based recovery for the population that still prefers itDon't remove existing capability, add to it
CouldProgressive profiling nudge to add a phone number at next login, for users who don't have one on fileBacklog item, not core to POC
CouldManager or helpdesk-assisted recovery path with elevated verification for edge casesUseful, deferred to phase 2
Won't (v1)Biometric or passkey-based recoveryDevice population too mixed (BYOD + shared kiosks) for phase 1
Won't (v1)Fully bypassing MFA re-enrollment post-recoveryExplicitly rejected by security team

Constraints we had to design around

A few facts about the environment shaped almost every downstream decision:

  • Okta Identity Engine (OIE), not Classic. Cloud Knowledge had migrated from Okta Classic Engine roughly a year earlier, which mattered a lot: OIE's Identity Flows are what make a custom, phone-first recovery experience achievable without a bolt-on IDP.
  • SMS/Voice via the built-in Okta phone factor was already licensed (Okta Adaptive MFA SKU included it), so we weren't starting a procurement conversation with a telephony vendor from scratch. A custom SMS provider (Twilio) was explored as an enhancement but not a hard requirement for the POC.
  • Universal Directory profile already had a primaryPhone / mobilePhone attribute populated for roughly 74% of users via an HR feed (Workday to Okta provisioning). The remaining 26% would need a backfill campaign: a real, separate workstream we flagged early.
  • No dedicated Okta Workflows SKU at the start of the engagement. This became relevant later when we talk about the custom flow options we considered and, in one case, had to shelve.
  • Multiple brands under one Okta org (three business units, each with its own custom domain and branded sign-in widget), so whatever we built had to be brand-aware, not a single hardcoded experience.
A note on scope discipline

It would have been easy to let this workshop balloon into "let's also redesign MFA enrollment, SSO, and lifecycle management while we're in here." We kept the POC scoped hard to recovery. Every adjacent idea went into a backlog document instead of the sprint. This is the single biggest reason the three-week POC timeline held.

04 . Architecture

Four ways to build phone-first recovery in Okta: and why we picked one

Okta gives you more than one lever to pull for a problem like this, and the honest answer is that "which one is right" depends on your OIE feature entitlements, your appetite for custom code, and how much you're willing to touch the hosted Sign-In Widget versus building your own. We put four real options on the table.

Option A: Native self-service password reset, unmodified

Okta's out-of-the-box self-service recovery already supports phone (SMS/voice) as a recovery factor, alongside email, security question, and other configured authenticators, through the standard Identity Engine recovery flow and its associated Authenticator policies. In theory we could simply reorder authenticator priority, make phone a required/available authenticator, and mark email optional in the recovery policy.

In favor

Zero custom code. Fully supported by Okta, no ongoing maintenance burden, upgrades transparently with the platform.

Against

The native flow still asks for a username up front (typically email) before it will offer you a choice of recovery factor: it doesn't natively support "look me up by phone number alone" as the very first step, which was our actual Must-have.

Option B: Custom Identifier First flow using phone as username

Okta supports an "Identifier First" routing rule in the sign-in flow, and Universal Directory lets you designate an attribute other than email as the login/username, or maintain a secondary lookup index. We explored remapping login to the phone number outright.

In favor

Very clean mental model: phone number becomes the actual username, so recovery-by-phone is just recovery, no special casing.

Against

Too invasive for a phase-1 POC: it changes the primary key of every account, touches SSO app assignments, breaks existing integrations that reference login as email, and is functionally a re-platforming project, not a recovery-flow project. Parked for a future phase.

Option C: Custom recovery experience via Okta's hosted widget customization + Inline Hooks

This is the option we ultimately built. Okta's Identity Engine lets you customize the Sign-In Widget's recovery flow presentation, combine it with a Registration Inline Hook and a Password Import Inline Hook-style pattern (repurposed for lookup logic), and layer in a lightweight external service that resolves "phone number to Okta user" before handing control back to Okta's native, secure OTP verification and reset machinery.

In favor

Keeps all secret verification: the actual OTP check and password reset: inside Okta's own secure recovery machinery. Our custom code only ever does identification (which account are we even talking about), never authentication. That's the correct boundary.

Against

Requires an external, lightweight lookup service (we used a small serverless function) and careful hook design so it can never be abused for account enumeration.

Option D: Third-party CIAM layer in front of Okta

Put a customer-identity product or a fully custom Node/Express app in front of Okta entirely, handling phone lookup and OTP ourselves, then federating into Okta once identity is established.

In favor

Maximum flexibility, no dependency on Okta's recovery primitives at all.

Against

Reinvents a huge amount of what Okta already does well and certifies (OTP generation, rate limiting, audit logging, SOC 2 controls). Also a much bigger licensing and maintenance commitment for a problem that didn't need it. Rejected.

The principle that decided it

We chose Option C because it respects a rule we hold to on every Okta engagement: custom code should only ever do identification and orchestration, never authentication. The moment a custom hook or external service starts deciding "this OTP is correct" or "this password is valid," you've built a second, unaudited identity provider sitting next to your real one. Keep Okta doing the one thing it's certified and battle-tested to do: verifying possession of a factor: and let custom code do the boring plumbing around it.

  ┌────────────┐     phone number      ┌───────────────────────┐
  │    User    │ ───────────────────▶  │  Custom Recovery Page │
  │ (mobile)   │                       │  (Okta hosted widget, │
  └────────────┘                       │   custom recovery view)│
                                       └──────────┬─────────────┘
                                                  │ lookup request
                                                  ▼
                                       ┌───────────────────────┐
                                       │  Lookup Function      │
                                       │  (serverless, stateless)│
                                       │  phone -> userId      │
                                       └──────────┬─────────────┘
                                                  │ resolved userId (or generic "check your phone")
                                                  ▼
                                       ┌───────────────────────┐
                                       │  Okta Identity Engine │
                                       │  native recovery flow:│
                                       │  - selects Phone      │
                                       │    authenticator      │
                                       │  - sends real OTP     │
                                       │  - verifies OTP       │
                                       │  - forces new password│
                                       │  - re-enroll MFA if req'd│
                                       └───────────────────────┘
          
Fig. 1: Custom code only resolves "who," Okta owns all of "prove it." Whiteboard sketch, later redrawn cleanly for the design doc.
05 . Resources

What we needed before we could touch the dev tenant

With Option C chosen, we put together a resource list: partly for our own sprint planning, partly because Cloud Knowledge's procurement team needed to know exactly what licenses and access we required. Nothing here is exotic, which was itself a relief.

Okta platform resources

ResourcePurposeAlready licensed?
Okta Identity Engine (OIE) dev tenantIsolated environment for the POC, mirroring prod org settingsProvisioned new for this engagement
Okta Adaptive MFA / Phone authenticatorNative SMS + Voice OTP delivery and verificationYes, already in the org's SKU
Okta Sign-In Widget (hosted, customizable)Front-end surface for the recovery experience, branded per business unitYes, included with OIE
Inline Hooks (Registration & custom SPI extension points)Server-side extension points to call our lookup function during the flowYes, included with OIE, no extra SKU
Okta System Log / Event HooksAudit trail and near-real-time alerting on recovery attemptsYes
Okta WorkflowsOptional orchestration (notifications, ticket creation on repeated failures)Not initially licensed: flagged as a Should-have gap, see Challenges

Supporting infrastructure

ResourcePurpose
Small serverless function (AWS Lambda behind API Gateway)Stateless phone-number-to-user lookup, called by the Inline Hook
Okta Management API access (scoped API token / OAuth service app)Used by the lookup function to query Users API by phone attribute, never to reset passwords directly
Secrets manager (AWS Secrets Manager)Storage for the Okta API token used by the lookup function
Test phone numbers (5 x carrier SIMs across 3 carriers)Real-world SMS delivery testing, not just Okta sandbox simulation
ServiceNow sandbox instanceTesting the "escalate to helpdesk" fallback path end-to-end

People

  • 1x Identity engineer (lead): Okta configuration, policy design, hook development
  • 1x Backend engineer: serverless lookup function, API integration
  • 1x Security architect (Cloud Knowledge side): policy review, threat modeling sign-off
  • 1x QA engineer: test plan execution across devices/carriers
  • 0.25 FTE Service Delivery Manager: helpdesk process alignment, UAT coordination
A resourcing lesson from a past engagement

We've learned the hard way on other clients that skipping the "real SIM cards on real carriers" line item is a false economy. Okta's sandbox and simulated OTP delivery will tell you your policy logic works. It will not tell you that a regional carrier silently throttles alphanumeric sender IDs, or that a voice fallback call gets routed to voicemail before the user picks up. We budgeted for this explicitly, and it paid off: more on that in the Challenges section.

06 . The Build

Inside the dev tenant: how we actually built this, step by step

This is the part most write-ups skip or gloss over, and it's the part we get asked for most often, so we're going to be thorough. Everything below happened in a throwaway Okta Identity Engine developer org, deliberately configured to mirror Cloud Knowledge's production policy structure (same authenticator set, same brand count, same group model) without touching a single real user record.

Step 1: Stand up the dev tenant and mirror the org shape

1

Create the tenant and baseline groups

We provisioned a fresh Okta Developer Edition org (OIE by default on new orgs) and recreated the three business-unit brand structure Cloud Knowledge runs in production: brand_corporate, brand_fieldops, and brand_contractors, each with its own custom domain placeholder and its own Sign-In Widget theme. We also recreated the core groups: Everyone, FieldStaff, CorporateStaff, Contractors, and HelpdeskTier2 (used later for the escalation path).

2

Seed representative test users

We generated 40 synthetic users covering the real-world edge cases from the ticket analysis: users with phone + email, phone only, email only (legacy), duplicate-looking names, international phone formats (Cloud Knowledge has staff in four countries), and a few deliberately "dirty data" records: phone numbers stored with inconsistent formatting: because production data is never as clean as anyone wants to admit.

File Tag

okta-cli / seed-users.json (excerpt)
{
  "profile": {
    "firstName": "Priya",
    "lastName": "Natarajan",
    "email": "priya.natarajan@fieldops.cloudknowledge-dev.test",
    "login": "priya.natarajan@fieldops.cloudknowledge-dev.test",
    "primaryPhone": "+91 98765 43210",
    "mobilePhone": "+91 98765 43210",
    "department": "FieldOps",
    "employeeStatus": "ACTIVE"
  },
  "credentials": {
    "password": { "value": "TempPass!2026Dev" }
  },
  "groupIds": ["00g1fieldstaffXXXXXXXX"]
}

Step 2: Configure the Phone authenticator as a recovery-eligible factor

2

Under Security to Authenticators, we confirmed the Phone authenticator was enabled org-wide and, critically, checked its configuration for Recovery eligibility: by default a factor can be available for MFA but not enabled for account recovery, and these are separate toggles that are easy to miss.

Admin Console path
Security -> Authenticators -> Phone -> Edit
  Method: SMS, Voice
  Allowed usages: ✅ Enroll   ✅ Authenticate   ✅ Recover
  OTP length: 6 digits
  OTP validity window: 5 minutes
  Delivery retry limit: 3 attempts per 10-minute rolling window
3

Set authenticator priority in the Authenticator Enrollment Policy

We created a dedicated Authenticator Enrollment Policy scoped to the FieldStaff and Contractors groups where Phone is Required at first sign-in, ahead of Email, so that by the time this population needs recovery, a verified phone number already exists on the profile: this directly addresses the "Should have: proactive enrollment" requirement from the workshop.

Step 3: Rebuild the Recovery policy around phone-first identification

4

Password policy to recovery settings

Inside Security to Authentication Policies, we edited the org's Password policy to change the recovery configuration: instead of "Email only," we enabled both Email and Phone as eligible recovery authenticators, and, per the agreed design, marked Email as optional at the identification step rather than a hard requirement.

Password policy JSON (via Okta Management API, GET /api/v1/policies/{policyId})
{
  "type": "PASSWORD",
  "name": "CloudKnowledge - FieldOps Recovery Policy",
  "settings": {
    "recovery": {
      "factors": {
        "recovery_question": { "status": "INACTIVE" },
        "okta_email": { "status": "ACTIVE", "properties": { "recoveryToken": { "tokenLifetimeMinutes": 60 } } },
        "okta_sms": { "status": "ACTIVE" },
        "okta_call": { "status": "ACTIVE" }
      }
    },
    "password": {
      "complexity": { "minLength": 12, "minLowerCase": 1, "minUpperCase": 1,
        "minNumber": 1, "minSymbol": 1, "excludeUsername": true }
    }
  }
}
A deliberate choice

We turned Recovery Question off entirely. Security questions are a legacy factor with well-documented weaknesses (answers are often guessable, discoverable via social media, or reused across sites), and leaving them active would have quietly undermined the security posture we were promising. This wasn't in the original scope, but our security architect partner flagged it during review and we agreed it belonged in the same change.

06 . The Build (continued)

Building the phone-first identification step

This is where the "custom flow" the client asked for actually lives. Okta's native recovery flow assumes you start by typing a username (usually email). We needed a step in front of that where a user can type only a phone number, and have the system quietly resolve which Okta account that maps to: without ever revealing to an attacker whether a given phone number exists in the directory.

Step 4: Customize the Sign-In Widget's recovery entry point

5

We used the Sign-In Widget's configuration hooks (the i18n overrides and custom features flags) plus a small amount of custom JavaScript on the hosted sign-in page to relabel the recovery identifier field and add client-side format validation for phone numbers, while leaving Okta's own submission handling untouched.

widget-config.js (excerpt, hosted sign-in customization)
const config = {
  baseUrl: 'https://fieldops.cloudknowledge-dev.test',
  clientId: OKTA_CLIENT_ID,
  redirectUri: window.location.origin + '/login/callback',
  authParams: { issuer: 'https://fieldops.cloudknowledge-dev.test/oauth2/default' },

  features: {
    selfServiceUnlock: true,
    router: true
  },

  i18n: {
    en: {
      'primaryauth.username.placeholder': 'Phone number or email',
      'account.unlock.email.or.username.placeholder': 'Phone number or email',
      'password.forgot.email.or.username.placeholder': 'Phone number or username',
      'password.forgot.email.or.username.tooltip':
        'Use the mobile number on file with HR. Don\u2019t have it handy? You can use your email instead.'
    }
  },

  helpLinks: {
    help: 'https://help.cloudknowledge-dev.test/account-recovery'
  }
};

Under the hood, when a user submits that field during the "Forgot Password" flow, Okta's Identity Engine treats the submitted value as the username parameter of the recovery request: exactly as it always has. The trick is what happens next: rather than relying purely on an exact match against the login attribute (which is email in this org), we needed a resolution step that also checks the primaryPhone / mobilePhone attributes and translates a phone-number submission into the correct login before Okta's native recovery logic takes over.

Step 5: The Inline Hook that does the translation

6

We implemented this using a Password Import Inline Hook pattern repurposed as a pre-recovery resolution hook, calling out to our serverless lookup function. Okta calls the external service synchronously during the recovery attempt and expects a fast, well-formed JSON response; if the external call fails or times out, Okta falls back to its own native matching behavior rather than blocking the user.

lookup-function/index.js: AWS Lambda handler
const { Client } = require('@okta/okta-sdk-nodejs');
const okta = new Client({
  orgUrl: process.env.OKTA_ORG_URL,
  token: process.env.OKTA_API_TOKEN // scoped, read-only Users API token
});

// Normalizes phone input to E.164 before comparison.
function normalizePhone(raw) {
  const digits = raw.replace(/[^\d+]/g, '');
  return digits.startsWith('+') ? digits : `+${digits}`;
}

exports.handler = async (event) => {
  const submitted = event.data.context.credential.username || '';
  const looksLikePhone = /^[\d\s()+-]{7,}$/.test(submitted);

  // Always return a generic 200 shape. Never leak whether a match
  // was found. That alone would enable phone-number enumeration.
  const genericResponse = {
    commands: [] // no-op: let Okta's native flow continue unmodified
  };

  if (!looksLikePhone) {
    return { statusCode: 200, body: JSON.stringify(genericResponse) };
  }

  const normalized = normalizePhone(submitted);

  try {
    const results = await okta.userApi.listUsers({
      search: `profile.primaryPhone eq "${normalized}" or profile.mobilePhone eq "${normalized}"`
    });

    const matches = [];
    for await (const user of results) matches.push(user);

    if (matches.length === 1) {
      // Rewrite the credential the native flow will use, so Okta's
      // own recovery machinery takes over from here. We never
      // touch OTP generation, verification, or password reset.
      return {
        statusCode: 200,
        body: JSON.stringify({
          commands: [{
            type: 'com.okta.identity.patch',
            value: [{ op: 'replace', path: '/credential/username', value: matches[0].profile.login }]
          }]
        })
      };
    }

    // Zero or multiple matches: do NOT reveal which. Fall through
    // to Okta's native (generic, non-enumerable) "if this account
    // exists" messaging.
    return { statusCode: 200, body: JSON.stringify(genericResponse) };

  } catch (err) {
    console.error('lookup-function error', err); // never logs the phone number itself
    return { statusCode: 200, body: JSON.stringify(genericResponse) };
  }
};
The line we were most careful about

Notice that the function returns the exact same generic shape whether the phone number matched zero, one, or more than one account. This was the single most heavily reviewed piece of code in the whole POC. An enumeration bug here: where the response timing or shape differs based on whether a match exists: would let an attacker fish for valid employee phone numbers. We added synthetic delay to equalize response time across all three branches for the same reason.

Step 6: Registering the hook in Okta

Admin Console -> Workflow -> Inline Hooks
Hook type:     Password Import Inline Hook (repurposed for pre-recovery resolution)
Hook name:     fieldops-phone-recovery-resolver
Invoked on:    Recovery flow, prior to authenticator selection
Endpoint URL:  https://xxxxxx.execute-api.ap-south-1.amazonaws.com/prod/resolve
Auth header:   Authorization: Bearer <shared secret, rotated 90 days>
Timeout:       3000 ms (Okta falls back to native behavior on timeout)

Once the hook rewrites credential/username to the resolved login, control returns entirely to Okta's own Identity Engine recovery flow: authenticator selection, OTP dispatch, OTP verification, and the forced password reset all happen inside Okta, using Okta's own rate limiting, lockout thresholds, and System Log entries. This was exactly the boundary we set out to hold in the architecture section: our code identifies, Okta authenticates.

Must Read