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
- In the Admin Console, go to Applications → Applications — the same regular Applications list you used to create TaskFlow back in Phase 1.
- Click Create App Integration.
- 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.
- Click Next.
- Give it a name:
TaskFlow Backend Service. - 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.
- On the General tab, find Client Credentials, click Edit.
- Select Public key / Private key instead of Client secret.
- Save.
A new Public keys section will appear below, currently empty.
Step 3: Generate the Key Pair
- Click Edit on the Public keys card, then Add.
- In the dialog, click Generate new key.
- You'll see two blocks: a public key, and a section labeled "Private key - Copy this!" — Okta shows this only once.
- Click Copy to clipboard (JSON format — this is what the Node SDK expects).
- Paste this immediately somewhere safe — a local file or password manager. Closing this dialog without copying it means starting over.
- 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.
- Click Edit on General Settings.
- Uncheck this option.
- 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
- 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.
- Find
okta.users.read, click Grant. - Find
okta.users.manage, click Grant. - 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.
- Go to Security → Administrators → Roles tab.
- Click to create a new role (or you can also reach this from the Role dropdown during assignment, via Create a role).
- Name it:
TaskFlow User Manager. - Select these permissions:
- Manage users (create, update, deactivate)
- View users and their details (read access)
- Leave every other permission category unchecked.
- 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.
- Go to Security → Administrators → Resources tab.
- Click Create new resource set.
- Name:
All TaskFlow Users - Description:
All TaskFlow Users - Under Resources, click Add Resource.
- In Find a resource type, select Users.
- Choose All users.
- Click Save selection.
- Click Create.
You should now see All TaskFlow Users listed on the Resources tab.
Step 8: Assign the Role to the Service App
- Go to Applications → TaskFlow Backend Service → Admin roles tab.
- Click Edit assignments.
- Under Role, select
TaskFlow User Manager. - Under Resource set, select
All TaskFlow Users. - 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 TaskFlow →
okta-auth-jsterritory (what Phase 2/3 already covers). - TaskFlow's backend is managing accounts on Okta's behalf (creating users, admin operations) →
okta-sdk-nodejsterritory (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.