Migration to SSO keeping original accounts
Issue
- Preiovusly using Matrix without SSO, when setting up SSO new matrix accounts are created for all users. Users would prefer to keep using there existing accounts, whilst also using SSO.
Environment
- Element On-Premise
- SSO Configured
- Element Cloud
- SSO Configured
Resolution
Transferring SSO external_ids
to original users
To transfer 'external_ids' from SSO accounts, to your original accounts you will need to use the Admin API.
Getting an Access Token
Before being able to use the Admin API, you will need an admin account and it's 'Access Token', you can make a user a Synapse Admin by either following the steps in the link above (required for On-Premise), or following these steps on the EMS Control Panel:
- Access the 'Server Admin' tab
- Under the 'Users' tab, select the user that should be made Synapse Admin
- Click the checkbox, next to 'Synapse Admin' and click 'Yes' to confirm
Once a user is a Synapse Admin, you can retrieve their 'Access Token' by logging in via the Element Matrix client:
- Click on the users' profile icon in the top-left and select 'All Settings'
- Open the 'Help & About' settings page, then scroll down to the 'Advanced' section
- Click 'Access Token' to reveal the token, copy this to interact with the Admin API
Using an Access Token
Access tokens will need to be passed into all API requests as an Authorization Bearer token, see examples below:
Bash:
curl -X GET -H 'Authorization: Bearer syt_adminToken'
Windows:
$headers.Add("Authorization", "Bearer syt_adminToken")
Python:
import requests
headers = {
'Authorization': 'Bearer syt_adminToken',
}
Getting SSO users' external_ids
-
Using the link above, you can use a GET request to
/_synapse/admin/v2/users
to retrieve a list of all accounts on your home server. Follow the guidance on pagination to ensure all users are retrieved.Bash:
curl -X GET -H 'Authorization: Bearer syt_adminToken' 'https://example.ems.host:443/_synapse/admin/v2/users?from=60&limit=10&guests=false'
Windows:
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $headers.Add("Authorization", "Bearer syt_adminToken") $headers.Add("Content-Type", "application/json") $response = Invoke-RestMethod 'https://example.ems.host:443/_synapse/admin/v2/users?from=60&limit=10&guests=false' -Method 'GET' -Headers $headers $response | ConvertTo-Json
-
For each user you can then use their
name
in another GET request to/_synapse/admin/v2/users/<user_id>
, replacing<user_id>
withname
.Bash:
curl -X GET -H 'Authorization: Bearer syt_adminToken' 'https://example.ems.host:443/_synapse/admin/v2/users/@exampleuser:example.ems.host'
Windows:
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $headers.Add("Authorization", "Bearer syt_adminToken") $headers.Add("Content-Type", "application/json") $response = Invoke-RestMethod 'https://example.ems.host:443/_synapse/admin/v2/users/@exampleuser:example.ems.host' -Method 'GET' -Headers $headers $response | ConvertTo-Json
-
You will find the
external_ids
for each user within the JSON output. You can programatically run through all users and generate a list of only those withexternal_ids
, removing unneeded information. (See example below)Python:
import requests # REPLACE THESE VALUES WITH ACCESS TOKEN AND HOME SERVER URL headers = { 'Authorization': 'Bearer syt_adminToken', } url = 'https://example.ems.host' # GET LIST OF ALL USERS ON HOME SERVER # OUTPUT: 'all_users' contains a list of all users next_token = '0' last_token = '' all_users = [] get_users = requests.get(url + '/_synapse/admin/v2/users?from=' + next_token + '&limit=10&guests=false', headers=headers).json() for user in get_users['users']: all_users.append(user['name']) while ('next_token' in get_users) and (next_token != last_token): next_token = get_users['next_token'] get_users = requests.get(url + '/_synapse/admin/v2/users?from=' + next_token + '&limit=10&guests=false', headers=headers).json() for user in get_users['users']: all_users.append(user['name']) # FOR EACH USER, GET ALL INFO, EXCLUDE THOSE WITHOUT 'external_ids' # OUTPUT: 'all_external_ids' contains a list of all users with external ids all_external_ids = [] for user in all_users: get_user = requests.get(url + '/_synapse/admin/v2/users/' + user, headers=headers).json() if get_user['external_ids'].__len__() != 0: all_external_ids.append( { 'sso_username': user, 'original_username': '', 'external_ids': get_user['external_ids'] } )
Transferring SSO external_ids
information
Admin API Create or Modify Account
With all external_ids
collected, you will need to identify each SSO Account and the associated original account that you'd like to transfer the associated SSO information over too.
If using the Python example above, you will need to store the original username within all_external_ids[X]['original_username']
, replacing X with the index of the SSO user. If you create a dictionary with keys
named of the SSO username, and values
of the desired Original username you could use the following to update all_external_ids
:
Python:
# ADD REQUIRED ORIGINAL USERNAME
for user in all_external_ids:
dict_storing_sso2orig = {'@example_sso_user:example.ems.host': '@example_orig_user:example.ems.host'}
user['original_username'] = dict_storing_sso2orig[str(user['sso_username'])]
Once you have related all SSO Usernames to Original Usernames you can then, using the link above, use a PUT request to /_synapse/admin/v2/users/<user_id>
to change the external_ids
data for each account. (Remember to remove the external_ids
information from the soon to be defunct SSO accounts)
Bash:
curl -X PUT -H 'Authorization: Bearer syt_example_2RCMR1' -d '{"external_ids":[]}' 'https://example.ems.host:443/_synapse/admin/v2/users/@exampleuser:example.ems.host'
Windows:
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("Authorization", "Bearer syt_adminToken")
$headers.Add("Content-Type", "application/json")
$body = "{`"external_ids`": [{`"auth_provider`": `"saml`",`"external_id`": `"email@address.com`"}]}"
$response = Invoke-RestMethod 'https://example.ems.host/_synapse/admin/v2/users/@user:example.com' -Method 'PUT' -Headers $headers -Body $body
$response | ConvertTo-Json
Continuing with the Python example, you can now use this to remove the external_ids
from each SSO account, and add that information to the associated Original account.
Python:
# REMOVE 'external_ids' from 'sso_username' ACCOUNTS FROM 'all_external_ids' THEN
# UPDATE ALL 'original_username' ACCOUNTS FROM 'all_external_ids' WITH 'external_ids' FROM 'sso_username'
for user in all_external_ids:
data = '{"external_ids":' + str(user['external_ids']).replace("'", '"').replace(" ", "") + '}'
remove_sso = requests.put(url + '/_synapse/admin/v2/users/' + user['sso_username'], headers=headers, data='{"external_ids":[]}')
if remove_sso.status_code == 200:
add_sso = requests.put(url + '/_synapse/admin/v2/users/' + user['original_username'], headers=headers, data=data)
Python Example
The full python script is available below:
import requests
# REPLACE THESE VALUES WITH ACCESS TOKEN AND HOME SERVER URL
headers = {
'Authorization': 'Bearer syt_adminToken',
}
url = 'https://example.ems.host'
# GET LIST OF ALL USERS ON HOME SERVER
# OUTPUT: 'all_users' contains a list of all users
next_token = '0'
last_token = ''
all_users = []
get_users = requests.get(url + '/_synapse/admin/v2/users?from=' + next_token + '&limit=10&guests=false',
headers=headers).json()
for user in get_users['users']:
all_users.append(user['name'])
while ('next_token' in get_users) and (next_token != last_token):
next_token = get_users['next_token']
get_users = requests.get(url + '/_synapse/admin/v2/users?from=' + next_token + '&limit=10&guests=false',
headers=headers).json()
for user in get_users['users']:
all_users.append(user['name'])
# FOR EACH USER, GET ALL INFO, EXCLUDE THOSE WITHOUT 'external_ids'
# OUTPUT: 'all_external_ids' contains a list of all users with external ids
all_external_ids = []
for user in all_users:
get_user = requests.get(url + '/_synapse/admin/v2/users/' + user, headers=headers).json()
if get_user['external_ids'].__len__() != 0:
all_external_ids.append(
{
'sso_username': user,
'original_username': '',
'external_ids': get_user['external_ids']
}
)
# ADD REQUIRED ORIGINAL USERNAME
# REPLACE CONTENTS OF 'dict_storing_sso2orig' TO SET UP RELATED SSO -> ORIGINAL ACCOUNTS
# CHANGE 'readme' VARIABLE BELOW TO 'True' TO CONTINUE
readme = False
dict_storing_sso2orig = {'@example_sso_user:example.ems.host': '@example_orig_user:example.ems.host'}
for user in all_external_ids:
if readme is True:
user['original_username'] = dict_storing_sso2orig[str(user['sso_username'])]
# REMOVE 'external_ids' from 'sso_username' ACCOUNTS FROM 'all_external_ids' THEN
# UPDATE ALL 'original_username' ACCOUNTS FROM 'all_external_ids' WITH 'external_ids' FROM 'sso_username'
# CHANGE 'dict_storing_sso2orig_check' VARIABLE BELOW TO 'True' TO CONTINUE
dict_storing_sso2orig_check = False
for user in all_external_ids:
if (dict_storing_sso2orig_check is True) and (dict_storing_sso2orig != {'@example_sso_user:example.ems.host': '@example_orig_user:example.ems.host'}):
data = '{"external_ids":' + str(user['external_ids']).replace("'", '"').replace(" ", "") + '}'
remove_sso = requests.put(url + '/_synapse/admin/v2/users/' + user['sso_username'], headers=headers, data='{"external_ids":[]}')
if remove_sso.status_code == 200:
add_sso = requests.put(url + '/_synapse/admin/v2/users/' + user['original_username'], headers=headers, data=data)
Root Cause
If SSO is not setup prior to using matrix, new SSO duplicate accounts are created following it's configuration. Users would prefer to keep their existing accounts and associated setup (Rooms / etc.) so migrating external_ids
from these new SSO accounts to the originals is required.