There are 5 comments.
 
 
OpenId Connect / cid:jira-generated-image-avatar-cd6ca011-600c-4891-9f1a-3992c082502f OIDC-96 Open

Mapping users to existing users

 
View issue   ·   Add comment
 

5 comments

 
cid:jira-generated-image-avatar-305590f9-0760-4bcc-8fb9-84b897d9fa73 Patrick Koska on 04/Nov/26 11:05
 

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

  1. Back up the database
  2. Fill in ISSUER and CSV, then view the page with DRY_RUN = true.
  3. Fix any red rows: missing UPNs, or conflicts with profiles from earlier test logins
  4. Set DRY_RUN = false and view the page once more.
  5. Set the OIDC configuration according to the format of your usernames, then restart: oidc.user.subjectFormater=${oidc.idtoken.upn._lowerCase}
  6. Delete duplicate profiles after checking they hold nothing
  7. 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'  } 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}}

 

 
cid:jira-generated-image-avatar-305590f9-0760-4bcc-8fb9-84b897d9fa73 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. LocalDocumentReferencedef 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. contextdef context
def
wiki = context. wikidef 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}
 
 
cid:jira-generated-image-avatar-305590f9-0760-4bcc-8fb9-84b897d9fa73 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}
 
cid:jira-generated-image-avatar-305590f9-0760-4bcc-8fb9-84b897d9fa73 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}
 
cid:jira-generated-image-avatar-305590f9-0760-4bcc-8fb9-84b897d9fa73 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}