Skip to main content

Migrating User Tokens from LDAP to SAML

This topic provides scripts and guidance to facilitate migrating existing Nexus Repository user tokens from Lightweight Directory Access Protocol (LDAP) to Security Assertion Markup Language (SAML).

These scripts require that the SAML users have the same username as your LDAP users and that the SAML roles and groups map to the same roles and groups as exist in LDAP.

Using the Scripts

These script moves LDAP user tokens to SAML user tokens with identically named userid. When a SAML user accessed and created a new user token that is different from LDAP, the scripts remove the LDAP token, and the existing SAML token remains in place.

  1. Upgrade Nexus Repository instance to the latest version.

  2. Set the SAML configuration in Nexus Repository, and enable the SAML realm

    See SAML

  3. Enable scripting for Nexus Repository

    See Script API

  4. Restart Nexus Repository

  5. Backup the database before running the script

  6. Run the script provided below using the script API or by adding it to a "Admin - Execute script" task

    See Tasks

  7. Once finished, disable scripting again

  8. Test that the migration has worked by disabling the LDAP realm and confirming that a user token works by trying a known user token

/**
 * Copyright (c) 2023-present Sonatype, Inc. All rights reserved.
 * Sonatype" is a trademark of Sonatype, Inc.
 * Provided "as is" with no implied official support.
 */

/**
 * IMPORTANT: requires nexus 3.70.0 or greater
 *
 * Scenario: Nexus 3 running with following configurations
 * 1) LDAP Realm enabled
 * 2) SAML Realm enabled
 * 3) User Tokens enabled
 * 4) Configured UserTokens are associated with the LDAP realm
 *
 * Objective: Update LDAP user tokens to now be associated with the SAML realm
 *
 * Notable Restrictions:
 * 1) The LDAP userId is expected to match the SAML userId exactly (case sensitivity counts)
 *
 * Process:
 * 1) Inspect every userToken, looking for those that are associated with the LdapRealm
 * 2) Update these tokens, replacing the LdapRealm association with the SamlRealm
 * 3) For each token updated, also create a local SamlUser from the associated LdapUser so that the tokens can be
 *    utilized without first requiring every user to authenticate with SAML
 *
 */

import com.sonatype.nexus.usertoken.plugin.apikey.store.UserTokenStore
import org.sonatype.nexus.security.user.User
import org.sonatype.nexus.security.user.UserManager

import org.apache.shiro.subject.SimplePrincipalCollection

UserTokenStore userTokenStore = container.lookup(UserTokenStore) as UserTokenStore
UserManager ldapUserManager = container.lookup(UserManager, "LDAP")
UserManager samlUserManager = container.lookup(UserManager, "SAML")

log.info('starting user token migration from LDAP to SAML')
userTokenStore.records().each { userTokenRecord ->
  log.info("Found token principal ${userTokenRecord.principals}")
  SimplePrincipalCollection oldPrincipals = new SimplePrincipalCollection(userTokenRecord.principals)
  if (!oldPrincipals.fromRealm('SamlRealm').empty) {
    log.info('skipping token as it already has SAML realm')
  }
  else if (!oldPrincipals.fromRealm('LdapRealm').empty) {
    SimplePrincipalCollection newPrincipals = new SimplePrincipalCollection()
    def userId = oldPrincipals.primaryPrincipal
    newPrincipals.add(userId, 'SamlRealm')
    userTokenStore.remove(userTokenRecord.principals)
    try {
      def newUserTokenRecord = userTokenStore.newUserTokenRecord(newPrincipals, userTokenRecord.userToken, userTokenRecord.created.toInstant().atOffset(java.time.ZoneOffset.UTC))
      log.info('Adding SAML realm principal to token')
      log.debug('{}', newUserTokenRecord)
      userTokenStore.add(newUserTokenRecord)
      addCachedSamlUser(userId, ldapUserManager, samlUserManager)
    }
    catch (com.sonatype.nexus.usertoken.plugin.store.DuplicateUserTokenException e) {
      log.info('skipping token as it already has SAML realm')
    }
    catch (Exception e) {
      try {
        log.warn('Attempting to add back the original UserTokenRecord')
        def originalUserTokenRecord = userTokenStore.newUserTokenRecord(userTokenRecord.principals, userTokenRecord.userToken, userTokenRecord.created.toInstant().atOffset(java.time.ZoneOffset.UTC))
        log.debug('{}', originalUserTokenRecord)
        userTokenStore.add(originalUserTokenRecord)
      }
      catch (Exception e1) {
        log.error('Unable to add back the original UserTokenRecord', e1)
      }
      throw e
    }
  }
  else {
    log.info('Skipping non-LDAP token')
  }
}

log.info('completed user token migration to SAML')

def addCachedSamlUser(userId, ldapUserManager, samlUserManager) {
  try {
    User ldapUser = ldapUserManager.getUser(userId)
    if (!ldapUser) {
      log.error("Unable to retrieve user ${userId} from ldap, skipping creation of local SAML user")
      return
    }

    samlUserManager.addUser(ldapUser, null)
  }
  catch (Exception e) {
    log.error("Failed to retrieve user ${userId} from ldap, skipping creation of local SAML user", e)
  }
}
import com.sonatype.nexus.usertoken.plugin.store.UserTokenStore
import org.apache.shiro.subject.SimplePrincipalCollection

UserTokenStore userTokenStore = container.lookup(UserTokenStore) as UserTokenStore
log.info('starting user token migration from LDAP to SAML')
userTokenStore.records().each { userTokenRecord ->
    log.info("Found token principal ${userTokenRecord.principals}")
    SimplePrincipalCollection newPrincipals = new SimplePrincipalCollection(userTokenRecord.principals)
    if (!newPrincipals.fromRealm('SamlRealm').empty) {
        log.info('skipping token as it already has SAML realm')
    }
    else if (!newPrincipals.fromRealm('LdapRealm').empty) {
      newPrincipals.add(newPrincipals.primaryPrincipal, 'SamlRealm')
        userTokenStore.remove(userTokenRecord.nameCode)
      try {
           def newUserTokenRecord = userTokenRecord.getClass().newInstance(newPrincipals, userTokenRecord.userToken, userTokenRecord.created)
           log.info('Adding SAML realm principal to token')
           log.debug("{}", newUserTokenRecord)
         userTokenStore.add(newUserTokenRecord)
      } catch (Exception e) {
         try {
            log.warn("Attempting to add back the original UserTokenRecord")
            def originalUserTokenRecord = userTokenRecord.getClass().newInstance(userTokenRecord.principals, userTokenRecord.userToken, userTokenRecord.created)
            log.debug("{}", originalUserTokenRecord)
            userTokenStore.add(originalUserTokenRecord)
         } catch (Exception e1) {
            log.error("Unable to add back the original UserTokenRecord", e1)
         }
         
         throw e;
      }
    }
    else {
        log.info('Skipping non-LDAP token')
    }
}

log.info('completed user token migration to SAML')