Skip to main content

Time-Limited Access

Time-limited access provisioning creates accounts with built-in expiry dates for personnel whose engagement has a defined end point. You provision these accounts for volunteers, interns, short-term consultants, secondees, and surge staff deployed during emergency responses. The procedures in this task ensure that temporary personnel receive appropriate access for their role duration while automated expiry prevents orphaned accounts from accumulating.

Prerequisites

RequirementDetail
AccessIdentity provider administrator role with user creation and group management permissions
PermissionsAbility to set account expiry dates and configure automated workflows
ToolsAccess to identity provider admin console (Entra ID, Okta, Keycloak, or equivalent)
InformationEngagement end date, role assignment, manager/supervisor details, sponsoring department
ApprovalSigned access request form with manager authorisation and HR confirmation of engagement dates
Time30 minutes for standard provisioning; 2 hours for complex role assignments

Verify you have the required administrative access before proceeding:

Terminal window
# For Entra ID (Azure AD) - check role assignments
az role assignment list --assignee $(az ad signed-in-user show --query id -o tsv) \
--query "[].roleDefinitionName" -o tsv
# Required: User Administrator or higher
# For Keycloak - verify realm admin access
curl -s -X GET "https://auth.example.org/admin/realms/production" \
-H "Authorization: Bearer $KEYCLOAK_TOKEN" | jq '.realm'
# Expected: realm name returned (not 403 Forbidden)

Confirm the engagement details match HR records. Discrepancies between the access request and HR system create audit findings and complicate offboarding.

Procedure

Determine access duration

  1. Identify the personnel category and retrieve the corresponding maximum access duration from your organisation’s policy. Standard durations provide consistency while allowing exceptions through documented approval:

    Personnel categoryStandard durationMaximum extensionApproval for extension
    Volunteer90 days90 days (once)Volunteer coordinator
    Intern180 days90 days (once)HR and supervisor
    Short-term consultantContract end date30 daysProcurement and manager
    SecondeeSecondment agreement endPer agreementHR director
    Surge staff (emergency)90 days90 days (renewable)Emergency coordinator
    Board/committee memberTerm end datePer governance rulesBoard secretary

    The access end date must not exceed the engagement end date recorded in HR systems. Where the engagement end date is uncertain (common for emergency surge deployments), set initial access for 90 days with documented review points.

  2. Calculate the account expiry date by adding the appropriate duration to the start date. For existing personnel whose access you are formalising, use the current date as the start:

    Start date: 2024-11-15
    Personnel category: Intern
    Standard duration: 180 days
    Calculated expiry: 2025-05-14
    Cross-check with HR engagement end: 2025-05-01
    Final expiry date: 2025-05-01 (earlier of calculated and HR date)

    Set expiry time to 23:59 in the user’s primary work timezone to avoid mid-day access termination during their final working day.

