Migration is usually a one-time operation. This probably is not worth a dedicated feature since setups and prior authentication methods might vary significantly. IMHO a script is the most flexible and easiest way to go. I solved it by parsing for users with an XWiki.LDAPProfileClass object and adding an XWiki.OIDC.UserClass object to their profiles. Here is the script I used:
Hint: Do steps 2–4 before users start logging in through OIDC, so no new duplicates appear in between
Back up the database
Fill in ISSUER and CSV, then view the page with DRY_RUN = true.
Fix any red rows: missing UPNs, or conflicts with profiles from earlier test logins
Set DRY_RUN = false and view the page once more.
Set the OIDC configuration according to the format of your usernames, then restart: oidc.user.subjectFormater=${oidc.idtoken.upn._lowerCase}
Delete duplicate profiles after checking they hold nothing
If you use the Microsoft Entra v2.0 endpoint, make sure upn is in the ID token: add it as an optional claim under token configuration in the app registration
{{groovy}}import org.xwiki.model.reference.LocalDocumentReferencedef DRY_RUN = true// Exactly the "iss" claim of the ID token (copy it from an OIDC-created profile)def ISSUER = 'https://login.microsoftonline.com/<tenant-id>/v2.0'// Contents of upn.csv: <LDAP uid>;<userPrincipalName>def CSV = '''user;user@example.com'''def upnByUid = [:]CSV.trim().eachLine { line -> def parts = line.split(';') if (parts.length == 2) { upnByUid[parts[0].trim().toLowerCase()] = parts[1].trim().toLowerCase() }}def oidcClass = new LocalDocumentReference(['XWiki', 'OIDC'], 'UserClass')def context = xcontext.contextdef wiki = context.wikidef linkedSubjects = [:]services.query.xwql('select oidc.subject, doc.fullName from Document doc, doc.object(XWiki.OIDC.UserClass) as oidc where oidc.issuer = :issuer') .bindValue('issuer', ISSUER).execute().each { linkedSubjects[it[0]] = it[1] }def profiles = services.query.xwql('select doc.fullName, ldap.uid from Document doc, doc.object(XWiki.LDAPProfileClass) as ldap order by doc.fullName').execute()println "|=Profile|=LDAP uid|=Subject|=Result"profiles.each { row -> def (fullName, uid) = [row[0], row[1]] def upn = uid ? upnByUid[uid.toLowerCase()] : null def doc = wiki.getDocument(services.model.resolveDocument(fullName), context) def result if (doc.getXObject(oidcClass) != null) { result = 'skipped: already linked' } elseif (upn == null) { result = '(% style="color:red" %)skipped: no UPN in CSV' } elseif (linkedSubjects.containsKey(upn)) { result = "(% style=\"color:red\" %)conflict: subject already on ${linkedSubjects[upn]}" } elseif (DRY_RUN) { result = 'would link' } else { doc = doc.clone() def obj = doc.newXObject(oidcClass, context) obj.setStringValue('issuer', ISSUER) obj.setStringValue('subject', upn) wiki.saveDocument(doc, 'Link LDAP profile to OIDC identity', true, context) result = 'linked' } println "|[[${fullName}]]|${uid}|${upn ?: ''}|${result}"}{{/groovy}}
Patrick Koska on 04/Nov/26 11:08
Migration is usually a one-time operation. This probably is not worth a dedicated feature since setups and prior authentication methods might vary significantly. IMHO a script is the most flexible and easiest way to go. I solved it by parsing for users with an XWiki.LDAPProfileClass object and adding an XWiki.OIDC.UserClass object to their profiles. Here is the script I used:
Hint: Do steps 2–4 before users start logging in through OIDC, so no new duplicates appear in between # Back up the database # Fill in {{ISSUER}} and {{{}CSV{}}}, then view the page with {{{}DRY_RUN = true{}}}. # Fix any red rows: missing UPNs, or conflicts with profiles from earlier test logins # Set {{DRY_RUN = false}} and view the page once more. # Set the OIDC configuration according to the format of your usernames, then restart: oidc.user.subjectFormater=${oidc.idtoken.upn._lowerCase} # Delete duplicate profiles after checking they hold nothing # If you use the Microsoft Entra v2.0 endpoint, make sure {{upn}} is in the ID token: add it as an optional claim under token configuration in the app registration
{code:java} {{groovy}} import org.xwiki.model.reference.LocalDocumentReferencedefLocalDocumentReference def DRY_RUN = true // Exactly the "iss" claim of the ID token (copy it from an OIDC-created profile) def ISSUER = 'https://login.microsoftonline.com/<tenant-id>/v2.0' // Contents of upn.csv: <LDAP uid>;<userPrincipalName> def CSV = ''' user;user@example.com ''' def upnByUid = [:] CSV.trim().eachLine { line -> def parts = line.split(';') if (parts.length == 2) { upnByUid[parts[0].trim().toLowerCase()] = parts[1].trim().toLowerCase() } } def oidcClass = new LocalDocumentReference(['XWiki', 'OIDC'], 'UserClass') def context = xcontext.contextdefcontext def wiki = context.wikidefwiki def linkedSubjects = [:] services.query.xwql('select oidc.subject, doc.fullName from Document doc, doc.object(XWiki.OIDC.UserClass) as oidc where oidc.issuer = :issuer') .bindValue('issuer', ISSUER).execute().each { linkedSubjects[it[0]] = it[1] } def profiles = services.query.xwql('select doc.fullName, ldap.uid from Document doc, doc.object(XWiki.LDAPProfileClass) as ldap order by doc.fullName').execute() println "|=Profile|=LDAP uid|=Subject|=Result" profiles.each { row -> def (fullName, uid) = [row[0], row[1]] def upn = uid ? upnByUid[uid.toLowerCase()] : null def doc = wiki.getDocument(services.model.resolveDocument(fullName), context) def result if (doc.getXObject(oidcClass) != null) { result = 'skipped: already linked' } else if (upn == null) { result = '(% style="color:red" %)skipped: no UPN in CSV' } else if (linkedSubjects.containsKey(upn)) { result = "(% style=\"color:red\" %)conflict: subject already on ${linkedSubjects[upn]}" } else if (DRY_RUN) { result = 'would link' } else { doc = doc.clone() def obj = doc.newXObject(oidcClass, context) obj.setStringValue('issuer', ISSUER) obj.setStringValue('subject', upn) wiki.saveDocument(doc, 'Link LDAP profile to OIDC identity', true, context) result = 'linked' } println "|[[${fullName}]]|${uid}|${upn ?: ''}|${result}" } {{/groovy}}{code}
Patrick Koska on 04/Nov/26 11:09
Migration is usually a one-time operation. This probably is not worth a dedicated feature since setups and prior authentication methods might vary significantly. IMHO a script is the most flexible and easiest way to go. I solved it by parsing for users with an XWiki.LDAPProfileClass object and adding an XWiki.OIDC.UserClass object to their profiles. Here is the script I used:
Hint: Do steps 2–4 before users start logging in through OIDC, so no new duplicates appear in between # Back up the database # Fill in {{ISSUER}} and {{{}CSV{}}}, then view the page with {{{}DRY_RUN = true{}}}. # Fix any red rows: missing UPNs, or conflicts with profiles from earlier test logins # Set {{DRY_RUN = false}} and view the page once more. # Set the OIDC configuration according to the format of your usernames, then restart: oidc.user.subjectFormater=${oidc.idtoken.upn._lowerCase} # Delete duplicate profiles after checking they hold nothing # If you use the Microsoft Entra v2.0 endpoint, make sure {{upn}} is in the ID token: add it as an optional claim under token configuration in the app registration
{code:java} {{groovy}} import org.xwiki.model.reference.LocalDocumentReference def DRY_RUN = true // Exactly the "iss" claim of the ID token (copy it from an OIDC-created profile) def ISSUER = 'https://login.microsoftonline.com/<tenant-id>/v2.0' // Contents of upn.csv: <LDAP uid>;<userPrincipalName> def CSV = ''' user;user@example.com ''' def upnByUid = [:] CSV.trim().eachLine { line -> def parts = line.split(';') if (parts.length == 2) { upnByUid[parts[0].trim().toLowerCase()] = parts[1].trim().toLowerCase() } } def oidcClass = new LocalDocumentReference(['XWiki', 'OIDC'], 'UserClass') def context = xcontext.context def wiki = context.wiki def linkedSubjects = [:] services.query.xwql('select oidc.subject, doc.fullName from Document doc, doc.object(XWiki.OIDC.UserClass) as oidc where oidc.issuer = :issuer') .bindValue('issuer', ISSUER).execute().each { linkedSubjects[it[0]] = it[1] } def profiles = services.query.xwql('select doc.fullName, ldap.uid from Document doc, doc.object(XWiki.LDAPProfileClass) as ldap order by doc.fullName').execute() println "|=Profile|=LDAP uid|=Subject|=Result" profiles.each { row -> def (fullName, uid) = [row[0], row[1]] def upn = uid ? upnByUid[uid.toLowerCase()] : null def doc = wiki.getDocument(services.model.resolveDocument(fullName), context) def result if (doc.getXObject(oidcClass) != null) { result = 'skipped: already linked' } else if (upn == null) { result = '(% style="color:red" %)skipped: no UPN in CSV' } else if (linkedSubjects.containsKey(upn)) { result = "(% style=\"color:red\" %)conflict: subject already on ${linkedSubjects[upn]}" } else if (DRY_RUN) { result = 'would link' } else { doc = doc.clone() def obj = doc.newXObject(oidcClass, context) obj.setStringValue('issuer', ISSUER) obj.setStringValue('subject', upn) wiki.saveDocument(doc, 'Link LDAP profile to OIDC identity', true, context) result = 'linked' } println "|[[${fullName}]]|${uid}|${upn ?: ''}|${result}" } {{/groovy}}{code}
Patrick Koska on 04/Nov/26 11:13
Migration is usually a one-time operation. This probably is not worth a dedicated feature since setups and prior authentication methods might vary significantly. IMHO a script is the most flexible and easiest way to go.
The current implementation of OpenID Connect Authenticator only adds XWiki.OIDC.UserClass when creating new profiles. As far as I can tell it always uses the profile where the correspoding XWiki.OIDC.UserClass exists from then on. So basically you need to add an XWiki.OIDC.UserClass object to your existing LDAP users (or whatever you've been using so far).
I solved it by parsing for users with an XWiki.LDAPProfileClass object and adding an XWiki.OIDC.UserClass object to their profiles. Here is the script I used:
Hint: Do steps 2–4 before users start logging in through OIDC, so no new duplicates appear in between # Back up the database # Fill in {{ISSUER}} and {{{}CSV{}}}, then view the page with {{{}DRY_RUN = true{}}}. # Fix any red rows: missing UPNs, or conflicts with profiles from earlier test logins # Set {{DRY_RUN = false}} and view the page once more. # Set the OIDC configuration according to the format of your usernames, then restart: oidc.user.subjectFormater=${oidc.idtoken.upn._lowerCase} # Delete duplicate profiles after checking they hold nothing # If you use the Microsoft Entra v2.0 endpoint, make sure {{upn}} is in the ID token: add it as an optional claim under token configuration in the app registration
{code:java} {{groovy}} import org.xwiki.model.reference.LocalDocumentReference def DRY_RUN = true // Exactly the "iss" claim of the ID token (copy it from an OIDC-created profile) def ISSUER = 'https://login.microsoftonline.com/<tenant-id>/v2.0' // Contents of upn.csv: <LDAP uid>;<userPrincipalName> def CSV = ''' user;user@example.com ''' def upnByUid = [:] CSV.trim().eachLine { line -> def parts = line.split(';') if (parts.length == 2) { upnByUid[parts[0].trim().toLowerCase()] = parts[1].trim().toLowerCase() } } def oidcClass = new LocalDocumentReference(['XWiki', 'OIDC'], 'UserClass') def context = xcontext.context def wiki = context.wiki def linkedSubjects = [:] services.query.xwql('select oidc.subject, doc.fullName from Document doc, doc.object(XWiki.OIDC.UserClass) as oidc where oidc.issuer = :issuer') .bindValue('issuer', ISSUER).execute().each { linkedSubjects[it[0]] = it[1] } def profiles = services.query.xwql('select doc.fullName, ldap.uid from Document doc, doc.object(XWiki.LDAPProfileClass) as ldap order by doc.fullName').execute() println "|=Profile|=LDAP uid|=Subject|=Result" profiles.each { row -> def (fullName, uid) = [row[0], row[1]] def upn = uid ? upnByUid[uid.toLowerCase()] : null def doc = wiki.getDocument(services.model.resolveDocument(fullName), context) def result if (doc.getXObject(oidcClass) != null) { result = 'skipped: already linked' } else if (upn == null) { result = '(% style="color:red" %)skipped: no UPN in CSV' } else if (linkedSubjects.containsKey(upn)) { result = "(% style=\"color:red\" %)conflict: subject already on ${linkedSubjects[upn]}" } else if (DRY_RUN) { result = 'would link' } else { doc = doc.clone() def obj = doc.newXObject(oidcClass, context) obj.setStringValue('issuer', ISSUER) obj.setStringValue('subject', upn) wiki.saveDocument(doc, 'Link LDAP profile to OIDC identity', true, context) result = 'linked' } println "|[[${fullName}]]|${uid}|${upn ?: ''}|${result}" } {{/groovy}}{code}
Patrick Koska on 04/Nov/26 11:15
Migration is usually a one-time operation. This probably is not worth a dedicated feature since setups and prior authentication methods might vary significantly. IMHO a script is the most flexible and easiest way to go.
The current implementation of OpenID Connect Authenticator only adds XWiki.OIDC.UserClass when creating new profiles users. As far as I can tell it always uses the profile where the correspoding XWiki.OIDC.UserClass exists from then on. So basically you need to add an XWiki.OIDC.UserClass object to your existing LDAP users (or whatever you've been using so far).
I solved it by parsing for users with an XWiki.LDAPProfileClass object and adding an XWiki.OIDC.UserClass object to their profiles. Here is the script I used:
Hint: Do steps 2–4 before users start logging in through OIDC, so no new duplicates appear in between # Back up the database # Fill in {{ISSUER}} and {{{}CSV{}}}, then view the page with {{{}DRY_RUN = true{}}}. # Fix any red rows: missing UPNs, or conflicts with profiles from earlier test logins # Set {{DRY_RUN = false}} and view the page once more. # Set the OIDC configuration according to the format of your usernames, then restart: oidc.user.subjectFormater=${oidc.idtoken.upn._lowerCase} # Delete duplicate profiles after checking they hold nothing # If you use the Microsoft Entra v2.0 endpoint, make sure {{upn}} is in the ID token: add it as an optional claim under token configuration in the app registration
{code:java} {{groovy}} import org.xwiki.model.reference.LocalDocumentReference def DRY_RUN = true // Exactly the "iss" claim of the ID token (copy it from an OIDC-created profile) def ISSUER = 'https://login.microsoftonline.com/<tenant-id>/v2.0' // Contents of upn.csv: <LDAP uid>;<userPrincipalName> def CSV = ''' user;user@example.com ''' def upnByUid = [:] CSV.trim().eachLine { line -> def parts = line.split(';') if (parts.length == 2) { upnByUid[parts[0].trim().toLowerCase()] = parts[1].trim().toLowerCase() } } def oidcClass = new LocalDocumentReference(['XWiki', 'OIDC'], 'UserClass') def context = xcontext.context def wiki = context.wiki def linkedSubjects = [:] services.query.xwql('select oidc.subject, doc.fullName from Document doc, doc.object(XWiki.OIDC.UserClass) as oidc where oidc.issuer = :issuer') .bindValue('issuer', ISSUER).execute().each { linkedSubjects[it[0]] = it[1] } def profiles = services.query.xwql('select doc.fullName, ldap.uid from Document doc, doc.object(XWiki.LDAPProfileClass) as ldap order by doc.fullName').execute() println "|=Profile|=LDAP uid|=Subject|=Result" profiles.each { row -> def (fullName, uid) = [row[0], row[1]] def upn = uid ? upnByUid[uid.toLowerCase()] : null def doc = wiki.getDocument(services.model.resolveDocument(fullName), context) def result if (doc.getXObject(oidcClass) != null) { result = 'skipped: already linked' } else if (upn == null) { result = '(% style="color:red" %)skipped: no UPN in CSV' } else if (linkedSubjects.containsKey(upn)) { result = "(% style=\"color:red\" %)conflict: subject already on ${linkedSubjects[upn]}" } else if (DRY_RUN) { result = 'would link' } else { doc = doc.clone() def obj = doc.newXObject(oidcClass, context) obj.setStringValue('issuer', ISSUER) obj.setStringValue('subject', upn) wiki.saveDocument(doc, 'Link LDAP profile to OIDC identity', true, context) result = 'linked' } println "|[[${fullName}]]|${uid}|${upn ?: ''}|${result}" } {{/groovy}}{code}
This message was sent by Atlassian Jira (v9.3.0#930000-sha1:287aeb6)
If image attachments aren't displayed, see this article.