Skip to content

Commit f38cf02

Browse files
authored
feat(backend): support removing user passwords (#9326)
1 parent 1ef84c3 commit f38cf02

3 files changed

Lines changed: 77 additions & 0 deletions

File tree

‎.changeset/remove-user-password.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/backend': minor
3+
---
4+
5+
Add `clerkClient.users.removePassword(userId, params?)` to remove a user's password through the Backend API. Password removal is allowed even when the user has no alternate sign-in method configured. Existing sessions remain active by default; pass `{ signOutOfOtherSessions: true }` to revoke them.

‎packages/backend/src/api/__tests__/UserApi.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,37 @@ describe('UserAPI', () => {
176176
expect(response.publicMetadata).toEqual({ replaced: true });
177177
});
178178
});
179+
180+
describe('removePassword', () => {
181+
it('calls POST /users/{id}/remove_password without a request body by default', async () => {
182+
const postHandler = vi.fn(async ({ request }: { request: Request }) => {
183+
expect(await request.text()).toBe('');
184+
return HttpResponse.json(mockUserResponse);
185+
});
186+
187+
server.use(http.post('https://api.clerk.test/v1/users/user_123/remove_password', validateHeaders(postHandler)));
188+
189+
const response = await apiClient.users.removePassword('user_123');
190+
191+
expect(postHandler).toHaveBeenCalledTimes(1);
192+
expect(response.id).toBe('user_123');
193+
});
194+
195+
it('passes signOutOfOtherSessions in the request body', async () => {
196+
const postHandler = vi.fn(async ({ request }: { request: Request }) => {
197+
expect(await request.json()).toEqual({ sign_out_of_other_sessions: true });
198+
return HttpResponse.json(mockUserResponse);
199+
});
200+
201+
server.use(http.post('https://api.clerk.test/v1/users/user_123/remove_password', validateHeaders(postHandler)));
202+
203+
await apiClient.users.removePassword('user_123', { signOutOfOtherSessions: true });
204+
205+
expect(postHandler).toHaveBeenCalledTimes(1);
206+
});
207+
208+
it('requires a user ID', async () => {
209+
await expect(apiClient.users.removePassword('')).rejects.toThrow('A valid resource ID is required.');
210+
});
211+
});
179212
});

‎packages/backend/src/api/endpoints/UserApi.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,12 @@ export type VerifyPasswordParams = {
366366
password: string;
367367
};
368368

369+
/** @inline */
370+
export type RemovePasswordParams = {
371+
/** When set to `true`, all of the user's active sessions are revoked after their password is removed. Defaults to `false`. */
372+
signOutOfOtherSessions?: boolean;
373+
};
374+
369375
/** @generateWithEmptyComment */
370376
export type VerifyTOTPParams = {
371377
/** The ID of the user to verify the TOTP for. */
@@ -697,6 +703,39 @@ export class UserAPI extends AbstractAPI {
697703
});
698704
}
699705

706+
/**
707+
* Removes the password credential from the given user. This is a privileged operation and does not require the user's current password. Password removal is allowed even when the user has no other sign-in method configured.
708+
*
709+
* By default, existing sessions remain active. Set `signOutOfOtherSessions` to `true` to revoke sessions active when the request is processed.
710+
* @param userId - The ID of the user whose password to remove.
711+
* @param params - Options for the request.
712+
* @returns The updated [`User`](https://clerk.com/docs/reference/backend/types/backend-user).
713+
* @example
714+
* ### Keep existing sessions active
715+
*
716+
* ```ts
717+
* const user = await clerkClient.users.removePassword('user_123');
718+
* ```
719+
*
720+
* @example
721+
* ### Revoke existing sessions
722+
*
723+
* ```ts
724+
* const user = await clerkClient.users.removePassword('user_123', {
725+
* signOutOfOtherSessions: true,
726+
* });
727+
* ```
728+
*/
729+
public async removePassword(userId: string, params: RemovePasswordParams = {}): Promise<User> {
730+
this.requireId(userId);
731+
732+
return this.request<User>({
733+
method: 'POST',
734+
path: joinPaths(basePath, userId, 'remove_password'),
735+
bodyParams: params,
736+
});
737+
}
738+
700739
/** Check that the user's password matches the supplied input. Useful for custom auth flows and re-verification. */
701740
public async verifyPassword(params: VerifyPasswordParams) {
702741
const { userId, password } = params;

0 commit comments

Comments
 (0)