Provision the account

  1. Create the user account with the expiry date configured at creation time. Setting expiry during initial provisioning prevents accounts from being created without time limits:

    Entra ID (Azure AD):

    Terminal window
    # Create user with account expiry
    $PasswordProfile = @{
    Password = (New-Guid).ToString() + "!Aa1"
    ForceChangePasswordNextSignIn = $true
    }
    New-MgUser -DisplayName "Jane Smith (Intern)" `
    -UserPrincipalName "jane.smith.intern@example.org" `
    -MailNickname "jane.smith.intern" `
    -AccountEnabled $true `
    -PasswordProfile $PasswordProfile `
    -Department "Programme Team" `
    -JobTitle "Programme Intern" `
    -EmployeeType "Intern" `
    -EmployeeHireDate "2024-11-15" `
    -AdditionalProperties @{
    "extension_accountExpires" = "2025-05-01T23:59:00Z"
    }

    Keycloak:

    Terminal window
    # Create user with custom expiry attribute
    curl -X POST "https://auth.example.org/admin/realms/production/users" \
    -H "Authorization: Bearer $KEYCLOAK_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
    "username": "jane.smith.intern",
    "email": "jane.smith.intern@example.org",
    "firstName": "Jane",
    "lastName": "Smith",
    "enabled": true,
    "attributes": {
    "employeeType": ["Intern"],
    "department": ["Programme Team"],
    "accountExpires": ["2025-05-01T23:59:00Z"],
    "sponsor": ["m.jones@example.org"],
    "engagementId": ["INT-2024-0847"]
    },
    "credentials": [{
    "type": "password",
    "value": "TemporaryP@ss1!",
    "temporary": true
    }]
    }'

    Okta:

    Terminal window
    curl -X POST "https://example.okta.com/api/v1/users?activate=true" \
    -H "Authorization: SSWS $OKTA_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
    "profile": {
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane.smith.intern@example.org",
    "login": "jane.smith.intern@example.org",
    "department": "Programme Team",
    "title": "Programme Intern",
    "employeeType": "Intern",
    "accountExpires": "2025-05-01"
    },
    "credentials": {
    "password": { "value": "TemporaryP@ss1!" }
    }
    }'
  2. Apply the naming convention that identifies temporary accounts. Consistent naming enables bulk queries, access reviews, and audit reporting:

    Personnel categoryUsername formatDisplay name format
    Volunteerfirstname.lastname.volFirstname Lastname (Volunteer)
    Internfirstname.lastname.internFirstname Lastname (Intern)
    Consultantfirstname.lastname.extFirstname Lastname (Consultant)
    Secondeefirstname.lastname.secFirstname Lastname (Secondee - Org)
    Surge stafffirstname.lastname.surgeFirstname Lastname (Surge)

    The suffix in usernames enables filtering. The parenthetical in display names ensures recipients of emails and meeting invitations recognise the sender’s temporary status.

  3. Assign the user to groups that grant role-appropriate access. Temporary personnel receive access through dedicated groups with restricted permissions rather than the same groups used for permanent staff:

    Terminal window
    # Entra ID - Add to temporary personnel groups
    $userId = (Get-MgUser -Filter "userPrincipalName eq 'jane.smith.intern@example.org'").Id
    # Base group for all temporary personnel (limited shared resources)
    Add-MgGroupMember -GroupId "a1b2c3d4-temp-personnel-group" -DirectoryObjectId $userId
    # Role-specific group (programme team read access)
    Add-MgGroupMember -GroupId "e5f6g7h8-programme-readonly" -DirectoryObjectId $userId
    # Time-limited group with dynamic expiry (if using dynamic groups)
    # This group auto-removes members based on accountExpires attribute
    Terminal window
    # Keycloak - Assign groups
    USER_ID=$(curl -s "https://auth.example.org/admin/realms/production/users?username=jane.smith.intern" \
    -H "Authorization: Bearer $KEYCLOAK_TOKEN" | jq -r '.[0].id')
    # Get group ID
    GROUP_ID=$(curl -s "https://auth.example.org/admin/realms/production/groups?search=temp-programme-readonly" \
    -H "Authorization: Bearer $KEYCLOAK_TOKEN" | jq -r '.[0].id')
    # Add user to group
    curl -X PUT "https://auth.example.org/admin/realms/production/users/$USER_ID/groups/$GROUP_ID" \
    -H "Authorization: Bearer $KEYCLOAK_TOKEN"
  4. Configure application-specific access where the identity provider group membership does not automatically provision application access. Some applications require direct provisioning:

    Terminal window
    # Example: Add user to project management system via API
    curl -X POST "https://projects.example.org/api/v1/users" \
    -H "Authorization: Bearer $PROJECT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "email": "jane.smith.intern@example.org",
    "role": "viewer",
    "projects": ["PRJ-2024-NUTRITION"],
    "expiresAt": "2025-05-01T23:59:00Z"
    }'

    Document all application-specific provisioning in the access request record. This documentation is essential for complete offboarding.

Configure automated expiry

  1. Enable the automated account disablement workflow that triggers on the expiry date. This automation runs daily and disables accounts whose expiry date has passed:

    Entra ID with Azure Automation:

    Disable-ExpiredAccounts.ps1
    # Schedule: Daily at 00:30 UTC
    Connect-MgGraph -Identity
    $today = Get-Date -Format "yyyy-MM-ddT00:00:00Z"
    # Find users with expired accountExpires attribute
    $expiredUsers = Get-MgUser -Filter "accountEnabled eq true" -All | Where-Object {
    $expires = $_.AdditionalProperties["extension_accountExpires"]
    $expires -and ([datetime]$expires -lt [datetime]$today)
    }
    foreach ($user in $expiredUsers) {
    # Disable the account
    Update-MgUser -UserId $user.Id -AccountEnabled $false
    # Log the action
    Write-Output "Disabled expired account: $($user.UserPrincipalName) - Expired: $($user.AdditionalProperties['extension_accountExpires'])"
    # Send notification to sponsor/manager
    $sponsor = $user.Manager.Id
    if ($sponsor) {
    # Trigger notification Logic App or send email
    }
    }

    Keycloak with scheduled task:

    /etc/cron.daily/disable-expired-keycloak-users
    #!/bin/bash
    KEYCLOAK_URL="https://auth.example.org"
    REALM="production"
    # Authenticate
    TOKEN=$(curl -s -X POST "$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" \
    -d "grant_type=client_credentials" \
    -d "client_id=admin-cli" \
    -d "client_secret=$KEYCLOAK_CLIENT_SECRET" | jq -r '.access_token')
    TODAY=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    # Get all enabled users with accountExpires attribute
    USERS=$(curl -s "$KEYCLOAK_URL/admin/realms/$REALM/users?enabled=true&max=1000" \
    -H "Authorization: Bearer $TOKEN")
    echo "$USERS" | jq -c '.[]' | while read USER; do
    EXPIRES=$(echo "$USER" | jq -r '.attributes.accountExpires[0] // empty')
    if [[ -n "$EXPIRES" && "$EXPIRES" < "$TODAY" ]]; then
    USER_ID=$(echo "$USER" | jq -r '.id')
    USERNAME=$(echo "$USER" | jq -r '.username')
    # Disable user
    curl -s -X PUT "$KEYCLOAK_URL/admin/realms/$REALM/users/$USER_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"enabled": false}'
    echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) Disabled expired user: $USERNAME (expired: $EXPIRES)" >> /var/log/keycloak-expiry.log
    fi
    done
  2. Configure advance warning notifications to alert sponsors, managers, and the user before account expiry. Notifications at 14 days, 7 days, and 1 day prevent surprise access termination:

    Send-ExpiryWarnings.ps1
    # Schedule: Daily at 08:00 local time
    $warningDays = @(14, 7, 1)
    foreach ($days in $warningDays) {
    $targetDate = (Get-Date).AddDays($days).ToString("yyyy-MM-dd")
    $expiringUsers = Get-MgUser -Filter "accountEnabled eq true" -All | Where-Object {
    $expires = $_.AdditionalProperties["extension_accountExpires"]
    $expires -and $expires.StartsWith($targetDate)
    }
    foreach ($user in $expiringUsers) {
    $expiryDate = $user.AdditionalProperties["extension_accountExpires"]
    $sponsor = $user.AdditionalProperties["sponsor"]
    # Send notification (via Logic App, Power Automate, or direct email)
    $notificationBody = @{
    recipient = $user.Mail
    cc = $sponsor
    subject = "Access expiry notice: $days day(s) remaining"
    body = "Your account $($user.UserPrincipalName) will expire on $expiryDate. Contact your supervisor if an extension is required."
    }
    Invoke-RestMethod -Uri $notificationWebhookUrl -Method Post -Body ($notificationBody | ConvertTo-Json)
    }
    }

Process extension requests

  1. Receive and validate extension requests before the account expires. Extensions require documented justification and appropriate approval:

    Extension request validation checklist:
    [ ] Request received before expiry date (not retroactive)
    [ ] Justification provided (project continuation, delayed completion)
    [ ] HR confirms engagement extended or new contract in place
    [ ] Requested duration within policy maximum for personnel category
    [ ] Approver has authority for this personnel category
    [ ] Previous extensions documented (check extension count limit)

    Reject retroactive extension requests. Once an account expires, treat reactivation as a new provisioning request requiring full approval workflow.

  2. Update the account expiry date after receiving documented approval. Record the extension in the account attributes for audit purposes:

    Terminal window
    # Entra ID - Extend account
    $userId = (Get-MgUser -Filter "userPrincipalName eq 'jane.smith.intern@example.org'").Id
    $currentExpiry = (Get-MgUser -UserId $userId).AdditionalProperties["extension_accountExpires"]
    $newExpiry = "2025-08-01T23:59:00Z"
    Update-MgUser -UserId $userId -AdditionalProperties @{
    "extension_accountExpires" = $newExpiry
    "extension_extensionHistory" = "$currentExpiry|Extended to $newExpiry on $(Get-Date -Format 'yyyy-MM-dd') by admin@example.org|REQ-2025-0123"
    }
    Terminal window
    # Keycloak - Extend account
    USER_ID="user-uuid-here"
    curl -X PUT "https://auth.example.org/admin/realms/production/users/$USER_ID" \
    -H "Authorization: Bearer $KEYCLOAK_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
    "attributes": {
    "accountExpires": ["2025-08-01T23:59:00Z"],
    "extensionHistory": ["2025-05-01|Extended to 2025-08-01 on 2025-04-15|REQ-2025-0123"]
    }
    }'

Offboard expired accounts

  1. Verify account disablement occurred as scheduled by checking the automated job logs and confirming the account state:

    Terminal window
    # Verify account is disabled
    $user = Get-MgUser -Filter "userPrincipalName eq 'jane.smith.intern@example.org'" -Property accountEnabled,signInActivity
    Write-Output "Account enabled: $($user.AccountEnabled)"
    Write-Output "Last sign-in: $($user.SignInActivity.LastSignInDateTime)"
    # Expected output for properly expired account:
    # Account enabled: False
    # Last sign-in: 2025-04-30T16:42:00Z (before expiry)
  2. Revoke application-specific access that was provisioned outside the identity provider. Applications with direct provisioning require direct deprovisioning:

    Terminal window
    # Revoke access from project management system
    curl -X DELETE "https://projects.example.org/api/v1/users/jane.smith.intern@example.org" \
    -H "Authorization: Bearer $PROJECT_API_KEY"
    # Revoke access from file sharing platform
    curl -X POST "https://files.example.org/api/users/jane.smith.intern@example.org/deactivate" \
    -H "Authorization: Bearer $FILES_API_KEY"
  3. Transfer or archive data owned by the departing user. Coordinate with the supervisor to identify data requiring retention:

    Terminal window
    # Transfer OneDrive content to manager (Entra ID / Microsoft 365)
    $userId = (Get-MgUser -Filter "userPrincipalName eq 'jane.smith.intern@example.org'").Id
    $managerId = (Get-MgUser -Filter "userPrincipalName eq 'm.jones@example.org'").Id
    # Initiate OneDrive transfer (requires SharePoint admin)
    Start-MgUserOneDriveTransfer -UserId $userId -TargetUserId $managerId
  4. Delete the account after the retention period (typically 30-90 days post-expiry). Deletion removes the account permanently; ensure all data transfer and audit requirements are complete:

    Terminal window
    # Entra ID - Delete user after retention period
    $user = Get-MgUser -Filter "userPrincipalName eq 'jane.smith.intern@example.org'"
    # Verify account has been disabled for required retention period
    $disabledDate = $user.AdditionalProperties["extension_accountExpires"]
    $retentionDays = 30
    $deleteEligible = ([datetime]$disabledDate).AddDays($retentionDays) -lt (Get-Date)
    if ($deleteEligible) {
    Remove-MgUser -UserId $user.Id
    Write-Output "Deleted user: $($user.UserPrincipalName)"
    } else {
    Write-Output "User not yet eligible for deletion. Retention period ends: $(([datetime]$disabledDate).AddDays($retentionDays))"
    }

Time-limited access lifecycle

The following diagram illustrates the complete lifecycle from provisioning through deletion:

+------------------+
| |
| ACCESS REQUEST |
| SUBMITTED |
| |
+--------+---------+
|
| Validate engagement dates
| Determine personnel category
v
+--------+---------+
| |
| CALCULATE |
| EXPIRY DATE |
| |
+--------+---------+
|
| Create account with expiry
| Assign temporary groups
v
+--------+---------+
| |
| ACCOUNT |
| ACTIVE |<------------------------------------------+
| | |
+--------+---------+ |
| |
| Automated daily check |
v |
+--------+---------+ +------------------+ |
| | | | |
| EXPIRY CHECK +---->| 14-DAY WARNING | |
| | | NOTIFICATION | |
+--------+---------+ +------------------+ |
| |
| Continue daily checks |
v |
+--------+---------+ +------------------+ |
| | | | +----------+ |
| EXPIRY CHECK +---->| 7-DAY WARNING +---->| EXTEND? +-+
| | | NOTIFICATION | | REQUEST | | Yes (approved)
+--------+---------+ +------------------+ +----+-----+ |
| | |
| No | |
v v |
+--------+---------+ +------------------+ +-----+------+
| | | | | |
| EXPIRY CHECK +---->| 1-DAY WARNING +---->| ACCOUNT |
| | | NOTIFICATION | | EXPIRES |
+--------+---------+ +------------------+ +-----+------+
|
| Automated disable
v
+-----+------+
| |
| ACCOUNT |
| DISABLED |
| |
+-----+------+
|
| Retention period
| (30-90 days)
v
+-----+------+
| |
| ACCOUNT |
| DELETED |
| |
+------------+

Figure 1: Time-limited access lifecycle showing automated expiry checks, warning notifications, and extension decision point

Extension approval workflow

Extension requests follow a defined approval path based on personnel category and requested duration:

+-------------------+
| |
| EXTENSION |
| REQUEST |
| |
+--------+----------+
|
v
+--------+----------+
| |
| VALIDATE |
| REQUEST |
| |
| - Before expiry? |
| - Within policy? |
| - HR confirmed? |
| |
+--------+----------+
|
+--------------+--------------+
| |
| Valid | Invalid
v v
+---------+----------+ +---------+----------+
| | | |
| ROUTE TO | | REJECT WITH |
| APPROVER | | EXPLANATION |
| | | |
+----+----------+----+ +--------------------+
| |
| +------------------+
| |
| Standard duration | Exceeds standard
v v
+--------+----------+ +---------+----------+
| | | |
| LINE MANAGER | | LINE MANAGER |
| APPROVAL | | + HR APPROVAL |
| | | + DEPT HEAD |
+--------+----------+ +---------+----------+
| |
+-------------+---------------+
|
v
+--------+----------+
| |
| UPDATE EXPIRY |
| DATE |
| |
| Log extension |
| history |
| |
+-------------------+

Figure 2: Extension approval workflow with escalation path for non-standard durations

Verification

After provisioning a time-limited account, verify the configuration:

Terminal window
# Comprehensive verification script (Entra ID)
$upn = "jane.smith.intern@example.org"
$user = Get-MgUser -Filter "userPrincipalName eq '$upn'" -Property Id,DisplayName,AccountEnabled,UserPrincipalName,EmployeeType,Department,JobTitle,AdditionalProperties
Write-Output "=== Account Verification ==="
Write-Output "UPN: $($user.UserPrincipalName)"
Write-Output "Display Name: $($user.DisplayName)"
Write-Output "Account Enabled: $($user.AccountEnabled)"
Write-Output "Employee Type: $($user.EmployeeType)"
Write-Output "Department: $($user.Department)"
Write-Output "Expiry Date: $($user.AdditionalProperties['extension_accountExpires'])"
Write-Output "Sponsor: $($user.AdditionalProperties['sponsor'])"
# Verify group memberships
Write-Output "`n=== Group Memberships ==="
Get-MgUserMemberOf -UserId $user.Id | ForEach-Object {
$group = Get-MgGroup -GroupId $_.Id
Write-Output "- $($group.DisplayName)"
}
# Verify naming convention compliance
$expectedSuffix = ".intern"
if ($user.UserPrincipalName -like "*$expectedSuffix@*") {
Write-Output "`n[PASS] Username follows naming convention"
} else {
Write-Output "`n[FAIL] Username does not follow naming convention"
}
# Verify expiry is set and within policy
$expiry = [datetime]$user.AdditionalProperties['extension_accountExpires']
$maxDuration = 180 # days for interns
$created = (Get-Date) # Approximate; use actual creation date in production
if ($expiry -le $created.AddDays($maxDuration)) {
Write-Output "[PASS] Expiry date within policy maximum"
} else {
Write-Output "[FAIL] Expiry date exceeds policy maximum"
}

