I recently added Asgardeo (WSO2's identity platform) to a hotel booking app I've been building. My main reason was to see how a managed identity provider compares to the JWT/OAuth authentication I'd built by hand. I expected to write a short "here's how you add login" post. Instead I found a scope-release gotcha worth understanding, a dependency bug worth filing, and one wrong conclusion I had to walk back before publishing this.
The setup
The app already has its own auth: a JWT held in Redux, attached to API calls via RTK Query, with role-based route protection. I added Asgardeo as a second, parallel system, not a replacement, using @asgardeo/react, wired into a protected /asgardeo route that shows the signed-in user's ID token claims. The two systems share no state; it was a deliberately additive integration, so I could compare "what I built myself" against "what a managed identity platform hands you for free" without touching what already worked.
Getting sign-in and sign-out working took about twenty minutes. Understanding what happened next took a lot longer, and taught me more.
Claim release isn't one setting. It's three.
The first surprise: I requested the profile scope and got back an ID token with username and sub, but email, given_name, and family_name all showed up empty. My first instinct was that I'd misconfigured something. The real answer was more interesting: a claim reaching an ID token has to clear three independent gates, and an empty field looks identical no matter which gate it failed.
Gate 1: the value has to exist. I'd self-registered with just an email and password, so there was no first or last name stored on the account at all. No amount of configuration releases a value that was never set.
Gate 2: the attribute has to be released to the application. Under the app's settings, each attribute needs to be explicitly marked as requested. This is a separate switch from the OAuth scope. Enabling one does nothing for the other.
Gate 3: the owning scope has to be requested. Claims are grouped under scopes, and the grouping isn't always where you'd guess. I checked the tenant's discovery document directly and found email listed as its own scope, entirely separate from profile. So given_name and family_name depend only on gates 1 and 2 (they live under profile, which I was already requesting). email would stay empty no matter what I did to the other two gates, because I'd never requested its scope.
To actually prove this rather than just assert it, I ran a controlled test: changed exactly one variable (set a first name, then released given_name under the app's attribute settings) while leaving everything else fixed, then re-authenticated.
Before:
json
{
"sub": "a8d1df04-3147-450f-9ebd-c3d1fccda043",
"username": "examplemail@gmail.com"
}
After (only given_name changed on either end):
json
{
"given_name": "Dinuka",
"sub": "a8d1df04-3147-450f-9ebd-c3d1fccda043",
"username": "examplemail@gmail.com"
}
email and family_name stayed empty, exactly as the three-gate model predicted. Changing every gate at once would have told me nothing; I'd have had three plausible explanations for one outcome. Isolating one variable is what turned a guess into a verified conclusion.
The bug I found, and the conclusion I had to retract
While digging into the SDK's dependency tree, I noticed @asgardeo/react pins react-dom as a hard dependency at an exact version (19.x), while react is a much looser peer dependency (>=16.8.0). On a React 18 project, that's a contradiction npm can't resolve cleanly. It installs a second, mismatched react-dom alongside the app's own, and flags the whole tree as invalid.
At first, I assumed this was the cause of a blank white screen I was seeing in the app. I built a workaround (an npm overrides entry plus a Vite dedupe config), the blank screen persisted, and I wrote up the dependency conflict as the culprit anyway. In hindsight, that persistence should have been the first red flag. A fix that doesn't fix the symptom is evidence the diagnosis is wrong, not a reason to keep the diagnosis and add more workaround.
The actual cause, once I read the real browser console error instead of guessing, was a completely unrelated pre-existing bug: a component rendering a raw API error object directly as JSX, which crashes React and unmounts the whole page. Nothing to do with Asgardeo at all.
To find out whether the dependency conflict was a real problem on its own merits, I built a clean, minimal reproduction: no workarounds, no other app code, just the SDK in a fresh Vite + React 18 project. It rendered fine, in both dev and production builds, with no console errors. The SDK's actual code never imports the mismatched react-dom copy, so the broken renderer just sits there unused, adding about 4KB of dead code to the bundle rather than causing any runtime failure.
So the bug is real, but smaller than I first claimed: a dependency-declaration defect and a bundle-size cost, not a crash. I filed it upstream with that corrected scope, asgardeo/javascript#571, including the clean reproduction, the exact npm output showing the invalid tree, and a suggested fix (moving react-dom to peerDependencies, matching how react is already handled).
What actually mattered here
Neither of these findings was hard to produce once I stopped accepting the first plausible explanation. The claim-release model came from checking a real discovery document instead of guessing at scope groupings. The corrected bug report came from testing my own fix in isolation instead of trusting that a workaround I'd written must have worked. In both cases, the useful output wasn't the code. It was going back and checking whether what I believed actually matched what I could observe.
Top comments (0)