Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
af28bed
implement pre-issue access token action versioning and token exchange…
Lashen1227 Dec 17, 2025
7ecfab2
Merge branch 'wso2-extensions:master' into feat/pre-issue-access-toke…
Lashen1227 Dec 17, 2025
737570f
refactor: improve log initialization formatting in PreIssueAccessToke…
Lashen1227 Dec 17, 2025
ed8cbb0
feat: add support for actor claim in access token generation
Lashen1227 Dec 29, 2025
e4579dc
feat: enhance user resolution for federated and local users in access…
Lashen1227 Jan 4, 2026
082f0cf
add pre-issue access token action v1 tests
Lashen1227 Jan 14, 2026
b90e490
fix: missing scopes in the request
Lashen1227 Jan 20, 2026
4a1d779
fix: update tenant handling for suborg logins
Lashen1227 Jan 20, 2026
80c6760
fix: use login tenant organization when building pre-issue access tok…
Lashen1227 Jan 21, 2026
33a32cd
feat: add unit tests for PreIssueAccessTokenRequestBuilderV2Test abd …
Lashen1227 Jan 23, 2026
4d60a27
feat: enhance handling of nested claims in access token processing
Lashen1227 Feb 8, 2026
08d96d3
test: improve formatting in PreIssueAccessTokenResponseProcessorTest
Lashen1227 Feb 8, 2026
88e2b10
feat: update action trigger evaluation for additional grant types in V2
Lashen1227 Feb 8, 2026
3b6bb18
feat: add support for SAML2 bearer grant type in pre issue access tok…
Lashen1227 Feb 12, 2026
644953a
fix: missing accessingOrganization in suborg to root org switch
Lashen1227 Feb 12, 2026
993a243
fix: enhance nested claim processing to handle array index paths
Lashen1227 Feb 22, 2026
ee822e2
feat: implement nested claim removal and addition of custom object su…
Lashen1227 Feb 23, 2026
b49f29e
feat: add CIBA grant type
Lashen1227 Mar 2, 2026
8058776
Merge branch 'master' into feat/pre-issue-access-token-action-token-e…
Lashen1227 Mar 2, 2026
a743cfa
fix: update copyright year
Lashen1227 Mar 2, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ public static class GrantTypes {
public static final String ORGANIZATION_SWITCH = "organization_switch";
public static final String TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange";
public static final String CIBA = "urn:openid:params:grant-type:ciba";
public static final String SAML20_BEARER = "urn:ietf:params:oauth:grant-type:saml2-bearer";

private GrantTypes() {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.wso2.carbon.identity.oauth.action.constant;

/**
* Constants related to Pre Issue Access Token Action.
*/
public class PreIssueAccessTokenActionConstants {

public static final String ACTION_VERSION_V1 = "v1";
public static final String ACTION_VERSION_V2 = "v2";

private PreIssueAccessTokenActionConstants() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.wso2.carbon.identity.oauth.action.execution;

import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException;
import org.wso2.carbon.identity.action.execution.api.model.ActionExecutionRequestContext;
import org.wso2.carbon.identity.action.execution.api.model.ActionType;
import org.wso2.carbon.identity.action.execution.api.model.FlowContext;
import org.wso2.carbon.identity.action.execution.api.service.impl.DefaultActionVersioningHandler;
import org.wso2.carbon.identity.action.management.api.model.Action;
import org.wso2.carbon.identity.oauth.action.versioning.ActionTriggerEvaluatorFactory;
import org.wso2.carbon.identity.oauth.action.versioning.ActionTriggerEvaluatorForVersion;

/**
* Implementation of the Version handler for pre issue access token action.
*/
public class PreIssueAccessTokenActionVersioningHandler extends DefaultActionVersioningHandler {

ActionTriggerEvaluatorFactory factory = ActionTriggerEvaluatorFactory.getInstance();

@Override
public ActionType getSupportedActionType() {

return ActionType.PRE_ISSUE_ACCESS_TOKEN;
}

@Override
public boolean canExecute(ActionExecutionRequestContext actionExecutionRequestContext, FlowContext flowContext)
throws ActionExecutionException {

Action action = actionExecutionRequestContext.getAction();
ActionTriggerEvaluatorForVersion versionTriggerEvaluator = factory.getVersionTriggerEvaluator(action);

if (!versionTriggerEvaluator.isTriggerableForActionV2SupportedGrants(
actionExecutionRequestContext.getActionType(), action, flowContext)) {
return false;
}

return true;
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -407,10 +407,12 @@ private OperationExecutionResult addToOtherClaims(PerformableOperation operation
}

Object claimValue = claim.getValue();
if (isValidPrimitiveValue(claimValue) || isValidListValue(claimValue)) {
if (isValidPrimitiveValue(claimValue)
|| isValidListValue(claimValue)
|| isValidMapValue(claimValue)) {
responseAccessToken.addClaim(claim.getName(), claimValue);
return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS, "Claim added.");

return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS,
"Claim added.");
} else {
return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE,
"Invalid claim value.");
Expand Down Expand Up @@ -510,10 +512,18 @@ private OperationExecutionResult removeOtherClaims(PerformableOperation operatio
AccessToken requestAccessToken,
AccessToken.Builder responseAccessToken) {

List<String> pathSegments = extractNestedClaimPath(operation.getPath());

// nested removal
if (pathSegments.size() > 1 && !isArrayIndexPath(pathSegments)) {
return removeNestedClaim(pathSegments, requestAccessToken, responseAccessToken, operation);
}
ClaimPathInfo claimPathInfo = parseOperationPath(operation.getPath());
AccessToken.Claim claim = requestAccessToken.getClaim(claimPathInfo.getClaimName());

if (claim == null) {
return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE, "Claim not found.");
return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE,
"Claim not found.");
}

if (claimPathInfo.getIndex() != -1) {
Expand All @@ -524,6 +534,57 @@ private OperationExecutionResult removeOtherClaims(PerformableOperation operatio
}
}

private OperationExecutionResult removeNestedClaim(List<String> pathSegments, AccessToken requestAccessToken,
AccessToken.Builder responseAccessToken,
PerformableOperation operation) {

String rootClaimName = pathSegments.get(0);
List<String> nestedPath = pathSegments.subList(1, pathSegments.size());

AccessToken.Claim rootClaim = requestAccessToken.getClaim(rootClaimName);
if (rootClaim == null || !(rootClaim.getValue() instanceof Map)) {
return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE,
"Root claim is not a complex object.");
}

Map<String, Object> rootValue = new HashMap<>((Map<String, Object>) rootClaim.getValue());
boolean removed = removeFromNestedMap(rootValue, nestedPath, 0);
if (!removed) {
return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE,
"Nested claim not found.");
}

// replace claim in response token
responseAccessToken.getClaims().removeIf(c -> c.getName().equals(rootClaimName));
if (!rootValue.isEmpty()) {
responseAccessToken.addClaim(rootClaimName, rootValue);
}

return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS,
"Nested claim removed.");
}

