Phase 6: Backend & Admin Operations — Part 1 (Okta Node Management SDK)

Every lecture until now has been about the login flow — authenticating a user through a redirect to Okta. This phase steps outside that entirely: instead of a user interacting with Okta through a browser, your Next.js backend talks directly to Okta's Management API to create, update, and manage users programmatically — the kind of thing an admin dashboard, a signup automation, or a support tool would need.

Two Ways to Authenticate Your Backend

Option A — a static API Token (SSWS scheme). Generate a token tied to an admin account, and every request includes it. The problem: this token isn't scoped — it has full access to everything that admin account can do. If it ever leaks, the blast radius is your entire org.

Option B — an OAuth 2.0 Service App using the Client Credentials flow. This is Okta's current recommended approach. A Service App gets its own identity, is granted only the specific permissions it actually needs, and authenticates using a private/public key pair rather than a shared secret sitting in a config file.

This course uses Option B. It takes a few more steps to set up, but it's the correct, current approach, and worth learning properly.

Step 1: Create the Service App

  1. In the Admin Console, go to Applications → Applications — the same regular Applications list you used to create TaskFlow back in Phase 1.
  2. Click Create App Integration.
  3. For Sign-in method, select API Services. This is a separate option from "OIDC – OpenID Connect" and "SAML 2.0" — it's specifically for machine-to-machine apps with no end user involved.
  4. Click Next.
  5. Give it a name: TaskFlow Backend Service.
  6. Click Save.

You'll land on the new app's page, showing a Client ID and, by default, a client secret already generated.

Step 2: Switch to Public Key / Private Key Authentication

The Client Credentials flow this course uses requires signing requests with a private key, not a shared secret.

  1. On the General tab, find Client Credentials, click Edit.
  2. Select Public key / Private key instead of Client secret.
  3. Save.

A new Public keys section will appear below, currently empty.

Step 3: Generate the Key Pair

  1. Click Edit on the Public keys card, then Add.
  2. In the dialog, click Generate new key.
  3. You'll see two blocks: a public key, and a section labeled "Private key - Copy this!" — Okta shows this only once.
  4. Click Copy to clipboard (JSON format — this is what the Node SDK expects).
  5. Paste this immediately somewhere safe — a local file or password manager. Closing this dialog without copying it means starting over.
  6. Click Done.

Confirm the Public keys table now shows one entry with Status: Active.

Step 4: Turn Off DPoP (For Now)

On the General tab, find Require Demonstrating Proof of Possession (DPoP) header in token requests, checked by default. DPoP is a genuine security upgrade, but it requires extra request signing that the basic SDK setup in this lecture doesn't implement.

  1. Click Edit on General Settings.
  2. Uncheck this option.
  3. Save.

(Worth revisiting later once you're comfortable — but off keeps this lecture's code working as written.)

Step 5: Grant Only the Scopes You Need

  1. Go to the Okta API Scopes tab. This list is long — use your browser's find-on-page (Ctrl+F / Cmd+F) to search it.
  2. Find okta.users.read, click Grant.
  3. Find okta.users.manage, click Grant.
  4. Leave everything else Not granted.

Step 6: Create a Custom Admin Role

Okta's standard role list (Application Administrator, Group Administrator, Organization Administrator, and so on) doesn't include a role that's just "manage users, nothing else." The correct approach — and the more precise one — is to build a custom role with exactly the permissions needed.

  1. Go to Security → Administrators → Roles tab.
  2. Click to create a new role (or you can also reach this from the Role dropdown during assignment, via Create a role).
  3. Name it: TaskFlow User Manager.
  4. Select these permissions:
    • Manage users (create, update, deactivate)
    • View users and their details (read access)
  5. Leave every other permission category unchecked.
  6. Save.

Step 7: Create a Resource Set

A custom role also needs a Resource Set — a separate definition of exactly which users it applies to. Without this, there's nothing to attach the role to.

  1. Go to Security → Administrators → Resources tab.
  2. Click Create new resource set.
  3. Name: All TaskFlow Users
  4. Description: All TaskFlow Users
  5. Under Resources, click Add Resource.
  6. In Find a resource type, select Users.
  7. Choose All users.
  8. Click Save selection.
  9. Click Create.

You should now see All TaskFlow Users listed on the Resources tab.

Step 8: Assign the Role to the Service App

  1. Go to Applications → TaskFlow Backend Service → Admin roles tab.
  2. Click Edit assignments.
  3. Under Role, select TaskFlow User Manager.
  4. Under Resource set, select All TaskFlow Users.
  5. Click Save Changes.

Go back to the Admin roles tab and confirm it now shows TaskFlow User Manager paired with All TaskFlow Users, instead of "No admin privileges assigned."

Step 9: Install and Configure the SDK


    npm install @okta/okta-sdk-nodejs

This installs the current major version (8.x), requiring Node.js 14 or later — well within the Node 20.9+ this course already uses.

Store the Service App's credentials in .env.local, alongside your existing Okta variables from Phase 1:


    OKTA_SERVICE_CLIENT_ID=your_service_app_client_id
    OKTA_SERVICE_PRIVATE_KEY='{"kty":"RSA","kid":"...", ...}'