Expected output for correctly provisioned account:

=== Account Verification ===
UPN: jane.smith.intern@example.org
Display Name: Jane Smith (Intern)
Account Enabled: True
Employee Type: Intern
Department: Programme Team
Expiry Date: 2025-05-01T23:59:00Z
Sponsor: m.jones@example.org
=== Group Memberships ===
- Temporary Personnel - All
- Programme Team - Read Only
[PASS] Username follows naming convention
[PASS] Expiry date within policy maximum

Verify the automated expiry job is operational:

Terminal window
# Check last execution of expiry automation
# Azure Automation
az automation job list --automation-account-name "identity-automation" \
--resource-group "identity-rg" \
--query "[?runbook.name=='Disable-ExpiredAccounts'].{Status:status,StartTime:startTime,EndTime:endTime}" \
--output table
# Expected: Recent successful execution within last 24 hours
# STATUS STARTTIME ENDTIME
# --------- ---------------------------- ----------------------------
# Completed 2024-11-16T00:30:00.000000Z 2024-11-16T00:31:15.000000Z

Test the expiry mechanism with a test account before relying on it for production:

Terminal window
# Create test account with expiry set to yesterday
$testUpn = "test.expiry.$(Get-Random)@example.org"
New-MgUser -DisplayName "Test Expiry Account" `
-UserPrincipalName $testUpn `
-MailNickname "test.expiry" `
-AccountEnabled $true `
-PasswordProfile @{Password="TestP@ss123!"; ForceChangePasswordNextSignIn=$true} `
-AdditionalProperties @{
"extension_accountExpires" = (Get-Date).AddDays(-1).ToString("yyyy-MM-ddTHH:mm:ssZ")
}
# Wait for automation to run (or trigger manually)
# Then verify account was disabled
Start-Sleep -Seconds 60 # Or wait for scheduled run
$testUser = Get-MgUser -Filter "userPrincipalName eq '$testUpn'"
Write-Output "Test account enabled: $($testUser.AccountEnabled)"
# Expected: False
# Clean up test account
Remove-MgUser -UserId $testUser.Id

Troubleshooting

SymptomCauseResolution
Account not disabled after expiry dateAutomation job failed or not runningCheck automation job execution logs; verify job schedule is enabled; confirm job has appropriate permissions
User reports access loss before expected expiryTimezone mismatch in expiry dateVerify expiry time includes timezone; set to 23:59 in user’s local timezone, not UTC midnight
Cannot find account to extendAccount already deleted after retentionTreat as new provisioning request; deleted accounts cannot be restored after permanent deletion
Extension request rejected despite approvalExtension count limit exceededPolicy limits extensions; requires department head exception approval for additional extension
Sponsor not receiving expiry notificationsSponsor attribute not set or incorrectVerify sponsor attribute populated with valid email; check notification job logs
User cannot sign in despite active accountAccount disabled by separate processCheck if user triggered account lockout policy; verify no conflicting conditional access policies
Automated job runs but no accounts processedQuery filter not matching accountsVerify attribute name used for expiry matches configured attribute; test query manually
Group membership not removed at expiryUsing static groups instead of dynamicConfigure dynamic group rules based on accountEnabled status, or add group removal to expiry job
Application access persists after account disabledApplication uses cached sessionForce session revocation in application; some applications require direct deprovisioning API call
Audit logs missing expiry actionsAutomation running with insufficient loggingAdd explicit logging to automation scripts; configure audit log retention
New account created without expiry dateProvisioning done outside standard processRequire expiry date as mandatory field; run weekly report of accounts without expiry
Multiple extension requests for same periodConfusion about current expiry dateInclude current expiry date in all extension-related communications; display prominently in request form

Automation verification

Confirm the automated components are functioning correctly:

Terminal window
# Verify cron job is scheduled (Linux/Keycloak)
crontab -l | grep -i expir
# Expected: 30 0 * * * /etc/cron.daily/disable-expired-keycloak-users
# Check cron execution log
grep "disable-expired" /var/log/cron
# Expected: Recent execution entries
# Verify log file is being written
tail -20 /var/log/keycloak-expiry.log
# Expected: Recent entries showing processed accounts
Terminal window
# Verify Azure Automation runbook schedule
Get-AzAutomationScheduledRunbook -AutomationAccountName "identity-automation" `
-ResourceGroupName "identity-rg" | Where-Object {$_.RunbookName -like "*Expired*"}
# Expected output showing schedule details:
# RunbookName ScheduleName
# ----------- ------------
# Disable-ExpiredAccounts Daily-0030-UTC
# Send-ExpiryWarnings Daily-0800-Local

See also