Last active
March 13, 2024 23:13
-
-
Save astachowski/d94980ebef74c3d05558dd1e5dbfe1f0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### Requirements: Federated Azure domain, usernames are consistent between Keycloak and Azure AD, | |
### Microsoft Graph Modules for Powershell. | |
### Azure domain and Keycloak realm names maybe different. | |
### Primary source of group and user information is Keycloak. | |
### Admin login for Keycloak is user-based, admin login for Azure AD is certificate based (app registration). | |
### $KC_-constants are related to Keycloak, $MG_ are related to Microsoft Graph/Azure AD | |
### The necessary ImmutableId is generated by converting the LDAP_ID from Keycloak. Your Keycloak | |
### setup may differ. | |
# Change Settings here: | |
$KC_Server = "https://keycloakserver.example.com" | |
$KC_Groups = @('365Group1','365Group2') | |
$KC_UserName = 'apiadmin' | |
$KC_Password = 'adminpw' | |
$KC_Realm = 'example.com' | |
$MG_Domain = 'microsoft.example.com' | |
$MG_CertFingerprint = '012345678901234567890123456789ABCDEF1234' | |
$MG_Group = 'AzureGroup' | |
$MG_TenantId = 'a1234567-1111-2222-4343-123456789012' | |
$MG_AppId = 'b1234567-1111-2222-4343-123456789012' | |
# Request Keycloak API token and convert from Json to Powershell object, construct auth header | |
try { | |
$KC_TokenRequestURL = $KC_Server + "/realms/master/protocol/openid-connect/token" | |
$KC_AuthBody = @{grant_type='password' | |
username=$KC_Username | |
password=$KC_Password | |
client_id='admin-cli' | |
} | |
$KC_AuthRequest = Invoke-WebRequest -Method POST -Uri $KC_TokenRequestURL -ContentType "application/x-www-form-urlencoded" -Body $KC_AuthBody | |
} catch { | |
# suppress all output and stop the powershell script | |
function Out-Default {} | |
'Keycloak: ' + ($_.ErrorDetails.Message) | |
Write-Error (".") -ErrorAction Stop | |
} | |
$KC_AccessToken = ( $KC_AuthRequest.Content | ConvertFrom-Json ).access_token | |
$KC_AuthHeaders = @{ | |
Authorization="Bearer $KC_AccessToken" | |
"cache-control"="no-cache" | |
} | |
# Connect to Microsoft Graph | |
try { | |
Connect-MgGraph -CertificateThumbprint $MG_CertFingerprint -TenantId $MG_TenantId -AppId $MG_AppId -ErrorAction Stop | out-null | |
} catch { | |
# suppress all output and stop the powershell script | |
function Out-Default {} | |
'Microsoft Graph: ' + $PSItem.Exception.Message | |
Write-Error (".") -ErrorAction Stop | |
} | |
# Get Keycloak GIDs for group names through API | |
$KC_GIdURLPrefix = $KC_Server + "/admin/realms/$KC_Realm/groups?search=" | |
function KCTranslatedGids() { | |
$KC_TranslatedGroupIds = @() | |
foreach($grp in $KC_Groups) { | |
$KC_GroupURL = $KC_GIdURLPrefix + $grp | |
$KC_TranslatedGroupIds += (Invoke-WebRequest -Method GET -Uri $KC_GroupURL -Headers $KC_AuthHeaders | ConvertFrom-Json).id | |
} | |
return $KC_TranslatedGroupIds | |
} | |
$KC_GIDs = KCTranslatedGids | |
# Get Keycloak group members for $KC_Groups and merge them into object $KC_Users | |
function KCGroupMembers() { | |
$KC_GroupMembers = @() | |
foreach($gid in $KC_GIDs) { | |
$KC_GroupMemberURL = $KC_Server + "/admin/realms/$KC_Realm/groups/$gid/members" | |
$KC_GroupMembers += (Invoke-RestMethod -Uri $KC_GroupMemberURL -Method GET -ContentType "application/json; charset=utf-8" -Headers $KC_AuthHeaders ) | |
} | |
return $KC_GroupMembers | |
} | |
# Remove duplicates while calling KCGroupMembers | |
$KC_Users = KCGroupMembers | Sort-Object -Unique -Property username | |
# Check if immutableId exists in $KC_Users, otherwise create it in Keycloak and Azure AD by converting attributes.LDAP_ID into Base64 | |
foreach($usr in $KC_Users) { | |
if ('saml.persistent.name.id.for.urn:federation:MicrosoftOnline' -in $usr.attributes.PSobject.Properties.Name) { | |
#$usr.email + " already has an immutable id: " + $usr.attributes.'saml.persistent.name.id.for.urn:federation:MicrosoftOnline' | |
} else { | |
# Convert attributes.LDAP_ID to immmutableId | |
$ldapId = ($usr.attributes.LDAP_ID).replace("-","") | |
$immutableId = [Convert]::ToBase64String([guid]::New($ldapId).ToByteArray()) | |
"Creating Immutable ID for " + $usr.email + ": " + $immutableId | |
# Query existing user attributes and add | |
# 'saml.persistent.name.id.for.urn:federation:MicrosoftOnline' | |
# for later update object | |
$KC_UserIdUri = $KC_Server + "/admin/realms/" + $KC_Realm + "/users/" + $usr.id | |
$GetUserAttributes = (Invoke-RestMethod -Uri $KC_UserIdUri -Method GET -Headers $KC_AuthHeaders).attributes | |
$UserAttributes = @{} | |
foreach ($property in $GetUserAttributes.PSObject.Properties) { | |
$addProperty = @{ $property.Name = ($property.Value).trim('{}') } | |
$UserAttributes += $addProperty | |
} | |
$UserAttributes += @{ 'saml.persistent.name.id.for.urn:federation:MicrosoftOnline' = $immutableId } | |
$attributeData = @{ | |
attributes = $UserAttributes | |
} | |
$jsonData = $attributeData | ConvertTo-Json | |
# Use PUT with all attributes on UID URI in Keycloak | |
Invoke-RestMethod -Uri $KC_UserIdUri -Method PUT -ContentType "application/json" -Headers $KC_AuthHeaders -Body $jsonData | |
# Create user with Microsoft Graph | |
$mailNick = ($usr.firstName + $usr.lastName).replace(" ","") | |
New-MgUser -DisplayName ($usr.firstName + " " + $usr.lastName) -OnPremisesImmutableId $immutableId -UserPrincipalName ($usr.username + "@" + $MG_Domain) -AccountEnabled -MailNickname $mailNick | out-null | |
# Add $MG_UId to specified $MG_GId. Group names and UserPricipalNames can't be used directly for adding group members | |
$MG_PrincipalUserName = $usr.username + "@" + $MG_Domain | |
$MG_UId = (Get-MgUser -UserId $MG_PrincipalUserName).id | |
$MG_GId = (Get-MgGroup -Filter "DisplayName eq '$MG_Group'").id | |
"Adding " + $MG_PrincipalUserName + " to " + $MG_Group | |
New-MgGroupMember -GroupId $MG_GId -DirectoryObjectId $MG_UId | |
} | |
} | |
# Get Azure AD users in $MG_Domain to check for removal. | |
# Remove $MG_Domain suffix for easier comparison | |
function GetAzureUserNames() { | |
$MG_Users = Get-MgUser -All | Where-Object {$_.userPrincipalName -like ('*@' + $MG_Domain)} | |
$MG_UserNames = @() | |
foreach($account in $MG_Users) { | |
$username = ($account.UserPrincipalName).split("@") | |
$MG_UserNames += $username[0] | |
} | |
return $MG_UserNames | |
} | |
$AzureUsers = GetAzureUserNames | |
# Remove if user from $AzureUsers doesn't exist in $KC_Users anymore. | |
foreach($username in $AzureUsers) { | |
if ($KC_Users.username -like $username ) { | |
#$username + " still active in Keycloak. Doing nothing." | |
} else { | |
# Construct UserPrincipalName and remove in Azure AD | |
$username + " doesn't exist in Keycloak groups. Removing in Azure AD, purging ImmutableID in Keycloak" | |
$UserPrincipalName = $username + "@" + $MG_Domain | |
Remove-MgUser -UserId $UserPrincipalName | |
# Delete ImmutableID in Keycloak as well, get user id from Keycloak first | |
$KC_UserSearchURL = "$KC_Server/admin/realms/$KC_Realm/users/?username=$username" | |
$KC_Uid = (Invoke-RestMethod -Uri $KC_UserSearchURL -Method GET -ContentType "application/json; charset=utf-8" -Headers $KC_AuthHeaders).id | |
$KC_UserDelAttributeURL = "$KC_Server/admin/realms/$KC_Realm/users/$KC_Uid" | |
# Query existing user attributes and set | |
# 'saml.persistent.name.id.for.urn:federation:MicrosoftOnline' | |
# to empty for removal | |
$GetUserAttributes = (Invoke-RestMethod -Uri $KC_UserDelAttributeURL -Method GET -Headers $KC_AuthHeaders).attributes | |
$UserAttributes = @{} | |
foreach ($property in $GetUserAttributes.PSObject.Properties) { | |
if ($property.Name -eq 'saml.persistent.name.id.for.urn:federation:MicrosoftOnline') { | |
$addProperty = @{ 'saml.persistent.name.id.for.urn:federation:MicrosoftOnline' = @() } | |
$UserAttributes += $addProperty | |
} else { | |
$addProperty = @{ $property.Name = ($property.Value).trim('{}') } | |
$UserAttributes += $addProperty | |
} | |
} | |
$attributeData = @{ | |
attributes = $UserAttributes | |
} | |
$jsonData = $attributeData | ConvertTo-Json | |
# Use PUT with all attributes on UID URI in Keycloak | |
Invoke-RestMethod -Uri $KC_UserDelAttributeURL -Method PUT -ContentType "application/json" -Headers $KC_AuthHeaders -Body $jsonData | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment