Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(jans-auth-server): add Token Exchange interception script #8157 #10520

Merged
merged 14 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/janssen-server/developer/interception-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ request authorization for each scope, and display the respective scope descripti
calling external APIs
1. ID Generator
1. [Update Token](./scripts/update-token.md) : Enables transformation of claims and values in id_token, Access token and Refresh tokens; allows the setting of token lifetime; allows the addition or removal of scopes to / from tokens; allows the addition of audit logs each time a token is created.
1. [Token Exchange](./scripts/token-exchange.md) : Token Exchange custom script which allows to perform custom validation, send error response and modify existing response if needed.
1. Session Management
1. SCIM
1. [Introspection](./scripts/introspection.md) : Introspection scripts allows to modify response of Introspection Endpoint spec and present additional meta information surrounding the token.
Expand Down
1 change: 1 addition & 0 deletions docs/janssen-server/developer/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ overridden to implement your business case.
| [ID Generator](../../../script-catalog/id_generator/id-generator.md) | |
| [Update Token](../../../script-catalog/update_token/update-token.md) | Enables transformation of claims and values in id_token, Access token and Refresh tokens; allows the setting of token lifetime; allows the addition or removal of scopes to / from tokens; allows the addition of audit logs each time a token is created. |
| Session Management | |
| [Token Exchange](../../../script-catalog/token_exchange/token-exchange.md) | Token Exchange custom script which allows to perform custom validation, send error response and modify existing response if needed. |
| [SCIM](../../../script-catalog/scim/scim.md) | |
| [Introspection](../../../script-catalog/introspection/introspection.md) | Introspection scripts allows to modify response of Introspection Endpoint spec and present additional meta information surrounding the token. |
| [Post Authentication](../../../script-catalog/post_authn/post-authentication.md) | |
Expand Down
81 changes: 81 additions & 0 deletions docs/script-catalog/token_exchange/TokenExchange.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
Copyright (c) 2025, Gluu
Author: Yuriy Z
*/
import io.jans.as.common.model.common.User;
import io.jans.as.server.service.external.context.ExternalScriptContext;
import io.jans.model.SimpleCustomProperty;
import io.jans.model.custom.script.model.CustomScript;
import io.jans.model.custom.script.type.token.ScriptTokenExchangeControl;
import io.jans.model.custom.script.type.token.TokenExchangeType;
import io.jans.service.custom.script.CustomScriptManager;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

public class TokenExchange implements TokenExchangeType {

private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class);

@Override
public ScriptTokenExchangeControl validate(Object context) {

ExternalScriptContext scriptContext = (ExternalScriptContext) context;
String audience = scriptContext.getRequestParameter("audience");
String subjectToken = scriptContext.getRequestParameter("subject_token");
String subjectTokenType = scriptContext.getRequestParameter("subject_token_type");
String deviceSecret = scriptContext.getRequestParameter("actor_token");
String actorTokenType = scriptContext.getRequestParameter("actor_token_type");

// perform all validations here
boolean validationFailed = false;
if (validationFailed) {
return ScriptTokenExchangeControl.fail();
}

// user identified by request information or otherwise null
User user = new User();

return ScriptTokenExchangeControl
.ok()
.setSkipBuiltinValidation(true) // this skip build-in validations of all parameters that come
.setUser(user);
}

/**
* @param responseAsJsonObject - response represented by org.json.JSONObject
* @param context - script context represented by io.jans.as.server.service.external.context.ExternalScriptContext
* @return true if changes must be applied to final response or false if whatever made in this method has to be cancelled
*/
@Override
public boolean modifyResponse(Object responseAsJsonObject, Object context) {
JSONObject response = (JSONObject) responseAsJsonObject;
response.accumulate("key_from_script", "value_from_script");
return true; // return false if you wish to cancel whatever modification was made before
}

@Override
public boolean init(Map<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Initialized TokenExchange Java custom script.");
return true;
}

@Override
public boolean init(CustomScript customScript, Map<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Initialized TokenExchange Java custom script.");
return true;
}

@Override
public boolean destroy(Map<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Destroyed TokenExchange Java custom script.");
return false;
}

@Override
public int getApiVersion() {
return 11;
}
}
159 changes: 159 additions & 0 deletions docs/script-catalog/token_exchange/token-exchange.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
tags:
- administration
- developer
- script-catalog
---

# Token Exchange Custom Script

## Overview

