diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenClientCredentialsGrantTestCase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenClientCredentialsGrantTestCase.java index 57b3af4cb9..a7fc8f8c9f 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenClientCredentialsGrantTestCase.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenClientCredentialsGrantTestCase.java @@ -18,6 +18,7 @@ package org.wso2.identity.integration.test.actions; +import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import org.apache.commons.lang.ArrayUtils; @@ -41,9 +42,21 @@ import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Factory; import org.testng.annotations.Test; import org.wso2.carbon.automation.engine.context.TestUserMode; import org.wso2.identity.integration.test.actions.mockserver.ActionsMockServer; +import org.wso2.identity.integration.test.actions.model.AccessToken; +import org.wso2.identity.integration.test.actions.model.ActionType; +import org.wso2.identity.integration.test.actions.model.AllowedOperation; +import org.wso2.identity.integration.test.actions.model.Operation; +import org.wso2.identity.integration.test.actions.model.PreIssueAccessTokenActionRequest; +import org.wso2.identity.integration.test.actions.model.PreIssueAccessTokenEvent; +import org.wso2.identity.integration.test.actions.model.Tenant; +import org.wso2.identity.integration.test.actions.model.TokenRequest; +import org.wso2.identity.integration.test.actions.model.User; +import org.wso2.identity.integration.test.actions.model.UserStore; import org.wso2.identity.integration.test.rest.api.server.action.management.v1.model.ActionModel; import org.wso2.identity.integration.test.rest.api.server.action.management.v1.model.AuthenticationType; import org.wso2.identity.integration.test.rest.api.server.action.management.v1.model.Endpoint; @@ -60,23 +73,27 @@ import org.wso2.identity.integration.test.rest.api.user.common.model.UserObject; import org.wso2.identity.integration.test.restclients.SCIM2RestClient; import org.wso2.identity.integration.test.utils.CarbonUtils; +import org.wso2.identity.integration.test.utils.FileUtils; import org.wso2.identity.integration.test.utils.OAuth2Constant; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import static org.wso2.identity.integration.test.utils.OAuth2Constant.ACCESS_TOKEN_ENDPOINT; import static org.wso2.identity.integration.test.utils.OAuth2Constant.AUTHORIZATION_HEADER; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS; /** * Integration test class for testing the pre issue access token flow with client credentials grant. @@ -100,19 +117,11 @@ public class PreIssueAccessTokenClientCredentialsGrantTestCase extends ActionsBa private static final String APPLICATION_AUDIENCE = "APPLICATION"; private static final String TEST_ROLE_APPLICATION = "test_role_application"; - private static final String INTERNAL_ACTION_MANAGEMENT_VIEW = "internal_action_mgt_view"; - private static final String INTERNAL_ACTION_MANAGEMENT_CREATE = "internal_action_mgt_create"; - private static final String INTERNAL_ACTION_MANAGEMENT_UPDATE = "internal_action_mgt_update"; - private static final String INTERNAL_ACTION_MANAGEMENT_DELETE = "internal_action_mgt_delete"; private static final String INTERNAL_ORG_USER_MANAGEMENT_LIST = "internal_org_user_mgt_list"; private static final String INTERNAL_ORG_USER_MANAGEMENT_VIEW = "internal_org_user_mgt_view"; private static final String INTERNAL_ORG_USER_MANAGEMENT_CREATE = "internal_org_user_mgt_create"; private static final String INTERNAL_ORG_USER_MANAGEMENT_UPDATE = "internal_org_user_mgt_update"; private static final String INTERNAL_ORG_USER_MANAGEMENT_DELETE = "internal_org_user_mgt_delete"; - private static final String INTERNAL_APPLICATION_MANAGEMENT_VIEW = "internal_application_mgt_view"; - private static final String INTERNAL_APPLICATION_MANAGEMENT_UPDATE = "internal_application_mgt_update"; - private static final String INTERNAL_API_RESOURCE_VIEW = "internal_api_resource_view"; - private static final String INTERNAL_API_RESOURCE_CREATE = "internal_api_resource_create"; private static final String CUSTOM_SCOPE_1 = "test_custom_scope_1"; private static final String CUSTOM_SCOPE_2 = "test_custom_scope_2"; private static final String CUSTOM_SCOPE_3 = "test_custom_scope_3"; @@ -122,27 +131,51 @@ public class PreIssueAccessTokenClientCredentialsGrantTestCase extends ActionsBa private static final String NEW_SCOPE_4 = "replaced_scope"; private static final String SCIM2_USERS_API = "/o/scim2/Users"; - private static final String ACTIONS_API = "/api/server/v1/actions"; - private static final String APPLICATION_MANAGEMENT_API = "/api/server/v1/applications"; - private static final String API_RESOURCE_MANAGEMENT_API = "/api/server/v1/api-resources"; - private static final String MOCK_SERVER_ENDPOINT = "/test/action"; - + private static final String CLAIMS_PATH_PREFIX = "/accessToken/claims/"; + private static final String SCOPES_PATH_PREFIX = "/accessToken/scopes/"; + private static final String MOCK_SERVER_ENDPOINT_RESOURCE_PATH = "/test/action"; + + private static final int UPDATED_EXPIRY_TIME_PERIOD = 7200; + private static final int CURRENT_EXPIRY_TIME_PERIOD = 3600; + private static final String MOCK_SERVER_AUTH_BASIC_USERNAME = "test"; + private static final String MOCK_SERVER_AUTH_BASIC_PASSWORD = "test"; private Lookup cookieSpecRegistry; private RequestConfig requestConfig; private CloseableHttpClient client; private SCIM2RestClient scim2RestClient; private List customScopes; + private List requestedScopes; private String accessToken; private String clientId; private String clientSecret; + private String subjectType; + private String tokenType; private String actionId; private String applicationId; private String domainAPIId; private String userId; private String roleId; + private String tenantId; private JWTClaimsSet jwtClaims; + private TestUserMode userMode; private ActionsMockServer actionsMockServer; + @Factory(dataProvider = "testExecutionContextProvider") + public PreIssueAccessTokenClientCredentialsGrantTestCase(TestUserMode testUserMode) { + + this.userMode = testUserMode; + this.tenantId = testUserMode == TestUserMode.SUPER_TENANT_USER ? "-1234" : "1"; + } + + @DataProvider(name = "testExecutionContextProvider") + public static Object[][] getTestExecutionContext() { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_USER}, + {TestUserMode.TENANT_USER} + }; + } + /** * Initializes Test environment and sets up necessary configurations. * @@ -151,7 +184,7 @@ public class PreIssueAccessTokenClientCredentialsGrantTestCase extends ActionsBa @BeforeClass(alwaysRun = true) public void testInit() throws Exception { - super.init(TestUserMode.TENANT_USER); + super.init(userMode); cookieSpecRegistry = RegistryBuilder.create() .register(CookieSpecs.DEFAULT, new RFC6265CookieSpecProvider()) @@ -179,34 +212,57 @@ protected boolean isRedirectable(String method) { OpenIDConnectConfiguration oidcConfig = getOIDCInboundDetailsOfApplication(applicationId); clientId = oidcConfig.getClientId(); clientSecret = oidcConfig.getClientSecret(); + subjectType = oidcConfig.getSubject().getSubjectType(); + tokenType = oidcConfig.getAccessToken().getType(); + if (!CarbonUtils.isLegacyAuthzRuntimeEnabled()) { - authorizeSystemAPIs(applicationId, new ArrayList<>(Arrays.asList(SCIM2_USERS_API, ACTIONS_API, - APPLICATION_MANAGEMENT_API, API_RESOURCE_MANAGEMENT_API))); + authorizeSystemAPIs(applicationId, Collections.singletonList(SCIM2_USERS_API)); } domainAPIId = createDomainAPI(EXTERNAL_SERVICE_NAME, EXTERNAL_SERVICE_URI, customScopes); authorizeDomainAPIs(applicationId, domainAPIId, customScopes); - addUserWithRole(applicationId, customScopes); - actionsMockServer = new ActionsMockServer(); + requestedScopes = new ArrayList<>(); + Collections.addAll(requestedScopes, + INTERNAL_ORG_USER_MANAGEMENT_LIST, + INTERNAL_ORG_USER_MANAGEMENT_VIEW, + INTERNAL_ORG_USER_MANAGEMENT_CREATE, + INTERNAL_ORG_USER_MANAGEMENT_UPDATE, + INTERNAL_ORG_USER_MANAGEMENT_DELETE); + requestedScopes.addAll(customScopes); - actionsMockServer.createMockServer(MOCK_SERVER_ENDPOINT); actionId = createPreIssueAccessTokenAction(); + + try { + Thread.sleep(3600); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + actionsMockServer = new ActionsMockServer(); + actionsMockServer.startServer(); + actionsMockServer.setupStub(MOCK_SERVER_ENDPOINT_RESOURCE_PATH, + "Basic " + getBase64EncodedString(MOCK_SERVER_AUTH_BASIC_USERNAME, MOCK_SERVER_AUTH_BASIC_PASSWORD), + FileUtils.readFileInClassPathAsString("actions/response/pre-issue-access-token-response.json")); } @AfterClass(alwaysRun = true) public void atEnd() throws Exception { + actionsMockServer.stopServer(); + deleteAction(PRE_ISSUE_ACCESS_TOKEN_API_PATH, actionId); deleteRole(roleId); deleteApp(applicationId); deleteDomainAPI(domainAPIId); scim2RestClient.deleteUser(userId); - actionsMockServer.shutDownMockServer(); + restClient.closeHttpClient(); scim2RestClient.closeHttpClient(); actionsRestClient.closeHttpClient(); client.close(); + + actionsMockServer = null; accessToken = null; jwtClaims = null; } @@ -217,19 +273,7 @@ public void testGetAccessTokenWithClientCredentialsGrant() throws Exception { List parameters = new ArrayList<>(); parameters.add(new BasicNameValuePair("grant_type", OAuth2Constant.OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS)); - List permissions = new ArrayList<>(); - Collections.addAll(permissions, - INTERNAL_ORG_USER_MANAGEMENT_LIST, - INTERNAL_ORG_USER_MANAGEMENT_VIEW, - INTERNAL_ORG_USER_MANAGEMENT_CREATE, - INTERNAL_ORG_USER_MANAGEMENT_UPDATE, - INTERNAL_ORG_USER_MANAGEMENT_DELETE - ); - permissions.addAll(customScopes); - - String scopes = permissions.stream() - .map(String::toLowerCase) - .collect(Collectors.joining(" ")); + String scopes = String.join(" ", requestedScopes); parameters.add(new BasicNameValuePair("scope", scopes)); List
headers = new ArrayList<>(); @@ -252,6 +296,19 @@ public void testGetAccessTokenWithClientCredentialsGrant() throws Exception { assertNotNull(jwtClaims); } + @Test(groups = "wso2.is", dependsOnMethods = "testGetAccessTokenWithClientCredentialsGrant", description = + "Verify the pre issue access token action request") + public void testPreIssueAccessTokenActionRequest() throws Exception { + + String actualRequestPayload = actionsMockServer.getReceivedRequestPayload(MOCK_SERVER_ENDPOINT_RESOURCE_PATH); + PreIssueAccessTokenActionRequest actualRequest = + new ObjectMapper().readValue(actualRequestPayload, PreIssueAccessTokenActionRequest.class); + + PreIssueAccessTokenActionRequest expectedRequest = getRequest(); + + assertEquals(actualRequest, expectedRequest); + } + @Test(groups = "wso2.is", description = "Verify that the access token contains the updated scopes " + "after action execution", dependsOnMethods = "testGetAccessTokenWithClientCredentialsGrant") public void testTokenScopeOperations() throws Exception { @@ -262,8 +319,8 @@ public void testTokenScopeOperations() throws Exception { Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_2)); Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_3)); Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_4)); - Assert.assertFalse(ArrayUtils.contains(scopes, CUSTOM_SCOPE_3)); - Assert.assertFalse(ArrayUtils.contains(scopes, CUSTOM_SCOPE_2)); + Assert.assertFalse(ArrayUtils.contains(scopes, INTERNAL_ORG_USER_MANAGEMENT_DELETE)); + Assert.assertFalse(ArrayUtils.contains(scopes, INTERNAL_ORG_USER_MANAGEMENT_CREATE)); } @Test(groups = "wso2.is", description = "Verify that the access token contains the updated 'aud' claims " + @@ -322,7 +379,89 @@ public void testTokenExpiresInClaimReplaceOperation() throws Exception { Date iat = jwtClaims.getDateClaim("iat"); long expiresIn = (exp.getTime() - iat.getTime()) / 1000; - Assert.assertEquals(expiresIn, 7200); + Assert.assertEquals(expiresIn, UPDATED_EXPIRY_TIME_PERIOD); + } + + /** + * Retrieves pre issue access token action request. + * + * @return pre issue access token request object + */ + private PreIssueAccessTokenActionRequest getRequest() { + + TokenRequest tokenRequest = createTokenRequest(); + AccessToken accessTokenInRequest = createAccessToken(); + + Tenant tenant = new Tenant(tenantId, tenantInfo.getDomain()); + + PreIssueAccessTokenEvent event = new PreIssueAccessTokenEvent.Builder() + .request(tokenRequest) + .accessToken(accessTokenInRequest) + .tenant(tenant) + .organization(null) + .build(); + + List allowedOperations = Arrays.asList( + createAllowedOperation(Operation.ADD, Arrays.asList(CLAIMS_PATH_PREFIX, SCOPES_PATH_PREFIX, + CLAIMS_PATH_PREFIX + AccessToken.ClaimNames.AUD.getName() + "/")), + createAllowedOperation(Operation.REMOVE, Arrays.asList(SCOPES_PATH_PREFIX, + CLAIMS_PATH_PREFIX + AccessToken.ClaimNames.AUD.getName() + "/")), + createAllowedOperation(Operation.REPLACE, Arrays.asList(SCOPES_PATH_PREFIX, + CLAIMS_PATH_PREFIX + AccessToken.ClaimNames.AUD.getName() + "/", + CLAIMS_PATH_PREFIX + AccessToken.ClaimNames.EXPIRES_IN.getName())) + ); + + return new PreIssueAccessTokenActionRequest.Builder() + .actionType(ActionType.PRE_ISSUE_ACCESS_TOKEN) + .event(event) + .allowedOperations(allowedOperations) + .build(); + } + + /** + * Creates token request. + * + * @return token request object + */ + private TokenRequest createTokenRequest() { + + return new TokenRequest.Builder() + .grantType(OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS) + .scopes(requestedScopes) + .clientId(clientId) + .build(); + } + + /** + * Creates access token. + * + * @return access token object + */ + private AccessToken createAccessToken() { + + List claims = new ArrayList<>(); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.ISS.getName(), + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain()))); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.CLIENT_ID.getName(), clientId)); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.AUTHORIZED_USER_TYPE.getName(), "APPLICATION")); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.EXPIRES_IN.getName(), CURRENT_EXPIRY_TIME_PERIOD)); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.AUD.getName(), Collections.singletonList(clientId))); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.SUBJECT_TYPE.getName(), subjectType)); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.SUB.getName(), clientId)); + + return new AccessToken.Builder() + .tokenType(tokenType) + .claims(claims) + .scopes(requestedScopes) + .build(); + } + + private AllowedOperation createAllowedOperation(Operation op, List paths) { + + AllowedOperation operation = new AllowedOperation(); + operation.setOp(op); + operation.setPaths(new ArrayList<>(paths)); + return operation; } /** @@ -336,8 +475,8 @@ private String createPreIssueAccessTokenAction() throws IOException { AuthenticationType authenticationType = new AuthenticationType(); authenticationType.setType(AuthenticationType.TypeEnum.BASIC); Map authProperties = new HashMap<>(); - authProperties.put(USERNAME_PROPERTY, TEST_USER); - authProperties.put(PASSWORD_PROPERTY, ADMIN_WSO2); + authProperties.put(USERNAME_PROPERTY, MOCK_SERVER_AUTH_BASIC_USERNAME); + authProperties.put(PASSWORD_PROPERTY, MOCK_SERVER_AUTH_BASIC_PASSWORD); authenticationType.setProperties(authProperties); Endpoint endpoint = new Endpoint(); @@ -406,6 +545,7 @@ private void addUserWithRole(String appID, List customScopes) throws Exc private List addPermissions(List customScopes) { List userPermissions = new ArrayList<>(); + Collections.addAll(userPermissions, new Permission(INTERNAL_ORG_USER_MANAGEMENT_LIST), new Permission(INTERNAL_ORG_USER_MANAGEMENT_VIEW), @@ -413,6 +553,7 @@ private List addPermissions(List customScopes) { new Permission(INTERNAL_ORG_USER_MANAGEMENT_UPDATE), new Permission(INTERNAL_ORG_USER_MANAGEMENT_DELETE) ); + customScopes.forEach(scope -> userPermissions.add(new Permission(scope))); return userPermissions; diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenCodeGrantTestCase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenCodeGrantTestCase.java new file mode 100644 index 0000000000..d51d9dbf5b --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenCodeGrantTestCase.java @@ -0,0 +1,701 @@ +/* + * Copyright (c) 2024, 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.identity.integration.test.actions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.apache.commons.lang.ArrayUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.config.Lookup; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.cookie.CookieSpecProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultRedirectStrategy; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.cookie.RFC6265CookieSpecProvider; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; +import org.wso2.carbon.automation.engine.context.TestUserMode; +import org.wso2.identity.integration.test.actions.mockserver.ActionsMockServer; +import org.wso2.identity.integration.test.actions.model.AccessToken; +import org.wso2.identity.integration.test.actions.model.ActionType; +import org.wso2.identity.integration.test.actions.model.AllowedOperation; +import org.wso2.identity.integration.test.actions.model.Operation; +import org.wso2.identity.integration.test.actions.model.PreIssueAccessTokenActionRequest; +import org.wso2.identity.integration.test.actions.model.PreIssueAccessTokenEvent; +import org.wso2.identity.integration.test.actions.model.Tenant; +import org.wso2.identity.integration.test.actions.model.TokenRequest; +import org.wso2.identity.integration.test.actions.model.User; +import org.wso2.identity.integration.test.actions.model.UserStore; +import org.wso2.identity.integration.test.oauth2.dataprovider.model.ApplicationConfig; +import org.wso2.identity.integration.test.oauth2.dataprovider.model.TokenScopes; +import org.wso2.identity.integration.test.oauth2.dataprovider.model.UserClaimConfig; +import org.wso2.identity.integration.test.rest.api.server.action.management.v1.model.ActionModel; +import org.wso2.identity.integration.test.rest.api.server.action.management.v1.model.AuthenticationType; +import org.wso2.identity.integration.test.rest.api.server.action.management.v1.model.Endpoint; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.ApplicationResponseModel; +import org.wso2.identity.integration.test.rest.api.server.application.management.v1.model.OpenIDConnectConfiguration; +import org.wso2.identity.integration.test.rest.api.server.roles.v2.model.Audience; +import org.wso2.identity.integration.test.rest.api.server.roles.v2.model.Permission; +import org.wso2.identity.integration.test.rest.api.server.roles.v2.model.RoleV2; +import org.wso2.identity.integration.test.rest.api.user.common.model.Email; +import org.wso2.identity.integration.test.rest.api.user.common.model.ListObject; +import org.wso2.identity.integration.test.rest.api.user.common.model.Name; +import org.wso2.identity.integration.test.rest.api.user.common.model.PatchOperationRequestObject; +import org.wso2.identity.integration.test.rest.api.user.common.model.RoleItemAddGroupobj; +import org.wso2.identity.integration.test.rest.api.user.common.model.UserObject; +import org.wso2.identity.integration.test.restclients.SCIM2RestClient; +import org.wso2.identity.integration.test.utils.CarbonUtils; +import org.wso2.identity.integration.test.utils.DataExtractUtil; +import org.wso2.identity.integration.test.utils.FileUtils; +import org.wso2.identity.integration.test.utils.OAuth2Constant; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.ACCESS_TOKEN_ENDPOINT; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.AUTHORIZATION_HEADER; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.AUTHORIZE_ENDPOINT_URL; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE; + +/** + * Integration test class for testing the pre issue access token flow with authorization code grant. + * This test case extends {@link ActionsBaseTestCase} and focuses on scenarios related + * to scopes and claims modifications through an external service. + */ +public class PreIssueAccessTokenCodeGrantTestCase extends ActionsBaseTestCase { + + private static final String USERS = "users"; + private static final String USERNAME_PROPERTY = "username"; + private static final String PASSWORD_PROPERTY = "password"; + private static final String TEST_USER = "test_user"; + private static final String ADMIN_WSO2 = "Admin@wso2"; + private static final String TEST_USER_FIRST_NAME = "test_user_given_name"; + private static final String REPLACED_FIRST_NAME = "replaced_given_name"; + private static final String TEST_USER_LAST_NAME = "test_user_last_name"; + private static final String TEST_USER_GMAIL_COM = "test.user@gmail.com"; + private static final String EXTERNAL_SERVICE_NAME = "TestExternalService"; + private static final String EXTERNAL_SERVICE_URI = "http://localhost:8587/test/action"; + private static final String APPLICATION_AUDIENCE = "APPLICATION"; + private static final String TEST_ROLE_APPLICATION = "test_role_application"; + private static final String OPENID_SCOPE = "openid"; + private static final String PROFILE_SCOPE = "profile"; + private static final String INTERNAL_ORG_USER_MANAGEMENT_LIST = "internal_org_user_mgt_list"; + private static final String INTERNAL_ORG_USER_MANAGEMENT_VIEW = "internal_org_user_mgt_view"; + private static final String INTERNAL_ORG_USER_MANAGEMENT_CREATE = "internal_org_user_mgt_create"; + private static final String INTERNAL_ORG_USER_MANAGEMENT_UPDATE = "internal_org_user_mgt_update"; + private static final String INTERNAL_ORG_USER_MANAGEMENT_DELETE = "internal_org_user_mgt_delete"; + private static final String CUSTOM_SCOPE_1 = "test_custom_scope_1"; + private static final String CUSTOM_SCOPE_2 = "test_custom_scope_2"; + private static final String CUSTOM_SCOPE_3 = "test_custom_scope_3"; + private static final String NEW_SCOPE_1 = "new_test_custom_scope_1"; + private static final String NEW_SCOPE_2 = "new_test_custom_scope_2"; + private static final String NEW_SCOPE_3 = "new_test_custom_scope_3"; + private static final String NEW_SCOPE_4 = "replaced_scope"; + private static final String CUSTOM_OIDC_SCOPE = "test_custom_oidc_scope"; + private static final String FIRST_NAME_CLAIM = "given_name"; + private static final String LAST_NAME_CLAIM = "family_name"; + private static final String PRE_ISSUE_ACCESS_TOKEN_API_PATH = "preIssueAccessToken"; + private static final String SCIM2_USERS_API = "/o/scim2/Users"; + private static final String CLAIMS_PATH_PREFIX = "/accessToken/claims/"; + private static final String SCOPES_PATH_PREFIX = "/accessToken/scopes/"; + private static final String MOCK_SERVER_ENDPOINT_RESOURCE_PATH = "/test/action"; + private static final int UPDATED_EXPIRY_TIME_PERIOD = 7200; + private static final int CURRENT_EXPIRY_TIME_PERIOD = 3600; + private static final String MOCK_SERVER_AUTH_BASIC_USERNAME = "test"; + private static final String MOCK_SERVER_AUTH_BASIC_PASSWORD = "test"; + private CloseableHttpClient client; + private SCIM2RestClient scim2RestClient; + private Lookup cookieSpecRegistry; + private RequestConfig requestConfig; + private List customScopes; + private List requestedScopes; + private TokenScopes appTokenScopes; + private String sessionDataKey; + private String authorizationCode; + private String accessToken; + private String clientId; + private String clientSecret; + private String subjectType; + private String tokenType; + private String actionId; + private String applicationId; + private String domainAPIId; + private String userId; + private String roleId; + private String tenantId; + private JWTClaimsSet jwtClaims; + private TestUserMode userMode; + private ActionsMockServer actionsMockServer; + + @Factory(dataProvider = "testExecutionContextProvider") + public PreIssueAccessTokenCodeGrantTestCase(TestUserMode testUserMode) { + + this.userMode = testUserMode; + this.tenantId = testUserMode == TestUserMode.SUPER_TENANT_USER ? "-1234" : "1"; + } + + @DataProvider(name = "testExecutionContextProvider") + public static Object[][] getTestExecutionContext() { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_USER}, + {TestUserMode.TENANT_USER} + }; + } + + /** + * Initializes Test environment and sets up necessary configurations. + * + * @throws Exception If an error occurs during initialization + */ + @BeforeClass(alwaysRun = true) + public void testInit() throws Exception { + + super.init(userMode); + + cookieSpecRegistry = RegistryBuilder.create() + .register(CookieSpecs.DEFAULT, new RFC6265CookieSpecProvider()) + .build(); + requestConfig = RequestConfig.custom() + .setCookieSpec(CookieSpecs.DEFAULT) + .build(); + client = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .setDefaultCookieSpecRegistry(cookieSpecRegistry) + .setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(String method) { + + return false; + } + }).build(); + + scim2RestClient = new SCIM2RestClient(serverURL, tenantInfo); + + customScopes = Arrays.asList(CUSTOM_SCOPE_1, CUSTOM_SCOPE_2, CUSTOM_SCOPE_3); + + applicationId = createOIDCAppWithClaims(); + OpenIDConnectConfiguration oidcConfig = getOIDCInboundDetailsOfApplication(applicationId); + subjectType = oidcConfig.getSubject().getSubjectType(); + tokenType = oidcConfig.getAccessToken().getType(); + + if (!CarbonUtils.isLegacyAuthzRuntimeEnabled()) { + authorizeSystemAPIs(applicationId, Collections.singletonList(SCIM2_USERS_API)); + } + domainAPIId = createDomainAPI(EXTERNAL_SERVICE_NAME, EXTERNAL_SERVICE_URI, customScopes); + authorizeDomainAPIs(applicationId, domainAPIId, customScopes); + addUserWithRole(applicationId, customScopes); + + requestedScopes = new ArrayList<>(); + Collections.addAll(requestedScopes, + INTERNAL_ORG_USER_MANAGEMENT_LIST, + INTERNAL_ORG_USER_MANAGEMENT_VIEW, + INTERNAL_ORG_USER_MANAGEMENT_CREATE, + INTERNAL_ORG_USER_MANAGEMENT_UPDATE, + INTERNAL_ORG_USER_MANAGEMENT_DELETE, + OPENID_SCOPE, + PROFILE_SCOPE); + requestedScopes.addAll(customScopes); + + actionId = createPreIssueAccessTokenAction(); + + try { + Thread.sleep(3600); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + actionsMockServer = new ActionsMockServer(); + actionsMockServer.startServer(); + actionsMockServer.setupStub(MOCK_SERVER_ENDPOINT_RESOURCE_PATH, + "Basic " + getBase64EncodedString(MOCK_SERVER_AUTH_BASIC_USERNAME, MOCK_SERVER_AUTH_BASIC_PASSWORD), + FileUtils.readFileInClassPathAsString("actions/response/pre-issue-access-token-response.json")); + } + + @AfterClass(alwaysRun = true) + public void atEnd() throws Exception { + + actionsMockServer.stopServer(); + + deleteAction(PRE_ISSUE_ACCESS_TOKEN_API_PATH, actionId); + deleteRole(roleId); + deleteApp(applicationId); + deleteDomainAPI(domainAPIId); + scim2RestClient.deleteUser(userId); + + restClient.closeHttpClient(); + scim2RestClient.closeHttpClient(); + actionsRestClient.closeHttpClient(); + client.close(); + + actionsMockServer = null; + authorizationCode = null; + accessToken = null; + jwtClaims = null; + } + + @Test(groups = "wso2.is", description = "Initiate authorize request") + public void testSendAuthorizeRequest() throws Exception { + + List urlParameters = new ArrayList<>(); + urlParameters.add(new BasicNameValuePair("response_type", OAuth2Constant.OAUTH2_GRANT_TYPE_CODE)); + urlParameters.add(new BasicNameValuePair("client_id", clientId)); + urlParameters.add(new BasicNameValuePair("redirect_uri", OAuth2Constant.CALLBACK_URL)); + + String scopes = String.join(" ", requestedScopes); + urlParameters.add(new BasicNameValuePair("scope", scopes)); + + HttpResponse response = sendPostRequestWithParameters(client, urlParameters, + getTenantQualifiedURL(AUTHORIZE_ENDPOINT_URL, tenantInfo.getDomain())); + + Header locationHeader = response.getFirstHeader(OAuth2Constant.HTTP_RESPONSE_HEADER_LOCATION); + assertNotNull(locationHeader, "Location header expected for authorize request is not available"); + EntityUtils.consume(response.getEntity()); + + response = sendGetRequest(client, locationHeader.getValue()); + + Map keyPositionMap = new HashMap<>(1); + keyPositionMap.put("name=\"sessionDataKey\"", 1); + List keyValues = DataExtractUtil.extractDataFromResponse(response, keyPositionMap); + assertNotNull(keyValues, "SessionDataKey key value is null"); + + sessionDataKey = keyValues.get(0).getValue(); + assertNotNull(sessionDataKey, "Session data key is null"); + EntityUtils.consume(response.getEntity()); + } + + @Test(groups = "wso2.is", description = "Perform login", dependsOnMethods = "testSendAuthorizeRequest") + public void testSendLoginPost() throws Exception { + + HttpResponse response = sendLoginPostForCustomUsers(client, sessionDataKey, TEST_USER, ADMIN_WSO2); + + Header locationHeader = response.getFirstHeader(OAuth2Constant.HTTP_RESPONSE_HEADER_LOCATION); + assertNotNull(locationHeader, "Location header expected post login is not available."); + EntityUtils.consume(response.getEntity()); + + response = sendGetRequest(client, locationHeader.getValue()); + locationHeader = response.getFirstHeader(OAuth2Constant.HTTP_RESPONSE_HEADER_LOCATION); + assertNotNull(locationHeader, "Redirection URL to the application with authorization code is null."); + EntityUtils.consume(response.getEntity()); + + authorizationCode = getAuthorizationCodeFromURL(locationHeader.getValue()); + assertNotNull(authorizationCode); + } + + @Test(groups = "wso2.is", description = "Retrieve access token", dependsOnMethods = "testSendLoginPost") + public void testGetAccessTokenWithAuthCodeGrant() throws Exception { + + List urlParameters = new ArrayList<>(); + urlParameters.add(new BasicNameValuePair("code", authorizationCode)); + urlParameters.add(new BasicNameValuePair("grant_type", OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE)); + urlParameters.add(new BasicNameValuePair("redirect_uri", OAuth2Constant.CALLBACK_URL)); + urlParameters.add(new BasicNameValuePair("client_id", clientId)); + + String scopes = String.join(" ", requestedScopes); + urlParameters.add(new BasicNameValuePair("scope", scopes)); + + List
headers = new ArrayList<>(); + headers.add(new BasicHeader(AUTHORIZATION_HEADER, + OAuth2Constant.BASIC_HEADER + " " + getBase64EncodedString(clientId, clientSecret))); + headers.add(new BasicHeader("Content-Type", "application/x-www-form-urlencoded")); + headers.add(new BasicHeader("User-Agent", OAuth2Constant.USER_AGENT)); + + HttpResponse response = sendPostRequest(client, headers, urlParameters, + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain())); + assertNotNull(response, "Failed to receive a response for access token request."); + + String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); + JSONObject jsonResponse = new JSONObject(responseString); + + assertTrue(jsonResponse.has("access_token"), "Access token not found in the token response."); + accessToken = jsonResponse.getString("access_token"); + assertNotNull(accessToken, "Access token is null."); + + jwtClaims = extractJwtClaims(accessToken); + assertNotNull(jwtClaims); + } + + @Test(groups = "wso2.is", dependsOnMethods = "testGetAccessTokenWithAuthCodeGrant", description = + "Verify the pre issue access token action request") + public void testPreIssueAccessTokenActionRequest() throws Exception { + + String actualRequestPayload = actionsMockServer.getReceivedRequestPayload(MOCK_SERVER_ENDPOINT_RESOURCE_PATH); + PreIssueAccessTokenActionRequest actualRequest = + new ObjectMapper().readValue(actualRequestPayload, PreIssueAccessTokenActionRequest.class); + + PreIssueAccessTokenActionRequest expectedRequest = getRequest(); + + assertEquals(actualRequest, expectedRequest); + } + + @Test(groups = "wso2.is", description = "Verify that the access token contains the updated scopes " + + "after action execution", dependsOnMethods = "testGetAccessTokenWithAuthCodeGrant") + public void testTokenScopeOperations() throws Exception { + + String[] scopes = jwtClaims.getStringClaim("scope").split("\\s+"); + + Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_1)); + Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_2)); + Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_3)); + Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_4)); + Assert.assertFalse(ArrayUtils.contains(scopes, INTERNAL_ORG_USER_MANAGEMENT_DELETE)); + Assert.assertFalse(ArrayUtils.contains(scopes, INTERNAL_ORG_USER_MANAGEMENT_CREATE)); + } + + @Test(groups = "wso2.is", description = "Verify that the access token contains the updated OIDC claims " + + "after action execution", dependsOnMethods = "testGetAccessTokenWithAuthCodeGrant") + public void testTokenOIDCScopeOperations() throws Exception { + + String firstName = jwtClaims.getStringClaim(FIRST_NAME_CLAIM); + String lastName = jwtClaims.getStringClaim(LAST_NAME_CLAIM); + Assert.assertEquals(firstName, REPLACED_FIRST_NAME); + Assert.assertNotEquals(firstName, TEST_USER_FIRST_NAME); + Assert.assertNotEquals(lastName, TEST_USER_LAST_NAME); + } + + @Test(groups = "wso2.is", description = "Verify that the access token contains the updated 'aud' claims " + + "after action execution", dependsOnMethods = "testGetAccessTokenWithAuthCodeGrant") + public void testTokenAUDClaimOperations() throws Exception { + + String[] audValueArray = jwtClaims.getStringArrayClaim("aud"); + + Assert.assertTrue(ArrayUtils.contains(audValueArray, "zzz1.com")); + Assert.assertTrue(ArrayUtils.contains(audValueArray, "zzz2.com")); + Assert.assertTrue(ArrayUtils.contains(audValueArray, "zzz3.com")); + Assert.assertTrue(ArrayUtils.contains(audValueArray, "zzzR.com")); + Assert.assertFalse(ArrayUtils.contains(audValueArray, clientId)); + } + + @Test(groups = "wso2.is", description = "Verify the presence of the specified custom string claim in the access " + + "token", dependsOnMethods = "testGetAccessTokenWithAuthCodeGrant") + public void testTokenStringClaimAddOperation() throws Exception { + + String claimStr = jwtClaims.getStringClaim("custom_claim_string_1"); + Assert.assertEquals(claimStr, "testCustomClaim1"); + } + + @Test(groups = "wso2.is", description = "Verify the presence of the specified custom number claim in the access " + + "token", dependsOnMethods = "testGetAccessTokenWithAuthCodeGrant") + public void testTokenNumberClaimAddOperation() throws Exception { + + Number claimValue = jwtClaims.getIntegerClaim("custom_claim_number_1"); + Assert.assertEquals(claimValue, 78); + } + + @Test(groups = "wso2.is", description = "Verify the presence of the specified custom boolean claim in the access " + + "token", dependsOnMethods = "testGetAccessTokenWithAuthCodeGrant") + public void testTokenBooleanClaimAddOperation() throws Exception { + + Boolean claimValue = jwtClaims.getBooleanClaim("custom_claim_boolean_1"); + Assert.assertTrue(claimValue); + } + + @Test(groups = "wso2.is", description = "Verify the presence of the specified custom string array claim in the " + + "access token", dependsOnMethods = "testGetAccessTokenWithAuthCodeGrant") + public void testTokenStringArrayClaimAddOperation() + throws Exception { + + String[] expectedClaimArrayInToken = {"TestCustomClaim1", "TestCustomClaim2", "TestCustomClaim3"}; + + String[] addedClaimArrayToToken = jwtClaims.getStringArrayClaim("custom_claim_string_array_1"); + Assert.assertEquals(addedClaimArrayToToken, expectedClaimArrayInToken); + } + + @Test(groups = "wso2.is", description = "Verify the replacement of the 'expires_in' claim in the access token", + dependsOnMethods = "testGetAccessTokenWithAuthCodeGrant") + public void testTokenExpiresInClaimReplaceOperation() throws Exception { + + Date exp = jwtClaims.getDateClaim("exp"); + Date iat = jwtClaims.getDateClaim("iat"); + long expiresIn = (exp.getTime() - iat.getTime()) / 1000; + + Assert.assertEquals(expiresIn, UPDATED_EXPIRY_TIME_PERIOD); + } + + /** + * Get authorization code from the provided URL. + * + * @param location Location header + * @return Authorization code + */ + private String getAuthorizationCodeFromURL(String location) { + + URI uri = URI.create(location); + return URLEncodedUtils.parse(uri, StandardCharsets.UTF_8).stream() + .filter(param -> "code".equals(param.getName())) + .map(NameValuePair::getValue) + .findFirst() + .orElse(null); + } + + /** + * Retrieves pre issue access token action request. + * + * @return pre issue access token request object + */ + private PreIssueAccessTokenActionRequest getRequest() { + + TokenRequest tokenRequest = createTokenRequest(); + AccessToken accessTokenInRequest = createAccessToken(); + + Tenant tenant = new Tenant(tenantId, tenantInfo.getDomain()); + User user = new User(userId); + UserStore userStore = + new UserStore(Base64.getEncoder().encodeToString("PRIMARY".getBytes(StandardCharsets.UTF_8)), + "PRIMARY"); + + PreIssueAccessTokenEvent event = new PreIssueAccessTokenEvent.Builder() + .request(tokenRequest) + .accessToken(accessTokenInRequest) + .tenant(tenant) + .organization(null) + .user(user) + .userStore(userStore) + .build(); + + List allowedOperations = Arrays.asList( + createAllowedOperation(Operation.ADD, Arrays.asList(CLAIMS_PATH_PREFIX, SCOPES_PATH_PREFIX, + CLAIMS_PATH_PREFIX + AccessToken.ClaimNames.AUD.getName() + "/")), + createAllowedOperation(Operation.REMOVE, Arrays.asList(CLAIMS_PATH_PREFIX + FIRST_NAME_CLAIM, + CLAIMS_PATH_PREFIX + LAST_NAME_CLAIM, + SCOPES_PATH_PREFIX, + CLAIMS_PATH_PREFIX + AccessToken.ClaimNames.AUD.getName() + "/")), + createAllowedOperation(Operation.REPLACE, Arrays.asList(CLAIMS_PATH_PREFIX + FIRST_NAME_CLAIM, + CLAIMS_PATH_PREFIX + LAST_NAME_CLAIM, + SCOPES_PATH_PREFIX, + CLAIMS_PATH_PREFIX + AccessToken.ClaimNames.AUD.getName() + "/", + CLAIMS_PATH_PREFIX + AccessToken.ClaimNames.EXPIRES_IN.getName())) + ); + + return new PreIssueAccessTokenActionRequest.Builder() + .actionType(ActionType.PRE_ISSUE_ACCESS_TOKEN) + .event(event) + .allowedOperations(allowedOperations) + .build(); + } + + /** + * Creates token request. + * + * @return token request object + */ + private TokenRequest createTokenRequest() { + + return new TokenRequest.Builder() + .grantType(OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE) + .scopes(requestedScopes) + .clientId(clientId) + .build(); + } + + /** + * Creates access token. + * + * @return access token object + */ + private AccessToken createAccessToken() { + + List claims = new ArrayList<>(); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.ISS.getName(), + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain()))); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.CLIENT_ID.getName(), clientId)); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.AUTHORIZED_USER_TYPE.getName(), "APPLICATION_USER")); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.EXPIRES_IN.getName(), CURRENT_EXPIRY_TIME_PERIOD)); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.AUD.getName(), Collections.singletonList(clientId))); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.SUBJECT_TYPE.getName(), subjectType)); + claims.add(new AccessToken.Claim(AccessToken.ClaimNames.SUB.getName(), userId)); + claims.add(new AccessToken.Claim(FIRST_NAME_CLAIM, TEST_USER_FIRST_NAME)); + claims.add(new AccessToken.Claim(LAST_NAME_CLAIM, TEST_USER_LAST_NAME)); + + return new AccessToken.Builder() + .tokenType(tokenType) + .claims(claims) + .scopes(requestedScopes) + .build(); + } + + /** + * Creates allowed operations. + * + * @param op operations allowed + * @param paths operation paths to be modified + * @return allowed operations object + */ + private AllowedOperation createAllowedOperation(Operation op, List paths) { + + AllowedOperation operation = new AllowedOperation(); + operation.setOp(op); + operation.setPaths(new ArrayList<>(paths)); + return operation; + } + + /** + * Creates an action for pre-issuing an access token with basic authentication. + * + * @return ID of the created action + * @throws IOException If an error occurred while creating the action + */ + private String createPreIssueAccessTokenAction() throws IOException { + + AuthenticationType authenticationType = new AuthenticationType(); + authenticationType.setType(AuthenticationType.TypeEnum.BASIC); + Map authProperties = new HashMap<>(); + authProperties.put(USERNAME_PROPERTY, MOCK_SERVER_AUTH_BASIC_USERNAME); + authProperties.put(PASSWORD_PROPERTY, MOCK_SERVER_AUTH_BASIC_PASSWORD); + authenticationType.setProperties(authProperties); + + Endpoint endpoint = new Endpoint(); + endpoint.setUri(EXTERNAL_SERVICE_URI); + endpoint.setAuthentication(authenticationType); + + ActionModel actionModel = new ActionModel(); + actionModel.setName("Access Token Pre Issue"); + actionModel.setDescription("This is a test pre issue access token type"); + actionModel.setEndpoint(endpoint); + + return createAction(PRE_ISSUE_ACCESS_TOKEN_API_PATH, actionModel); + } + + /** + * Extracts the JWT claims set from a given JWT token. + * + * @param jwtToken JWT token from which claims are to be extracted + * @return JWTClaimsSet extracted from the provided JWT token + * @throws ParseException If there is an error in parsing the JWT token + */ + private JWTClaimsSet extractJwtClaims(String jwtToken) throws ParseException { + + SignedJWT signedJWT = SignedJWT.parse(jwtToken); + return signedJWT.getJWTClaimsSet(); + } + + /** + * Adds a user with a role and specific permissions based on custom scopes. + * + * @param appID Application ID to which the role is associated + * @param customScopes The custom scopes based on which permissions are added + * @throws JSONException If there is an error in processing JSON + * @throws IOException If there is an IO exception during user or role creation + */ + private void addUserWithRole(String appID, List customScopes) throws Exception { + // Creates roles + List userPermissions = addPermissions(customScopes); + Audience roleAudience = new Audience(APPLICATION_AUDIENCE, appID); + RoleV2 role = new RoleV2(roleAudience, TEST_ROLE_APPLICATION, userPermissions, Collections.emptyList()); + roleId = addRole(role); + + // Creates user + UserObject userInfo = new UserObject(); + userInfo.setUserName(TEST_USER); + userInfo.setPassword(ADMIN_WSO2); + userInfo.setName(new Name().givenName(TEST_USER_FIRST_NAME)); + userInfo.getName().setFamilyName(TEST_USER_LAST_NAME); + userInfo.addEmail(new Email().value(TEST_USER_GMAIL_COM)); + userId = scim2RestClient.createUser(userInfo); + + // Assigns role to the created user + RoleItemAddGroupobj rolePatchReqObject = new RoleItemAddGroupobj(); + rolePatchReqObject.setOp(RoleItemAddGroupobj.OpEnum.ADD); + rolePatchReqObject.setPath(USERS); + rolePatchReqObject.addValue(new ListObject().value(userId)); + scim2RestClient.updateUserRole(new PatchOperationRequestObject().addOperations(rolePatchReqObject), roleId); + } + + /** + * Adds permissions based on the provided custom scopes. + * + * @param customScopes A list of custom scopes to add as permissions + * @return A list of permissions including both predefined and custom scope-based permissions + */ + private List addPermissions(List customScopes) { + + List userPermissions = new ArrayList<>(); + + Collections.addAll(userPermissions, + new Permission(INTERNAL_ORG_USER_MANAGEMENT_LIST), + new Permission(INTERNAL_ORG_USER_MANAGEMENT_VIEW), + new Permission(INTERNAL_ORG_USER_MANAGEMENT_CREATE), + new Permission(INTERNAL_ORG_USER_MANAGEMENT_UPDATE), + new Permission(INTERNAL_ORG_USER_MANAGEMENT_DELETE) + ); + + customScopes.forEach(scope -> userPermissions.add(new Permission(scope))); + + return userPermissions; + } + + private String createOIDCAppWithClaims() throws Exception { + + List userClaimConfigs = Arrays.asList( + new UserClaimConfig.Builder().localClaimUri("http://wso2.org/claims/givenname"). + oidcClaimUri(FIRST_NAME_CLAIM).build(), + new UserClaimConfig.Builder().localClaimUri("http://wso2.org/claims/lastname"). + oidcClaimUri(LAST_NAME_CLAIM).build() + ); + + ApplicationConfig applicationConfig = new ApplicationConfig.Builder() + .claimsList(userClaimConfigs) + .grantTypes(new ArrayList<>(Collections.singleton(OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE))) + .tokenType(ApplicationConfig.TokenType.JWT) + .expiryTime(3600) + .skipConsent(true) + .build(); + + appTokenScopes = new TokenScopes.Builder().requestedScopes(Arrays.asList(OPENID_SCOPE, PROFILE_SCOPE, + CUSTOM_OIDC_SCOPE)) + .grantedScopes(Arrays.asList(OPENID_SCOPE, PROFILE_SCOPE, CUSTOM_OIDC_SCOPE)).build(); + + ApplicationResponseModel application = addApplication(applicationConfig); + String applicationId = application.getId(); + + OpenIDConnectConfiguration oidcConfig = getOIDCInboundDetailsOfApplication(applicationId); + clientId = oidcConfig.getClientId(); + clientSecret = oidcConfig.getClientSecret(); + + return applicationId; + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenPasswordGrantTestCase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenPasswordGrantTestCase.java index d3745d8a7e..a8d45a0747 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenPasswordGrantTestCase.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenPasswordGrantTestCase.java @@ -230,6 +230,11 @@ protected boolean isRedirectable(String method) { actionId = createPreIssueAccessTokenAction(); + try { + Thread.sleep(3600); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } actionsMockServer = new ActionsMockServer(); actionsMockServer.startServer(); actionsMockServer.setupStub(MOCK_SERVER_ENDPOINT_RESOURCE_PATH, @@ -312,8 +317,8 @@ public void testTokenScopeOperations() throws Exception { Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_2)); Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_3)); Assert.assertTrue(ArrayUtils.contains(scopes, NEW_SCOPE_4)); - Assert.assertFalse(ArrayUtils.contains(scopes, CUSTOM_SCOPE_3)); - Assert.assertFalse(ArrayUtils.contains(scopes, CUSTOM_SCOPE_2)); + Assert.assertFalse(ArrayUtils.contains(scopes, INTERNAL_ORG_USER_MANAGEMENT_DELETE)); + Assert.assertFalse(ArrayUtils.contains(scopes, INTERNAL_ORG_USER_MANAGEMENT_CREATE)); } @Test(groups = "wso2.is", dependsOnMethods = "testPreIssueAccessTokenActionRequest", description = diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/mockserver/ActionsMockServer.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/mockserver/ActionsMockServer.java index d5b34470f2..2a8902f4e9 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/mockserver/ActionsMockServer.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/mockserver/ActionsMockServer.java @@ -19,21 +19,17 @@ package org.wso2.identity.integration.test.actions.mockserver; import com.github.tomakehurst.wiremock.WireMockServer; -import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; -import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; import com.github.tomakehurst.wiremock.verification.LoggedRequest; import org.apache.commons.lang.StringUtils; import java.util.List; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; /** * Provides a mock server using WireMock for testing purposes. @@ -76,133 +72,4 @@ public String getReceivedRequestPayload(String url) { return requestList.get(0).getBodyAsString(); } - - /** - * Create a mock server with wiremock. - * - * @throws Exception If an error occurred while creating the server - */ - public void createMockServer(String mockEndpoint) throws Exception { - - wireMockServer = new WireMockServer(wireMockConfig().port(8587)); - - wireMockServer.start(); - - try { - // TODO: Read the response from a file - // TODO: Filter the response from the action type - String jsonResponse = "{\n" + - " \"actionStatus\": \"SUCCESS\",\n" + - " \"operations\": [\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/scopes/-\",\n" + - " \"value\": \"new_test_custom_scope_1\"\n" + - " },\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/scopes/-\",\n" + - " \"value\": \"new_test_custom_scope_2\"\n" + - " },\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/scopes/-\",\n" + - " \"value\": \"new_test_custom_scope_3\"\n" + - " },\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/claims/aud/-\",\n" + - " \"value\": \"zzz1.com\"\n" + - " },\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/claims/aud/-\",\n" + - " \"value\": \"zzz2.com\"\n" + - " },\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/claims/aud/-\",\n" + - " \"value\": \"zzz3.com\"\n" + - " },\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/claims/-\",\n" + - " \"value\": {\n" + - " \"name\": \"custom_claim_string_1\",\n" + - " \"value\": \"testCustomClaim1\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/claims/-\",\n" + - " \"value\": {\n" + - " \"name\": \"custom_claim_number_1\",\n" + - " \"value\": 78\n" + - " }\n" + - " },\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/claims/-\",\n" + - " \"value\": {\n" + - " \"name\": \"custom_claim_boolean_1\",\n" + - " \"value\": true\n" + - " }\n" + - " },\n" + - " {\n" + - " \"op\": \"add\",\n" + - " \"path\": \"/accessToken/claims/-\",\n" + - " \"value\": {\n" + - " \"name\": \"custom_claim_string_array_1\",\n" + - " \"value\": [\n" + - " \"TestCustomClaim1\",\n" + - " \"TestCustomClaim2\",\n" + - " \"TestCustomClaim3\"\n" + - " ]\n" + - " }\n" + - " },\n" + - " {\n" + - " \"op\": \"replace\",\n" + - " \"path\": \"/accessToken/scopes/7\",\n" + - " \"value\": \"replaced_scope\"\n" + - " },\n" + - " {\n" + - " \"op\": \"replace\",\n" + - " \"path\": \"/accessToken/claims/aud/-\",\n" + - " \"value\": \"zzzR.com\"\n" + - " },\n" + - " {\n" + - " \"op\": \"replace\",\n" + - " \"path\": \"/accessToken/claims/expires_in\",\n" + - " \"value\": 7200\n" + - " },\n" + - " {\n" + - " \"op\": \"remove\",\n" + - " \"path\": \"/accessToken/scopes/6\"\n" + - " },\n" + - " {\n" + - " \"op\": \"remove\",\n" + - " \"path\": \"/accessToken/claims/aud/-\"\n" + - " }\n" + - " ]\n" + - " }"; - - // TODO: Handle status codes related to different scenarios - wireMockServer.stubFor(post(urlEqualTo(mockEndpoint)) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(jsonResponse))); - - } catch (Exception e) { - throw new Exception("Error occurred while creating the mock server: " + e); - } - } - - /** - * Shut down the wiremock server instance. - */ - public void shutDownMockServer() { - - wireMockServer.stop(); - } } diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/model/PreIssueAccessTokenEvent.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/model/PreIssueAccessTokenEvent.java index e293edf1d6..385bf1983f 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/model/PreIssueAccessTokenEvent.java +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/model/PreIssueAccessTokenEvent.java @@ -54,15 +54,21 @@ public AccessToken getAccessToken() { @Override public boolean equals(Object o) { - if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + PreIssueAccessTokenEvent that = (PreIssueAccessTokenEvent) o; - return Objects.equals(request, that.request) && + + boolean isEqualGeneral = Objects.equals(request, that.request) && Objects.equals(accessToken, that.accessToken) && - Objects.equals(tenant, that.tenant) && - Objects.equals(user, that.user) && - Objects.equals(userStore, that.userStore); + Objects.equals(tenant, that.tenant); + + if (!"client_credentials".equals(that.request.getGrantType())) { + return isEqualGeneral && + Objects.equals(user, that.user) && + Objects.equals(userStore, that.userStore); + } + return isEqualGeneral; } @Override diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/pre-issue-access-token-response.json b/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/pre-issue-access-token-response.json index e2feae3bd6..40779284b4 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/pre-issue-access-token-response.json +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/pre-issue-access-token-response.json @@ -69,7 +69,7 @@ }, { "op": "replace", - "path": "/accessToken/scopes/7", + "path": "/accessToken/scopes/1", "value": "replaced_scope" }, { @@ -77,6 +77,11 @@ "path": "/accessToken/claims/aud/-", "value": "zzzR.com" }, + { + "op": "replace", + "path": "/accessToken/claims/given_name", + "value": "replaced_given_name" + }, { "op": "replace", "path": "/accessToken/claims/expires_in", @@ -84,11 +89,15 @@ }, { "op": "remove", - "path": "/accessToken/scopes/6" + "path": "/accessToken/scopes/0" }, { "op": "remove", "path": "/accessToken/claims/aud/-" + }, + { + "op": "remove", + "path": "/accessToken/claims/family_name" } ] -} \ No newline at end of file +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/testng.xml b/modules/integration/tests-integration/tests-backend/src/test/resources/testng.xml index f5a8e557b5..e8dbf096a9 100644 --- a/modules/integration/tests-integration/tests-backend/src/test/resources/testng.xml +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/testng.xml @@ -138,6 +138,7 @@ +