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.
The same transformations also work in UserSync-connector. There may be slight differences between SAML-attributes and Connector-attributes that are described below.
Migration
If you upgraded from SAML SSO version 4.x and already used Groovy-transformation, check to 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 the Source type and you can enter the script code:
h
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!
StructuredData
The incoming data is represented as StructuredData (see ReTransform APIDocs).
StructuredData allows representing hierarchical data structures in a simplified way.
A StructuredData-object is either MapStructuredData, ListStructuredData or StringStructuredData
MapStructuredData behaves like a Map with Strings as keys and StructuredData as values (in fact, it implements Map<String,StructuredData>).
{
"username" : "Gonzo",
"fullname" : "Gonzo the Great"
}
ListStructuredData implements List<StructuredData>.
{
"groups" : [
"users",
"administrators"
]
}
StringStructuredData behaves like a String (at least mostly) and encapsulates a String-value.
{
"profile" : {
"phone" : "123456",
"department" : "Finance",
"coworkers" : ["Kermit","Piggy"]
}
}
When comparing another value to a StringStructuredData, this other value is converted to a String (using Java's String.valueOf()-method) and compared to the encapsulated value.
This has the effect that when comparing it to a numeric value, a lexicographical comparison happens.
When StructuredData is created from JSON using StructuredData.parseJson(jsonString), the jsonString is parsed using Groovy's JsonSluper (see https://docs.groovy-lang.org/latest/html/gapi/groovy/json/JsonSlurper.html) and all values are converted to Strings using String.valueOf()
Comparing StructuredData with Strings
With this data:
{
"con": { "name" : "Erwin" }
}
the value "Erwin" is available under con.name
. But as this Object is if type StringStructuredData
and not String
, comparing it to a String
could produce unexpected results:
name = con.name // Name is of type StructuredData
name.equals("Erwin") // true because equals() in StringStructuredData compares the String-value with the other String
"Erwin".equals(name) // false because equals() in String returns false if the types are different
"Erwin".equals(name.asString()) // true because name is explicitly turned into a String before the comparison, this is the recommended way.
Access attribute values
SAML
The Groovy Script's binding (in a Groovy-script, the Binding is the set of the Script's variables) contains a MapStructuredData-object mapping
which is containing the attributes.
When transforming attributes from the SAML Identity Provider in the SAML SingleSignOn-configuration, the attributes are ListStructuredData-Objects (because the SAML-assertions contain a value list for each attribute).
One way to find out what data is contained is to add the logging statement logger.warn(asJson(mapping)) and log in again with SAML. This will write a JSON representation of the data into the log file.
This should be avoided on a production instance as it clutters the log.
Here you can see an example.
After a successful login, the following info is sent in the SAML response.
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="https://daniel-jira-sd.lab.resolution.de/plugins/servlet/samlsso" ID="_3de5c781535665584082b183ba1bd179" InResponseTo="RESOLUTION_855253a1-c43b-4029-9573-33987bb26c8c" IssueInstant="2022-03-25T09:50:50.773Z" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://testidp.klab.resolution.de/daniel-jira-sd/</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">...</ds:Signature>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_593d78530a592540fb97ccaed14179bf" IssueInstant="2022-03-25T09:50:50.773Z" Version="2.0">
<saml2:Issuer>https://testidp.klab.resolution.de/daniel-jira-sd/</saml2:Issuer>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">user1@example.com</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData InResponseTo="RESOLUTION_855253a1-c43b-4029-9573-33987bb26c8c" NotBefore="2022-03-25T09:48:17.070Z" NotOnOrAfter="2022-03-25T10:48:17.070Z" Recipient="https://daniel-jira-sd.lab.resolution.de/plugins/servlet/samlsso"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="2022-03-25T09:48:17.070Z" NotOnOrAfter="2022-03-25T10:48:17.070Z"/>
<saml2:AuthnStatement AuthnInstant="2022-03-25T09:50:50.774Z" SessionIndex="_896152c080647c2e259ed06cf7cb4ebf">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
<saml2:AttributeStatement>
<saml2:Attribute Name="att1">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">gonzo</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">kermit</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">piggy</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="att2">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">piggy</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="att3">
<saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:string">kermit</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
</saml2:Assertion>
</saml2p:Response>
Above you see the SAML Assertion which contains the attributes att1, att2, and att3 as well as the NameID value. These attributes are transformed into ListStructuredData-Objects as shown below. This is a log excerpt that came from the logger.warn(asJson(mapping)) statement explained earlier.
/plugins/servlet/samlsso [d.r.retransform.impl.GroovyAttributeTransformerScript] {
"att1" : [ "gonzo", "kermit", "piggy" ],
"att2" : [ "piggy" ],
"att3" : [ "kermit" ],
"ATTR_NAMEID" : "user1@example.com",
"ATTR_SD_CUSTOMER" : [ "false" ]
}
The SAML NameID-value can be accessed under the special attribute ATTR_NAMEID
.
This expression returns the SAML NameID:
mapping.ATTR_NAMEID
In case the attribute contains special characters it needs to be enclosed in '':
mapping.'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
Returning data
The value returned from the script is used as the 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 a single attribute to a list.
In a Groovy-Script, the last statement's result is returned automatically, so
return someValue
can just be written as
someValue
In our example above we returned the attribute att1 for the Jira user property attribute demo.
There are some special values that can be returned. DROP, EMPTY, and DROP_ALL.
Remove the attribute
If the script returns null or the special value EMPTY, the attribute on an existing user is removed.
Taking the example above the IdP now sends the following attributes in the SAML response.
Att1 is not present anymore but it is still mapped to the Jira attribute Demo.
When the user now logs in via SAML with the attributes att12, att2, and att3 being sent by the IdP an empty list will be returned for the Jira Attribute Demo. If the script returns null, the attribute on an existing user is removed.
You can also return the special value EMPTY which has the same effect.
The attribute is removed from the user's profile page.
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.
We are doing the same test as in the previous section. Att1 was there before and updated the user's profile page.
The user logs in again via SAML but this time the attribute att1 is again not present.
When you now check the attribute transformations in the Tracker you see the "drop" is now true for this Jira attribute.
This means the attribute remains unchanged for the user.
Filter the user
If a user should be filtered, return the special value DROP_ALL.
UserSync
When transforming attributes in a UserSync-connector, mapping contains three MapStructuredData-Objects:
- The data delivered from the Connector is available under con. The structure depends on the Connector.
- If there is an existing user to be updated, this user's attributes are available under
existing
- If the connector is called during a SAML-authentication (because the update-method "update from Usersync-Connector" is configured for the IdP), the data from the SAML assertion is available under
saml
One way to find out what data is contained is to add this logging-statement:
logger.warn(asJson(mapping))
And trigger a single-user-sync. This will write a JSON-representation of the date to the log file. This should be avoided on a production-instance as it clutters the log.
Load another user
It's possible to load data from another user during the transformation. A possible use case for this is to store relationships between users, e.G. who the manager of the synced user is.
There are some helper method to be used in the script:
These methods return the user-key as String or null of no user is found:
findUserKeyByUsername(username) loads the user-key by the given username. This returns null if there is no user with that username. This may happen if the user to be found is not synced yet.
findUserKeyByEmail(email) loads the user-key by the given email-address. This returns null if there is no user with that username. This may happen if the user to be found is not synced yet. The transformation will fail if the email-address is not unique.
findUserKey(attribute_name,attributeValue) loads the user-key by another attribute
These method return a Map<String,Set<String>> with all attributes:
findUser(attribute_name,attributeValue)
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 log in 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"
]
}
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