The private key is the JSON object you copied in Step 3 — paste the whole thing as a single-line string.

Step 10: Create the Management Client

Create src/lib/oktaManagementClient.ts:


    import { Client } from "@okta/okta-sdk-nodejs";

    export const oktaClient = new Client({
        orgUrl: process.env.OKTA_ORG_URL,
        authorizationMode: "PrivateKey",
        clientId: process.env.OKTA_SERVICE_CLIENT_ID,
        scopes: ["okta.users.read", "okta.users.manage"],
        privateKey: JSON.parse(process.env.OKTA_SERVICE_PRIVATE_KEY!),
    });

Setting authorizationMode: "PrivateKey" tells the SDK to use the Client Credentials + signed-JWT flow — it handles requesting and refreshing the OAuth access token internally from here on.

Step 11: Use It — Creating Users From Your Backend

A Route Handler that creates a new Okta user, src/app/api/admin/users/route.ts:


    import { NextRequest, NextResponse } from "next/server";
    import { cookies } from "next/headers";
    import { verifyIdToken } from "@/lib/verifyToken";
    import { isAdmin } from "@/lib/authz";
    import { oktaClient } from "@/lib/oktaManagementClient";

    export async function POST(request: NextRequest) {
        // Reuse the same claims-based authorization check from Phase 5 —
        // only a TaskFlow admin should be able to hit this endpoint.
        const cookieStore = await cookies();
        const idToken = cookieStore.get("id_token")?.value;
        if (!idToken) {
            return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
        }
        const claims = await verifyIdToken(idToken);
        if (!isAdmin(claims)) {
            return NextResponse.json({ error: "Forbidden" }, { status: 403 });
        }

        const body = await request.json(); // { email, firstName, lastName }

        try {
            const newUser = await oktaClient.userApi.createUser({
                body: {
                    profile: {
                        firstName: body.firstName,
                        lastName: body.lastName,
                        email: body.email,
                        login: body.email,
                    },
                },
                activate: true, // sends an activation email immediately
            });

            return NextResponse.json({ user: newUser }, { status: 201 });
        } catch (err) {
            console.error("Failed to create Okta user:", err);
            return NextResponse.json({ error: "Failed to create user" }, { status: 500 });
        }
    }

