Intrexx OData Server Bearer Token Authentication

The Intrexx OData Server currently only offers HTTP Basic authentication with the Intrexx Portal Login modules. The OAuth2-OpendID Connect method possible in the portal is not supported here, as this requires the user to interact via the browser (Authorization Code Flow).

To be able to access an Intrexx OData Service with an previously authenticated OData Client, Intrexx offers the option of reusing client-side Access Tokens for authentication at the OData Service. Furthermore, as with the portal, there is an option to directly create Intrexx user accounts that do not yet exist via Groovy script hooks during the login, or to update existing ones.

Authentication procedure

OData Client

The client sends an Access Token or API Key in JSON format as an HTTP Authentication Bearer header to the OData Service login endpoint. Optionally, the identity provider can be specified via a query string parameter. An optional Refresh Token can be sent as another HTTP header (RefreshToken) to send further requests to the external IdentityProvider/API service in the portal at a later time.

GET http://10.10.101.128:9090/tokentest.svc/?idp=azure

Authorization: Bearer eyJ0eXAiOiJKV1QiLCJub25jZSI6IlJTU...

RefreshToken: eyJ0eXAiOiJKV1QiLCJub25jZSI6IlJTU...

Host: 10.10.101.128:9090

Content-Length: 0

Example in Groovy

import de.uplanet.lucy.server.odata.v4.consumer.http.MsGraphSdkAuthenticationProviderFactory
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpRequest.*
import java.net.http.HttpResponse
import java.net.http.HttpResponse.*
import com.google.gson.*
import com.google.gson.reflect.*

def clientId = "teams"  // Name der MS Graph OAuth2 Konfiguration
def authFactory = new MsGraphSdkAuthenticationProviderFactory()
def accessTokenProvider = authFactory.createForCurrentUser(clientId) // 1) Anmeldung mit aktuellem Portaluser (Authorization Code)

def token = accessTokenProvider.getAuthorizationTokenAsync(null).get()

def client = HttpClient.newBuilder().build()
def request = HttpRequest.newBuilder()
		.uri(URI.create("http://localhost:9090/tokentest.svc/?idp=azure"))
		.header("Authorization", "Bearer " + token)
		.header("Accept", "application/json")
		.GET()
		.build()

def response = client.send(request, BodyHandlers.ofString())
def statusCode = response.statusCode()
def body = response.body()
def cookie = response.headers().firstValue("Set-Cookie").orElse("")


g_log.info("Status code: " + statusCode)
g_log.info("Cookie: " + cookie)
g_log.info("Response body" + body)

if (statusCode != 200)
	throw new RuntimeException("Request failed")

g_sharedState["odataResponse"] = body

OData Server Token Validation

The OData Authentication Filter delegates access token validation to a user-specific Groovy script (internal/cfg/oauth2_validate_token.groovy). This must send a request to the external IdP based on the access token in order to validate the token and return the user details to Intrexx for further login. For this purpose, the script calls the IdP user endpoint and returns a Java HashMap instance with the user details (user name, email, etc.) in the case of success, or an exception in the case of error, and the OData request is answered with HTTP 401 after that. The script must not use any internal or public Intrexx APIs, since there is no security context in the server at the time of execution.

import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpRequest.*
import java.net.http.HttpResponse
import java.net.http.HttpResponse.*
import com.google.gson.*
import com.google.gson.reflect.*

def token = accessTokenDetails["accessToken"]
if (token == null)
	throw new RuntimeException("Invalid token")

if (accessTokenDetails["idp"] == "" || accessTokenDetails["idp"] == "azure") {
	g_log.info("Validating token with Azure")

	try {
		def client = HttpClient.newBuilder().build()
		def request = HttpRequest.newBuilder()
				.uri(URI.create("https://graph.microsoft.com/v1.0/me"))
				.header("Authorization", "Bearer " + token)
				.header("Accept", "application/json")
				.GET()
				.build()

		def response = client.send(request, BodyHandlers.ofString())
		def statusCode = response.statusCode()
		def body = response.body()

		def gson = new Gson()
		def mapType = new TypeToken<Map<String, Object>>(){}.getType()
		def result = gson.fromJson(body, mapType)

		return result
	} catch (e) {
		g_log.error(e.message, e)
		throw new RuntimeException(e)
	}
} else if (accessTokenDetails["idp"] == "salesforce") {
	// handle Salesforce authentication
} else {
	// use default provider or throw an exception
  throw new RuntimeException("Unknown provider")
}

OData Authentication Filter

The OData server now checks with the user details received from the script if the user exists in the user database based on the configured claim (user name, email, etc.). If this is not the case and automatic user registration is enabled, delegate to it and create a user account, otherwise terminate the request with HTTP 401.

Intrexx OAuth2 Login Module

The login module receives the user details as credential tokens and performs the user login in the Intrexx server.

OData Session Filter

After successful login via the login module, the access/refresh token is stored in the user session (optional). The OData request is now answered with HTTP 200 and contains the current session ID as HTTP Response Header Set-Cookie: co_SId=.... This must be resent to the OData service in further OData requests as an HTTP header cookie: co_SId=... in order to reuse the existing Intrexx session and not have to log in again.

