ScriptState
Introduction and overview
The ScriptState is used to compile and evaluate scripts written in Groovy or JavaScript or any other JSR-223 (`http://www.jcp.org/en/jsr/detail?id=223) supported scripting language. With access to the request and response objects and other nevisAuth beans (see the table Beans available in the script's environment], special user requirements can be scripted and, thus, this state serves as an alternative to a custom AuthState implementation. The following table lists the differences between a custom AuthState and a script in the ScriptState:
Custom AuthState | ScriptState |
---|---|
Can be configured Can be tested with the nevisAuth test harness * Can be debugged | Rapid prototyping / rapid demonstrations No complex project setup Easy deployment No restart of nevisAuth necessary if script changes. |
Scripts can either be embedded into the AuthState or through an external file. If the script is stored in another file, changes to it will be detected without restarting the nevisAuth instance. The scripts are compiled into Java byte code and are recompiled only if an external script file is used and the file contents change.
Scripts can access context data from the current request. The following beans are bound to the script's context:
Bean name | Type | Description |
---|---|---|
request | AuthRequest | The request object. Can be used to access request data such as the resource, userid, client certificate, language and more. |
response | AuthResponse | The response object. Can be used to set response data such as the content, the auth level, cookies, the userid and more. |
dataPersistenceService | DataPersistenceService | The DataPersistenceService object as described in the [nevisAuth SDK](. |
LOG | Tracer | The logging service. |
session | java.util.Map | Contains the session data. If the session was not available, the system returns an empty java.util.Map. The changes to the Map will have no effect, that is, the creation of the session will not take place. |
notes | java.util.Properties | The collection of notes. |
outargs | java.util.Properties | The collection of outargs. |
inargs | java.util.Properties | The collection of inargs. |
parameters | java.util.Map | Defines an unmodifiable Map containing the configuration parameters. All configuration properties have the following name: parameter.[parameterName] . |
Description
The following table describes the characteristics of the AuthState.
Topic | Description |
---|---|
Class | ch.nevis.esauth.auth.states.scripting.ScriptState |
Logging | Script |
Auditing | none |
Marker | none |
Properties | scriptLanguage (string, "groovy")The language used in the script. Only JavaScript and Groovy are supported. Other JSR233 languages can be used if the appropriate libraries are put on the class path. |
script (string, -)Either the script itself or the path to the script on the file system. If the script is located on the file system, the value of the script property must start with file://.While it is possible to specify a multi-line script as the value of this property, this is strongly discouraged. During parsing of the AuthState XML, any line breaks present in this property value will be collapsed to single spaces, which can distort the syntax and semantics of the script. We recommend referencing a script file on the file system instead. | |
scriptTraceGroup (string, "Script")The trace group used for logging from the script. | |
parameter.[parameterName] (string, -)Properties prefixed with the string parameter are accessible from within the script via the parameters bean using the parameterName. For example: parameters.get("parameterName"). | |
Methods | process (all events) |
Input | depends on the script |
Transitions | depends on the script |
Output | depends on the script |
Errors | depends on the script 99: if the script fails to compile |
Notes | depends on the script |
Example inline scripting in JavaScript
<AuthState name="Script" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="true">
<ResultCond name="default" next="Script"/>
<Response value="AUTH_CONTINUE">
<Gui name="AuthDialog" label="login.test.label">
<GuiElem name="info" type="info"
label="${notes:result}"/>
<GuiElem name="input" type="text"
label="Print factorial of: "/>
<GuiElem name="submit" type="button"
label="submit.button.label" value="Continue"/>
</Gui>
</Response>
<property name="scriptLanguage" value="javascript"/>
<property name="script" value="notes.setProperty('result', Math.sqrt(parseInt(inargs.getProperty('input'))));"/>
</AuthState>
As explained above, multi-line inline scripts are possible but strongly discouraged. Use external scripts as described below instead.
Example referencing an external Groovy script
<AuthState name="Script" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="true">
<ResultCond name="default" next="Script"/>
<Response value="AUTH_CONTINUE">
<Gui name="AuthDialog" label="login.test.label">
<GuiElem name="info" type="info"
label="${notes:result}"/>
<GuiElem name="input" type="text"
label="Print factorial of: "/>
<GuiElem name="submit" type="button"
label="submit.button.label" value="Continue"/>
</Gui>
</Response>
<property name="script" value="file:///var/opt/nevisauth/default/conf/factorial.gy"/>
</AuthState>
Example Groovy script
// source of file: /var/opt/nevisauth/default/conf/factorial.gy
int number = Integer.parseInt(inargs.getProperty('input'));
notes.setProperty('result', 'Factorial of ' + number + ': ' + String.valueOf(factorial(number)));
def factorial(number) {
if (number == 1) {
return number;
} else {
return number * factorial(number - 1);
}
}
Example referencing an external JavaScript
<AuthState name="Script" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="true">
<ResultCond name="default" next="Script"/>
<Response value="AUTH_CONTINUE">
<Gui name="AuthDialog" label="login.test.label">
<GuiElem name="info" type="info" label="${notes:result}"/>
<GuiElem name="input" type="text" label="Print factorial of: "/>
<GuiElem name="submit" type="button" label="submit.button.label" value="Continue"/>
</Gui>
</Response>
<property name="scriptLanguage" value="javascript"/>
<property name="script" value="file:///var/opt/nevisauth/default/conf/factorial.js"/>
</AuthState>
Example JavaScript
// source of file: /var/opt/nevisauth/default/conf/factorial.j
var number = parseInt(inargs.getProperty('input'))
notes.setProperty('result', 'Factorial of ' + number + ': ' + factorial(number));
function factorial(number)
if (number == 1)
return number
} else
return number * factorial(number - 1)
}
}
HTTP processing examples (migration from HttpAuthState)
ScriptState AuthState configuration
<AuthState name="HttpScriptState" class="ch.nevis.esauth.auth.states.scripting.ScriptState" final="false">
<ResultCond name="error" next="AuthErrorGui"/>
<ResultCond name="ok" next="AuthDone"/>
<property name="parameter.url" value="https://siven.ch/application"/>
<property name="parameter.keystore-path" value="<path>/keystore.jks"/>
<property name="parameter.keystore-password" value="password"/>
<property name="parameter.keystore-type" value="PKCS12"/>
<property name="parameter.truststore-path" value="<path>/truststore.jks"/>
<property name="parameter.truststore-password" value="password"/>
<property name="parameter.truststore-type" value="PKCS12"/>
<property name="parameter.json" value="{ "attribute": "value" }"/>
<property name="script" value="file:///<path>/httpScript.groovy"/>
</AuthState>
HTTP processing using the core libraries
The following examples do not require additional libraries. They use the JDK core libraries to execute an HTTP request on the URL specified in the configuration, and to log the response.
HTTP GET request
The script in the following example creates a non-secure HTTP connection and logs the result of the operation.
HTTP GET request
import static ch.nevis.esauth.auth.engine.AuthConst.AUTH_FAILED
// This is assumed to be an HTTP URL
def url = parameters.get('url')
try {
def connection = new URL(url)
.openConnection()
connection.setRequestMethod('GET')
LOG.info('Response Message: ' + connection.getResponseMessage())
LOG.info('Response Status Code: ' + connection.getResponseCode())
def sb = new StringBuilder()
connection.getInputStream().withCloseable { stream ->
sb.append(stream.text)
}
LOG.info('Response: ' + sb)
response.setResult('ok')
} catch (all) {
// Handle exception and set the transition
LOG.error('error: ' + all, all)
response.setResult('error')
response.setError(AUTH_FAILED, 'Error description')
}
HTTP POST request
The script in the next example
- configures a truststore and a keystore to create a 2-way TLS connection (client authentication) with the server,
- executes an HTTP POST request, and
- logs the response contents.
HTTP POST request
import javax.net.ssl.KeyManager
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import java.security.KeyStore
import java.security.SecureRandom
import static ch.nevis.esauth.auth.engine.AuthConst.AUTH_FAILED
def trustManagers =
def trustStorePath = parameters.get('truststore-path')
new FileInputStream(trustStorePath).withCloseable { inputStream ->
def trustStoreType = parameters.get('truststore-type')
def trustStore = KeyStore.getInstance(trustStoreType)
def trustStorePassword = parameters.get('truststore-password').toCharArray()
trustStore.load(inputStream, trustStorePassword)
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(trustStore)
trustManagers.addAll(trustManagerFactory.trustManagers)
}
def keyManagers =
def keyStorePath = parameters.get('keystore-path')
new FileInputStream(keyStorePath).withCloseable { inputStream ->
def keyStoreType = parameters.get('keystore-type')
def keyStore = KeyStore.getInstance(keyStoreType)
def keyStorePassword = parameters.get('keystore-password').toCharArray()
keyStore.load(inputStream, keyStorePassword)
def keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, keyStorePassword)
keyManagers.addAll(keyManagerFactory.keyManagers)
}
def sslContext = SSLContext.getInstance('TLS')
sslContext.init((KeyManager[]) keyManagers.toArray(), (TrustManager[]) trustManagers.toArray(), new SecureRandom())
def url = parameters.get('url')
LOG.info('connecting to ' + url)
def connection = new URL(url)
.openConnection()
connection.setRequestMethod('POST')
connection.setDoOutput(true)
connection.setRequestProperty("Content-Type", "application/json;charset=utf-8")
connection.setSSLSocketFactory(sslContext.getSocketFactory())
def json = parameters.get('json')
try {
connection.getOutputStream().write(json.getBytes("UTF-8"))
connection.getOutputStream().close()
LOG.info('Response Message: ' + connection.getResponseMessage())
LOG.info('Response Status Code: ' + connection.getResponseCode())
def sb = new StringBuilder()
connection.getInputStream().withCloseable { stream ->
sb.append(stream.text)
}
LOG.info('Response: ' + sb)
response.setResult('ok')
} catch (all) {
// Handle exception
LOG.error('error: ' + all, all)
response.setResult('error')
response.setError(AUTH_FAILED, 'Error description')
}
HTTP processing using the Apache HTTP Client library
nevisAuth is delivered with the(https://hc.apache.org/httpcomponents-client-4.5.x/index.html) library. You can use this library to write Groovy scripts that can be executed by the ScriptState.
The script in the next example
- configures a truststore and a keystore to create a 2-way TLS connection (client authentication) with the server,
- executes an HTTP POST request, and
- logs the response contents.
There is no guarantee that nevisAuth will deliver the Apache HTTP Client library in the future. The next example is provided for convenience only, because you do not need additional libraries and it works for sure with the documented nevisAuth version. But be aware that scripts which require libraries delivered by nevisAuth might break after an upgrade. This can happen, for example, if nevisAuth upgrades the library to a version that is not compatible with the script, or simply if the library is not delivered anymore.
HTTP processing with Apache library
import org.apache.http.HttpHeaders
import org.apache.http.client.entity.EntityBuilder
import org.apache.http.client.methods.RequestBuilder
import org.apache.http.conn.ssl.DefaultHostnameVerifier
import org.apache.http.conn.ssl.SSLConnectionSocketFactory
import org.apache.http.entity.ContentType
import org.apache.http.impl.client.HttpClients
import org.apache.http.ssl.SSLContextBuilder
import static ch.nevis.esauth.auth.engine.AuthConst.AUTH_FAILED
// The URL to connect to is specified as a parameter in the AuthState configuration definition
def url = parameters.get('url')
LOG.info('connecting to ' + url)
// The truststore contains the public of the server to be contacted
def trustStoreFile = new File((String) parameters.get('truststore-path'))
def trustStorePassword = parameters.get('truststore-password').toCharArray()
// The client certificate to be used to authenticate with the server
def keyStoreFile = new File((String) parameters.get('keystore-path'))
def keyStorePassword = parameters.get('keystore-password').toCharArray()
// This hostname verifier will strictly verify that the hostname in
// the server certificate matches the hostname we contact
def hostnameVerifier = new DefaultHostnameVerifier()
def sslContext = SSLContextBuilder.create()
.loadTrustMaterial(trustStoreFile, trustStorePassword)
.loadKeyMaterial(keyStoreFile, keyStorePassword, keyStorePassword)
.build()
def socketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier)
try {
HttpClients.custom()
.setSSLSocketFactory(socketFactory)
.build()
.withCloseable { client ->
// Send a POST request with some JSON
def json = parameters.get('json')
def httpEntity = EntityBuilder.create()
.setText(json)
.build()
def request = RequestBuilder.post(url)
.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
.setEntity(httpEntity)
.build()
client.execute(request).withCloseable { response ->
// Manage the response: response.getStatusLine() (to get the HTTP status code
// information), response.getEntity() (to get the body of the response).
LOG.info(response.toString())
LOG.info(response.getEntity().getContent().getText())
}
}
response.setResult('default')
} catch (all) {
// Handle exception and set the transition
LOG.error('error: ' + all, all)
response.setResult('error')
response.setError(AUTH_FAILED, 'Error description')
}
HTTP processing using the HttpBuilder-NG library
The(https://http-builder-ng.github.io/http-builder-ng/) library is a popular Groovy library to execute HTTP requests.
The script in the next example
- configures a truststore and a keystore to create a 2-way TLS connection (client authentication) with the server,
- executes an HTTP GET request, and
- logs the response contents.
The HTTP response is supposed to be of content type application/fido.trusted-apps+json.
To be able to use this library " for additional information). For example, suppose you want to employ the HttpBuilder-NG library version 1.0.3, which uses core Java libraries. Then copy the files http-builder-ng-core-1.0.3.jarand xml-resolver-1.2.jar to one of the classpath directories, such as the default classpath directory /var/opt/nevisauth/<instance_name>/plugin .
This approach requires adding new dependencies to the classpath for the AuthEngine and the AuthStates. Perform this action with care: If you add libraries that are incompatible with the libraries delivered by nevisAuth, issues could occur when the AuthEngine or AuthStates use these incompatible libraries.
HTTP processing with HttpBuilder-NG library
import groovyx.net.http.NativeHandlers
import javax.net.ssl.*
import java.security.KeyStore
import java.security.SecureRandom
import static groovyx.net.http.HttpBuilder.configure
import static ch.nevis.esauth.auth.engine.AuthConst.AUTH_FAILED
def trustManagers =
def trustStorePath = parameters.get('truststore-path')
new FileInputStream(trustStorePath).withCloseable { inputStream ->
def trustStoreType = parameters.get('truststore-type')
def trustStore = KeyStore.getInstance(trustStoreType)
def trustStorePassword = parameters.get('truststore-password').toCharArray()
trustStore.load(inputStream, trustStorePassword)
trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(trustStore)
trustManagers.addAll(trustManagerFactory.trustManagers)
}
def keyManagers =
def keyStorePath = parameters.get('keystore-path')
new FileInputStream(keyStorePath).withCloseable { inputStream ->
def keyStoreType = parameters.get('keystore-type')
def keyStore = KeyStore.getInstance(keyStoreType)
def keyStorePassword = parameters.get('keystore-password').toCharArray()
keyStore.load(inputStream, keyStorePassword)
def keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, keyStorePassword)
keyManagers.addAll(keyManagerFactory.keyManagers)
}
def sslContext = SSLContext.getInstance('TLS')
sslContext.init((KeyManager[]) keyManagers.toArray(), (TrustManager[]) trustManagers.toArray(), new SecureRandom())
def url = parameters.get('url')
LOG.info('connecting to ' + url)
configure {
execution.sslContext = sslContext
request.uri = url
response.parser('application/fido.trusted-apps+json') { config, resp ->
if (resp.statusCode == 200) {
def json = NativeHandlers.Parsers.json(config, resp)
// Handle the JSON response (it is a LazyMap)
LOG.info(json.toString())
} else {
// Handle error
LOG.error('unexpected status code returned: ' + resp.statusCode)
response.setResult('error')
response.setError(AUTH_FAILED, 'Error description')
}
}
}.get()