Conversation
WalkthroughThis PR extends OAuth callback handling to support OAuth providers that return multiple identifier values for a single user. The implementation normalizes single or multiple Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/hydrooj/src/handler/user.ts (1)
470-526:⚠️ Potential issue | 🟠 MajorCarry the normalized id array through deferred registration.
Line 525 still stores raw
r._idin the registration token, while Line 323 later forwardsthis.tdoc.identity.idinto a singleoauth.set(...)call. For multi-id providers, that makes the registration path persist a different shape than the direct bind/email paths, andOauthMap.idcan end up asstring[]even though the model/API still assumesstring.🧩 Suggested follow-up
- const ids = Array.isArray(r._id) ? r._id : [r._id]; + const ids = [...new Set(Array.isArray(r._id) ? r._id : [r._id])]; ... identity: { provider: args.type, platform: args.type, - id: r._id, + id: ids, },Then mirror the same normalization in
UserRegisterWithCodeHandlerbefore writing bindings:const ids = Array.isArray(this.tdoc.identity.id) ? this.tdoc.identity.id : [this.tdoc.identity.id]; await Promise.all(ids.map((id) => this.ctx.oauth.set(this.tdoc.identity.platform, id, uid)));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/hydrooj/src/handler/user.ts` around lines 470 - 526, The registration token is storing raw r._id (possibly a string) which causes inconsistent shapes for identity.id when providers return multiple ids; normalize into an array before adding the token and mirror the same normalization when consuming the token. Change the token creation in the registration flow (where token.add is called) to compute ids = Array.isArray(r._id) ? r._id : [r._id] and put that normalized ids into the identity.id field instead of raw r._id, and update UserRegisterWithCodeHandler (before calling this.ctx.oauth.set) to normalize this.tdoc.identity.id the same way (const ids = Array.isArray(this.tdoc.identity.id) ? this.tdoc.identity.id : [this.tdoc.identity.id]; then await Promise.all(ids.map(id => this.ctx.oauth.set(this.tdoc.identity.platform, id, uid)))) so oauth.set always receives individual string ids.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/hydrooj/src/handler/user.ts`:
- Around line 470-485: The code currently picks the first truthy UID from
existing and logs in that account; instead, ensure all non-null entries in
existing refer to the same UID before calling successfulAuth. Change the logic
around existing/effective: collect unique non-null UIDs from existing (from
this.ctx.oauth.get results), if more than one throw BadRequestError('OAuth
identities already bound to different accounts'), if exactly one use that UID as
effective and call successfulAuth(await user.getById('system', effective)),
otherwise proceed as before; keep the oauth set flow (this.ctx.oauth.set)
unchanged.
---
Outside diff comments:
In `@packages/hydrooj/src/handler/user.ts`:
- Around line 470-526: The registration token is storing raw r._id (possibly a
string) which causes inconsistent shapes for identity.id when providers return
multiple ids; normalize into an array before adding the token and mirror the
same normalization when consuming the token. Change the token creation in the
registration flow (where token.add is called) to compute ids =
Array.isArray(r._id) ? r._id : [r._id] and put that normalized ids into the
identity.id field instead of raw r._id, and update UserRegisterWithCodeHandler
(before calling this.ctx.oauth.set) to normalize this.tdoc.identity.id the same
way (const ids = Array.isArray(this.tdoc.identity.id) ? this.tdoc.identity.id :
[this.tdoc.identity.id]; then await Promise.all(ids.map(id =>
this.ctx.oauth.set(this.tdoc.identity.platform, id, uid)))) so oauth.set always
receives individual string ids.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7b52cc84-edea-469f-ab0a-a1b1ae3e4a9c
📒 Files selected for processing (2)
packages/hydrooj/src/handler/user.tspackages/hydrooj/src/model/oauth.ts
| const ids = Array.isArray(r._id) ? r._id : [r._id]; | ||
| const existing = await Promise.all(ids.map((id) => this.ctx.oauth.get(args.type, id))); | ||
| if (this.session.oauthBind === args.type) { | ||
| delete this.session.oauthBind; | ||
| const existing = await this.ctx.oauth.get(args.type, r._id); | ||
| if (existing && existing !== this.user._id) { | ||
| if (existing.some((id) => id && id !== this.user._id)) { | ||
| throw new BadRequestError('Already binded to another account'); | ||
| } | ||
| this.response.redirect = '/home/security'; | ||
| if (existing !== this.user._id) await this.ctx.oauth.set(args.type, r._id, this.user._id); | ||
| await Promise.all(ids.map((i) => this.ctx.oauth.set(args.type, i, this.user._id))); | ||
| return; | ||
| } | ||
| const effective = existing.find((i) => i); | ||
|
|
||
| const uid = await this.ctx.oauth.get(args.type, r._id); | ||
| if (uid) { | ||
| await successfulAuth.call(this, await user.getById('system', uid)); | ||
| if (effective) { | ||
| await successfulAuth.call(this, await user.getById('system', effective)); | ||
| this.response.redirect = '/'; |
There was a problem hiding this comment.
Fail closed when the candidate ids map to different users.
Line 481 picks the first truthy uid from existing. If two returned ids are already bound to different Hydro accounts, provider order decides which account gets logged in. Require all non-null bindings to collapse to a single uid before calling successfulAuth.
🔒 Suggested guard
if (this.session.oauthBind === args.type) {
delete this.session.oauthBind;
- if (existing.some((id) => id && id !== this.user._id)) {
+ if (existing.some((uid) => uid !== null && uid !== this.user._id)) {
throw new BadRequestError('Already binded to another account');
}
this.response.redirect = '/home/security';
await Promise.all(ids.map((i) => this.ctx.oauth.set(args.type, i, this.user._id)));
return;
}
- const effective = existing.find((i) => i);
+ const boundUids = [...new Set(existing.filter((uid): uid is number => uid !== null))];
+ if (boundUids.length > 1) {
+ throw new BadRequestError('OAuth ids are bound to multiple accounts');
+ }
+ const effective = boundUids[0];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const ids = Array.isArray(r._id) ? r._id : [r._id]; | |
| const existing = await Promise.all(ids.map((id) => this.ctx.oauth.get(args.type, id))); | |
| if (this.session.oauthBind === args.type) { | |
| delete this.session.oauthBind; | |
| const existing = await this.ctx.oauth.get(args.type, r._id); | |
| if (existing && existing !== this.user._id) { | |
| if (existing.some((id) => id && id !== this.user._id)) { | |
| throw new BadRequestError('Already binded to another account'); | |
| } | |
| this.response.redirect = '/home/security'; | |
| if (existing !== this.user._id) await this.ctx.oauth.set(args.type, r._id, this.user._id); | |
| await Promise.all(ids.map((i) => this.ctx.oauth.set(args.type, i, this.user._id))); | |
| return; | |
| } | |
| const effective = existing.find((i) => i); | |
| const uid = await this.ctx.oauth.get(args.type, r._id); | |
| if (uid) { | |
| await successfulAuth.call(this, await user.getById('system', uid)); | |
| if (effective) { | |
| await successfulAuth.call(this, await user.getById('system', effective)); | |
| this.response.redirect = '/'; | |
| const ids = Array.isArray(r._id) ? r._id : [r._id]; | |
| const existing = await Promise.all(ids.map((id) => this.ctx.oauth.get(args.type, id))); | |
| if (this.session.oauthBind === args.type) { | |
| delete this.session.oauthBind; | |
| if (existing.some((uid) => uid !== null && uid !== this.user._id)) { | |
| throw new BadRequestError('Already binded to another account'); | |
| } | |
| this.response.redirect = '/home/security'; | |
| await Promise.all(ids.map((i) => this.ctx.oauth.set(args.type, i, this.user._id))); | |
| return; | |
| } | |
| const boundUids = [...new Set(existing.filter((uid): uid is number => uid !== null))]; | |
| if (boundUids.length > 1) { | |
| throw new BadRequestError('OAuth ids are bound to multiple accounts'); | |
| } | |
| const effective = boundUids[0]; | |
| if (effective) { | |
| await successfulAuth.call(this, await user.getById('system', effective)); | |
| this.response.redirect = '/'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/hydrooj/src/handler/user.ts` around lines 470 - 485, The code
currently picks the first truthy UID from existing and logs in that account;
instead, ensure all non-null entries in existing refer to the same UID before
calling successfulAuth. Change the logic around existing/effective: collect
unique non-null UIDs from existing (from this.ctx.oauth.get results), if more
than one throw BadRequestError('OAuth identities already bound to different
accounts'), if exactly one use that UID as effective and call
successfulAuth(await user.getById('system', effective)), otherwise proceed as
before; keep the oauth set flow (this.ctx.oauth.set) unchanged.
Summary by CodeRabbit
New Features
Bug Fixes