private boolean removeFromNestedMap(Map<String, Object> current, List<String> path, int index) {

String key = path.get(index);
if (index == path.size() - 1) {
return current.remove(key) != null;
}

Object next = current.get(key);
if (!(next instanceof Map)) {
return false;
}

return removeFromNestedMap((Map<String, Object>) next, path, index + 1);
}

private List<String> extractNestedClaimPath(String operationPath) {

String relativePath = operationPath.substring(ACCESS_TOKEN_CLAIMS_PATH_PREFIX.length());
return List.of(relativePath.split("/"));
}

private OperationExecutionResult removeClaimValueAtIndexFromArrayTypeClaim(PerformableOperation operation,
ClaimPathInfo claimPathInfo,
AccessToken.Claim claim,
Expand Down Expand Up @@ -671,19 +732,78 @@ private OperationExecutionResult replaceExpiresIn(PerformableOperation operation
private OperationExecutionResult replaceOtherClaims(PerformableOperation operation, AbstractToken token,
AbstractToken.AbstractBuilder<?> tokenBuilder) {

List<String> pathSegments = extractNestedClaimPath(operation.getPath());
// nested replace
if (pathSegments.size() > 1 && !isArrayIndexPath(pathSegments)) {
return replaceNestedClaim(pathSegments, token, tokenBuilder, operation);
}

ClaimPathInfo claimPathInfo = parseOperationPath(operation.getPath());
AccessToken.Claim claim = token.getClaim(claimPathInfo.getClaimName());

if (claim == null) {
return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE,
"Claim not found.");
}

if (claimPathInfo.getIndex() != -1) {
return replaceClaimValueAtIndexFromArrayTypeClaim(operation, claimPathInfo, claim, tokenBuilder);
} else {
return replacePrimitiveTypeClaim(operation, claimPathInfo, tokenBuilder);
}

return replacePrimitiveTypeClaim(operation, claimPathInfo, tokenBuilder);
}

private OperationExecutionResult replaceNestedClaim(List<String> pathSegments, AbstractToken token,
AbstractToken.AbstractBuilder<?> tokenBuilder,
PerformableOperation operation) {

String rootClaimName = pathSegments.get(0);
List<String> nestedPath = pathSegments.subList(1, pathSegments.size());

AccessToken.Claim rootClaim = token.getClaim(rootClaimName);
if (rootClaim == null || !(rootClaim.getValue() instanceof Map)) {
return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE,
"Root claim is not a complex object.");
}

Map<String, Object> rootValue =
new HashMap<>((Map<String, Object>) rootClaim.getValue());
boolean replaced = replaceInNestedMap(rootValue, nestedPath, 0, operation.getValue());
if (!replaced) {
return new OperationExecutionResult(operation, OperationExecutionResult.Status.FAILURE,
"Nested claim not found.");
}

tokenBuilder.getClaims().removeIf(c -> c.getName().equals(rootClaimName));
if (!rootValue.isEmpty()) {
tokenBuilder.addClaim(rootClaimName, rootValue);
}

return new OperationExecutionResult(operation, OperationExecutionResult.Status.SUCCESS,
"Nested claim replaced.");
}

