Important Update Effective February 1, 2024!
Due to recent changes in Jira and Confluence, we've made the tough decision to discontinue the OpenID Connect (OIDC)/OAuth app and no longer provide new versions for the newest Jira/Confluence releases as of January 31, 2024.
This is due to some necessary components no longer shipping with Jira/Confluence, which would require some extensive rewrites of the OIDC App.
Important Update! This app will be discontinued soon!
Due to recent changes in Jira, which no longer ships with some components required for our Read Receipts app to run, we've made the tough decision to discontinue the app, as of Februar 5, 2025.
Important Update! This app will be discontinued soon!
We've made the tough business decision to discontinue the app, as of January 11, 2025.
Transformations with Groovy
The Attributes coming from the SAML Identity Provider can be transformed using Groovy-scripts.
If you upgraded from version 4.x and already used Groovy-transformation, check migrate
General Information
General information about Groovy can be found here: https://groovy-lang.org/documentation.html
In the IdP's Attribute Mapping configuration, select Groovy Code as Source type and you can enter the script-code:
Access attribute values
All attributes coming from the SAML Identity Provider are available in the Script's Binding (in a Groovy-script, the Binding is the set of the Script's variables), so they are "just there" as variable. If the attribute name is not a valid identifier in Groovy, it can be addressed with mapping.'attribute name', e.G.
mapping.'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
The SAML NameID-value can be accessed under the special attribute ATTR_NAMEID
.
Returning data
The value returned from the script is used as value for the mapped attribute. Some conversion automatically happens:
- If the returned value is a list, all entries are converted to Strings. This conversion is done using String.valueOf() (see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html)
- If the returned value is not a list, it is converted to a String and added as single attribute to a list.
- There are some special values which can be returned, see below.
In a Groovy-Script, the last statement's result is returned automatically, so
return someValue
can just be written as
someValue
Ignore an attribute
If the script returns the special value DROP, the mapped attribute is not used to create or update the user. If an existing user already has this attribute, it is kept as it is.
Remove the attribute
If the script returns null or the special value EMPTY, the attribute on an existing user is removed.
Filter the user
If a user should be filtered, return the special value DROP_ALL. In this case, an existing user will be disabled and the authentication for the user will fail.
Logging
There is an SLF4j-logger available as logger:
logger.warn("Hello World!")
will occur in the logs log this:
2021-05-18 16:00:56,774+0000 ForkJoinPool.commonPool-worker-3 WARN [d.r.retransform.impl.GroovyAttributeTransformerScript] Hello World!
Special Implementation - Distinguish whether a user has logged into the JSM portal or into Jira
In SAML 5 we have implemented an additional attribute ATTR_SD_CUSTOMER. This attribute is not coming from the IdP but is set to "true" or "false" depending on the relative URL used by the user to sign into Jira.
To use the attribute you first need to add a new User Property in the Attribute mapping table of the Identity Provider tab.
Set the Jira attribute name and click Next
Enter the name of the attribute in the SAML response and click Apply
When you now login you will see this value being returned in the SAML response based on whether the user logs in as a service desk customer or a regular user. In the example below the user logged in as a regular user.
{
"nameId": "testuser",
"attributes": {
"groups": [
"testgroup"
],
"ATTR_SD_CUSTOMER": [
"false"
],
"ATTR_NAMEID": [
"testuser"
]
}
You can find example scripts below.
Examples
Login any user as guestuser
if the attribute groups contains guests.
This script should be mapped to the Application-attribute Username and assumes that the usernames comes from the SAML Name-ID:
// Check the group-attribute for the entry "guests"
if(mapping.groups.contains("guests")) {
// Return the static value "guestuser"
return "guestuser"
} else {
// Otherwise use the value from the Name ID
return ATTR_NAMEID
}
Set the last name to uppercase
This script should be applied to the Application-attribute Full Name and assumes the first name is in first
and the last name in last
// If the attribute lastName is present transform it to uppercase,
// otherwise used an empty string
def lastName = mapping.lastName[0] ? mapping.lastName[0].toUpperCase() : ""
// Groovy GStrings allow variable substition with $variable or ${expression}
// toString() is required here because GStrings must be explicitly turned into Java-Strings.
def fullName = "${mapping.firstName[0]} $lastName".toString()
// The SLF4J-logger de.resolution.retransform.impl.transformers.groovy.GroovyTransformerScript
// is available as logger and can be used to write to the application-log.
// warn is enabled by default, so this message should be visible in the log
logger.warn("########## This is the new full name: {}",fullName)
// Return the value
return fullName
Combine groups from attributes with the value true
In this example. the IdP sends a fixed set of group names as keys with the value true if the user is member of that group:
"attributes": {
"grp1": [ "true"],
"grp2": [ "true"],
"grp3": [ "false"]
},
def groups = []
if(mapping.grp1?.contains("true")) {
groups.add("grp1");
}
if(mapping.grp2?.contains("true")) {
groups.add("grp2");
}
if(mapping.grp3?.contains("true")) {
groups.add("grp3");
}
logger.warn("Groups are {}", groups)
return groups
Handle Groups Not Sent As Multivalue Attribute in SAML Response
def trafoMap = ["20368564" : "stash-users", "10096280" : "other-group"] // list of key/ value to replace group names after splitting
def splitted = mapping.Groups[0].split(",") // read "Groups" attribute from SAML Response split by comma
return splitted
.collect{trafoMap[it]} // apply transformation rules from trafoMap (search and replace)
.findAll{it} // filter null values a.k.a. drop groups not in the trafoMap
Transform one group from the SAML response to two or more groups
// Input your data as per the descriptions below
// Replace YourIDPGroupAttribute with your actual IdP Group Attribute
def idpGroupAttribute = "YourIDPGroupAttribute"
// Replace IdP_groupName with the group name that you need to transform
def idpGroupName = "IdP_groupName"
// Replace "replacement1" and "replacement2" with the actual group names replacements, and you can add other elements if needed
def replacements = ["replacement1", "replacement2"]
// No need to change anything in the following section
def groups = mapping.get(idpGroupAttribute)
if (groups.contains(idpGroupName)) {
groups.remove(idpGroupName)
groups.addAll(replacements)
}
return groups
Transform one group from the SAML response to two or more groups and also perform more direct transformations
// Input your data as per the descriptions below
// Replace YourIDPGroupAttribute with your actual IdP Group Attribute
// The example below is the Azure AD Default groups claim
def idpGroupAttribute = "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"
// Replace IdP_groupName_1 with the group name that you want to transform into multiple other groups
def idpGroupName_1 = "my-group-1"
// Add as many groups as you want to be assigned to the user, if idpGroupName_1 is present
def idpGroupName_1_transform_to_groups = ["your-group-1", "your-group-2"]
// No need to change anything in the following block, this takes
def groups = mapping.get(idpGroupAttribute)
if (groups.contains(idpGroupName_1)) {
groups.remove(idpGroupName_1)
groups.addAll(idpGroupName_1_transform_to_groups)
}
// Add more 1:1 replacements here
def idpGroupName_2 = "transform-me-1"
def idpGroupNameReplacement_2 = "your-group-3"
if (groups.contains(idpGroupName_2)) {
groups.remove(idpGroupName_2)
groups.add(idpGroupNameReplacement_2)
}
def idpGroupName_3 = "transform-me-2"
def idpGroupNameReplacement_3 = "your-group-4"
if (groups.contains(idpGroupName_3)) {
groups.remove(idpGroupName_3)
groups.add(idpGroupNameReplacement_3)
}
return groups
Allow user authentication based on the email domain of the user
// Check the email-attribute and if it is not empty check if it contains the email domain
if (mapping.email[0] != null) {
if (mapping.email[0].contains("@lab.resolution.de")) {
// if it contains the domain return the NameID
return mapping.ATTR_NAMEID
} else {
// Otherwise drop the authentication
return DROP_ALL
}
} else {
// if email-attribute and if it is empty log a warning message and drop the authentication
logger.warn("Dropping User authentication due to missing SAML attribute email")
return DROP_ALL
}
Add a user into a specific group when not calling the JSM portal and the IdP returns a specific group
def groupsToReturn = (groups != null && groups != DROP) ? groups : []
//Check ATTR_SD_CUSTOMER attribute is false and if user is in group g1, when both conditions are met add the user to the group jira-servicedesk-users
if(ATTR_SD_CUSTOMER != ["true"]) {
logger.warn("SD Portal is NOT called")
if(groupsToReturn.contains("g1")) {
logger.warn("User is in g1")
groupsToReturn.add("jira-servicedesk-users")
} else {
//If the user is not in group g1
logger.warn("User is not in g1")
}
//If user has called the JSM portal
} else {
//If user has called the JSM portal
logger.warn("SD Portal is called")
}
return groupsToReturn
Migrate Groovy-Transformations from version 4.x
The way to write transformation-scripts has changed with SAML 5.0. "old-style"-script should still work, but we strongly recommend to update them to the new way.
Attributes are available without mapping-prefix
The attributes are accessible directly in the script's Binding, so e.g. instead of mapping.ATTR_NAMEID
you can write ATTR_NAMEID
Return value instead of setting mapping.groovyResult
Instead of setting mapping.groovyResult = <valueToSet>
, just return the value. It is no longer necessary to create a list for this value.
Return DROP_ALL instead of setting mapping.drop = true
When a user should be dropped, return the predefined variable DROP_ALL
instead of setting mapping.drop = true