Migrating 3,000+ Users
This post aims to provide guidance and lessons learned for those undertaking similar authentication migrations in their SonarQube environments, whether moving from Azure AD, LDAP, or other existing authentication integrations to a SAML-based approach.
When migrating authentication methods in SonarQube, seemingly small details can make a significant difference in user access. I’ll dive into the important externalProvider
property and how it impacted our migration of over 3,000
users to the new SAML authentication setup.

Before we proceed, what is SonarQube again?
TLDR; It’s a code quality and security platform that performs automatic reviews of code to detect bugs, code smells, and security vulnerabilities
.
As a cornerstone of modern DevSecOps
practices, it empowers teams to integrate security directly into the development process. SonarQube supports 27+ programming languages
and integrates seamlessly with popular CI/CD pipelines
to ensure code quality throughout the development lifecycle.
Understanding the Authentication Landscape
SonarQube supports multiple authentication methods, including Azure Active Directory/Entra ID (formerly known as AAD) and SAML. Each method relies on a specific mechanism to identify and authenticate users. The key to this process lies in the externalProvider
property, a behind-the-scenes attribute that determines user login capabilities.
The Authentication Property: externalProvider
In our specific scenario, we were transitioning from an AAD plugin to SAML authentication. The externalProvider
property plays a crucial role in this migration:
- For AAD authentication:
externalProvider = aad
- For SAML authentication:
externalProvider = saml
Migration
Migration Challenges
Here’s the critical insight: Users will be locked out if the externalProvider
is not updated during the SAML migration.
This means that even with correct SAML configuration, users won’t be able to log in unless their externalProvider
value is explicitly changed to saml
.

Migration Steps
- Always test authentication changes in a staging environment
- Document each step of the migration process
- Communicate changes clearly to your team and impacted departments
Create a backup
def create_backup_file(data: list, file: str) -> None:
data = data
format_data = json.dumps(data, indent=2)
# Create the backup directory if it doesn't exist
backup_dir = "./backup"
os.makedirs(backup_dir, exist_ok=True)
# Append timestamp to the file name
timestamp = time.strftime("%Y%m%d%H%M%S")
file_name = f"{file}_{timestamp}.json"
file_path = os.path.join(backup_dir, file_name)
with open(file_path, 'w') as backup:
backup.write(str(format_data))
print("Backup saved in:", file_path)
Prepare rollback functionality
def restore_authentication_method(account: str, stage: str):
secret: dict = json.loads(get_secret_value(account))
base_url: str = stage
# sonarqube token is sent via the login field of HTTP basic authentication, without password. (curl -u token: $URL)
token: str = secret["token"]
no_pwd: str = ""
file = open('restore_file.json')
migration_data = json.load(file)
for user in migration_data:
login: str = user["login"]
externalIdentity: str = user["externalIdentity"]
initialExternalProvider: str = user["externalProvider"]
url: str = f"{base_url}/api/users/update_identity_provider?login={login}&newExternalProvider={initialExternalProvider}&newExternalIdentity={externalIdentity}"
response: Response = post(url, auth=HTTPBasicAuth(token, no_pwd))
print("-----------------------------------------------------------")
print(f"Updating auth for {login}")
print(url)
response.raise_for_status()
Verify Current Configuration
- Check existing user configurations
- Identify users with aad as their externalProvider
def get_user_migration_data(account: str, stage: str) -> list:
data = get_user_data(account, stage)
migration_data = []
for item in data:
new_dict = {
'login': item['login'],
'externalIdentity': item['externalIdentity'],
'externalProvider': item['externalProvider']
}
migration_data.append(new_dict)
return migration_data
def get_user_data(account: str, stage: str) -> None:
secret: dict = json.loads(get_secret_value(account))
base_url: str = stage
# sonarqube token is sent via the login field of HTTP basic authentication, without password. (curl -u token: $URL)
token: str = secret["token"]
no_pwd: str = ""
params = {
"p": 1,
"ps": 500
}
user_data: List[dict] = []
url: str = f"{base_url}/api/users/search"
# handle pagination
while True:
response: Response = get(url, auth=HTTPBasicAuth(token, no_pwd), params=params)
raise_for_status(response=response)
data: dict = response.json()
user_data.extend(data["users"])
if len(data["users"]) < params["ps"]:
break
params["p"] += 1
return user_data
Update User Properties
Modify the externalProvider
to saml
def update_authentication_method(account: str, stage: str):
secret: dict = json.loads(get_secret_value(account))
base_url: str = stage
# sonarqube token is sent via the login field of HTTP basic authentication, without password. (curl -u token: $URL)
token: str = secret["token"]
no_pwd: str = ""
migration_data = get_user_migration_data(account, stage)
for user in migration_data:
login: str = user["login"]
externalIdentity: str = user["externalIdentity"]
externalProvider: str = user["externalProvider"]
newExternalProvider: str = "saml"
# only update users with aad auth method
if externalProvider != "aad":
continue
url: str = f"{base_url}/api/users/update_identity_provider?login={login}&newExternalProvider={newExternalProvider}&newExternalIdentity={externalIdentity}"
response: Response = post(url, auth=HTTPBasicAuth(token, no_pwd))
print("-----------------------------------------------------------")
print(f"Updating auth for {login}")
print(url)
response.raise_for_status()
Test Incrementally
- Update a small group of users first in your dev environment
- Validate SAML login functionality
- Update a small group of users in production
- Gradually roll out to entire user base
Conclusion
Authentication migrations require careful attention. The externalProvider
property might seemed like a minor configuration element, but it could be the difference between smooth access and user lockout.
Remember: In authentication migrations, the devil is in the details!