-
Notifications
You must be signed in to change notification settings - Fork 67
fix(nextjs): update organizations through a server action #558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@asgardeo/nextjs': patch | ||
| --- | ||
|
|
||
| Editing an organization through `<OrganizationProfile />` works. The component called the Organizations API from the browser without an access token (the token lives in the HttpOnly session cookie), so every save was rejected. Updates now go through a server action (`updateOrganizationAction`, backed by `AsgardeoNextClient.updateOrganization()`) that attaches the token on the server, and a failed save surfaces its reason instead of a bare request error. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,11 +18,12 @@ | |||||
|
|
||||||
| 'use client'; | ||||||
|
|
||||||
| import {OrganizationDetails, updateOrganization, createPatchOperations} from '@asgardeo/node'; | ||||||
| import {OrganizationDetails, createPatchOperations} from '@asgardeo/node'; | ||||||
| import {BaseOrganizationProfile, BaseOrganizationProfileProps, useTranslation} from '@asgardeo/react'; | ||||||
| import {FC, ReactElement, useEffect, useState} from 'react'; | ||||||
| import getOrganizationAction from '../../../../server/actions/getOrganizationAction'; | ||||||
| import getSessionId from '../../../../server/actions/getSessionId'; | ||||||
| import updateOrganizationAction from '../../../../server/actions/updateOrganizationAction'; | ||||||
| import logger from '../../../../utils/logger'; | ||||||
| import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; | ||||||
|
|
||||||
|
|
@@ -188,11 +189,15 @@ const OrganizationProfile: FC<OrganizationProfileProps> = ({ | |||||
| const operations: Array<{operation: 'REPLACE' | 'REMOVE'; path: string; value?: any}> = | ||||||
| createPatchOperations(payload); | ||||||
|
|
||||||
| await updateOrganization({ | ||||||
| baseUrl, | ||||||
| operations, | ||||||
| organizationId, | ||||||
| }); | ||||||
| // The access token only exists on the server (HttpOnly session cookie), so the update goes through | ||||||
| // a server action rather than calling the Organizations API from the browser. | ||||||
| const result: {data: {organization?: OrganizationDetails}; error: string | null; success: boolean} = | ||||||
| await updateOrganizationAction(organizationId, operations, (await getSessionId()) as string); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find session-ID consumers and determine whether a session ID is an authenticated capability.
rg -n -C 4 'verifySessionToken|sessionId|setSessionData|getSessionData|sessionId\?' packages/nextjs packages/node
fd -t f 'SessionManager.*' packages -x sh -c 'sed -n "1,260p" "$1"' sh {}Repository: asgardeo/javascript Length of output: 50376 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '470,515p' packages/nextjs/src/AsgardeoNextClient.ts
rg -n -C 8 'updateOrganization|setSession\(|getSession\(|getAccessToken\(' packages/nextjs/src/AsgardeoNextClient.ts packages/nextjs/src/server/actions/updateOrganizationAction.ts
sed -n '1,120p' packages/nextjs/src/server/actions/updateOrganizationAction.tsRepository: asgardeo/javascript Length of output: 25457 Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor Reachability: External Keep the session identifier on the server.
Proposed fix- await updateOrganizationAction(organizationId, operations, (await getSessionId()) as string);
+ await updateOrganizationAction(organizationId, operations);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| if (!result.success) { | ||||||
| throw new Error(result.error ?? 'Failed to update organization'); | ||||||
| } | ||||||
|
|
||||||
| // Refetch organization data after update | ||||||
| await fetchOrganization(); | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /** | ||
| * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). | ||
| * | ||
| * WSO2 LLC. licenses this file to you under the Apache License, | ||
| * Version 2.0 (the "License"); you may not use this file except | ||
| * in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| import {beforeEach, describe, expect, it, vi, Mock} from 'vitest'; | ||
| import AsgardeoNextClient from '../../../AsgardeoNextClient'; | ||
| import updateOrganizationAction from '../updateOrganizationAction'; | ||
|
|
||
| vi.mock('../../../AsgardeoNextClient', () => ({ | ||
| default: { | ||
| getInstance: vi.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| describe('updateOrganizationAction', () => { | ||
| type ActionResult = Awaited<ReturnType<typeof updateOrganizationAction>>; | ||
|
|
||
| const client: {updateOrganization: Mock} = {updateOrganization: vi.fn()}; | ||
| const operations: Array<{operation: 'REPLACE'; path: string; value: string}> = [ | ||
| {operation: 'REPLACE', path: '/name', value: 'Acme Inc.'}, | ||
| ]; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| (AsgardeoNextClient.getInstance as unknown as Mock).mockReturnValue(client); | ||
| }); | ||
|
|
||
| it('returns the updated organization when the update succeeds', async () => { | ||
| const organization: Record<string, unknown> = {id: 'org-1', name: 'Acme Inc.'}; | ||
|
|
||
| client.updateOrganization.mockResolvedValue(organization); | ||
|
|
||
| const result: ActionResult = await updateOrganizationAction('org-1', operations, 'session-1'); | ||
|
|
||
| expect(client.updateOrganization).toHaveBeenCalledWith('org-1', operations, 'session-1'); | ||
| expect(result).toEqual({data: {organization}, error: null, success: true}); | ||
| }); | ||
|
|
||
| it('reports the failure reason instead of throwing when the update fails', async () => { | ||
| client.updateOrganization.mockRejectedValue(new Error('Failed to update the organization org-1: forbidden')); | ||
|
|
||
| const result: ActionResult = await updateOrganizationAction('org-1', operations); | ||
|
|
||
| expect(result.success).toBe(false); | ||
| expect(result.error).toBe('Failed to update the organization org-1: forbidden'); | ||
| expect(result.data.organization).toBeUndefined(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| /** | ||
| * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). | ||
| * | ||
| * WSO2 LLC. licenses this file to you under the Apache License, | ||
| * Version 2.0 (the "License"); you may not use this file except | ||
| * in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
|
|
||
| 'use server'; | ||
|
|
||
| import {OrganizationDetails, UpdateOrganizationConfig} from '@asgardeo/node'; | ||
| import AsgardeoNextClient from '../../AsgardeoNextClient'; | ||
|
|
||
| /** | ||
| * Server action to update an organization with a set of patch operations. | ||
| * | ||
| * The access token stays on the server: it is read from the session cookie and attached to the request, | ||
| * which is why the browser cannot call the Organizations API directly. | ||
| * | ||
| * @param organizationId - The ID of the organization to update. | ||
| * @param operations - The patch operations to apply (see `createPatchOperations`). | ||
| * @param sessionId - Optional session ID; resolved from the session cookie when omitted. | ||
| */ | ||
| const updateOrganizationAction = async ( | ||
| organizationId: string, | ||
| operations: UpdateOrganizationConfig['operations'], | ||
| sessionId?: string, | ||
| ): Promise<{ | ||
| data: {organization?: OrganizationDetails}; | ||
| error: string | null; | ||
| success: boolean; | ||
| }> => { | ||
| try { | ||
| const client: AsgardeoNextClient = AsgardeoNextClient.getInstance(); | ||
| const organization: OrganizationDetails = await client.updateOrganization(organizationId, operations, sessionId); | ||
|
|
||
| return {data: {organization}, error: null, success: true}; | ||
| } catch (error) { | ||
| return { | ||
| data: {}, | ||
| error: error instanceof Error ? error.message : String(error), | ||
| success: false, | ||
| }; | ||
| } | ||
| }; | ||
|
|
||
| export default updateOrganizationAction; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: asgardeo/javascript
Length of output: 29033
Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information
Reachability: Internal · Exploitability: Difficult
Require HTTPS for
baseUrlbefore sending the bearer token.updateOrganizationvalidates only URL syntax. Anhttp:baseUrlcan therefore receive the session bearer token without transport encryption. Reject non-HTTPS URLs before this request.🤖 Prompt for AI Agents