private boolean replaceInNestedMap(Map<String, Object> current, List<String> path, int index, Object newValue) {

String key = path.get(index);

if (index == path.size() - 1) {
if (newValue == null) {
return current.remove(key) != null;
}
current.put(key, newValue);
return true;
}

Object next = current.get(key);
if (!(next instanceof Map)) {
return false;
}
boolean updated = replaceInNestedMap((Map<String, Object>) next, path, index + 1, newValue);
Map<String, Object> nextMap = (Map<String, Object>) next;
if (updated && nextMap.isEmpty()) {
current.remove(key);
}

return updated;
}

private OperationExecutionResult replaceClaimValueAtIndexFromArrayTypeClaim(PerformableOperation operation,
Expand Down Expand Up @@ -813,6 +933,15 @@ private boolean isValidListValue(Object value) {
return list.stream().allMatch(item -> item instanceof String);
}

private boolean isValidMapValue(Object value) {

if (!(value instanceof Map<?, ?>)) {
return false;
}
Map<?, ?> map = (Map<?, ?>) value;
return true;
}

private int validateIndex(String operationPath, int listSize) {

String indexPart = operationPath.substring(operationPath.lastIndexOf(PATH_SEPARATOR) + 1);
Expand All @@ -836,6 +965,25 @@ private int validateIndex(String operationPath, int listSize) {
return -1;
}

private boolean isArrayIndexPath(List<String> pathSegments) {

if (pathSegments.size() != 2) {
return false;
}

String lastSegment = pathSegments.get(1);
if (LAST_ELEMENT_CHARACTER.equals(lastSegment)) {
return true;
}

try {
Integer.parseInt(lastSegment);
return true;
} catch (NumberFormatException e) {
return false;
}
}

private boolean validateNQChar(String input) {

Matcher matcher = NQCHAR_PATTERN.matcher(input);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ public enum ClaimNames {
CLIENT_ID("client_id"),
AUTHORIZED_USER_TYPE("aut"),
EXPIRES_IN("expires_in"),
ACT("act"),

TOKEN_BINDING_REF("binding_ref"),
TOKEN_BINDING_TYPE("binding_type"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.wso2.carbon.identity.oauth.action.versioning;

import org.wso2.carbon.identity.action.execution.api.exception.ActionExecutionException;
import org.wso2.carbon.identity.action.management.api.model.Action;
import org.wso2.carbon.identity.oauth.action.constant.PreIssueAccessTokenActionConstants;
import org.wso2.carbon.identity.oauth.action.versioning.v2.ActionTriggerEvaluatorForVersionV2;

/**
* Factory class for getting the ActionVersioningHandler by Action version.
*/
public class ActionTriggerEvaluatorFactory {

private static final ActionTriggerEvaluatorFactory instance = new ActionTriggerEvaluatorFactory();

public static ActionTriggerEvaluatorFactory getInstance() {

return instance;
}

public ActionTriggerEvaluatorForVersion getVersionTriggerEvaluator(Action action)
throws ActionExecutionException {

switch (action.getActionVersion()) {
case PreIssueAccessTokenActionConstants.ACTION_VERSION_V1:
return ActionTriggerEvaluatorForVersion.getInstance();
case PreIssueAccessTokenActionConstants.ACTION_VERSION_V2:
return ActionTriggerEvaluatorForVersionV2.getInstance();
default:
throw new ActionExecutionException("Unsupported action version: " + action.getActionVersion());
}
}
}
Loading
Loading