Sample answer

200

Content-Type: application/xml; charset=utf-8

DataServiceVersion: 1.0

Set-Cookie: co_SId=...

<?xml version='1.0' encoding='utf-8' standalone='yes'?><service xmlns="http://www.w3.org/2007/app" xml:base="http://10.10.101.128:9090/tokentest.svc/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app"><workspace><atom:title>Default</atom:title><collection href="ixtokentest"><atom:title>ixtokentest</atom:title></collection></workspace></service>

Sample request with session cookie

GET http://10.10.101.128:9090/tokentest.svc/?idp=azure

Cookie: co_SId=...

Host: 10.10.101.128:9090

User registration

Automatic user creation can be enabled in the same way as for the OAuth2 login module and implemented in the internal/cfg/oauth2_token_user_registration.groovy file. This contains the user details map from the token validation script:

// creates new user after successful authentication

g_syslog.info(accessTokenDetails)

try
{
	// generate a random password
	def pwGuid = newGuid()
	def pw = g_om.getEncryptedPassword(["password": pwGuid])

	// create the new user
	def user = g_om.createUser {
		container     = "System" // name of the parent container for new users
		name          =  accessTokenDetails["displayName"]
		password      =  pw
		loginName     =  accessTokenDetails["mail"]
		emailBiz      =  accessTokenDetails["mail"]
		description   = "OIDC user created at ${now().withoutFractionalSeconds}"

		// a list of GUIDs or names of user groups
		memberOf = ["Users"]
	}

	g_syslog.info("Created user: ${user.loginName}")
	return true
}
catch (Exception e)
{
	g_syslog.error("Failed to create user: " + e.message, e)
	return false
}

User update

The automatic update of existing user accounts is implemented in the internal/cfg/oauth2_token_user_update.groovy file in the same way as for the OAuth2 login module:

// updates an existing user after successful authentication

g_syslog.info(accessTokenDetails)

try
{
	// log user details
	g_syslog.info(accessTokenDetails)
	g_syslog.info(accessTokenDetails["ixUserRecord"])

	// update user/roles etc. as in registration script

	return true
}
catch (Exception e)
{
	g_syslog.error("Failed to create user: " + e.message, e)
	return false
}

Configuration

Spring Beans

The following describes the Spring Bean settings in the internal/cfg/00-oauth2-context.xml file in the section <bean id="oAuth2BearerTokenLogin" ...>.

Token Claim User Attribute Mapping

To be able to identify a user account in Intrexx using a token attribute, a token attribute (e.g., name or email depending on the identity provider) is stored in the HashMap from the token validation script. An Intrexx user schema field is assigned to the key name of this value in the HashMap. At runtime, the value from the HashMap is compared with the value in the user data field.

<bean id="oAuth2BearerTokenLogin" class="de.uplanet.lucy.server.login.OAuth2BearerTokenLoginBean">

...

<property name="userClaimAttribute" value="mail" />

<property name="userClaimDbField" value="emailBiz" />

...

</bean>

  • userClaimAttribute: Name of the entry in the HashMap that contains the User Claim Token value

  • userClaimDbField: Name or GUID of an Intrexx user schema field

Groovy scripts

The paths to the Groovy scripts are stored in internal/cfg/spring/00-oauth2-context.xml:

<bean id="oAuth2BearerTokenLogin" class="de.uplanet.lucy.server.login.OAuth2BearerTokenLoginBean">

        <constructor-arg ref="portalPathProvider" />

        <property name="tokenValidationScript" value="internal/cfg/oauth2_token_validation.groovy" />

        <property name="userMappingScript" value="internal/cfg/oauth2_token_user_registration.groovy" />

        <property name="userUpdateScript" value="internal/cfg/oauth2_token_user_update.groovy" />

        <property name="userClaimAttribute" value="mail" />

        <property name="userClaimDbField" value="emailBiz" />

        <property name="userRegistrationEnabled" value="false" />

</bean>

Enabling user registration

User registration can be enabled via the property userRegistrationEnabled. By default, no user accounts are created automatically.

<bean id="oAuth2BearerTokenLogin" class="de.uplanet.lucy.server.login.OAuth2BearerTokenLoginBean">

...

<property name="userRegistrationEnabled" value="false" />

</bean>

LucyAuth.cfg

In the file internal/cfg/LucyAuth.cfg, the IntrexxOAuth2LoginModule must be added to the ODataAuth entry for the OData server:

ODataAuth

{

        de.uplanet.lucy.server.auth.module.intrexx.IntrexxOAuth2LoginModule sufficient

                de.uplanet.auth.compareClaimCaseInsensitive=true

                debug=false;

        de.uplanet.lucy.server.auth.module.intrexx.IntrexxLoginModule sufficient

                de.uplanet.auth.allowEmptyPassword=true

                debug=false;

        de.uplanet.lucy.server.auth.module.anonymous.AnonymousLoginModule sufficient

                debug=false;

};

More information

General

System requirements

Consume data

Provide data

Integration in applications

Use in processes

Expert settings

Appendix