fix team invites (#993)

<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Invitation flow now derives the invitation link from a provided origin
rather than accepting a full callback URL.

* **Bug Fixes / Security**
* Enforced origin whitelist for invitation redirects to prevent
untrusted callback URLs.

* **Tests**
* Added a test ensuring untrusted callback URLs are rejected with a
proper error response.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
BilalG1 2025-11-05 13:17:33 -08:00 committed by GitHub
parent fbf36d1004
commit 9fa7e3b0c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 32 additions and 1 deletions

View File

@ -29,7 +29,8 @@ export async function listInvitations(teamId: string) {
}));
}
export async function inviteUser(teamId: string, email: string, callbackUrl: string) {
export async function inviteUser(teamId: string, email: string, origin: string) {
const callbackUrl = new URL(stackServerApp.urls.teamInvitation, origin).toString();
const user = await stackServerApp.getUser();
const team = await user?.getTeam(teamId);
if (!team) {

View File

@ -525,3 +525,33 @@ it("errors with item_quantity_insufficient_amount when accepting invite without
}
`);
});
it("should error when untrusted callback URL is provided", async ({ expect }) => {
const { teamId } = await Team.create();
const receiveMailbox = createMailbox();
backendContext.set({ userAuth: null });
const sendTeamInvitationResponse = await niceBackendFetch("/api/v1/team-invitations/send-code", {
method: "POST",
accessType: "server",
body: {
email: receiveMailbox.emailAddress,
team_id: teamId,
callback_url: "https://malicious.com/callback",
},
});
expect(sendTeamInvitationResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 400,
"body": {
"code": "REDIRECT_URL_NOT_WHITELISTED",
"error": "Redirect URL not whitelisted. Did you forget to add this domain to the trusted domains list on the Stack Auth dashboard?",
},
"headers": Headers {
"x-stack-known-error": "REDIRECT_URL_NOT_WHITELISTED",
<some fields may have been hidden>,
},
}
`);
});