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.
...
<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;
};