diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 253f96a74b8..363542bca1f 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -90,15 +90,17 @@ const dictionary = [
'property',
'records',
'recycle',
+ 'registration',
'request',
'resolver',
- 'registration',
'retention',
+ 'revoke',
'role',
'room',
'schema',
'sensitivity',
'service',
+ 'session',
'set',
'setting',
'settings',
diff --git a/docs/docs/cmd/entra/user/user-session-revoke.mdx b/docs/docs/cmd/entra/user/user-session-revoke.mdx
new file mode 100644
index 00000000000..8ec9d996489
--- /dev/null
+++ b/docs/docs/cmd/entra/user/user-session-revoke.mdx
@@ -0,0 +1,65 @@
+import Global from '/docs/cmd/_global.mdx';
+
+# entra user session revoke
+
+Revokes all sign-in sessions for a given user
+
+## Usage
+
+```sh
+m365 entra user session revoke [options]
+```
+
+## Options
+```md definition-list
+`-i, --userId [userId]`
+: The id of the user. Specify either `userId` or `userName`, but not both.
+
+`-n, --userName [userName]`
+: The user principal name of the user. Specify either `userId` or `userName`, but not both.
+
+`-f, --force`
+: Don't prompt for confirmation.
+```
+
+
+
+## Remarks
+
+:::info
+
+To use this command you must be either **User Administrator** or **Global Administrator**.
+
+:::
+
+:::note
+
+There might be a small delay of a few minutes before tokens are revoked.
+
+This API doesn't revoke sign-in sessions for external users, because external users sign in through their home tenant.
+
+:::
+
+## Examples
+
+Revoke sign-in sessions of a user specified by id
+
+```sh
+m365 entra user session revoke --userId 4fb72b9b-d0b0-4a35-8bc1-83f9a6488c48
+```
+
+Revoke sign-in sessions of a user specified by its UPN
+
+```sh
+m365 entra user session revoke --userName john.doe@contoso.onmicrosoft.com
+```
+
+Revoke sign-in sessions of a user specified by its UPN without prompting for confirmation
+
+```sh
+m365 entra user session revoke --userName john.doe@contoso.onmicrosoft.com --force
+```
+
+## Response
+
+The command won't return a response on success.
diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts
index 68fa5ec4249..f06c61b45ac 100644
--- a/docs/src/config/sidebars.ts
+++ b/docs/src/config/sidebars.ts
@@ -757,6 +757,11 @@ const sidebars: SidebarsConfig = {
label: 'user registrationdetails list',
id: 'cmd/entra/user/user-registrationdetails-list'
},
+ {
+ type: 'doc',
+ label: 'user session revoke',
+ id: 'cmd/entra/user/user-session-revoke'
+ },
{
type: 'doc',
label: 'user signin list',
diff --git a/src/m365/entra/commands.ts b/src/m365/entra/commands.ts
index dc270f50c70..a5c33dd320e 100644
--- a/src/m365/entra/commands.ts
+++ b/src/m365/entra/commands.ts
@@ -115,6 +115,7 @@ export default {
USER_REGISTRATIONDETAILS_LIST: `${prefix} user registrationdetails list`,
USER_REMOVE: `${prefix} user remove`,
USER_RECYCLEBINITEM_RESTORE: `${prefix} user recyclebinitem restore`,
+ USER_SESSION_REVOKE: `${prefix} user session revoke`,
USER_SET: `${prefix} user set`,
USER_SIGNIN_LIST: `${prefix} user signin list`
};
diff --git a/src/m365/entra/commands/user/user-session-revoke.spec.ts b/src/m365/entra/commands/user/user-session-revoke.spec.ts
new file mode 100644
index 00000000000..73e591d9682
--- /dev/null
+++ b/src/m365/entra/commands/user/user-session-revoke.spec.ts
@@ -0,0 +1,173 @@
+import assert from 'assert';
+import sinon from 'sinon';
+import auth from '../../../../Auth.js';
+import commands from '../../commands.js';
+import request from '../../../../request.js';
+import { Logger } from '../../../../cli/Logger.js';
+import { telemetry } from '../../../../telemetry.js';
+import { pid } from '../../../../utils/pid.js';
+import { session } from '../../../../utils/session.js';
+import command from './user-session-revoke.js';
+import { sinonUtil } from '../../../../utils/sinonUtil.js';
+import { CommandError } from '../../../../Command.js';
+import { z } from 'zod';
+import { CommandInfo } from '../../../../cli/CommandInfo.js';
+import { cli } from '../../../../cli/cli.js';
+import { formatting } from '../../../../utils/formatting.js';
+
+describe(commands.USER_SESSION_REVOKE, () => {
+ const userId = 'abcd1234-de71-4623-b4af-96380a352509';
+ const userName = 'john.doe@contoso.com';
+
+ let log: string[];
+ let logger: Logger;
+ let promptIssued: boolean;
+ let commandInfo: CommandInfo;
+ let commandOptionsSchema: z.ZodTypeAny;
+
+ before(() => {
+ sinon.stub(auth, 'restoreAuth').resolves();
+ sinon.stub(telemetry, 'trackEvent').returns();
+ sinon.stub(pid, 'getProcessName').returns('');
+ sinon.stub(session, 'getId').returns('');
+ auth.connection.active = true;
+ commandInfo = cli.getCommandInfo(command);
+ commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
+ });
+
+ beforeEach(() => {
+ log = [];
+ logger = {
+ log: async (msg: string) => {
+ log.push(msg);
+ },
+ logRaw: async (msg: string) => {
+ log.push(msg);
+ },
+ logToStderr: async (msg: string) => {
+ log.push(msg);
+ }
+ };
+ sinon.stub(cli, 'promptForConfirmation').callsFake(async () => {
+ promptIssued = true;
+ return false;
+ });
+ promptIssued = false;
+ });
+
+ afterEach(() => {
+ sinonUtil.restore([
+ request.post,
+ cli.promptForConfirmation
+ ]);
+ });
+
+ after(() => {
+ sinon.restore();
+ auth.connection.active = false;
+ });
+
+ it('has correct name', () => {
+ assert.strictEqual(command.name, commands.USER_SESSION_REVOKE);
+ });
+
+ it('has a description', () => {
+ assert.notStrictEqual(command.description, null);
+ });
+
+ it('fails validation if userId is not a valid GUID', () => {
+ const actual = commandOptionsSchema.safeParse({
+ userId: 'foo'
+ });
+ assert.notStrictEqual(actual.success, true);
+ });
+
+ it('fails validation if userName is not a valid UPN', () => {
+ const actual = commandOptionsSchema.safeParse({
+ userName: 'foo'
+ });
+ assert.notStrictEqual(actual.success, true);
+ });
+
+ it('fails validation if both userId and userName are provided', () => {
+ const actual = commandOptionsSchema.safeParse({
+ userId: userId,
+ userName: userName
+ });
+ assert.notStrictEqual(actual.success, true);
+ });
+
+ it('fails validation if neither userId nor userName is provided', () => {
+ const actual = commandOptionsSchema.safeParse({});
+ assert.notStrictEqual(actual.success, true);
+ });
+
+ it('prompts before revoking all sign-in sessions when confirm option not passed', async () => {
+ const parsedSchema = commandOptionsSchema.safeParse({ userId: userId });
+ await command.action(logger, { options: parsedSchema.data });
+
+ assert(promptIssued);
+ });
+
+ it('aborts revoking all sign-in sessions when prompt not confirmed', async () => {
+ const postStub = sinon.stub(request, 'post').resolves();
+
+ const parsedSchema = commandOptionsSchema.safeParse({ userId: userId });
+ await command.action(logger, { options: parsedSchema.data });
+ assert(postStub.notCalled);
+ });
+
+ it('revokes all sign-in sessions for a user specified by userId without prompting for confirmation', async () => {
+ const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/users('${formatting.encodeQueryParameter(userId)}')/revokeSignInSessions`) {
+ return {
+ value: true
+ };
+ }
+
+ throw 'Invalid request';
+ });
+
+ const parsedSchema = commandOptionsSchema.safeParse({ userId: userId, force: true, verbose: true });
+ await command.action(logger, { options: parsedSchema.data });
+ assert(postStub.calledOnce);
+ });
+
+ it('revokes all sign-in sessions for a user specified by UPN while prompting for confirmation', async () => {
+ const postRequestStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/users('${formatting.encodeQueryParameter(userName)}')/revokeSignInSessions`) {
+ return {
+ value: true
+ };
+ }
+
+ throw 'Invalid request';
+ });
+
+ sinonUtil.restore(cli.promptForConfirmation);
+ sinon.stub(cli, 'promptForConfirmation').resolves(true);
+
+ const parsedSchema = commandOptionsSchema.safeParse({ userName: userName });
+ await command.action(logger, { options: parsedSchema.data });
+ assert(postRequestStub.calledOnce);
+ });
+
+ it('handles error when user specified by userId was not found', async () => {
+ sinon.stub(request, 'post').rejects({
+ error:
+ {
+ code: 'Request_ResourceNotFound',
+ message: `Resource '${userId}' does not exist or one of its queried reference-property objects are not present.`
+ }
+ });
+
+ sinonUtil.restore(cli.promptForConfirmation);
+ sinon.stub(cli, 'promptForConfirmation').resolves(true);
+
+ const parsedSchema = commandOptionsSchema.safeParse({ userId: userId });
+ await assert.rejects(
+ command.action(logger, { options: parsedSchema.data }),
+ new CommandError(`Resource '${userId}' does not exist or one of its queried reference-property objects are not present.`)
+ );
+ });
+});
\ No newline at end of file
diff --git a/src/m365/entra/commands/user/user-session-revoke.ts b/src/m365/entra/commands/user/user-session-revoke.ts
new file mode 100644
index 00000000000..a5f4bdd5cca
--- /dev/null
+++ b/src/m365/entra/commands/user/user-session-revoke.ts
@@ -0,0 +1,84 @@
+import { z } from 'zod';
+import { globalOptionsZod } from '../../../../Command.js';
+import { zod } from '../../../../utils/zod.js';
+import GraphCommand from '../../../base/GraphCommand.js';
+import commands from '../../commands.js';
+import { validation } from '../../../../utils/validation.js';
+import { Logger } from '../../../../cli/Logger.js';
+import request, { CliRequestOptions } from '../../../../request.js';
+import { cli } from '../../../../cli/cli.js';
+import { formatting } from '../../../../utils/formatting.js';
+
+const options = globalOptionsZod
+ .extend({
+ userId: zod.alias('i', z.string().refine(id => validation.isValidGuid(id), id => ({
+ message: `'${id}' is not a valid GUID.`
+ })).optional()),
+ userName: zod.alias('n', z.string().refine(name => validation.isValidUserPrincipalName(name), name => ({
+ message: `'${name}' is not a valid UPN.`
+ })).optional()),
+ force: zod.alias('f', z.boolean().optional())
+ })
+ .strict();
+
+declare type Options = z.infer;
+
+interface CommandArgs {
+ options: Options;
+}
+
+class EntraUserSessionRevokeCommand extends GraphCommand {
+ public get name(): string {
+ return commands.USER_SESSION_REVOKE;
+ }
+ public get description(): string {
+ return 'Revokes all sign-in sessions for a given user';
+ }
+ public get schema(): z.ZodTypeAny | undefined {
+ return options;
+ }
+ public getRefinedSchema(schema: typeof options): z.ZodEffects | undefined {
+ return schema
+ .refine(options => [options.userId, options.userName].filter(o => o !== undefined).length === 1, {
+ message: `Specify either 'userId' or 'userName'.`
+ });
+ }
+ public async commandAction(logger: Logger, args: CommandArgs): Promise {
+ const revokeUserSessions = async (): Promise => {
+ try {
+ const userIdentifier = args.options.userId ?? args.options.userName;
+
+ if (this.verbose) {
+ await logger.logToStderr(`Invalidating all the refresh tokens for user ${userIdentifier}...`);
+ }
+
+ const requestOptions: CliRequestOptions = {
+ url: `${this.resource}/v1.0/users('${formatting.encodeQueryParameter(userIdentifier!)}')/revokeSignInSessions`,
+ headers: {
+ accept: 'application/json;odata.metadata=none'
+ },
+ responseType: 'json',
+ data: {}
+ };
+
+ await request.post(requestOptions);
+ }
+ catch (err: any) {
+ this.handleRejectedODataJsonPromise(err);
+ }
+ };
+
+ if (args.options.force) {
+ await revokeUserSessions();
+ }
+ else {
+ const result = await cli.promptForConfirmation({ message: `This will revoke all sessions for the user '${args.options.userId || args.options.userName}', requiring the user to re-sign in from all devices. Are you sure?` });
+
+ if (result) {
+ await revokeUserSessions();
+ }
+ }
+ }
+}
+
+export default new EntraUserSessionRevokeCommand();
\ No newline at end of file