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