Skip to content
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

New command: Revoke Sign-in Sessions. Closes #6514 #6544

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,17 @@ const dictionary = [
'property',
'records',
'recycle',
'registration',
'request',
'resolver',
'registration',
'retention',
'revoke',
'role',
'room',
'schema',
'sensitivity',
'service',
'session',
'set',
'setting',
'settings',
Expand Down
105 changes: 105 additions & 0 deletions docs/docs/cmd/entra/user/user-session-revoke.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# 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.
```

<Global />

## Remarks
MartinM85 marked this conversation as resolved.
Show resolved Hide resolved

:::info

The user with at least User Administrator role can revoke sign-in sessions of other users.

:::

:::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 [email protected]
```

Revoke sign-in sessions of a user specified by its UPN without prompting for confirmation

```sh
m365 entra user session revoke --userName [email protected] --force
```

## Response

<Tabs>
<TabItem value="JSON">

```json
{
"value": true
}
```

</TabItem>
<TabItem value="Text">

```text
value: true
```

</TabItem>
<TabItem value="CSV">

```csv
value
1
```

</TabItem>
<TabItem value="Markdown">

```md
# entra user session revoke --userName "[email protected]" --force "true"

Date: 1/5/2025

Property | Value
---------|-------
value | true
```

</TabItem>
</Tabs>
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/m365/entra/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
};
174 changes: 174 additions & 0 deletions src/m365/entra/commands/user/user-session-revoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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';

describe(commands.USER_SESSION_REVOKE, () => {
const userId = 'abcd1234-de71-4623-b4af-96380a352509';
const userName = '[email protected]';

let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
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;
loggerLogSpy = sinon.spy(logger, 'log');
});

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 () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/users('${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(loggerLogSpy.calledOnceWith({ value: true }));
});

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('${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.`)
);
});
});
Loading
Loading