diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md
index 1f89ac30..875b13fc 100644
--- a/docs/reference/providers/provider-entraID.md
+++ b/docs/reference/providers/provider-entraID.md
@@ -21,6 +21,7 @@ sidebar_label: Entra ID
- Create, read, update, disable, enable, and delete (opt-in) user accounts in Microsoft Entra ID
- Set and update user attributes (givenName, surname, department, jobTitle, etc.)
- List group memberships and manage group entitlements (grant/revoke)
+ - Revoke active sign-in sessions (refresh tokens) for user accounts
- Resolve identities by objectId (GUID), UserPrincipalName (UPN), or mail address
- **Out of scope / non-goals:**
- Establishing authentication or obtaining Graph access tokens (handled by host-provided broker)
@@ -49,6 +50,7 @@ sidebar_label: Entra ID
- `IdLE.Identity.Attribute.Ensure` - Set/update identity attributes
- `IdLE.Identity.Disable` - Disable user accounts
- `IdLE.Identity.Enable` - Enable user accounts
+ - `IdLE.Identity.RevokeSessions` - Revoke active sign-in sessions
- `IdLE.Entitlement.List` - List group memberships
- `IdLE.Entitlement.Grant` - Add group membership
- `IdLE.Entitlement.Revoke` - Remove group membership
@@ -305,25 +307,25 @@ This provider has **no provider-specific option bag**. All configuration is done
## Required Microsoft Graph Permissions
-### Delegated Permissions (User Context)
+This section lists the Microsoft Graph API permissions required for each step type supported by this provider. The same permissions apply for both delegated (user context) and application (app-only) permissions.
-Minimum required:
+### Permissions by Step Type
-- `User.Read.All` (read user information)
-- `User.ReadWrite.All` (create/update/delete users)
-- `Group.Read.All` (list group memberships)
-- `GroupMember.ReadWrite.All` (add/remove group members)
+| Step Type | Required Permissions | Notes |
+|-----------|---------------------|-------|
+| `IdLE.Step.CreateIdentity` | `User.ReadWrite.All` | Requires write permissions to create users |
+| `IdLE.Step.DisableIdentity` | `User.ReadWrite.All` | Modifies `accountEnabled` property |
+| `IdLE.Step.EnableIdentity` | `User.ReadWrite.All` | Modifies `accountEnabled` property |
+| `IdLE.Step.EnsureAttribute` | `User.ReadWrite.All` | Modifies user properties (displayName, department, etc.) |
+| `IdLE.Step.DeleteIdentity` | `User.ReadWrite.All` | Requires `AllowDelete = $true` on provider |
+| `IdLE.Step.RevokeIdentitySessions` | `User.RevokeSessions.All` | Security-sensitive; invalidates all active sessions |
+| `IdLE.Step.EnsureEntitlement` | `Group.Read.All`
`GroupMember.ReadWrite.All` | Lists and modifies group memberships |
-### Application Permissions (App-Only Context)
-
-Minimum required (same as delegated):
-
-- `User.Read.All`
-- `User.ReadWrite.All`
-- `Group.Read.All`
-- `GroupMember.ReadWrite.All`
-
-**Note**: Application permissions require admin consent in the tenant.
+**Notes:**
+- Application permissions require admin consent in the tenant
+- `User.Read.All` is included in `User.ReadWrite.All` for identity resolution
+- Grant only the permissions you need based on the workflow steps you will use
+- `User.RevokeSessions.All` is security-sensitive; ensure appropriate approval processes are in place before granting
---
@@ -426,6 +428,46 @@ All operations are idempotent:
| Grant membership | If already a member, returns `Changed=$false` |
| Revoke membership | If not a member, returns `Changed=$false` |
| Set attribute | If already at desired value, returns `Changed=$false` |
+| Revoke sessions | Returns `Changed` based on Graph API response (true if sessions existed, false if none to revoke) |
+
+### Session Revocation Behavior
+
+The `RevokeSessions` operation invalidates all active sign-in sessions and refresh tokens for a user account. This is typically used in Leaver workflows after disabling an account to ensure immediate sign-out.
+
+**Important characteristics:**
+
+- **Immediate effect**: Sign-in sessions are invalidated, forcing re-authentication on the next request
+- **Propagation delay**: Due to token caching and Conditional Access Evaluation (CAE), there may be a short delay (typically a few minutes) before all sessions are terminated
+- **Changed flag**: The operation passes through the Graph API response: `Changed=$true` if active sessions were revoked, `Changed=$false` if there were no active sessions to revoke
+- **Idempotency**: Safe to call multiple times; if no active sessions exist, returns `Changed=$false`
+- **No account state change**: This operation does NOT disable the account; use `DisableIdentity` separately if account disabling is also required
+
+**Workflow pattern for Leaver scenarios:**
+
+```powershell
+Steps = @(
+ @{
+ Name = 'DisableAccount'
+ Type = 'IdLE.Step.DisableIdentity'
+ With = @{
+ Provider = 'Identity'
+ AuthSessionName = 'MicrosoftGraph'
+ IdentityKey = '{{Request.Input.UserObjectId}}'
+ }
+ }
+ @{
+ Name = 'RevokeActiveSessions'
+ Type = 'IdLE.Step.RevokeIdentitySessions'
+ With = @{
+ Provider = 'Identity'
+ AuthSessionName = 'MicrosoftGraph'
+ IdentityKey = '{{Request.Input.UserObjectId}}'
+ }
+ }
+)
+```
+
+**Note**: The `DisableIdentity` step does NOT automatically revoke sessions. Session revocation must be explicitly requested via the `RevokeIdentitySessions` step.
### Error mapping and retry behavior
@@ -570,6 +612,7 @@ The provider works with these built-in IdLE steps:
- `IdLE.Step.EnsureAttribute`
- `IdLE.Step.DisableIdentity`
- `IdLE.Step.EnableIdentity`
+- `IdLE.Step.RevokeIdentitySessions` (revokes active sign-in sessions)
- `IdLE.Step.DeleteIdentity` (when `AllowDelete = $true`)
- `IdLE.Step.EnsureEntitlement`
diff --git a/docs/reference/steps.md b/docs/reference/steps.md
index a89fea83..f9e461c1 100644
--- a/docs/reference/steps.md
+++ b/docs/reference/steps.md
@@ -16,4 +16,5 @@
| [IdLE.Step.Mailbox.EnsureType](steps/step-mailbox-ensure-type.md) | ``IdLE.Steps.Mailbox`` | Ensures that a mailbox is of the desired type (User, Shared, Room, Equipment). |
| [IdLE.Step.Mailbox.GetInfo](steps/step-mailbox-get-info.md) | ``IdLE.Steps.Mailbox`` | Retrieves mailbox details and returns a structured report. |
| [IdLE.Step.MoveIdentity](steps/step-move-identity.md) | ``IdLE.Steps.Common`` | Moves an identity to a different container/OU in the target system. |
+| [IdLE.Step.RevokeIdentitySessions](steps/step-revoke-identity-sessions.md) | ``IdLE.Steps.Common`` | Revokes all active sign-in sessions for an identity in the target system. |
| [IdLE.Step.TriggerDirectorySync](steps/step-trigger-directory-sync.md) | ``IdLE.Steps.DirectorySync`` | Triggers a directory sync cycle and optionally waits for completion. |
diff --git a/docs/reference/steps/step-revoke-identity-sessions.md b/docs/reference/steps/step-revoke-identity-sessions.md
new file mode 100644
index 00000000..a0a6f861
--- /dev/null
+++ b/docs/reference/steps/step-revoke-identity-sessions.md
@@ -0,0 +1,64 @@
+# IdLE.Step.RevokeIdentitySessions
+
+> Generated file. Do not edit by hand.
+> Source: tools/Generate-IdleStepReference.ps1
+
+## Summary
+
+- **Step Type**: `IdLE.Step.RevokeIdentitySessions`
+- **Module**: `IdLE.Steps.Common`
+- **Implementation**: `Invoke-IdleStepRevokeIdentitySessions`
+- **Idempotent**: `Unknown`
+
+## Synopsis
+
+Revokes all active sign-in sessions for an identity in the target system.
+
+## Description
+
+This is a provider-agnostic step that revokes active sign-in sessions (refresh tokens)
+for a given identity. The host must supply a provider instance via
+Context.Providers[<ProviderAlias>] that implements RevokeSessions(identityKey)
+and returns an object with properties 'IdentityKey' and 'Changed'.
+
+This step is typically used in Leaver workflows after disabling an identity to ensure
+that existing sessions are terminated immediately, rather than waiting for tokens to expire.
+
+The step does not modify the identity itself (e.g., does not disable the account).
+Use IdLE.Step.DisableIdentity separately if account disabling is also required.
+
+Authentication:
+
+- If With.AuthSessionName is present, the step acquires an auth session via
+ Context.AcquireAuthSession(Name, Options) and passes it to the provider method
+ if the provider supports an AuthSession parameter.
+
+- With.AuthSessionOptions (optional, hashtable) is passed to the broker for
+ session selection (e.g., @\{ Role = 'Tier0' \}).
+
+- ScriptBlocks in AuthSessionOptions are rejected (security boundary).
+
+## Inputs (With.*)
+
+The following keys are required in the step's ``With`` configuration:
+
+| Key | Required | Description |
+| --- | --- | --- |
+| `IdentityKey` | Yes | Unique identifier for the identity |
+
+## Example
+
+```powershell
+@{
+ Name = 'IdLE.Step.RevokeIdentitySessions Example'
+ Type = 'IdLE.Step.RevokeIdentitySessions'
+ With = @{
+ IdentityKey = 'user.name'
+ }
+}
+```
+
+## See Also
+
+- [Capabilities Reference](../capabilities.md) - Overview of IdLE capabilities
+- [Providers](../providers.md) - Available provider implementations
diff --git a/examples/workflows/templates/entraid-leaver-offboarding.psd1 b/examples/workflows/templates/entraid-leaver-offboarding.psd1
index dcfc9bf0..86a633d2 100644
--- a/examples/workflows/templates/entraid-leaver-offboarding.psd1
+++ b/examples/workflows/templates/entraid-leaver-offboarding.psd1
@@ -1,7 +1,7 @@
@{
Name = 'EntraID Leaver - Offboarding with Optional Delete'
LifecycleEvent = 'Leaver'
- Description = 'Disables user account and optionally deletes (requires AllowDelete provider flag).'
+ Description = 'Disables user account, revokes active sessions, and optionally deletes (requires AllowDelete provider flag).'
Steps = @(
@{
Name = 'RevokeAllGroupMemberships'
@@ -44,6 +44,15 @@
IdentityKey = '{{Request.Input.UserObjectId}}'
}
}
+ @{
+ Name = 'RevokeActiveSessions'
+ Type = 'IdLE.Step.RevokeIdentitySessions'
+ With = @{
+ AuthSessionName = 'MicrosoftGraph'
+ AuthSessionOptions = @{ Role = 'Admin' }
+ IdentityKey = '{{Request.Input.UserObjectId}}'
+ }
+ }
@{
Name = 'DeleteAccountAfterRetention'
Type = 'IdLE.Step.DeleteIdentity'
diff --git a/src/IdLE.Core/Private/Get-IdleStepRegistry.ps1 b/src/IdLE.Core/Private/Get-IdleStepRegistry.ps1
index 83bcc64d..2a4cc497 100644
--- a/src/IdLE.Core/Private/Get-IdleStepRegistry.ps1
+++ b/src/IdLE.Core/Private/Get-IdleStepRegistry.ps1
@@ -163,6 +163,13 @@ function Get-IdleStepRegistry {
}
}
+ if (-not $registry.ContainsKey('IdLE.Step.RevokeIdentitySessions')) {
+ $handler = Resolve-IdleStepHandlerName -CommandName 'Invoke-IdleStepRevokeIdentitySessions' -ModuleName 'IdLE.Steps.Common'
+ if (-not [string]::IsNullOrWhiteSpace($handler)) {
+ $registry['IdLE.Step.RevokeIdentitySessions'] = $handler
+ }
+ }
+
if (-not $registry.ContainsKey('IdLE.Step.TriggerDirectorySync')) {
$handler = Resolve-IdleStepHandlerName -CommandName 'Invoke-IdleStepTriggerDirectorySync' -ModuleName 'IdLE.Steps.DirectorySync'
if (-not [string]::IsNullOrWhiteSpace($handler)) {
diff --git a/src/IdLE.Provider.EntraID/Private/New-IdleEntraIDAdapter.ps1 b/src/IdLE.Provider.EntraID/Private/New-IdleEntraIDAdapter.ps1
index 0dbb4fee..61b615ad 100644
--- a/src/IdLE.Provider.EntraID/Private/New-IdleEntraIDAdapter.ps1
+++ b/src/IdLE.Provider.EntraID/Private/New-IdleEntraIDAdapter.ps1
@@ -429,5 +429,24 @@ function New-IdleEntraIDAdapter {
}
} -Force
+ $adapter | Add-Member -MemberType ScriptMethod -Name RevokeSignInSessions -Value {
+ param(
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [string] $ObjectId,
+
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [string] $AccessToken
+ )
+
+ $uri = "$($this.BaseUri)/users/$ObjectId/revokeSignInSessions"
+
+ $response = $this.InvokeGraphRequest('POST', $uri, $AccessToken, $null)
+ # Graph returns { "@odata.context": "...", "value": true/false }
+ # The value indicates whether sessions were revoked
+ return $response
+ } -Force
+
return $adapter
}
diff --git a/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1 b/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1
index 2b89d284..d801b5fc 100644
--- a/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1
+++ b/src/IdLE.Provider.EntraID/Public/New-IdleEntraIDIdentityProvider.ps1
@@ -304,6 +304,7 @@ function New-IdleEntraIDIdentityProvider {
'IdLE.Identity.Attribute.Ensure'
'IdLE.Identity.Disable'
'IdLE.Identity.Enable'
+ 'IdLE.Identity.RevokeSessions'
'IdLE.Entitlement.List'
'IdLE.Entitlement.Grant'
'IdLE.Entitlement.Revoke'
@@ -699,6 +700,49 @@ function New-IdleEntraIDIdentityProvider {
}
} -Force
+ $provider | Add-Member -MemberType ScriptMethod -Name RevokeSessions -Value {
+ param(
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [string] $IdentityKey,
+
+ [Parameter()]
+ [AllowNull()]
+ [object] $AuthSession
+ )
+
+ $accessToken = $this.ExtractAccessToken($AuthSession)
+ $user = $this.ResolveIdentity($IdentityKey, $AuthSession)
+
+ # Get id from user object
+ $userId = if ($user -is [System.Collections.IDictionary]) {
+ $user['id']
+ }
+ else {
+ $user.id
+ }
+
+ # Call the adapter to revoke sign-in sessions
+ $response = $this.Adapter.RevokeSignInSessions($userId, $accessToken)
+
+ # Graph returns a response with a 'value' property indicating whether sessions were revoked
+ # value=true means active sessions existed and were revoked
+ # value=false means there were no active sessions to revoke
+ $changed = $true
+
+ # Pass through the actual Graph API response for accurate Changed status
+ if ($null -ne $response -and ($response.PSObject.Properties.Name -contains 'value')) {
+ $changed = [bool]$response.value
+ }
+
+ return [pscustomobject]@{
+ PSTypeName = 'IdLE.ProviderResult'
+ Operation = 'RevokeSessions'
+ IdentityKey = $IdentityKey
+ Changed = $changed
+ }
+ } -Force
+
$provider | Add-Member -MemberType ScriptMethod -Name ListEntitlements -Value {
param(
[Parameter(Mandatory)]
diff --git a/src/IdLE.Steps.Common/IdLE.Steps.Common.psd1 b/src/IdLE.Steps.Common/IdLE.Steps.Common.psd1
index b4e270f3..88389488 100644
--- a/src/IdLE.Steps.Common/IdLE.Steps.Common.psd1
+++ b/src/IdLE.Steps.Common/IdLE.Steps.Common.psd1
@@ -19,7 +19,8 @@
'Invoke-IdleStepDisableIdentity',
'Invoke-IdleStepEnableIdentity',
'Invoke-IdleStepMoveIdentity',
- 'Invoke-IdleStepDeleteIdentity'
+ 'Invoke-IdleStepDeleteIdentity',
+ 'Invoke-IdleStepRevokeIdentitySessions'
)
PrivateData = @{
diff --git a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1
index d1d85993..8e5c5f9b 100644
--- a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1
+++ b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1
@@ -53,5 +53,6 @@ Export-ModuleMember -Function @(
'Invoke-IdleStepDisableIdentity',
'Invoke-IdleStepEnableIdentity',
'Invoke-IdleStepMoveIdentity',
- 'Invoke-IdleStepDeleteIdentity'
+ 'Invoke-IdleStepDeleteIdentity',
+ 'Invoke-IdleStepRevokeIdentitySessions'
)
diff --git a/src/IdLE.Steps.Common/Public/Get-IdleStepMetadataCatalog.ps1 b/src/IdLE.Steps.Common/Public/Get-IdleStepMetadataCatalog.ps1
index 42f2818a..cc5a95b7 100644
--- a/src/IdLE.Steps.Common/Public/Get-IdleStepMetadataCatalog.ps1
+++ b/src/IdLE.Steps.Common/Public/Get-IdleStepMetadataCatalog.ps1
@@ -63,5 +63,10 @@ function Get-IdleStepMetadataCatalog {
RequiredCapabilities = @('IdLE.Entitlement.List', 'IdLE.Entitlement.Grant', 'IdLE.Entitlement.Revoke')
}
+ # IdLE.Step.RevokeIdentitySessions - requires identity session revocation capability
+ $catalog['IdLE.Step.RevokeIdentitySessions'] = @{
+ RequiredCapabilities = @('IdLE.Identity.RevokeSessions')
+ }
+
return $catalog
}
diff --git a/src/IdLE.Steps.Common/Public/Invoke-IdleStepRevokeIdentitySessions.ps1 b/src/IdLE.Steps.Common/Public/Invoke-IdleStepRevokeIdentitySessions.ps1
new file mode 100644
index 00000000..0f27ceea
--- /dev/null
+++ b/src/IdLE.Steps.Common/Public/Invoke-IdleStepRevokeIdentitySessions.ps1
@@ -0,0 +1,115 @@
+function Invoke-IdleStepRevokeIdentitySessions {
+ <#
+ .SYNOPSIS
+ Revokes all active sign-in sessions for an identity in the target system.
+
+ .DESCRIPTION
+ This is a provider-agnostic step that revokes active sign-in sessions (refresh tokens)
+ for a given identity. The host must supply a provider instance via
+ Context.Providers[] that implements RevokeSessions(identityKey)
+ and returns an object with properties 'IdentityKey' and 'Changed'.
+
+ This step is typically used in Leaver workflows after disabling an identity to ensure
+ that existing sessions are terminated immediately, rather than waiting for tokens to expire.
+
+ The step does not modify the identity itself (e.g., does not disable the account).
+ Use IdLE.Step.DisableIdentity separately if account disabling is also required.
+
+ Authentication:
+ - If With.AuthSessionName is present, the step acquires an auth session via
+ Context.AcquireAuthSession(Name, Options) and passes it to the provider method
+ if the provider supports an AuthSession parameter.
+ - With.AuthSessionOptions (optional, hashtable) is passed to the broker for
+ session selection (e.g., @{ Role = 'Tier0' }).
+ - ScriptBlocks in AuthSessionOptions are rejected (security boundary).
+
+ .PARAMETER Context
+ Execution context created by IdLE.Core.
+
+ .PARAMETER Step
+ Normalized step object from the plan. Must contain a 'With' hashtable with keys:
+ - IdentityKey (required): the identity identifier
+ - Provider (optional): provider alias, defaults to 'Identity'
+ - AuthSessionName (optional): name for auth session acquisition
+ - AuthSessionOptions (optional): routing options for auth session broker
+
+ .OUTPUTS
+ PSCustomObject (PSTypeName: IdLE.StepResult)
+
+ .EXAMPLE
+ # In a workflow definition (PSD1):
+ @{
+ Name = 'Revoke Entra sessions'
+ Type = 'IdLE.Step.RevokeIdentitySessions'
+ With = @{
+ Provider = 'Entra'
+ IdentityKey = 'max.power@contoso.com'
+ AuthSessionName = 'MicrosoftGraph'
+ AuthSessionOptions = @{ Role = 'Admin' }
+ }
+ }
+
+ .NOTES
+ Requires provider capability: IdLE.Identity.RevokeSessions
+
+ For Entra ID provider, this calls Microsoft Graph API:
+ POST /users/{id}/revokeSignInSessions
+
+ Required Graph permissions: User.RevokeSessions.All
+
+ Note: Session revocation may not be instantaneous; a small propagation delay may occur
+ depending on the identity provider and token lifetime policies.
+ #>
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [ValidateNotNull()]
+ [object] $Context,
+
+ [Parameter(Mandatory)]
+ [ValidateNotNull()]
+ [object] $Step
+ )
+
+ $with = $Step.With
+ if ($null -eq $with -or -not ($with -is [hashtable])) {
+ throw "RevokeIdentitySessions requires 'With' to be a hashtable."
+ }
+
+ if (-not $with.ContainsKey('IdentityKey')) {
+ throw "RevokeIdentitySessions requires With.IdentityKey."
+ }
+
+ $providerAlias = if ($with.ContainsKey('Provider')) { [string]$with.Provider } else { 'Identity' }
+
+ if (-not ($Context.PSObject.Properties.Name -contains 'Providers')) {
+ throw "Context does not contain a Providers hashtable."
+ }
+ if ($null -eq $Context.Providers -or -not ($Context.Providers -is [hashtable])) {
+ throw "Context.Providers must be a hashtable."
+ }
+ if (-not $Context.Providers.ContainsKey($providerAlias)) {
+ throw "Provider '$providerAlias' was not supplied by the host."
+ }
+
+ $result = Invoke-IdleProviderMethod `
+ -Context $Context `
+ -With $with `
+ -ProviderAlias $providerAlias `
+ -MethodName 'RevokeSessions' `
+ -MethodArguments @([string]$with.IdentityKey)
+
+ $changed = $false
+ if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) {
+ $changed = [bool]$result.Changed
+ }
+
+ return [pscustomobject]@{
+ PSTypeName = 'IdLE.StepResult'
+ Name = [string]$Step.Name
+ Type = [string]$Step.Type
+ Status = 'Completed'
+ Changed = $changed
+ Error = $null
+ }
+}
diff --git a/tests/Providers/EntraIDIdentityProvider.Tests.ps1 b/tests/Providers/EntraIDIdentityProvider.Tests.ps1
index 8296df97..f2896391 100644
--- a/tests/Providers/EntraIDIdentityProvider.Tests.ps1
+++ b/tests/Providers/EntraIDIdentityProvider.Tests.ps1
@@ -230,6 +230,7 @@ Describe 'EntraID identity provider - Capabilities' {
$caps | Should -Contain 'IdLE.Identity.Attribute.Ensure'
$caps | Should -Contain 'IdLE.Identity.Disable'
$caps | Should -Contain 'IdLE.Identity.Enable'
+ $caps | Should -Contain 'IdLE.Identity.RevokeSessions'
$caps | Should -Contain 'IdLE.Entitlement.List'
$caps | Should -Contain 'IdLE.Entitlement.Grant'
$caps | Should -Contain 'IdLE.Entitlement.Revoke'
@@ -796,3 +797,147 @@ Describe 'EntraID identity provider - Entitlement operations' {
@($afterRevoke | Where-Object { $_.Kind -eq 'Group' -and $_.Id -eq $entitlement.Id }).Count | Should -Be 0
}
}
+
+Describe 'EntraID identity provider - RevokeSessions' {
+ BeforeAll {
+ # Create a fake adapter that tracks revocation calls
+ $fakeAdapter = [pscustomobject]@{
+ PSTypeName = 'IdLE.EntraIDAdapter.Fake'
+ RevocationCallLog = @()
+ RevocationResponses = @{}
+ }
+
+ $fakeAdapter | Add-Member -MemberType ScriptMethod -Name GetUserById -Value {
+ param($ObjectId, $AccessToken)
+ return @{
+ id = $ObjectId
+ userPrincipalName = "$ObjectId@test.local"
+ accountEnabled = $true
+ }
+ }
+
+ $fakeAdapter | Add-Member -MemberType ScriptMethod -Name GetUserByUpn -Value {
+ param($Upn, $AccessToken)
+ return @{
+ id = 'test-user-id'
+ userPrincipalName = $Upn
+ accountEnabled = $true
+ }
+ }
+
+ $fakeAdapter | Add-Member -MemberType ScriptMethod -Name GetUserByMail -Value {
+ param($Mail, $AccessToken)
+ return @{
+ id = 'test-user-id'
+ mail = $Mail
+ userPrincipalName = "$Mail"
+ accountEnabled = $true
+ }
+ }
+
+ $fakeAdapter | Add-Member -MemberType ScriptMethod -Name RevokeSignInSessions -Value {
+ param($ObjectId, $AccessToken)
+ $this.RevocationCallLog += @{
+ ObjectId = $ObjectId
+ AccessToken = $AccessToken
+ Timestamp = [datetime]::UtcNow
+ }
+ # Return a response that simulates Graph API behavior
+ if ($this.RevocationResponses.ContainsKey($ObjectId)) {
+ return $this.RevocationResponses[$ObjectId]
+ }
+ return [pscustomobject]@{
+ '@odata.context' = 'https://graph.microsoft.com/v1.0/$metadata#Edm.Boolean'
+ value = $true
+ }
+ }
+
+ $script:RevokeAdapter = $fakeAdapter
+ $script:RevokeProvider = New-IdleEntraIDIdentityProvider -Adapter $script:RevokeAdapter
+ }
+
+ It 'Advertises IdLE.Identity.RevokeSessions capability' {
+ $caps = $script:RevokeProvider.GetCapabilities()
+ $caps | Should -Contain 'IdLE.Identity.RevokeSessions'
+ }
+
+ It 'Exposes RevokeSessions method' {
+ $script:RevokeProvider.PSObject.Methods.Name | Should -Contain 'RevokeSessions'
+ }
+
+ It 'RevokeSessions calls adapter with correct user ID' {
+ $userId = [guid]::NewGuid().ToString()
+ $script:RevokeAdapter.RevocationCallLog = @()
+
+ $result = $script:RevokeProvider.RevokeSessions($userId, 'fake-token')
+
+ $script:RevokeAdapter.RevocationCallLog.Count | Should -Be 1
+ $script:RevokeAdapter.RevocationCallLog[0].ObjectId | Should -Be $userId
+ }
+
+ It 'RevokeSessions returns ProviderResult with correct shape' {
+ $userId = [guid]::NewGuid().ToString()
+
+ $result = $script:RevokeProvider.RevokeSessions($userId, 'fake-token')
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.PSObject.TypeNames[0] | Should -Be 'IdLE.ProviderResult'
+ $result.Operation | Should -Be 'RevokeSessions'
+ $result.IdentityKey | Should -Be $userId
+ $result.PSObject.Properties.Name | Should -Contain 'Changed'
+ }
+
+ It 'RevokeSessions reports Changed=true when Graph returns value=true' {
+ $userId = [guid]::NewGuid().ToString()
+ $script:RevokeAdapter.RevocationResponses[$userId] = [pscustomobject]@{
+ value = $true
+ }
+
+ $result = $script:RevokeProvider.RevokeSessions($userId, 'fake-token')
+
+ $result.Changed | Should -Be $true
+ }
+
+ It 'RevokeSessions reports Changed=false when Graph returns value=false' {
+ $userId = [guid]::NewGuid().ToString()
+ $script:RevokeAdapter.RevocationResponses[$userId] = [pscustomobject]@{
+ value = $false
+ }
+
+ $result = $script:RevokeProvider.RevokeSessions($userId, 'fake-token')
+
+ $result.Changed | Should -Be $false
+ }
+
+ It 'RevokeSessions resolves identity by UPN' {
+ $upn = 'test.user@contoso.com'
+ $script:RevokeAdapter.RevocationCallLog = @()
+
+ $result = $script:RevokeProvider.RevokeSessions($upn, 'fake-token')
+
+ $script:RevokeAdapter.RevocationCallLog.Count | Should -Be 1
+ $script:RevokeAdapter.RevocationCallLog[0].ObjectId | Should -Be 'test-user-id'
+ }
+
+ It 'RevokeSessions resolves identity by mail' {
+ $mail = 'test.user@contoso.com'
+ $script:RevokeAdapter.RevocationCallLog = @()
+
+ $result = $script:RevokeProvider.RevokeSessions($mail, 'fake-token')
+
+ $script:RevokeAdapter.RevocationCallLog.Count | Should -Be 1
+ $script:RevokeAdapter.RevocationCallLog[0].ObjectId | Should -Be 'test-user-id'
+ }
+
+ It 'RevokeSessions accepts AuthSession object' {
+ $userId = [guid]::NewGuid().ToString()
+ $authSession = [pscustomobject]@{
+ AccessToken = 'session-token'
+ }
+
+ $result = $script:RevokeProvider.RevokeSessions($userId, $authSession)
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.Operation | Should -Be 'RevokeSessions'
+ }
+}
diff --git a/tests/Steps/Invoke-IdleStepRevokeIdentitySessions.Tests.ps1 b/tests/Steps/Invoke-IdleStepRevokeIdentitySessions.Tests.ps1
new file mode 100644
index 00000000..76dce941
--- /dev/null
+++ b/tests/Steps/Invoke-IdleStepRevokeIdentitySessions.Tests.ps1
@@ -0,0 +1,179 @@
+Set-StrictMode -Version Latest
+
+BeforeAll {
+ . (Join-Path (Split-Path -Path $PSScriptRoot -Parent) '_testHelpers.ps1')
+ Import-IdleTestModule
+}
+
+Describe 'Invoke-IdleStepRevokeIdentitySessions (built-in step)' {
+ BeforeEach {
+ # Create a fake provider with RevokeSessions support
+ $script:FakeProvider = [pscustomobject]@{
+ PSTypeName = 'IdLE.Provider.FakeWithRevoke'
+ CallLog = @()
+ }
+
+ $script:FakeProvider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value {
+ return @('IdLE.Identity.RevokeSessions')
+ }
+
+ $script:FakeProvider | Add-Member -MemberType ScriptMethod -Name RevokeSessions -Value {
+ param(
+ [Parameter(Mandatory)]
+ [string] $IdentityKey,
+
+ [Parameter()]
+ [object] $AuthSession
+ )
+
+ $this.CallLog += @{
+ Method = 'RevokeSessions'
+ IdentityKey = $IdentityKey
+ AuthSession = $AuthSession
+ }
+
+ return [pscustomobject]@{
+ PSTypeName = 'IdLE.ProviderResult'
+ Operation = 'RevokeSessions'
+ IdentityKey = $IdentityKey
+ Changed = $true
+ }
+ }
+
+ $script:Context = [pscustomobject]@{
+ PSTypeName = 'IdLE.ExecutionContext'
+ Plan = $null
+ Providers = @{ Identity = $script:FakeProvider }
+ EventSink = [pscustomobject]@{ WriteEvent = { param($Type, $Message, $StepName, $Data) } }
+ }
+
+ $script:Context | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value {
+ param($Name, $Options)
+ return [pscustomobject]@{
+ SessionName = $Name
+ Options = $Options
+ Token = 'fake-auth-token'
+ }
+ }
+
+ $script:StepTemplate = [pscustomobject]@{
+ Name = 'Revoke sessions'
+ Type = 'IdLE.Step.RevokeIdentitySessions'
+ With = @{
+ IdentityKey = 'user@contoso.com'
+ Provider = 'Identity'
+ }
+ }
+ }
+
+ It 'calls provider RevokeSessions method with correct identity key' {
+ $step = $script:StepTemplate
+ $handler = 'IdLE.Steps.Common\Invoke-IdleStepRevokeIdentitySessions'
+
+ $result = & $handler -Context $script:Context -Step $step
+
+ $result.Status | Should -Be 'Completed'
+ $result.Changed | Should -Be $true
+ $script:FakeProvider.CallLog.Count | Should -Be 1
+ $script:FakeProvider.CallLog[0].IdentityKey | Should -Be 'user@contoso.com'
+ }
+
+ It 'returns StepResult with correct shape' {
+ $handler = 'IdLE.Steps.Common\Invoke-IdleStepRevokeIdentitySessions'
+ $result = & $handler -Context $script:Context -Step $script:StepTemplate
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.PSObject.TypeNames[0] | Should -Be 'IdLE.StepResult'
+ $result.Name | Should -Be 'Revoke sessions'
+ $result.Type | Should -Be 'IdLE.Step.RevokeIdentitySessions'
+ $result.Status | Should -Be 'Completed'
+ $result.PSObject.Properties.Name | Should -Contain 'Changed'
+ $result.PSObject.Properties.Name | Should -Contain 'Error'
+ $result.Error | Should -BeNullOrEmpty
+ }
+
+ It 'acquires auth session when AuthSessionName is provided' {
+ $step = $script:StepTemplate
+ $step.With.AuthSessionName = 'MicrosoftGraph'
+ $step.With.AuthSessionOptions = @{ Role = 'Admin' }
+
+ $handler = 'IdLE.Steps.Common\Invoke-IdleStepRevokeIdentitySessions'
+ $result = & $handler -Context $script:Context -Step $step
+
+ $result.Status | Should -Be 'Completed'
+ $script:FakeProvider.CallLog.Count | Should -Be 1
+ $script:FakeProvider.CallLog[0].AuthSession | Should -Not -BeNullOrEmpty
+ $script:FakeProvider.CallLog[0].AuthSession.SessionName | Should -Be 'MicrosoftGraph'
+ }
+
+ It 'throws when With.IdentityKey is missing' {
+ $step = $script:StepTemplate
+ $step.With.Remove('IdentityKey')
+
+ $handler = 'IdLE.Steps.Common\Invoke-IdleStepRevokeIdentitySessions'
+ { & $handler -Context $script:Context -Step $step } | Should -Throw '*requires With.IdentityKey*'
+ }
+
+ It 'throws when provider is missing' {
+ $script:Context.Providers.Clear()
+
+ $handler = 'IdLE.Steps.Common\Invoke-IdleStepRevokeIdentitySessions'
+ { & $handler -Context $script:Context -Step $script:StepTemplate } | Should -Throw '*Provider*was not supplied*'
+ }
+
+ It 'throws when provider does not support RevokeSessions method' {
+ # Create a provider without RevokeSessions support
+ $unsupportedProvider = [pscustomobject]@{
+ PSTypeName = 'IdLE.Provider.FakeWithoutRevoke'
+ }
+ $unsupportedProvider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value {
+ return @('IdLE.Identity.Read')
+ }
+
+ $script:Context.Providers['Identity'] = $unsupportedProvider
+
+ $handler = 'IdLE.Steps.Common\Invoke-IdleStepRevokeIdentitySessions'
+ { & $handler -Context $script:Context -Step $script:StepTemplate } | Should -Throw -ErrorId *
+ }
+
+ It 'respects Changed flag from provider result' {
+ # Modify provider to return Changed=false
+ $script:FakeProvider | Add-Member -MemberType ScriptMethod -Name RevokeSessions -Value {
+ param($IdentityKey, $AuthSession)
+ return [pscustomobject]@{
+ PSTypeName = 'IdLE.ProviderResult'
+ Operation = 'RevokeSessions'
+ IdentityKey = $IdentityKey
+ Changed = $false
+ }
+ } -Force
+
+ $handler = 'IdLE.Steps.Common\Invoke-IdleStepRevokeIdentitySessions'
+ $result = & $handler -Context $script:Context -Step $script:StepTemplate
+
+ $result.Changed | Should -Be $false
+ }
+
+ It 'uses default provider alias "Identity" when not specified' {
+ $step = $script:StepTemplate
+ $step.With.Remove('Provider')
+
+ $handler = 'IdLE.Steps.Common\Invoke-IdleStepRevokeIdentitySessions'
+ $result = & $handler -Context $script:Context -Step $step
+
+ $result.Status | Should -Be 'Completed'
+ $script:FakeProvider.CallLog.Count | Should -Be 1
+ }
+
+ It 'supports custom provider alias' {
+ $script:Context.Providers['CustomEntra'] = $script:FakeProvider
+ $step = $script:StepTemplate
+ $step.With.Provider = 'CustomEntra'
+
+ $handler = 'IdLE.Steps.Common\Invoke-IdleStepRevokeIdentitySessions'
+ $result = & $handler -Context $script:Context -Step $step
+
+ $result.Status | Should -Be 'Completed'
+ $script:FakeProvider.CallLog.Count | Should -Be 1
+ }
+}