diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailureClientCredentialsGrantTestCase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailureClientCredentialsGrantTestCase.java new file mode 100644 index 0000000000..4bacd83e70 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailureClientCredentialsGrantTestCase.java @@ -0,0 +1,201 @@ +/* + * 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 org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +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.dataprovider.model.ActionResponse; +import org.wso2.identity.integration.test.actions.dataprovider.model.ExpectedTokenResponse; +import org.wso2.identity.integration.test.actions.mockserver.ActionsMockServer; +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.utils.FileUtils; +import org.wso2.identity.integration.test.utils.OAuth2Constant; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +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.wso2.identity.integration.test.utils.OAuth2Constant.ACCESS_TOKEN_ENDPOINT; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.AUTHORIZATION_HEADER; + +/** + * Tests the pre-issue access token action failure scenarios with password grant type. + */ +public class PreIssueAccessTokenActionFailureClientCredentialsGrantTestCase extends ActionsBaseTestCase { + + private static final String USERNAME_PROPERTY = "username"; + private static final String PASSWORD_PROPERTY = "password"; + private static final String EXTERNAL_SERVICE_URI = "http://localhost:8587/test/action"; + private static final String PRE_ISSUE_ACCESS_TOKEN_API_PATH = "preIssueAccessToken"; + private static final String MOCK_SERVER_ENDPOINT_RESOURCE_PATH = "/test/action"; + private static final String MOCK_SERVER_AUTH_BASIC_USERNAME = "test"; + private static final String MOCK_SERVER_AUTH_BASIC_PASSWORD = "test"; + private static final String CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials"; + private CloseableHttpClient client; + private List requestedScopes; + private String clientId; + private String clientSecret; + private String actionId; + private String applicationId; + private final TestUserMode userMode; + private ActionsMockServer actionsMockServer; + private final ActionResponse actionResponse; + private final ExpectedTokenResponse expectedTokenResponse; + + @Factory(dataProvider = "testExecutionContextProvider") + public PreIssueAccessTokenActionFailureClientCredentialsGrantTestCase(TestUserMode testUserMode, + ActionResponse actionResponse, + ExpectedTokenResponse expectedTokenResponse) { + + this.userMode = testUserMode; + this.actionResponse = actionResponse; + this.expectedTokenResponse = expectedTokenResponse; + } + + @DataProvider(name = "testExecutionContextProvider") + public static Object[][] getTestExecutionContext() throws Exception { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_USER, new ActionResponse(200, + FileUtils.readFileInClassPathAsString("actions/response/failure-response.json")), + new ExpectedTokenResponse(400, "Some failure reason", "Some description")}, + {TestUserMode.TENANT_USER, new ActionResponse(200, + FileUtils.readFileInClassPathAsString("actions/response/failure-response.json")), + new ExpectedTokenResponse(400, "Some failure reason", "Some description")}, + {TestUserMode.TENANT_USER, new ActionResponse(500, + FileUtils.readFileInClassPathAsString("actions/response/error-response.json")), + new ExpectedTokenResponse(500, "server_error", "Internal Server Error.")}, + {TestUserMode.TENANT_USER, new ActionResponse(401, "Unauthorized"), + new ExpectedTokenResponse(500, "server_error", "Internal Server Error.")}, + }; + } + + @BeforeClass(alwaysRun = true) + public void testInit() throws Exception { + + super.init(userMode); + client = HttpClientBuilder.create().build(); + + ApplicationResponseModel application = addApplicationWithGrantType(CLIENT_CREDENTIALS_GRANT_TYPE); + applicationId = application.getId(); + OpenIDConnectConfiguration oidcConfig = getOIDCInboundDetailsOfApplication(applicationId); + clientId = oidcConfig.getClientId(); + clientSecret = oidcConfig.getClientSecret(); + actionId = createPreIssueAccessTokenAction(); + + requestedScopes = new ArrayList<>(Arrays.asList("scope_1", "scope_2")); + + actionsMockServer = new ActionsMockServer(); + actionsMockServer.startServer(); + actionsMockServer.setupStub(MOCK_SERVER_ENDPOINT_RESOURCE_PATH, + "Basic " + getBase64EncodedString(MOCK_SERVER_AUTH_BASIC_USERNAME, MOCK_SERVER_AUTH_BASIC_PASSWORD), + actionResponse.getResponseBody(), actionResponse.getStatusCode()); + } + + @AfterClass(alwaysRun = true) + public void atEnd() throws Exception { + + actionsMockServer.stopServer(); + + deleteAction(PRE_ISSUE_ACCESS_TOKEN_API_PATH, actionId); + deleteApp(applicationId); + + restClient.closeHttpClient(); + actionsRestClient.closeHttpClient(); + client.close(); + + actionsMockServer = null; + } + + @Test(groups = "wso2.is", description = "Verify token response when pre-issue access token action fails with " + + "client credentials grant type.") + public void testPreIssueAccessTokenActionFailure() throws Exception { + + HttpResponse response = sendTokenRequestForClientCredentialsGrant(); + + assertNotNull(response); + assertEquals(response.getStatusLine().getStatusCode(), expectedTokenResponse.getStatusCode()); + + String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); + JSONObject jsonResponse = new JSONObject(responseString); + assertEquals(jsonResponse.getString("error"), expectedTokenResponse.getErrorMessage()); + assertEquals(jsonResponse.getString("error_description"), expectedTokenResponse.getErrorDescription()); + } + + public HttpResponse sendTokenRequestForClientCredentialsGrant() throws Exception { + + List parameters = new ArrayList<>(); + parameters.add(new BasicNameValuePair("grant_type", OAuth2Constant.OAUTH2_GRANT_TYPE_CLIENT_CREDENTIALS)); + + String scopes = String.join(" ", requestedScopes); + parameters.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)); + + return sendPostRequest(client, headers, parameters, + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain())); + } + + 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); + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailureCodeGrantTestCase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailureCodeGrantTestCase.java new file mode 100644 index 0000000000..80defdc50b --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailureCodeGrantTestCase.java @@ -0,0 +1,326 @@ +/* + * 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 org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +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.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +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.dataprovider.model.ActionResponse; +import org.wso2.identity.integration.test.actions.dataprovider.model.ExpectedTokenResponse; +import org.wso2.identity.integration.test.actions.mockserver.ActionsMockServer; +import org.wso2.identity.integration.test.oauth2.dataprovider.model.ApplicationConfig; +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.user.common.model.Email; +import org.wso2.identity.integration.test.rest.api.user.common.model.Name; +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.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.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +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.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; + +/** + * This class tests the pre issue access token action failure scenarios with code grant type. + */ +public class PreIssueAccessTokenActionFailureCodeGrantTestCase extends ActionsBaseTestCase { + + 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 TEST_WSO2 = "Test@wso2"; + private static final String EXTERNAL_SERVICE_URI = "http://localhost:8587/test/action"; + private static final String PRE_ISSUE_ACCESS_TOKEN_API_PATH = "preIssueAccessToken"; + private static final String MOCK_SERVER_ENDPOINT_RESOURCE_PATH = "/test/action"; + 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 List requestedScopes; + private String sessionDataKey; + private String authorizationCode; + private String clientId; + private String clientSecret; + private String actionId; + private String applicationId; + private String userId; + private final TestUserMode userMode; + private ActionsMockServer actionsMockServer; + private final ActionResponse actionResponse; + private final ExpectedTokenResponse expectedResponse; + + @Factory(dataProvider = "testExecutionContextProvider") + public PreIssueAccessTokenActionFailureCodeGrantTestCase(TestUserMode testUserMode, ActionResponse actionResponse, + ExpectedTokenResponse expectedResponse) { + + this.userMode = testUserMode; + this.actionResponse = actionResponse; + this.expectedResponse = expectedResponse; + } + + @DataProvider(name = "testExecutionContextProvider") + public static Object[][] getTestExecutionContext() throws Exception { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_USER, new ActionResponse(200, + FileUtils.readFileInClassPathAsString("actions/response/failure-response.json")), + new ExpectedTokenResponse(400, "Some failure reason", "Some description")}, + {TestUserMode.TENANT_USER, new ActionResponse(200, + FileUtils.readFileInClassPathAsString("actions/response/failure-response.json")), + new ExpectedTokenResponse(400, "Some failure reason", "Some description")}, + {TestUserMode.TENANT_USER, new ActionResponse(500, + FileUtils.readFileInClassPathAsString("actions/response/error-response.json")), + new ExpectedTokenResponse(500, "server_error", "Internal Server Error.")}, + {TestUserMode.TENANT_USER, new ActionResponse(401, "Unauthorized"), + new ExpectedTokenResponse(500, "server_error", "Internal Server Error.")}, + }; + } + + @BeforeClass(alwaysRun = true) + public void testInit() throws Exception { + + super.init(userMode); + client = HttpClientBuilder.create() + .setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(String method) { + + return false; + } + }).build(); + + scim2RestClient = new SCIM2RestClient(serverURL, tenantInfo); + applicationId = createOIDCAppWithClaims(); + actionId = createPreIssueAccessTokenAction(); + addUser(); + + requestedScopes = new ArrayList<>(Arrays.asList("openid", "profile")); + + actionsMockServer = new ActionsMockServer(); + actionsMockServer.startServer(); + actionsMockServer.setupStub(MOCK_SERVER_ENDPOINT_RESOURCE_PATH, + "Basic " + getBase64EncodedString(MOCK_SERVER_AUTH_BASIC_USERNAME, MOCK_SERVER_AUTH_BASIC_PASSWORD), + actionResponse.getResponseBody(), actionResponse.getStatusCode()); + } + + @AfterClass(alwaysRun = true) + public void atEnd() throws Exception { + + actionsMockServer.stopServer(); + + deleteAction(PRE_ISSUE_ACCESS_TOKEN_API_PATH, actionId); + deleteApp(applicationId); + scim2RestClient.deleteUser(userId); + + restClient.closeHttpClient(); + scim2RestClient.closeHttpClient(); + actionsRestClient.closeHttpClient(); + client.close(); + + actionsMockServer = null; + authorizationCode = null; + } + + @Test(groups = "wso2.is", description = "Verify token response when pre-issue access token action fails with " + + "authorization code grant type.") + public void testPreIssueAccessActionFailure() throws Exception { + + sendAuthorizeRequest(); + performUserLogin(); + HttpResponse response = sendTokenRequestForCodeGrant(); + + assertNotNull(response); + assertEquals(response.getStatusLine().getStatusCode(), expectedResponse.getStatusCode()); + + String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); + JSONObject jsonResponse = new JSONObject(responseString); + assertEquals(jsonResponse.getString("error"), expectedResponse.getErrorMessage()); + assertEquals(jsonResponse.getString("error_description"), expectedResponse.getErrorDescription()); + } + + private void sendAuthorizeRequest() 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()); + } + + public void performUserLogin() throws Exception { + + HttpResponse response = sendLoginPostForCustomUsers(client, sessionDataKey, TEST_USER, TEST_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); + } + + private HttpResponse sendTokenRequestForCodeGrant() 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)); + + return sendPostRequest(client, headers, urlParameters, + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain())); + } + + 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); + } + + 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); + } + + private void addUser() throws Exception { + + UserObject userInfo = new UserObject(); + userInfo.setUserName(TEST_USER); + userInfo.setPassword(TEST_WSO2); + userInfo.setName(new Name().givenName("test_user_given_name")); + userInfo.getName().setFamilyName("test_user_last_name"); + userInfo.addEmail(new Email().value("test.user@gmail.com")); + userId = scim2RestClient.createUser(userInfo); + } + + private String createOIDCAppWithClaims() throws Exception { + + List userClaimConfigs = Arrays.asList( + new UserClaimConfig.Builder().localClaimUri("http://wso2.org/claims/givenname"). + oidcClaimUri("given_name").build(), + new UserClaimConfig.Builder().localClaimUri("http://wso2.org/claims/lastname"). + oidcClaimUri("family_name").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(); + + 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/PreIssueAccessTokenActionFailurePasswordGrantTestCase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailurePasswordGrantTestCase.java new file mode 100644 index 0000000000..e2ca94f144 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailurePasswordGrantTestCase.java @@ -0,0 +1,227 @@ +/* + * 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 org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +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.dataprovider.model.ActionResponse; +import org.wso2.identity.integration.test.actions.dataprovider.model.ExpectedTokenResponse; +import org.wso2.identity.integration.test.actions.mockserver.ActionsMockServer; +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.user.common.model.Email; +import org.wso2.identity.integration.test.rest.api.user.common.model.Name; +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.FileUtils; +import org.wso2.identity.integration.test.utils.OAuth2Constant; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +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.wso2.identity.integration.test.utils.OAuth2Constant.ACCESS_TOKEN_ENDPOINT; +import static org.wso2.identity.integration.test.utils.OAuth2Constant.AUTHORIZATION_HEADER; + +/** + * Tests the pre-issue access token action failure scenarios with password grant type. + */ +public class PreIssueAccessTokenActionFailurePasswordGrantTestCase extends ActionsBaseTestCase { + + 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 TEST_WSO2 = "Test@wso2"; + private static final String EXTERNAL_SERVICE_URI = "http://localhost:8587/test/action"; + private static final String PRE_ISSUE_ACCESS_TOKEN_API_PATH = "preIssueAccessToken"; + private static final String MOCK_SERVER_ENDPOINT_RESOURCE_PATH = "/test/action"; + private static final String MOCK_SERVER_AUTH_BASIC_USERNAME = "test"; + private static final String MOCK_SERVER_AUTH_BASIC_PASSWORD = "test"; + private static final String PASSWORD_GRANT_TYPE = "password"; + private CloseableHttpClient client; + private SCIM2RestClient scim2RestClient; + private List requestedScopes; + private String clientId; + private String clientSecret; + private String actionId; + private String applicationId; + private String userId; + private final TestUserMode userMode; + private ActionsMockServer actionsMockServer; + private final ActionResponse actionResponse; + private final ExpectedTokenResponse expectedTokenResponse; + + @Factory(dataProvider = "testExecutionContextProvider") + public PreIssueAccessTokenActionFailurePasswordGrantTestCase(TestUserMode testUserMode, + ActionResponse actionResponse, + ExpectedTokenResponse expectedTokenResponse) { + + this.userMode = testUserMode; + this.actionResponse = actionResponse; + this.expectedTokenResponse = expectedTokenResponse; + } + + @DataProvider(name = "testExecutionContextProvider") + public static Object[][] getTestExecutionContext() throws Exception { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_USER, new ActionResponse(200, + FileUtils.readFileInClassPathAsString("actions/response/failure-response.json")), + new ExpectedTokenResponse(400, "Some failure reason", "Some description")}, + {TestUserMode.TENANT_USER, new ActionResponse(200, + FileUtils.readFileInClassPathAsString("actions/response/failure-response.json")), + new ExpectedTokenResponse(400, "Some failure reason", "Some description")}, + {TestUserMode.TENANT_USER, new ActionResponse(500, + FileUtils.readFileInClassPathAsString("actions/response/error-response.json")), + new ExpectedTokenResponse(500, "server_error", "Internal Server Error.")}, + {TestUserMode.TENANT_USER, new ActionResponse(401, "Unauthorized"), + new ExpectedTokenResponse(500, "server_error", "Internal Server Error.")}, + }; + } + + @BeforeClass(alwaysRun = true) + public void testInit() throws Exception { + + super.init(userMode); + client = HttpClientBuilder.create().build(); + + scim2RestClient = new SCIM2RestClient(serverURL, tenantInfo); + ApplicationResponseModel application = addApplicationWithGrantType(PASSWORD_GRANT_TYPE); + applicationId = application.getId(); + OpenIDConnectConfiguration oidcConfig = getOIDCInboundDetailsOfApplication(applicationId); + clientId = oidcConfig.getClientId(); + clientSecret = oidcConfig.getClientSecret(); + actionId = createPreIssueAccessTokenAction(); + + addUser(); + + requestedScopes = new ArrayList<>(Arrays.asList("openid", "profile")); + + actionsMockServer = new ActionsMockServer(); + actionsMockServer.startServer(); + actionsMockServer.setupStub(MOCK_SERVER_ENDPOINT_RESOURCE_PATH, + "Basic " + getBase64EncodedString(MOCK_SERVER_AUTH_BASIC_USERNAME, MOCK_SERVER_AUTH_BASIC_PASSWORD), + actionResponse.getResponseBody(), actionResponse.getStatusCode()); + } + + @AfterClass(alwaysRun = true) + public void atEnd() throws Exception { + + actionsMockServer.stopServer(); + + deleteAction(PRE_ISSUE_ACCESS_TOKEN_API_PATH, actionId); + deleteApp(applicationId); + scim2RestClient.deleteUser(userId); + + restClient.closeHttpClient(); + scim2RestClient.closeHttpClient(); + actionsRestClient.closeHttpClient(); + client.close(); + + actionsMockServer = null; + } + + @Test(groups = "wso2.is", description = "Verify token response when pre-issue access token action fails with " + + "password grant type.") + public void testPreIssueAccessTokenActionFailure() throws Exception { + + HttpResponse response = sendTokenRequestForPasswordGrant(); + + assertNotNull(response); + assertEquals(response.getStatusLine().getStatusCode(), expectedTokenResponse.getStatusCode()); + + String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); + JSONObject jsonResponse = new JSONObject(responseString); + assertEquals(jsonResponse.getString("error"), expectedTokenResponse.getErrorMessage()); + assertEquals(jsonResponse.getString("error_description"), expectedTokenResponse.getErrorDescription()); + } + + private HttpResponse sendTokenRequestForPasswordGrant() throws Exception { + + List parameters = new ArrayList<>(); + parameters.add(new BasicNameValuePair("grant_type", OAuth2Constant.OAUTH2_GRANT_TYPE_RESOURCE_OWNER)); + parameters.add(new BasicNameValuePair("username", TEST_USER)); + parameters.add(new BasicNameValuePair("password", TEST_WSO2)); + + String scopes = String.join(" ", requestedScopes); + parameters.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)); + + return sendPostRequest(client, headers, parameters, + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain())); + } + + 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); + } + + private void addUser() throws Exception { + + UserObject userInfo = new UserObject(); + userInfo.setUserName(TEST_USER); + userInfo.setPassword(TEST_WSO2); + userInfo.setName(new Name().givenName("test_user_given_name")); + userInfo.getName().setFamilyName("test_user_last_name"); + userInfo.addEmail(new Email().value("test.user@gmail.com")); + userId = scim2RestClient.createUser(userInfo); + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailureRefreshTokenGrantTestCase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailureRefreshTokenGrantTestCase.java new file mode 100644 index 0000000000..56f2c23548 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionFailureRefreshTokenGrantTestCase.java @@ -0,0 +1,387 @@ +/* + * 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 org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +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.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +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.dataprovider.model.ActionResponse; +import org.wso2.identity.integration.test.actions.dataprovider.model.ExpectedTokenResponse; +import org.wso2.identity.integration.test.actions.mockserver.ActionsMockServer; +import org.wso2.identity.integration.test.oauth2.dataprovider.model.ApplicationConfig; +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.user.common.model.Email; +import org.wso2.identity.integration.test.rest.api.user.common.model.Name; +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.DataExtractUtil; +import org.wso2.identity.integration.test.utils.FileUtils; +import org.wso2.identity.integration.test.utils.OAuth2Constant; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +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; + +/** + * Tests the pre-issue access token action success scenarios with refresh token grant type. + */ +public class PreIssueAccessTokenActionFailureRefreshTokenGrantTestCase extends ActionsBaseTestCase { + + 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 TEST_WSO2 = "Test@wso2"; + private static final String EXTERNAL_SERVICE_URI = "http://localhost:8587/test/action"; + private static final String PRE_ISSUE_ACCESS_TOKEN_API_PATH = "preIssueAccessToken"; + private static final String MOCK_SERVER_ENDPOINT_RESOURCE_PATH = "/test/action"; + private static final String MOCK_SERVER_AUTH_BASIC_USERNAME = "test"; + private static final String MOCK_SERVER_AUTH_BASIC_PASSWORD = "test"; + private static final int APP_CONFIGURED_EXPIRY_TIME = 3600; + private CloseableHttpClient client; + private SCIM2RestClient scim2RestClient; + private List requestedScopes; + private String sessionDataKey; + private String authorizationCode; + private String clientId; + private String clientSecret; + private String actionId; + private String applicationId; + private String userId; + private String refreshToken; + private final TestUserMode userMode; + private final ActionResponse actionResponse; + private final ExpectedTokenResponse expectedResponse; + private ActionsMockServer actionsMockServer; + + @Factory(dataProvider = "testExecutionContextProvider") + public PreIssueAccessTokenActionFailureRefreshTokenGrantTestCase(TestUserMode testUserMode, + ActionResponse actionResponse, + ExpectedTokenResponse expectedResponse) { + + this.userMode = testUserMode; + this.actionResponse = actionResponse; + this.expectedResponse = expectedResponse; + } + + @DataProvider(name = "testExecutionContextProvider") + public static Object[][] getTestExecutionContext() throws Exception { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_USER, new ActionResponse(200, + FileUtils.readFileInClassPathAsString("actions/response/failure-response.json")), + new ExpectedTokenResponse(400, "Some failure reason", "Some description")}, + {TestUserMode.TENANT_USER, new ActionResponse(200, + FileUtils.readFileInClassPathAsString("actions/response/failure-response.json")), + new ExpectedTokenResponse(400, "Some failure reason", "Some description")}, + {TestUserMode.TENANT_USER, new ActionResponse(500, + FileUtils.readFileInClassPathAsString("actions/response/error-response.json")), + new ExpectedTokenResponse(500, "server_error", "Internal Server Error.")}, + {TestUserMode.TENANT_USER, new ActionResponse(401, "Unauthorized"), + new ExpectedTokenResponse(500, "server_error", "Internal Server Error.")}, + }; + } + + @BeforeClass(alwaysRun = true) + public void testInit() throws Exception { + + super.init(userMode); + client = HttpClientBuilder.create() + .setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(String method) { + + return false; + } + }).build(); + + scim2RestClient = new SCIM2RestClient(serverURL, tenantInfo); + applicationId = createOIDCAppWithClaims(); + actionId = createPreIssueAccessTokenAction(); + addUser(); + + requestedScopes = new ArrayList<>(Arrays.asList("openid", "profile")); + + actionsMockServer = new ActionsMockServer(); + actionsMockServer.startServer(); + } + + @AfterClass(alwaysRun = true) + public void atEnd() throws Exception { + + actionsMockServer.stopServer(); + + deleteAction(PRE_ISSUE_ACCESS_TOKEN_API_PATH, actionId); + deleteApp(applicationId); + scim2RestClient.deleteUser(userId); + + restClient.closeHttpClient(); + scim2RestClient.closeHttpClient(); + actionsRestClient.closeHttpClient(); + client.close(); + + actionsMockServer = null; + authorizationCode = null; + } + + @BeforeMethod + public void setupMockServerStub(Method method) throws Exception { + + if (method.getName().equals("testGetAccessTokenWithCodeGrant")) { + 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-code-before-refresh.json"), 200); + } else if (method.getName().equals("testPreIssueAccessTokenActionFailureForRefreshGrant")) { + actionsMockServer.setupStub(MOCK_SERVER_ENDPOINT_RESOURCE_PATH, + "Basic " + getBase64EncodedString(MOCK_SERVER_AUTH_BASIC_USERNAME, MOCK_SERVER_AUTH_BASIC_PASSWORD), + actionResponse.getResponseBody(), actionResponse.getStatusCode()); + } + } + + @Test(groups = "wso2.is", description = + "Get access token with authorization code grant when pre-issue access token action is successful") + public void testGetAccessTokenWithCodeGrant() throws Exception { + + sendAuthorizeRequest(); + performUserLogin(); + HttpResponse response = sendTokenRequestForCodeGrant(); + + 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."); + assertTrue(jsonResponse.has("refresh_token"), "Refresh token not found in the token response."); + assertTrue(jsonResponse.has("expires_in"), "Expiry time not found in the token response."); + assertTrue(jsonResponse.has("token_type"), "Token type not found in the token response."); + + String accessToken = jsonResponse.getString("access_token"); + assertNotNull(accessToken, "Access token is null."); + + refreshToken = jsonResponse.getString("refresh_token"); + assertNotNull(refreshToken, "Refresh token is null."); + + int expiresIn = jsonResponse.getInt("expires_in"); + assertEquals(expiresIn, APP_CONFIGURED_EXPIRY_TIME, "Invalid expiry time for the access token."); + + String tokenType = jsonResponse.getString("token_type"); + assertEquals(tokenType, "Bearer", "Invalid token type for the access token."); + } + + @Test(groups = "wso2.is", description = + "Get access token from refresh token when pre-issue access token action is successful", + dependsOnMethods = "testGetAccessTokenWithCodeGrant") + public void testPreIssueAccessTokenActionFailureForRefreshGrant() throws Exception { + + HttpResponse response = sendTokenRequestForRefreshGrant(); + assertNotNull(response); + assertEquals(response.getStatusLine().getStatusCode(), expectedResponse.getStatusCode()); + + String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); + JSONObject jsonResponse = new JSONObject(responseString); + assertEquals(jsonResponse.getString("error"), expectedResponse.getErrorMessage()); + assertEquals(jsonResponse.getString("error_description"), expectedResponse.getErrorDescription()); + } + + private HttpResponse sendTokenRequestForRefreshGrant() throws IOException { + + List parameters = new ArrayList<>(); + parameters.add(new BasicNameValuePair("grant_type", OAuth2Constant.OAUTH2_GRANT_TYPE_REFRESH_TOKEN)); + parameters.add(new BasicNameValuePair(OAuth2Constant.OAUTH2_GRANT_TYPE_REFRESH_TOKEN, refreshToken)); + + 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)); + + return sendPostRequest(client, headers, parameters, + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain())); + } + + private void sendAuthorizeRequest() 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()); + } + + public void performUserLogin() throws Exception { + + HttpResponse response = sendLoginPostForCustomUsers(client, sessionDataKey, TEST_USER, TEST_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); + } + + private HttpResponse sendTokenRequestForCodeGrant() 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)); + + return sendPostRequest(client, headers, urlParameters, + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain())); + } + + 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); + } + + 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); + } + + private void addUser() throws Exception { + + UserObject userInfo = new UserObject(); + userInfo.setUserName(TEST_USER); + userInfo.setPassword(TEST_WSO2); + userInfo.setName(new Name().givenName("test_user_given_name")); + userInfo.getName().setFamilyName("test_user_last_name"); + userInfo.addEmail(new Email().value("test.user@gmail.com")); + userId = scim2RestClient.createUser(userInfo); + } + + private String createOIDCAppWithClaims() throws Exception { + + List userClaimConfigs = Arrays.asList( + new UserClaimConfig.Builder().localClaimUri("http://wso2.org/claims/givenname"). + oidcClaimUri("given_name").build(), + new UserClaimConfig.Builder().localClaimUri("http://wso2.org/claims/lastname"). + oidcClaimUri("family_name").build() + ); + + ApplicationConfig applicationConfig = new ApplicationConfig.Builder() + .claimsList(userClaimConfigs) + .grantTypes(new ArrayList<>(Arrays.asList("authorization_code", "refresh_token"))) + .tokenType(ApplicationConfig.TokenType.JWT) + .expiryTime(APP_CONFIGURED_EXPIRY_TIME) + .skipConsent(true) + .build(); + + ApplicationResponseModel application = addApplication(applicationConfig); + String applicationIdentifier = application.getId(); + + OpenIDConnectConfiguration oidcConfig = getOIDCInboundDetailsOfApplication(applicationIdentifier); + clientId = oidcConfig.getClientId(); + clientSecret = oidcConfig.getClientSecret(); + + return applicationIdentifier; + } +} 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/PreIssueAccessTokenActionSuccessClientCredentialsGrantTestCase.java similarity index 99% rename from modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenClientCredentialsGrantTestCase.java rename to modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionSuccessClientCredentialsGrantTestCase.java index b9803ae552..5a65ef702d 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/PreIssueAccessTokenActionSuccessClientCredentialsGrantTestCase.java @@ -86,7 +86,7 @@ * This test case extends {@link ActionsBaseTestCase} and focuses on scenarios related * to scopes and claims modifications through an external service. */ -public class PreIssueAccessTokenClientCredentialsGrantTestCase extends ActionsBaseTestCase { +public class PreIssueAccessTokenActionSuccessClientCredentialsGrantTestCase extends ActionsBaseTestCase { private static final String USERNAME_PROPERTY = "username"; private static final String PASSWORD_PROPERTY = "password"; @@ -137,7 +137,7 @@ public class PreIssueAccessTokenClientCredentialsGrantTestCase extends ActionsBa private ActionsMockServer actionsMockServer; @Factory(dataProvider = "testExecutionContextProvider") - public PreIssueAccessTokenClientCredentialsGrantTestCase(TestUserMode testUserMode) { + public PreIssueAccessTokenActionSuccessClientCredentialsGrantTestCase(TestUserMode testUserMode) { this.userMode = testUserMode; this.tenantId = testUserMode == TestUserMode.SUPER_TENANT_USER ? "-1234" : "1"; 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/PreIssueAccessTokenActionSuccessCodeGrantTestCase.java similarity index 99% rename from modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenCodeGrantTestCase.java rename to modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionSuccessCodeGrantTestCase.java index ca71adf674..1e6041e4b5 100644 --- 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/PreIssueAccessTokenActionSuccessCodeGrantTestCase.java @@ -59,7 +59,6 @@ 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; @@ -107,7 +106,7 @@ * 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 { +public class PreIssueAccessTokenActionSuccessCodeGrantTestCase extends ActionsBaseTestCase { private static final String USERS = "users"; private static final String USERNAME_PROPERTY = "username"; @@ -171,7 +170,7 @@ public class PreIssueAccessTokenCodeGrantTestCase extends ActionsBaseTestCase { private ActionsMockServer actionsMockServer; @Factory(dataProvider = "testExecutionContextProvider") - public PreIssueAccessTokenCodeGrantTestCase(TestUserMode testUserMode) { + public PreIssueAccessTokenActionSuccessCodeGrantTestCase(TestUserMode testUserMode) { this.userMode = testUserMode; this.tenantId = testUserMode == TestUserMode.SUPER_TENANT_USER ? "-1234" : "1"; 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/PreIssueAccessTokenActionSuccessPasswordGrantTestCase.java similarity index 99% rename from modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenPasswordGrantTestCase.java rename to modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionSuccessPasswordGrantTestCase.java index c030bfcb70..146c773325 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/PreIssueAccessTokenActionSuccessPasswordGrantTestCase.java @@ -99,7 +99,7 @@ * This test case extends {@link ActionsBaseTestCase} and focuses on scenarios related * to scopes and claims modifications through an external service. */ -public class PreIssueAccessTokenPasswordGrantTestCase extends ActionsBaseTestCase { +public class PreIssueAccessTokenActionSuccessPasswordGrantTestCase extends ActionsBaseTestCase { private static final String USERS = "users"; private static final String USERNAME_PROPERTY = "username"; @@ -158,7 +158,7 @@ public class PreIssueAccessTokenPasswordGrantTestCase extends ActionsBaseTestCas private ActionsMockServer actionsMockServer; @Factory(dataProvider = "testExecutionContextProvider") - public PreIssueAccessTokenPasswordGrantTestCase(TestUserMode testUserMode) { + public PreIssueAccessTokenActionSuccessPasswordGrantTestCase(TestUserMode testUserMode) { this.userMode = testUserMode; this.tenantId = testUserMode == TestUserMode.SUPER_TENANT_USER ? "-1234" : "1"; diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionSuccessRefreshTokenGrantTestCase.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionSuccessRefreshTokenGrantTestCase.java new file mode 100644 index 0000000000..354bc794b8 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/PreIssueAccessTokenActionSuccessRefreshTokenGrantTestCase.java @@ -0,0 +1,501 @@ +/* + * 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.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.utils.URLEncodedUtils; +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.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +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.oauth2.dataprovider.model.ApplicationConfig; +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.user.common.model.Email; +import org.wso2.identity.integration.test.rest.api.user.common.model.Name; +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.DataExtractUtil; +import org.wso2.identity.integration.test.utils.FileUtils; +import org.wso2.identity.integration.test.utils.OAuth2Constant; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +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; + +/** + * Tests the pre-issue access token action success scenarios with refresh token grant type. + */ +public class PreIssueAccessTokenActionSuccessRefreshTokenGrantTestCase extends ActionsBaseTestCase { + + 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 TEST_WSO2 = "Test@wso2"; + private static final String EXTERNAL_SERVICE_URI = "http://localhost:8587/test/action"; + private static final String PRE_ISSUE_ACCESS_TOKEN_API_PATH = "preIssueAccessToken"; + private static final String MOCK_SERVER_ENDPOINT_RESOURCE_PATH = "/test/action"; + private static final String MOCK_SERVER_AUTH_BASIC_USERNAME = "test"; + private static final String MOCK_SERVER_AUTH_BASIC_PASSWORD = "test"; + private static final int APP_CONFIGURED_EXPIRY_TIME = 3600; + private static final int UPDATED_EXPIRY_TIME_BY_ACTION = 7200; + private CloseableHttpClient client; + private SCIM2RestClient scim2RestClient; + private List requestedScopes; + private String sessionDataKey; + private String authorizationCode; + private String clientId; + private String clientSecret; + private String actionId; + private String applicationId; + private String userId; + private String accessToken; + private String refreshToken; + private JWTClaimsSet accessTokenClaims; + private final TestUserMode userMode; + private ActionsMockServer actionsMockServer; + + @Factory(dataProvider = "testExecutionContextProvider") + public PreIssueAccessTokenActionSuccessRefreshTokenGrantTestCase(TestUserMode testUserMode) { + + this.userMode = testUserMode; + } + + @DataProvider(name = "testExecutionContextProvider") + public static Object[][] getTestExecutionContext() { + + return new Object[][]{ + {TestUserMode.SUPER_TENANT_USER}, + {TestUserMode.TENANT_USER} + }; + } + + @BeforeClass(alwaysRun = true) + public void testInit() throws Exception { + + super.init(userMode); + client = HttpClientBuilder.create() + .setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(String method) { + + return false; + } + }).build(); + + scim2RestClient = new SCIM2RestClient(serverURL, tenantInfo); + applicationId = createOIDCAppWithClaims(); + actionId = createPreIssueAccessTokenAction(); + addUser(); + + requestedScopes = new ArrayList<>(Arrays.asList("openid", "profile")); + + actionsMockServer = new ActionsMockServer(); + actionsMockServer.startServer(); + } + + @AfterClass(alwaysRun = true) + public void atEnd() throws Exception { + + actionsMockServer.stopServer(); + + deleteAction(PRE_ISSUE_ACCESS_TOKEN_API_PATH, actionId); + deleteApp(applicationId); + scim2RestClient.deleteUser(userId); + + restClient.closeHttpClient(); + scim2RestClient.closeHttpClient(); + actionsRestClient.closeHttpClient(); + client.close(); + + actionsMockServer = null; + authorizationCode = null; + } + + @BeforeMethod + public void setupMockServerStub(Method method) throws Exception { + + if (method.getName().equals("testGetAccessTokenWithCodeGrant")) { + 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-code-before-refresh.json"), 200); + } else if (method.getName().equals("testGetAccessTokenFromRefreshToken")) { + 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"), + 200); + } + } + + @Test(groups = "wso2.is", description = + "Get access token with authorization code grant when pre-issue access token action is successful") + public void testGetAccessTokenWithCodeGrant() throws Exception { + + sendAuthorizeRequest(); + performUserLogin(); + HttpResponse response = sendTokenRequestForCodeGrant(); + + 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."); + assertTrue(jsonResponse.has("refresh_token"), "Refresh token not found in the token response."); + assertTrue(jsonResponse.has("expires_in"), "Expiry time not found in the token response."); + assertTrue(jsonResponse.has("token_type"), "Token type not found in the token response."); + + accessToken = jsonResponse.getString("access_token"); + assertNotNull(accessToken, "Access token is null."); + + refreshToken = jsonResponse.getString("refresh_token"); + assertNotNull(refreshToken, "Refresh token is null."); + + int expiresIn = jsonResponse.getInt("expires_in"); + assertEquals(expiresIn, APP_CONFIGURED_EXPIRY_TIME, "Invalid expiry time for the access token."); + + String tokenType = jsonResponse.getString("token_type"); + assertEquals(tokenType, "Bearer", "Invalid token type for the access token."); + + accessTokenClaims = getJWTClaimSetFromToken(accessToken); + assertNotNull(accessTokenClaims); + } + + @Test(groups = "wso2.is", description = "Verify the custom string claim in the access token added by action", + dependsOnMethods = "testGetAccessTokenWithCodeGrant") + public void testClaimAddOperationFromPreIssueAccessTokenActionForCodeGrant() throws Exception { + + String claimValue = accessTokenClaims.getStringClaim("custom_claim_string_0"); + Assert.assertEquals(claimValue, "testCustomClaim0"); + } + + @Test(groups = "wso2.is", description = + "Get access token from refresh token when pre-issue access token action is successful", + dependsOnMethods = "testGetAccessTokenWithCodeGrant") + public void testGetAccessTokenFromRefreshToken() throws Exception { + + HttpResponse response = sendTokenRequestForRefreshGrant(); + + 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."); + assertTrue(jsonResponse.has("refresh_token"), "Refresh token not found in the token response."); + assertTrue(jsonResponse.has("expires_in"), "Expiry time not found in the token response."); + assertTrue(jsonResponse.has("token_type"), "Token type not found in the token response."); + + accessToken = jsonResponse.getString("access_token"); + assertNotNull(accessToken, "Access token is null."); + + refreshToken = jsonResponse.getString("refresh_token"); + assertNotNull(refreshToken, "Refresh token is null."); + + int expiresIn = jsonResponse.getInt("expires_in"); + assertEquals(expiresIn, UPDATED_EXPIRY_TIME_BY_ACTION, "Invalid expiry time for the access token."); + + String tokenType = jsonResponse.getString("token_type"); + assertEquals(tokenType, "Bearer", "Invalid token type for the access token."); + + accessTokenClaims = getJWTClaimSetFromToken(accessToken); + assertNotNull(accessTokenClaims); + } + + @Test(groups = "wso2.is", description = "Verify the custom string claim added by action in " + + "code grant is available in the access token", dependsOnMethods = "testGetAccessTokenFromRefreshToken") + public void testClaimAddForAccessTokenFromPreIssueAccessTokenActionForRefreshTokenGrant() + throws Exception { + + testClaimAddOperationFromPreIssueAccessTokenActionForCodeGrant(); + } + + @Test(groups = "wso2.is", description = "Verify the custom boolean claim added by action in the access token", + dependsOnMethods = "testGetAccessTokenFromRefreshToken") + public void testBooleanClaimAddOperationFromPreIssueAccessTokenActionForRefreshTokenGrant() throws Exception { + + boolean claimValue = accessTokenClaims.getBooleanClaim("custom_claim_boolean_1"); + Assert.assertTrue(claimValue); + } + + @Test(groups = "wso2.is", description = "Verify the custom string claim added by action in the access token", + dependsOnMethods = "testGetAccessTokenFromRefreshToken") + public void testStringClaimAddOperationFromPreIssueAccessTokenActionForRefreshTokenGrant() throws Exception { + + String claimValue = accessTokenClaims.getStringClaim("custom_claim_string_1"); + Assert.assertEquals(claimValue, "testCustomClaim1"); + } + + @Test(groups = "wso2.is", description = "Verify the custom number claim added by action in the access token", + dependsOnMethods = "testGetAccessTokenFromRefreshToken") + public void testNumberClaimAddOperationFromPreIssueAccessTokenActionForRefreshTokenGrant() throws Exception { + + int claimValue = accessTokenClaims.getIntegerClaim("custom_claim_number_1"); + Assert.assertEquals(claimValue, 78); + } + + @Test(groups = "wso2.is", description = "Verify the custom string array claim added by action in the " + + "access token", dependsOnMethods = "testGetAccessTokenFromRefreshToken") + public void testClaimArrayAddOperationFromPreIssueAccessTokenActionForRefreshTokenGrant() + throws Exception { + + String[] expectedClaimArrayInToken = {"TestCustomClaim1", "TestCustomClaim2", "TestCustomClaim3"}; + + String[] addedClaimArrayToToken = accessTokenClaims.getStringArrayClaim("custom_claim_string_array_1"); + Assert.assertEquals(addedClaimArrayToToken, expectedClaimArrayInToken); + } + + @Test(groups = "wso2.is", description = "Verify the given_name claim replaced by the action in " + + "access token", dependsOnMethods = "testGetAccessTokenFromRefreshToken") + public void testGivenNameReplaceOperationFromPreIssueAccessTokenActionForRefreshTokenGrant() + throws Exception { + + String givenNameClaim = accessTokenClaims.getStringClaim("given_name"); + Assert.assertEquals(givenNameClaim, "replaced_given_name"); + } + + @Test(groups = "wso2.is", description = "Verify the 'aud' claim updated by action in the " + + "access token", dependsOnMethods = "testGetAccessTokenFromRefreshToken") + public void testAUDUpdateOperationsFromPreIssueAccessTokenActionForRefreshTokenGrant() throws Exception { + + String[] audValueArray = accessTokenClaims.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 scopes updated by action in the access token ", + dependsOnMethods = "testGetAccessTokenFromRefreshToken") + public void testScopeUpdateOperationsFromPreIssueAccessTokenActionForRefreshTokenGrant() throws Exception { + + String[] scopes = accessTokenClaims.getStringClaim("scope").split("\\s+"); + + Assert.assertTrue(ArrayUtils.contains(scopes, "new_test_custom_scope_1")); + Assert.assertTrue(ArrayUtils.contains(scopes, "new_test_custom_scope_2")); + Assert.assertTrue(ArrayUtils.contains(scopes, "new_test_custom_scope_3")); + Assert.assertTrue(ArrayUtils.contains(scopes, "replaced_scope")); + } + + @Test(groups = "wso2.is", description = "Verify the 'expires_in' claim updated by action in the access token", + dependsOnMethods = "testGetAccessTokenFromRefreshToken") + public void testExpiresInClaimReplaceOperationFromPreIssueAccessTokenActionForRefreshTokenGrant() throws Exception { + + Date exp = accessTokenClaims.getDateClaim("exp"); + Date iat = accessTokenClaims.getDateClaim("iat"); + long expiresIn = (exp.getTime() - iat.getTime()) / 1000; + + Assert.assertEquals(expiresIn, UPDATED_EXPIRY_TIME_BY_ACTION); + } + + private HttpResponse sendTokenRequestForRefreshGrant() throws IOException { + + List parameters = new ArrayList<>(); + parameters.add(new BasicNameValuePair("grant_type", OAuth2Constant.OAUTH2_GRANT_TYPE_REFRESH_TOKEN)); + parameters.add(new BasicNameValuePair(OAuth2Constant.OAUTH2_GRANT_TYPE_REFRESH_TOKEN, refreshToken)); + + 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)); + + return sendPostRequest(client, headers, parameters, + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain())); + } + + private void sendAuthorizeRequest() 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()); + } + + public void performUserLogin() throws Exception { + + HttpResponse response = sendLoginPostForCustomUsers(client, sessionDataKey, TEST_USER, TEST_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); + } + + private HttpResponse sendTokenRequestForCodeGrant() 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)); + + return sendPostRequest(client, headers, urlParameters, + getTenantQualifiedURL(ACCESS_TOKEN_ENDPOINT, tenantInfo.getDomain())); + } + + 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); + } + + 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); + } + + private void addUser() throws Exception { + + UserObject userInfo = new UserObject(); + userInfo.setUserName(TEST_USER); + userInfo.setPassword(TEST_WSO2); + userInfo.setName(new Name().givenName("test_user_given_name")); + userInfo.getName().setFamilyName("test_user_last_name"); + userInfo.addEmail(new Email().value("test.user@gmail.com")); + userId = scim2RestClient.createUser(userInfo); + } + + private String createOIDCAppWithClaims() throws Exception { + + List userClaimConfigs = Arrays.asList( + new UserClaimConfig.Builder().localClaimUri("http://wso2.org/claims/givenname"). + oidcClaimUri("given_name").build(), + new UserClaimConfig.Builder().localClaimUri("http://wso2.org/claims/lastname"). + oidcClaimUri("family_name").build() + ); + + ApplicationConfig applicationConfig = new ApplicationConfig.Builder() + .claimsList(userClaimConfigs) + .grantTypes(new ArrayList<>(Arrays.asList("authorization_code", "refresh_token"))) + .tokenType(ApplicationConfig.TokenType.JWT) + .expiryTime(APP_CONFIGURED_EXPIRY_TIME) + .skipConsent(true) + .build(); + + ApplicationResponseModel application = addApplication(applicationConfig); + String applicationId = application.getId(); + + OpenIDConnectConfiguration oidcConfig = getOIDCInboundDetailsOfApplication(applicationId); + clientId = oidcConfig.getClientId(); + clientSecret = oidcConfig.getClientSecret(); + + return applicationId; + } + + private JWTClaimsSet getJWTClaimSetFromToken(String jwtToken) throws ParseException { + + SignedJWT signedJWT = SignedJWT.parse(jwtToken); + return signedJWT.getJWTClaimsSet(); + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/dataprovider/model/ActionResponse.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/dataprovider/model/ActionResponse.java new file mode 100644 index 0000000000..3ea145ab3f --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/dataprovider/model/ActionResponse.java @@ -0,0 +1,44 @@ +/* + * 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.dataprovider.model; + +/** + * This class is used to represent the response from the extension, when an action is invoked. + */ +public class ActionResponse { + + private final int statusCode; + private final String responseBody; + + public ActionResponse(int statusCode, String responseBody) { + + this.statusCode = statusCode; + this.responseBody = responseBody; + } + + public int getStatusCode() { + + return statusCode; + } + + public String getResponseBody() { + + return responseBody; + } +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/dataprovider/model/ExpectedTokenResponse.java b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/dataprovider/model/ExpectedTokenResponse.java new file mode 100644 index 0000000000..c2ac527af7 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/java/org/wso2/identity/integration/test/actions/dataprovider/model/ExpectedTokenResponse.java @@ -0,0 +1,51 @@ +/* + * 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.dataprovider.model; + +/** + * This class is used to represent the response from the token api, in a pre issue access token action invocation. + */ +public class ExpectedTokenResponse { + + private final int statusCode; + private final String errorMessage; + private final String errorDescription; + + public ExpectedTokenResponse(int statusCode, String errorMessage, String errorDescription) { + + this.statusCode = statusCode; + this.errorMessage = errorMessage; + this.errorDescription = errorDescription; + } + + public int getStatusCode() { + + return statusCode; + } + + public String getErrorMessage() { + + return errorMessage; + } + + public String getErrorDescription() { + + return errorDescription; + } +} 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 d49b0e36c5..102d8bc265 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 @@ -39,6 +39,7 @@ public class ActionsMockServer { private WireMockServer wireMockServer; + public void startServer() { wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().port(8587)); @@ -63,6 +64,17 @@ public void setupStub(String url, String authMethod, String responseBody) { .withBody(responseBody))); } + public void setupStub(String url, String authMethod, String responseBody, int statusCode) { + + wireMockServer.stubFor(post(urlEqualTo(url)) + .withHeader("Authorization", matching(authMethod)) + .willReturn(aResponse() + .withStatus(statusCode) + .withHeader("Content-Type", "application/json") + .withHeader("Connection", "Close") + .withBody(responseBody))); + } + public String getReceivedRequestPayload(String url) { List requestList = wireMockServer.findAll(postRequestedFor(urlEqualTo(url))); diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/error-response.json b/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/error-response.json new file mode 100644 index 0000000000..bb71eeca3c --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/error-response.json @@ -0,0 +1,5 @@ +{ + "actionStatus": "ERROR", + "errorMessage": "Some error message", + "errorDescription": "Some error description" +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/failure-response.json b/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/failure-response.json new file mode 100644 index 0000000000..cffa0126db --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/failure-response.json @@ -0,0 +1,5 @@ +{ + "actionStatus": "FAILED", + "failureReason": "Some failure reason", + "failureDescription": "Some description" +} diff --git a/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/pre-issue-access-token-response-code-before-refresh.json b/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/pre-issue-access-token-response-code-before-refresh.json new file mode 100644 index 0000000000..4736e90ac2 --- /dev/null +++ b/modules/integration/tests-integration/tests-backend/src/test/resources/actions/response/pre-issue-access-token-response-code-before-refresh.json @@ -0,0 +1,13 @@ +{ + "actionStatus": "SUCCESS", + "operations": [ + { + "op": "add", + "path": "/accessToken/claims/-", + "value": { + "name": "custom_claim_string_0", + "value": "testCustomClaim0" + } + } + ] +} 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 33bc54e200..bc918087b6 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 @@ -140,9 +140,14 @@ - - - + + + + + + + +