Notice the layered authorization here: this endpoint checks that the caller is specifically a TaskFlow admin (Phase 5's isAdmin() helper) before using the Service App's elevated permissions to create a new Okta user. The Service App's own scopes and role (Steps 5–8) are a second, independent layer underneath that — even if this check had a bug, the Service App itself literally cannot call anything beyond okta.users.read / okta.users.manage, on any resource beyond All TaskFlow Users.

Updating a user follows the same pattern: oktaClient.userApi.updateUser(userId, { profile: {...} }).

Two Different SDKs, Two Different Jobs

@okta/okta-auth-js — this is an Authentication SDK. Its job is everything we built in Phase 2 and Phase 3: driving the OAuth/OIDC login flow (Authorization Code + PKCE), handling tokens, session management, silent renewal. It operates as a user — it needs someone to actually log in through Okta's hosted page. We didn't end up using this package directly in this course (we hand-rolled the flow with fetch calls and jose for verification, back in Phase 2), but conceptually, this is the package that would have wrapped that same login flow.

@okta/okta-sdk-nodejs — this is a Management SDK. Its job is calling Okta's Admin/Management API: creating users, updating profiles, managing groups, and so on — entirely from your backend, with no end user involved at all. This is what Phase 6 actually needs, because the goal here is your server creating a new Okta user programmatically (e.g., an admin panel button that provisions a new account), not logging someone in.

Why okta-auth-js Can't Do This

okta-auth-js has no methods for creating, updating, or managing other users' accounts — that capability simply isn't part of what it's built for. It only knows how to authenticate one user (whoever is going through the login flow) and manage their session and tokens. There's no createUser() or updateUser() anywhere in it, because that's not what it's designed to do — that functionality lives entirely in @okta/okta-sdk-nodejs, which is why this phase installs that package instead.

Simple Way to Remember the Split

  • Someone is logging into TaskFlowokta-auth-js territory (what Phase 2/3 already covers).
  • TaskFlow's backend is managing accounts on Okta's behalf (creating users, admin operations) → okta-sdk-nodejs territory (what Phase 6 covers).

So the package you linked is genuinely useful and relevant to this course — just not for this particular lecture. It's already effectively what powers everything we built in Phase 2, conceptually. For Phase 6's backend user-management goal specifically, @okta/okta-sdk-nodejs is the correct and only right choice.


Where TaskFlow Stands

TaskFlow's backend can now manage Okta users directly — authenticated through a properly scoped OAuth 2.0 Service App, with its own custom role and resource set rather than an unscoped static token, and gated behind the same admin authorization check built in Phase 5.

In the continuation of "phase 6" to cover Inline Hooks and Event Hooks, and the basics of SCIM provisioning in next part.

Phase 5: Authorization — Part 2 (Securing API Routes & Fine-Grained Authorization)

Securing Your Own Next.js API Route Handlers

Everything we've protected so far has been a page — a UI a browser navigates to, where cookies are automatically included by the browser. But real apps also expose API endpoints, and those need their own protection, especially once other clients (a mobile app, a third-party integration) might call them directly using a bearer access token instead of a cookie.

The Pattern: Bearer Token Authentication

Suppose TaskFlow needs an API endpoint to fetch a user's tasks: GET /api/tasks. The correct way to secure it is to require the caller to send the access token from Phase 2/3 in an Authorization: Bearer <token> header — the standard OAuth 2.0 way APIs expect access tokens, separate from how a browser page reads its session cookie.

Create src/lib/verifyAccessToken.ts:


    import { jwtVerify, createRemoteJWKSet } from "jose";

    const JWKS = createRemoteJWKSet(
        new URL(`${process.env.OKTA_ORG_URL}/oauth2/default/v1/keys`)
    );

    export interface AccessTokenClaims {
        sub: string;
        scp?: string[];
        groups?: string[];
        [key: string]: unknown;
    }

    export async function verifyAccessToken(token: string): Promise<AccessTokenClaims> {
        const { payload } = await jwtVerify(token, JWKS, {
            issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`,
            audience: "api://default", // the default audience for Okta's built-in custom authorization server
        });
        return payload as AccessTokenClaims;
    }

Notice this reuses the same jose verification pattern from Phase 2 — signature, issuer, and expiry are all checked the same way. The one meaningful difference is the audience value: ID tokens are audienced to your Client ID (they're "for" your app), while access tokens issued by the default authorization server are audienced to api://default by convention, since access tokens are meant to be presented to APIs, not just consumed by the app that requested them.

Now build the protected endpoint, src/app/api/tasks/route.ts:


    import { NextRequest, NextResponse } from "next/server";
    import { verifyAccessToken } from "@/lib/verifyAccessToken";

    export async function GET(request: NextRequest) {
        const authHeader = request.headers.get("Authorization");

        if (!authHeader?.startsWith("Bearer ")) {
            return NextResponse.json({ error: "Missing bearer token" }, { status: 401 });
        }

        const token = authHeader.slice("Bearer ".length);

        let claims;
        try {
            claims = await verifyAccessToken(token);
        } catch {
            return NextResponse.json({ error: "Invalid or expired token" }, { status: 401 });
        }

        // At this point, claims.sub is the verified user ID — safe to use for a database lookup.
        const tasks = [
            { id: 1, title: "Design the login page", ownerId: claims.sub },
            { id: 2, title: "Write Phase 5 notes", ownerId: claims.sub },
        ];

        return NextResponse.json({ tasks });
    }

The key distinction to hold onto: 401 Unauthorized means "I don't know who you are" (missing or invalid token) — that's what this whole block checks. 403 Forbidden means "I know who you are, but you're not allowed to do this" — that's an authorization decision, layered on top, using the groups/role checks we built in Part 1. For example, a DELETE /api/tasks/:id handler would first run this same token verification, then separately check isAdmin(claims) (or a task-ownership check) before allowing the delete, returning 403 if that check fails even though the token itself was perfectly valid.

Calling It From the Client

If a Client Component needs to call this from the browser, it needs the access token available to JavaScript — which conflicts with the httpOnly cookie approach we deliberately chose in Phase 2 for security. The clean solution is to not expose the raw token to the browser at all; instead, proxy the call through a Server Action or another Route Handler that reads the httpOnly cookie server-side and forwards it internally. That keeps the access token out of browser-accessible JavaScript entirely, preserving the same security property we established back in Phase 2.

Beyond Roles and Groups: Fine-Grained Authorization

Roles and groups (Part 1) answer questions like "is this user an admin?" — a fixed, small number of categories. But real apps often need something more precise: "can this specific user edit this specific task that another specific user created and shared with them?" Modeling that with groups alone gets unwieldy fast — you'd end up creating a new group for every single resource, which doesn't scale.

This is exactly the problem Relationship-Based Access Control (ReBAC) solves, and Okta's offering here is built on OpenFGA — an open-source authorization engine (originally built by Auth0/Okta, since donated to the Cloud Native Computing Foundation) inspired by the same model Google uses internally (called Zanzibar). One naming note worth flagging clearly, since it trips people up: this product is currently branded Auth0 FGA (Fine-Grained Authorization) rather than "Okta FGA" — same underlying technology and team, but if you go looking for it, the dashboard and docs live under the Auth0 FGA name.

How It's Different From What We've Built

Instead of encoding permissions as claims inside a token, FGA moves authorization out of the token entirely, into a separate, centralized service you query at request time. You define:

  • Object types — e.g., task, folder, user.
  • Relations — e.g., a user can be an owner, editor, or viewer of a task.
  • Relationship tuples — actual facts, like "user:priya is owner of task:42" or "task:42's folder is folder:7, and user:arjun is a viewer of folder:7" (note this last example shows relationships can be inherited — a viewer of a folder becomes a viewer of everything inside it, without you writing that logic yourself).

At request time, instead of reading a groups array from a token, your API asks FGA a direct question: "can user:arjun view task:42?" — and FGA evaluates the full relationship graph (including inherited folder permissions) and returns a yes/no answer, fast, even across relationships with billions of tuples.

When You Actually Need This

To be clear about scope: TaskFlow, as we've built it, doesn't need FGA yet — the groups-based admin check from Part 1 comfortably covers "is this user an admin." You'd reach for FGA once TaskFlow grows features like sharing individual tasks with specific other users, nested folder permissions, or per-resource collaboration — the moment "which group is this user in" stops being able to express the permission you need. This course won't build a full FGA integration into TaskFlow, since it's a genuinely separate service with its own SDKs and modeling process, but knowing it exists — and specifically that it's the tool for relationship-shaped permissions rather than role-shaped ones — is the important takeaway for this lecture.


Phase 5 Is Complete

TaskFlow now enforces authorization at every layer: pages check verified claims server-side before rendering, API Route Handlers require and verify bearer access tokens independently of page cookies, and you know exactly when to reach past roles and groups into a relationship-based model like FGA.

In phase 6 we move to the backend: using the Okta Node Management SDK to manage users programmatically, and customizing Okta's own behavior with Inline and Event Hooks.

Phase 5: Authorization — Part 1 (Custom Claims & Role-Based Access Control)

Everything so far has answered "who is this user?" This phase answers the other half: "what is this specific user allowed to do inside TaskFlow?" That's authorization, and it's a genuinely separate concern from authentication — a user can be perfectly, verifiably logged in and still be forbidden from doing certain things.

Custom Claims: Putting Your Own Data Inside the Token

Step 1: Add a Custom User Attribute

  1. In the Admin Console, go to Directory → Profile Editor, select User (default), click Add Attribute.
  2. Fill in:
    • Data type: string
    • Display name: role
    • Variable name: role
    • Enum: check Define enumerated list of values, and add two entries under Attribute members: member / member and admin / admin.
    • Restriction: leave Value must be unique for each user unchecked.
    • Attribute required: leave unchecked.
    • Default value: optional — set to member if you want new users to default to it.
    • User permission: set to Read Only, so users can see their own role but not change it themselves.
  3. Click Save.

Step 2: Set the Value on a Real Test User

Go to Directory → People, open the specific user you intend to test with, go to their Profile tab, click Edit, and set role to admin. Save. Do this for at least one test account so there's real data for the claim to read.

Step 3: Turn the Attribute Into a Token Claim

Go to Security → API → Authorization Servers → default → Claims, click Add Claim.

A single claim entry in Okta can only target one token type at a time — Access Token or ID Token, never both together. Since TaskFlow needs this data in both places (the /admin page reads the ID token, while any API Route Handler reads the Access token), you create the same claim twice, once per token type.

Also worth knowing upfront: certain plain, common words — including role — are reserved and can't be used as a claim name on the ID Token. Using a slightly more specific name avoids this entirely, so this course uses userRole as the claim name throughout (the underlying Okta profile attribute is still called role — only the name of the claim inside the token changes).

Claim entry 1 — Access Token:

  • Name: userRole
  • Include in token type: Access Token, Always
  • Value type: Expression
  • Value: user.profile.role
  • Include in: Any scope
  • Click Create.

Claim entry 2 — ID Token:

  • Name: userRole
  • Include in token type: ID Token, Always (not the default Userinfo/id_token request, which only includes the claim when specifically requested — Always guarantees it's present every time)
  • Value type: Expression
  • Value: user.profile.role
  • Include in: Any scope
  • Click Create.

You should now see two rows named userRole in the Claims table — one with Type access, one with Type id. That's correct.

Groups: The More Scalable Alternative

Okta's native, purpose-built mechanism for this same problem — rather than a single string attribute — is Groups, read through a dedicated groups claim type, which isn't a reserved name and works the same way structurally.

Step 1: Create the Group

Go to Directory → Groups, click Add group, name it TaskFlow-Admins, and add your test user(s) to it (from the group's People tab, click Assign people).

Step 2: Create the Claim — Again, Twice

Back in Security → API → Authorization Servers → default → Claims → Add Claim:

Claim entry 1 — Access Token:

  • Name: groups
  • Include in token type: Access Token, Always
  • Value type: Groups
  • Filter: Starts withTaskFlow-
  • Include in: Any scope
  • Click Create.

Claim entry 2 — ID Token:

  • Name: groups
  • Include in token type: ID Token, Always
  • Value type: Groups
  • Filter: Starts withTaskFlow-
  • Include in: Any scope
  • Click Create.

The Starts with TaskFlow- filter is Okta's recommended pattern — it prevents irrelevant internal Okta groups (like admin-console-only groups) from leaking into your app's tokens. Avoid a broad match like .*, which would expose every group in your org.

With both entries created, every token — ID and Access alike — will include a groups array like ["TaskFlow-Admins"] for users in that group, and an empty array for everyone else.

Enforcing It in Next.js

Update src/lib/verifyToken.ts:


    export interface OktaIdTokenClaims {
        sub: string;
        email: string;
        name: string;
        userRole?: string;
        groups?: string[];
        [key: string]: unknown;
    }

Create src/app/admin/page.tsx:


    import { cookies } from "next/headers";
    import { redirect } from "next/navigation";
    import { verifyIdToken } from "@/lib/verifyToken";

    export default async function AdminPage() {
        const cookieStore = await cookies();
        const idToken = cookieStore.get("id_token")?.value;
        if (!idToken) redirect("/login");

        const claims = await verifyIdToken(idToken);
        const isAdmin = claims.groups?.includes("TaskFlow-Admins");

        if (!isAdmin) {
            redirect("/dashboard");
        }

        return (
            <div className="p-8">
                <h1 className="text-2xl font-semibold text-slate-800">Admin Panel</h1>
                <p className="text-slate-500 mt-2">Only TaskFlow-Admins can see this page.</p>
            </div>
        );
    }

This is deliberately a server-side check using verified token claims, not something decided by hiding a link in the UI — a user without the TaskFlow-Admins group typing /admin directly into the URL bar is still redirected away, before any admin content renders.

A Reusable Helper

Create src/lib/authz.ts:


    import type { OktaIdTokenClaims } from "@/lib/verifyToken";

    export function hasGroup(claims: OktaIdTokenClaims, groupName: string): boolean {
        return claims.groups?.includes(groupName) ?? false;
    }

    export function isAdmin(claims: OktaIdTokenClaims): boolean {
        return hasGroup(claims, "TaskFlow-Admins");
    }

Now the admin page check simplifies to if (!isAdmin(claims)) redirect("/dashboard");.

Protecting the Route in Middleware

Update src/middleware.ts's matcher from Phase 2/3 to also cover this new page:


    export const config = {
        matcher: ["/dashboard/:path*", "/admin/:path*"],
    };

This ensures unauthenticated users are bounced by Middleware before even reaching the page — layered underneath the claims check inside the page itself, the same defense-in-depth pattern established back in Phase 2.

Verifying It Works

A couple of things matter here that are easy to overlook:

  • Claims only apply to newly issued tokens. If you change anything under the Authorization Server's Claims tab, an already-logged-in session's token was minted before that change existed and won't reflect it. Always log out completely (not just reload the page) and log back in before testing a claims change.
  • To inspect exactly what's inside a token, add a temporary debug line in the Server Component:

    console.log("ID token claims:", claims);

Since this code runs on the server, the output appears in your terminal (wherever npm run dev is running) — not the browser's DevTools console. This is the fastest way to confirm exactly what a token contains rather than guessing why a redirect is happening.


Where TaskFlow Stands

Tokens now carry real, meaningful authorization data — a userRole attribute and a groups array — available in both the ID token and the Access token. TaskFlow enforces access to an admin-only page based on these verified claims, checked server-side, before any protected content ever renders.

Phase 4: Security Features — Part 3 (Social Login & Passwordless)

Social Login: Signing In With Google, Microsoft, or GitHub

Okta calls this Inbound Federation — Okta sits between your app and an external identity provider, handling the handshake so your app never talks to Google (or whichever provider) directly. TaskFlow still receives the same ID token and access token through the exact same callback route built in Phase 2 — nothing changes on the code side.

Step 1: Register TaskFlow as an OAuth App With the Provider

Before Okta can offer "Sign in with Google," Google needs to know about this integration. In the Google Cloud Console, create an OAuth 2.0 Client ID (under APIs & Services → Credentials). For the Authorized redirect URI, use Okta's own callback URL — not yours — which follows the pattern https://{yourOktaDomain}/oauth2/v1/authorize/callback. You'll get a Client ID and Client Secret from Google; these go into Okta, not your .env.local.

Step 2: Add the Identity Provider in Okta

  1. Go to Security → Identity Providers → Add identity provider.
  2. Select Google (or Microsoft, GitHub, etc.).
  3. Paste in the Client ID and Client Secret from Step 1.
  4. Configure two important settings:
    • Account linking — whether a Google sign-in should link to an existing Okta user with the same email, or always create a new one.
    • Just-In-Time (JIT) provisioning — whether a brand-new Okta user profile should be created automatically the first time someone signs in via Google. Leave this on for a smooth first-time experience.

Step 3: Add a Routing Rule — Required, Not Optional

Creating the Identity Provider alone does not make it appear on the login page. Okta needs a separate rule telling it when to offer this IdP:

  1. Go to Security → Identity Providers → Routing Rules.
  2. Click Add Routing Rule.
  3. Rule name: something descriptive, e.g. Google for TaskFlow.
  4. Set the IF conditions — who this applies to. For a straightforward setup, leave conditions broad (e.g., User is accessing: Any application, User matches: Anything).
  5. Under THEN — Use this identity provider, select Use specific IdP(s), and choose the IdP you just created (e.g., Google).
  6. Save.

Once both the Identity Provider and the Routing Rule exist, the Sign in with Google button appears automatically on Okta's hosted Sign-In page — no widget code, no SDK changes.

Try It

Open a fresh incognito window, go to /login, click through to Okta's hosted page. You should now see a Sign in with Google button alongside the password form. Clicking it takes the user to Google's real login page, then bounces them back through Okta and into TaskFlow's /dashboard, exactly like a normal login.

Repeat Steps 1–3 for any other provider (Microsoft, GitHub) — the pattern is identical each time.


Passwordless Authentication

Important Correction: Magic Link Is Not a Separate Authenticator

If you go looking under Security → Authenticators → Add Authenticator, you will not find a tile called "Magic Link" — it doesn't exist as its own authenticator type. This is a genuinely easy thing to expect and not find.

Instead, Magic Link is a built-in behavior of the Email authenticator, which is already added to your org by default. Opening Email → Actions → Edit confirms this directly — Okta's own description states: "If email is selected, Okta will send an email magic link and security token (code) to the email address enrolled by the user. The user can click on the link or enter the token (code) to gain access." Both the OTP and the magic link come from the same single email, automatically, with no separate toggle to enable one or the other.

Why You Might Not Have Seen It Yet

Having the Email authenticator enabled is not the same as offering a genuinely passwordless experience. If your existing Authentication Policy only ever uses Email as one of several acceptable second factors (alongside a required password), users will never see a standalone "sign in with just email" option — they'll always be asked for the password first. To get true passwordless sign-in, you need a dedicated rule that allows Email to satisfy login on its own, with no password required at all.

Step 1: Add a Dedicated Passwordless Rule

  1. Go to Security → Authentication Policies → App sign-in, and open the policy attached to TaskFlow.
  2. Click Add rule.
  3. Rule name: Passwordless email sign-in.
  4. Leave every IF condition at its default (Any user type, Any group, Any user, No policy, Any platform, Any IP, Risk: Any, empty custom expression) — these control who the rule applies to, and broad defaults are fine to start.
  5. Under THEN:
    • Access is: Allowed after successful authentication.
    • User must authenticate with: change this from the default Any 2 factor types to Any 1 factor type. This is the key change — it's what allows a single factor (Email) to be sufficient on its own, instead of requiring a password plus something else.
    • Authentication methods: change from Allow any method to Allow specific authentication methods, then check only Email and leave every other authenticator unchecked. This restricts this specific rule to Email alone.
  6. Leave the remaining settings (stay-signed-in, prompt-for-authentication timing) at their defaults.
  7. Click Save.

Step 2: Set the Rule's Priority Correctly

This step matters as much as creating the rule itself. Okta evaluates rules top to bottom and stops at the first match. Go back to the policy's Rules tab and confirm your new Passwordless email sign-in rule sits at Priority 1, above the existing Catch-all Rule (which still requires password + another factor). If the Catch-all Rule is evaluated first, it will catch every login before your new rule ever gets considered, and you'll never see the passwordless option.

Drag the rule using the grip icon on its row if it isn't already on top, or use whichever reordering control your console shows.

A Note on Scope

With both rules left at broad "Any request" conditions, this new rule currently applies to every login attempt — everyone gets offered the passwordless email path first. That's fine for testing, but worth knowing: if you later want only some users to get a passwordless option while others stay on password + MFA, you'd narrow the IF conditions on this rule (for example, scoping it to a specific group) rather than leaving it wide open.

Try It

  1. Open a fresh incognito window, go to /login, click Log in with Okta.
  2. Enter your username/email, click Next.
  3. Instead of a password field, Okta now prompts for Email verification directly — a "send code" / "enter code" style screen, since Email is the only method this top-priority rule allows.
  4. Check your inbox — you'll receive an email containing both a one-time code and a magic link.
  5. Either click the magic link directly, or copy the code into the widget.

This completes the login and redirects straight to /dashboard — no password ever entered.

Passkeys: The Other Genuine Passwordless Option

Separately from Magic Links, Passkeys (built on the FIDO2/WebAuthn standard) let a user authenticate using their device's built-in biometrics or a security key — Face ID, Windows Hello, a fingerprint reader, a physical YubiKey — with no password and no OTP at all. This is listed as its own authenticator tile, named Passkey (FIDO2 WebAuthn) in the current console (renamed from the older "FIDO2 (WebAuthn)" label — if you see the old name anywhere, it's the same feature).

To enable it: it's likely already added (check Security → Authenticators — it showed "Already added" in your org). Set it to Required or Optional in your Authenticator Enrollment Policy, and include it as an allowed method in any Authentication Policy rule where you want to offer it. The first time an enabled user logs in, Okta's hosted page prompts them to register a passkey using their device's biometrics, and every login afterward can use that instead of a password.


Phase 4 Is Complete

TaskFlow now supports, all through the same login button built in Phase 2: adaptive multi-factor authentication, self-service registration with email verification, self-service password recovery, social login through Google (or any provider added the same way), and two genuinely distinct forms of passwordless authentication — Email magic links via a dedicated single-factor rule, and device-based Passkeys. All of it verified working end to end against a real org, not just documentation.

Phase 4: Security Features — Part 2 (Self-Service Registration & Password Recovery)

Same theme as the rest of this phase: because TaskFlow uses the redirect model built in Phase 2 — where the login experience lives on Okta's hosted page — everything below is configured entirely in the Okta Admin Console. No changes to your Next.js code are needed for either feature.

Self-Service Registration: Letting Users Sign Themselves Up

Right now, every TaskFlow user has to be manually created by you in the Admin Console. Self-service registration adds a "Sign up" link directly on Okta's hosted login page, letting new users create their own account.

Step 1: Create a User Profile Policy

  1. In the Admin Console, go to Security → User Profile Policies.
  2. Click Add user profile policy — creating one scoped specifically to TaskFlow keeps registration rules isolated from any other apps in your org.
  3. Give it a name, e.g. TaskFlow Registration Policy, and click Save.

Step 2: Configure Enrollment Settings

  1. Open the policy, go to its Enrollment tab.
  2. Under Profile Enrollment, click Edit.
  3. Set Self-service registration to Allowed — this is what makes a "Sign up" link appear on Okta's hosted Sign-In page.
  4. Leave Progressive Profiling as Enabled — this lets Okta ask for any missing attributes on a later sign-in rather than forcing everything up front.
  5. Set Email verification to Required before access is granted — this means a self-registered user must click a confirmation link in their email before their account becomes usable, which keeps out spam signups and typo'd addresses.
  6. Add the user to group — optionally select a group here to auto-assign every self-registered user into it (e.g., a TaskFlow-Free group). Leave as None if you don't need this yet.
  7. Click Save.

Step 3: Configure the Registration Form

Still on the policy, scroll to Profile enrollment form:

  1. By default you'll see First name, Last name, and Primary email, all Required.
  2. Click Add form input for any additional fields you want — for example Mobile phone as Optional, or any custom attribute defined back in Phase 3.

Step 4: Attach the Policy to TaskFlow

  1. On the policy, click the Apps tab.
  2. Click Add an App to This Policy.
  3. Select TaskFlow, click Apply, then Close.
  4. Confirm TaskFlow now appears under Apps using this policy.

Step 5: Assign TaskFlow to a Group

Self-service registration specifically requires the app to be assigned to a Group, not just individual people — this is an Okta requirement worth setting up correctly from the start:

  1. Go to Applications → TaskFlow → Assignments.
  2. Click Assign → Assign to Groups.
  3. Select Everyone (or a custom group your org uses), click Assign, then Save and Go Back, then Done.

Try It

Open http://localhost:3000/login, click Log in with Okta. On Okta's hosted Sign In page, you'll now see:

Don't have an account? Sign up

Click through it — you'll get a registration form based on the fields configured in Step 3, an email verification step, and then land back into TaskFlow's /dashboard as a brand-new, real Okta user — created entirely through Okta's own UI, no code of ours involved.

A Note on Username

By default, Okta uses the registrant's email address as both their username and their primary email — this matches how most modern apps behave, and is the current recommended default.


Self-Service Password Recovery

This is the "Forgot password?" flow — one of the clearest wins for offloading identity to Okta instead of building it yourself.

Step 1: Enable It on the Password Authenticator

  1. Go to Security → Authenticators, find Password, click Actions → Edit.
  2. Under Rules, edit the default rule (or add a new one).
  3. Enable Password reset under Users can perform self-service.
  4. Under Recovery authenticators, choose which methods a user can use to prove their identity before resetting — Email at minimum is a solid default.
  5. Set Access control to This rule (legacy) for straightforward behavior, or Authentication policy if you want recovery strictness to follow the same Authentication Policy rules from Phase 4, Part 1.
  6. Save.

An Important Security Detail

If you configure recovery authenticators to require something stronger than what you allow for regular sign-in — for instance, requiring Okta Verify push specifically for recovery — you can lock out a genuine user who lost the device holding Okta Verify, which is often the whole reason they needed recovery in the first place. A sensible default for TaskFlow: allow Email as a standalone recovery method, since it doesn't depend on a device the user might have already lost.

Step 2: Try It

From /login, enter the account's email or username on Okta's first screen and click Next. On the password screen that follows, you'll see Forgot password? directly below the password field. Click it, enter the account's email, and Okta sends a reset link — clicking it lets the user set a new password directly on Okta's page, then returns them through the same flow into /dashboard.


Where TaskFlow Stands

New users can self-register through a properly configured registration form with email verification, and any user can recover a forgotten password safely — all without a single new line of application code, because the entire experience is Okta's hosted page reacting to policies configured here.

Phase 4: Security Features — Part 1 (MFA & Adaptive Policies)

Here's the genuinely good news for this entire phase: because TaskFlow uses the redirect model we built in Phase 2 — where the actual login form lives on Okta's hosted page, not inside our Next.js app — everything in this lecture requires zero code changes. MFA, adaptive sign-on, self-service registration, social login, and passwordless — all of it is configured entirely in the Okta Admin Console, and your existing /api/auth/login route automatically benefits, because Okta simply shows a different, richer login experience on its own page. This is one of the strongest arguments for the redirect model over trying to build a custom login form yourself.

Multi-Factor Authentication: The Two Separate Policies You Need to Understand

A common point of confusion: people expect one setting called "enable MFA." Okta actually splits this into two distinct policies that work together, and understanding the difference will save you a lot of head-scratching:

  1. Authenticator Enrollment Policy — Controls which MFA methods (Okta calls these "authenticators") users are allowed or required to enroll in, and when they're prompted to enroll (immediately, or with a grace period of skips). This is under Security → Authenticators in the Admin Console. Note the naming: in Okta's current Identity Engine (what your Integrator Free Plan org runs), this was renamed from the older "MFA Enrollment Policy" you'll see in outdated tutorials.
  2. Authentication Policy (App Sign-in Policy) — Controls when an already-enrolled user is actually challenged for MFA during login — for example, every time, once per session, or only under risky conditions. This lives under Security → Authentication Policies → App sign-in.

In short: Enrollment Policy decides what methods exist for a user to use. Authentication Policy decides when Okta actually asks for them.

Step 1: Enabling Authenticators

Go to Security → Authenticators in the Admin Console. You'll see a list of available authenticator types. Okta enables Okta Verify (its own push-notification/TOTP app) and Password by default. To add more:

  • Click Add authenticator.
  • Common beginner-friendly choices: Email (sends a one-time code — good default, no app install needed), Phone (SMS or voice call), and Okta Verify (push notification to a phone, most secure and lowest-friction once installed).
  • For each one you add, you'll configure whether it's Required, Optional, or used only for specific policies.

For this course, enable Okta Verify and Email — enough to demonstrate a real MFA prompt without needing extra third-party accounts.

Step 2: Building the Authenticator Enrollment Policy

Still under Security → Authenticators, click the Enrollment tab. Here you define enrollment policies that determine which authenticators a given group of users must set up, and how strictly. For TaskFlow:

  1. Click on the default policy (or Add a Policy for a specific group, like requiring stricter enrollment for an "Admins" group later).
  2. Set Okta Verify to Required.
  3. Set Email to Optional (a backup method).
  4. Save.

Once saved, the next time a user without Okta Verify enrolled tries to log in, Okta's hosted page will automatically prompt them to set it up — scanning a QR code with their phone — entirely on Okta's side, before redirecting back to your redirect_uri.

Step 3: Requiring MFA at Login — the App Sign-in Policy

Enrollment alone doesn't force MFA to be checked at every login — that's the Authentication Policy's job. Go to Security → Authentication Policies → App sign-in, and find (or create) the policy attached to your TaskFlow application.

Every App Sign-in Policy starts with a single catch-all rule that applies to everyone by default. Click into it (or Add rule to create a more specific one above it) and configure:

  • IF conditions — who this rule applies to (e.g., "any user," or scoped to a specific group).
  • THEN — Access — set to Allowed.
  • THEN — Authentication requirements — this is the key setting. Choose Password + Another factor to require MFA on every login, or explore the Possession factor options (like Okta Verify specifically) for stronger requirements.
  • Re-authentication frequency — how often a returning user must re-prove MFA: every sign-in, once per session, or on a custom interval.

Save, and log out and back into TaskFlow (http://localhost:3000/login) to see it in action — Okta's hosted page will now prompt for your password, then a second factor, before redirecting back to /dashboard.

Step 4: Making It Adaptive — Contextual Rules

"Adaptive MFA" just means: instead of one blanket rule for everyone, you stack multiple rules with different conditions, and Okta evaluates them in priority order (rules are checked top to bottom; the first matching rule wins). This lets you ask for MFA only when something looks risky, and skip friction otherwise.

Inside the same App Sign-in Policy, click Add rule and explore the IF conditions available — these are the actual signals Okta can evaluate:

  • User's risk score — Okta's own behavioral risk analysis (available depending on plan/features enabled).
  • Device is not registered / New device — the specific behavior detector mentioned in Okta's own current release notes as something you can combine with "MFA required" in a policy.
  • Network zone — e.g., require stricter MFA outside your office's known IP range, or when connecting through an anonymizing proxy.
  • User's group membership — different rules for different groups (this becomes very useful once we build role-based access in Phase 5).

A realistic adaptive setup for TaskFlow: one rule at the top that says "if device is new/unrecognized, require Password + Okta Verify," and the catch-all rule below it set to "Password only" for recognized, trusted devices. Because rules are evaluated in order and the first match wins, place your stricter, more specific rules above the general catch-all.

Step 5: Verifying the Right Engine

One thing worth double-checking now, since old tutorials frequently mix this up: if you ever see menu items called "Sign On Policies" (singular, under an older-looking menu) instead of "Authentication Policies → App sign-in", that means you're looking at documentation for Okta Classic Engine, not Identity Engine — the two have genuinely different menus and concepts, and Classic Engine guidance won't match what you see in your own console. Every Integrator Free Plan org (what we set up in Phase 1) runs on Identity Engine, so always confirm you're following Identity Engine-specific docs and instructions like the ones in this lecture.


What TaskFlow Has Now

Without touching a single line of Next.js code, TaskFlow now enforces real, configurable, adaptive multi-factor authentication — enrollment requirements, per-app authentication rules, and contextual conditions like new-device detection — all live-tested through the same login button we built in Phase 2.

Phase 6: Backend & Admin Operations — Part 1 (Okta Node Management SDK)

Every lecture until now has been about the login flow — authenticating a user through a redirect to Okta. This phase steps outside that ent...