Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/remove-user-password.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': minor
---

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.
33 changes: 33 additions & 0 deletions packages/backend/src/api/__tests__/UserApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,37 @@ describe('UserAPI', () => {
expect(response.publicMetadata).toEqual({ replaced: true });
});
});

describe('removePassword', () => {
it('calls POST /users/{id}/remove_password without a request body by default', async () => {
const postHandler = vi.fn(async ({ request }: { request: Request }) => {
expect(await request.text()).toBe('');
return HttpResponse.json(mockUserResponse);
});

server.use(http.post('https://api.clerk.test/v1/users/user_123/remove_password', validateHeaders(postHandler)));

const response = await apiClient.users.removePassword('user_123');

expect(postHandler).toHaveBeenCalledTimes(1);
expect(response.id).toBe('user_123');
});

it('passes signOutOfOtherSessions in the request body', async () => {
const postHandler = vi.fn(async ({ request }: { request: Request }) => {
expect(await request.json()).toEqual({ sign_out_of_other_sessions: true });
return HttpResponse.json(mockUserResponse);
});

server.use(http.post('https://api.clerk.test/v1/users/user_123/remove_password', validateHeaders(postHandler)));

await apiClient.users.removePassword('user_123', { signOutOfOtherSessions: true });

expect(postHandler).toHaveBeenCalledTimes(1);
});

it('requires a user ID', async () => {
await expect(apiClient.users.removePassword('')).rejects.toThrow('A valid resource ID is required.');
});
});
});
39 changes: 39 additions & 0 deletions packages/backend/src/api/endpoints/UserApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,12 @@ export type VerifyPasswordParams = {
password: string;
};

/** @inline */
export type RemovePasswordParams = {
/** When set to `true`, all of the user's active sessions are revoked after their password is removed. Defaults to `false`. */
signOutOfOtherSessions?: boolean;
};

/** @generateWithEmptyComment */
export type VerifyTOTPParams = {
/** The ID of the user to verify the TOTP for. */
Expand Down Expand Up @@ -697,6 +703,39 @@ export class UserAPI extends AbstractAPI {
});
}

/**
* 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.
*
* By default, existing sessions remain active. Set `signOutOfOtherSessions` to `true` to revoke sessions active when the request is processed.
* @param userId - The ID of the user whose password to remove.
* @param params - Options for the request.
* @returns The updated [`User`](https://clerk.com/docs/reference/backend/types/backend-user).
* @example
* ### Keep existing sessions active
*
* ```ts
* const user = await clerkClient.users.removePassword('user_123');
* ```
*
* @example
* ### Revoke existing sessions
*
* ```ts
* const user = await clerkClient.users.removePassword('user_123', {
* signOutOfOtherSessions: true,
* });
* ```
*/
public async removePassword(userId: string, params: RemovePasswordParams = {}): Promise<User> {
this.requireId(userId);

return this.request<User>({
method: 'POST',
path: joinPaths(basePath, userId, 'remove_password'),
bodyParams: params,
});
}

/** Check that the user's password matches the supplied input. Useful for custom auth flows and re-verification. */
public async verifyPassword(params: VerifyPasswordParams) {
const { userId, password } = params;
Expand Down
Loading