Intrexx OData Server Bearer Token Authentication
The Intrexx OData Server currently supports only HTTP Basic authentication with the available Intrexx Portal login modules. The OAuth2 OpendID Connect method available in the portal is not supported, as it requires user interaction via the browser (Authorization Code Flow).
To access an Intrexx OData service using an already authenticated OData client, Intrexx allows you to reuse existing client-side access tokens for authentication with the OData service. Furthermore, just like in the portal, you can use Groovy script hooks during the login process to create new Intrexx user accounts or update existing ones.
Authentication Process
OData Client
The client sends an access token or API key in JSON format as an HTTP Bearer authentication header to the OData service login endpoint. Optionally, the identity provider can be specified using a query string parameter. An optional refresh token can be sent as an additional HTTP header (RefreshToken) to enable the portal to send further requests to the external identity provider/API service 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). It must use the access token to send a request to the external IdP in order to validate the token and return the user details to Intrexx for further authentication. To do this, the script calls the IdP user endpoint and, if successful, returns a Java HashMap instance containing the user details (username, email, etc.); if an error occurs, it throws an exception, and the OData request is subsequently responded to with an HTTP 401 status code. The script must not use any internal or public Intrexx APIs, since no security context exists on 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 uses the user details obtained from the script to check whether the user exists in the user database based on the configured claims (user name, email, etc.). If this is not the case and automatic user registration is enabled, the request is delegated to that process and a user account is created; otherwise, the request is terminated with an HTTP 401 response.
Intrexx OAuth2 Login Module
The login module receives the user details as credential tokens and logs the user into the Intrexx server.
OData Session Filter
After successfully logging in via the login module, the access/refresh token is stored in the user's session (optional). The OData request now receives an HTTP 200 response and includes the current session ID as the HTTP response header `Set-Cookie: co_SId=...`. This must be sent back to the OData service in subsequent OData requests as the HTTP header `Cookie: co_SId=...` in order to reuse the existing Intrexx session and avoid having 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 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>
Example Request with a 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 with the OAuth2 login module and implemented in the file internal/cfg/oauth2_token_user_registration.groovy. It receives 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
Automatic user updates for existing user accounts are implemented in the file `internal/cfg/oauth2_token_user_update.groovy`, just as with 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 within the <bean id="oAuth2BearerTokenLogin" ...> section.
Token Claim User Attribute Mapping
To identify a user account in Intrexx based on a token attribute, a token attribute (e.g., name or email, depending on the identity provider) from the token validation script is stored in the returned HashMap. 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 then 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 specified in the internal/cfg/spring/00-oauth2-context.xml file:
<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>
User Registration Activation
User registration can be enabled using the userRegistrationEnabled property. By default, no user accounts are created automatically.
...
<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;
};