Skip to content

Instantly share code, notes, and snippets.

@astachowski
Last active March 13, 2024 23:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save astachowski/d94980ebef74c3d05558dd1e5dbfe1f0 to your computer and use it in GitHub Desktop.
Save astachowski/d94980ebef74c3d05558dd1e5dbfe1f0 to your computer and use it in GitHub Desktop.
### 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