Intrexx OData Server Bearer Token Authentifizierung

Der Intrexx OData Server bietet derzeit nur HTTP Basic-Authentifizierung mit den gegebenen Intrexx Portal Login Modulen. Die im Portal mögliche OAuth2-OpendID Connect Methode wird dabei nicht unterstützt, da diese eine Interaktion des Benutzers über den Browser erfordert (Authorization Code Flow).

Um mit einem bereits authentifizierten OData Client auf einen Intrexx OData Service zugreifen zu können, bietet Intrexx die Möglichkeit, client-seitig vorhandene Access Tokens für die Authentifizierung am OData Service wiederzuverwenden. Des Weiteren gibt es wie im Portal die Möglichkeit, noch nicht existierende Intrexx Benutzerkonten über Groovy Skript Hooks während der Anmeldung direkt zu erstellen oder bestehende zu aktualisieren.

Ablauf der Authentifizierung

OData Client

Der Client sendet einen Access Token oder API Key im JSON Format als HTTP Authentication Bearer Header an den OData Service Login Endpunkt. Optional kann über einen Query String Parameter der Identity Provider angegeben werden. Ein optionaler Refresh Token kann als weiterer HTTP Header (RefreshToken) gesendet werden, um im Portal zu einem späteren Zeitpunkt weitere Anfragen an den externen IdentityProvider/API Service zu senden.

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

Beispiel 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 Validierung

Der OData Authentication Filter delegiert die Access Token Validierung an ein benutzerspezifisches Groovy Skript (internal/cfg/oauth2_validate_token.groovy). Dieses muss anhand des Access Tokens einen Request an den externen IdP senden, um den Token zu validieren und die Benutzerdetails zur weiteren Anmeldung an Intrexx zurückzugeben. Dazu ruf das Skript den IdP User Endpunkt auf und liefert im Erfolgsfall eine Java HashMap Instanz mit den User Details (user name, email, etc.) oder im Fehlerfall eine Exception und die OData Anfrage wird im weiteren Verlauf mit HTTP 401 beantwortet. Das Skript darf keine internen oder öffentliche Intrexx APIs verwenden, da zu dem Zeitpunkt der Ausführung noch kein Sicherheitskontext im Server besteht.

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

Der OData Server prüft nun mit den aus dem Skript erhaltenen User Details, ob der Benutzer anhand des konfigurierten Claims (User Name, email, etc.) in der Benutzerdatenbank existiert. Ist dies nicht der Fall und die automatische Benutzerregistrierung aktiviert, so wird an diese delegiert und ein Benutzerkonto erstellt, ansonsten die Anfrage mit HTTP 401 beendet.

Intrexx OAuth2 Login Module

Das Login Modul erhält die User Details als Credential Tokens und führt die Anmeldung des Users im Intrexx Server durch.

OData Session Filter

Nach erfolgreicher Anmeldung über das Login Modul werden der Access/Refresh-Token in die User Session gespeichert (optional). Die OData Anfrage wird nun mit HTTP 200 beantwortet und enthält die aktuelle Session-ID als HTTP Response Header Set-Cookie: co_SId=.... Diese muss in weiteren OData Requests als HTTP Header Cookie: co_SId=... wieder an den OData Service gesendet werden, um die bestehende Intrexx Session wiederzuverwenden und nicht erneut die Anmeldung durchführen zu müssen.

Beispiel Antwort

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>

Beispiel Anfrage mit Session Cookie

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

Cookie: co_SId=...

Host: 10.10.101.128:9090

Benutzerregistrierung

Die automatische Benutzeranlage kann wie bei dem OAuth2 Login Modul aktiviert und in der Datei internal/cfg/oauth2_token_user_registration.groovy implementiert werden. Diese erhält die Benutzerdetails-Map aus dem Token-Validierungs-Skript:

// 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
}

Benutzeraktualisierung

Die automatische Benutzeraktualisierung von bestehenden User Accounts wird wie bei dem OAuth2 Login Modul in der Datei internal/cfg/oauth2_token_user_update.groovy implementiert:

// 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
}

Konfiguration

Spring Beans

Im Folgenden werden die Einstellungen des Spring Beans in der internal/cfg/00-oauth2-context.xml Datei im Abschnitt <bean id="oAuth2BearerTokenLogin" ...> beschrieben.

Token Claim User Attribute Mapping

Um ein Benutzerkonto in Intrexx anhand eines Token Attributs identifizieren zu können, wird in der zurückgegebenen HashMap aus dem Token Validierungsskript ein Token Attribut (z.B. name oder email je nach Identity Provider) gespeichert. Dem Key-Name dieses Werts in der HashMap wird ein Intrexx Benutzerschemafeld zugewiesen. Zur Laufzeit wird dann der Wert aus der HashMap mit dem Wert in dem Benutzerdatenfeld verglichen.

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

...

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

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

...

</bean>

  • userClaimAttribute: Name des Eintrags in der HashMap der den User Claim Token Wert enthält

  • userClaimDbField: Name oder GUID eines Intrexx Benutzerschemafelds

Groovy Skripte

Die Pfade zu den Groovy Skripten werden in der internal/cfg/spring/00-oauth2-context.xml hinterlegt:

<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>

Aktivierung der Benutzerregistrierung

Die Benutzerregistrierung kann über die Property userRegistrationEnabled aktiviert werden. Standardmäßig werden keine Benutzerkonten automatisch angelegt.

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

...

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

</bean>

LucyAuth.cfg

In der Datei internal/cfg/LucyAuth.cfg muss dem ODataAuth Eintrag für den OData Server das IntrexxOAuth2LoginModule hinzugefügt werden:

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;

};

Weitere Informationen

Allgemeines

Systemvoraussetzungen

Daten konsumieren

Daten anbieten

Integration in Applikationen

Verwendung in Prozessen

Expert-Settings

Appendix