The Jans-Auth server supports [OAuth 2.0 Token Exchange (RFC 8693)](https://tools.ietf.org/html/rfc8693),
which enables clients to exchange one type of token for another.
This capability allows scenarios such as delegation, constrained token issuance, and token transformation
to meet various security and interoperability requirements.

This script is used to control the Token Exchange behavior at Token Endpoint as defined by the OAuth 2.0 Token Exchange specification.

## Behavior

The Token Exchange Endpoint accepts requests where a client submits an existing token (the _subject token_) and
requests a new token (the _requested token_) with potentially different properties, scopes, or lifetimes.
Upon receiving a request, the endpoint validates the provided token, applies custom business or security logic,
and returns a response indicating whether the token exchange is approved.
If approved, the Token Endpoint issues a new token based on the rules defined in the custom script.

During token exchange processing, the `TokenExchange` custom script is executed. Jans-Auth includes a sample demo script that illustrates a simple example of custom token exchange logic.

**Sample request**
```http
POST /jans-auth/restv1/token HTTP/1.1
Host: happy-example.gluu.info
Content-Type: application/json
Authorization: Bearer eyJraWQiOiJr...

{
"subject_token": "eyJraWQiOiJr...",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"audience": "<audience>",
"actor_token": "eyJraWQiOiJr...",
"actor_token_type": "urn:ietf:params:oauth:token-type:access_token"
}
```

## Interface

The Token Exchange script implements the [TokenExchangeType](https://github.com/JanssenProject/jans/blob/main/jans-core/script/src/main/java/io/jans/model/custom/script/type/token/TokenExchangeType.java)
interface. This interface extends the base custom script type with methods specific to token exchange operations:

### Inherited Methods
| Method header | Method description |
|:-----|:------|
| `def init(self, customScript, configurationAttributes)` | This method is only called once during the script initialization. It can be used for global script initialization, initiate objects etc |
| `def destroy(self, configurationAttributes)` | This method is called once to destroy events. It can be used to free resource and objects created in the `init()` method |
| `def getApiVersion(self, configurationAttributes, customScript)` | The getApiVersion method allows API changes in order to do transparent migration from an old script to a new API. Only include the customScript variable if the value for getApiVersion is greater than 10 |

### New methods
| Method header | Method description |
|:-----|:------|
|`def validate(self, context)`| Called before built-in validation, main purpose is to perform all validations in this method. It must return `ScriptTokenExchangeControl`. |
|`def modifyResponse(self, responseAsJsonObject, context)`| Called after validation and actual token exchange. It allows to modify response or return custom error if needed. |


### Objects
| Object name | Object description |
|:-----|:------|
|`responseAsJsonObject`| Response represented as json by `org.json.JSONObject` |
|`context`| [Reference](https://github.com/JanssenProject/jans/blob/main/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/ExternalScriptContext.java) |


## Sample Demo Custom Script

### Script Type: Java

```java
package io.jans.as.server._scripts;

import io.jans.as.common.model.common.User;
import io.jans.as.server.service.external.context.ExternalScriptContext;
import io.jans.model.SimpleCustomProperty;
import io.jans.model.custom.script.model.CustomScript;
import io.jans.model.custom.script.type.token.ScriptTokenExchangeControl;
import io.jans.model.custom.script.type.token.TokenExchangeType;
import io.jans.service.custom.script.CustomScriptManager;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
* @author Yuriy Z
*/
public class TokenExchange implements TokenExchangeType {

private static final Logger scriptLogger = LoggerFactory.getLogger(CustomScriptManager.class);

@Override
public ScriptTokenExchangeControl validate(Object context) {

ExternalScriptContext scriptContext = (ExternalScriptContext) context;
String audience = scriptContext.getRequestParameter("audience");
String subjectToken = scriptContext.getRequestParameter("subject_token");
String subjectTokenType = scriptContext.getRequestParameter("subject_token_type");
String deviceSecret = scriptContext.getRequestParameter("actor_token");
String actorTokenType = scriptContext.getRequestParameter("actor_token_type");

// perform all validations here
boolean validationFailed = false;
if (validationFailed) {
return ScriptTokenExchangeControl.fail();
}

// user identified by request information or otherwise null
User user = new User();

return ScriptTokenExchangeControl
.ok()
.setSkipBuiltinValidation(true) // this skip build-in validations of all parameters that come
.setUser(user);
}

/**
* @param responseAsJsonObject - response represented by org.json.JSONObject
* @param context - script context represented by io.jans.as.server.service.external.context.ExternalScriptContext
* @return true if changes must be applied to final response or false if whatever made in this method has to be cancelled
*/
@Override
public boolean modifyResponse(Object responseAsJsonObject, Object context) {
JSONObject response = (JSONObject) responseAsJsonObject;
response.accumulate("key_from_script", "value_from_script");
return true; // return false if you wish to cancel whatever modification was made before
}

@Override
public boolean init(Map<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Initialized TokenExchange Java custom script.");
return true;
}

@Override
public boolean init(CustomScript customScript, Map<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Initialized TokenExchange Java custom script.");
return true;
}

@Override
public boolean destroy(Map<String, SimpleCustomProperty> configurationAttributes) {
scriptLogger.info("Destroyed TokenExchange Java custom script.");
return false;
}

@Override
public int getApiVersion() {
return 11;
}
}

```
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public class RegisterRequest extends BaseRequest {
private List<String> spontaneousScopeScriptDns;
private List<String> updateTokenScriptDns;
private List<String> postAuthnScriptDns;
private List<String> tokenExchangeScriptDns;
private List<String> consentGatheringScriptDns;
private List<String> introspectionScriptDns;
private List<String> rptClaimsScriptDns;
Expand Down Expand Up @@ -198,6 +199,7 @@ public RegisterRequest() {
this.spontaneousScopeScriptDns = new ArrayList<>();
this.updateTokenScriptDns = new ArrayList<>();
this.postAuthnScriptDns = new ArrayList<>();
this.tokenExchangeScriptDns = new ArrayList<>();
this.consentGatheringScriptDns = new ArrayList<>();
this.introspectionScriptDns = new ArrayList<>();
this.rptClaimsScriptDns = new ArrayList<>();
Expand Down Expand Up @@ -1701,6 +1703,26 @@ public void setPostAuthnScriptDns(List<String> postAuthnScriptDns) {
this.postAuthnScriptDns = postAuthnScriptDns;
}

/**
* Gets token exchange script dns
*
* @return token exchange script dns
*/
public List<String> getTokenExchangeScriptDns() {
return tokenExchangeScriptDns;
}

/**
* Sets token exchange script dns
*
* @param tokenExchangeScriptDns token exchange script dns
* @return register request object
*/
public RegisterRequest setTokenExchangeScriptDns(List<String> tokenExchangeScriptDns) {
this.tokenExchangeScriptDns = tokenExchangeScriptDns;
return this;
}

/**
* Gets consent gathering script dns
*
Expand Down Expand Up @@ -1854,6 +1876,7 @@ public static RegisterRequest fromJson(JSONObject requestObject) throws JSONExce
result.setSpontaneousScopeScriptDns(extractListByKey(requestObject, SPONTANEOUS_SCOPE_SCRIPT_DNS.toString()));
result.setUpdateTokenScriptDns(extractListByKey(requestObject, UPDATE_TOKEN_SCRIPT_DNS.toString()));
result.setPostAuthnScriptDns(extractListByKey(requestObject, POST_AUTHN_SCRIPT_DNS.toString()));
result.setTokenExchangeScriptDns(extractListByKey(requestObject, TOKEN_EXCHANGE_SCRIPT_DNS.toString()));
result.setConsentGatheringScriptDns(extractListByKey(requestObject, CONSENT_GATHERING_SCRIPT_DNS.toString()));
result.setIntrospectionScriptDns(extractListByKey(requestObject, INTROSPECTION_SCRIPT_DNS.toString()));
result.setRptClaimsScriptDns(extractListByKey(requestObject, RPT_CLAIMS_SCRIPT_DNS.toString()));
Expand Down Expand Up @@ -2174,6 +2197,7 @@ public void getParameters(BiFunction<String, Object, Void> function) {
applyArray(function, SPONTANEOUS_SCOPE_SCRIPT_DNS, spontaneousScopeScriptDns);
applyArray(function, UPDATE_TOKEN_SCRIPT_DNS, updateTokenScriptDns);
applyArray(function, POST_AUTHN_SCRIPT_DNS, postAuthnScriptDns);
applyArray(function, TOKEN_EXCHANGE_SCRIPT_DNS, tokenExchangeScriptDns);
applyArray(function, CONSENT_GATHERING_SCRIPT_DNS, consentGatheringScriptDns);
applyArray(function, INTROSPECTION_SCRIPT_DNS, introspectionScriptDns);
applyArray(function, RPT_CLAIMS_SCRIPT_DNS, rptClaimsScriptDns);
Expand Down
Loading
Loading