diff --git a/.github/workflows/kapua-ci.yaml b/.github/workflows/kapua-ci.yaml index b7f5ccbbbaa..2e7e8ce17cb 100755 --- a/.github/workflows/kapua-ci.yaml +++ b/.github/workflows/kapua-ci.yaml @@ -307,6 +307,17 @@ jobs: with: tag: '@endpoint' needs-docker-images: 'true' + test-api-auth: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Clones Kapua repo inside the runner + uses: actions/checkout@v3 + - uses: ./.github/actions/runTestsTaggedAs + with: + tag: '@rest_auth' + needs-docker-images: 'true' junit-tests: needs: build runs-on: ubuntu-latest diff --git a/assembly/api/docker/Dockerfile b/assembly/api/docker/Dockerfile index 3e01ca9885c..da56069c3d8 100644 --- a/assembly/api/docker/Dockerfile +++ b/assembly/api/docker/Dockerfile @@ -45,6 +45,8 @@ ENV JAVA_OPTS "-Dapi.cors.origins.allowed=\${API_CORS_ORIGINS_ALLOWED} \ -Djob.engine.client.auth.mode=access_token \ -Dcipher.key=\${CIPHER_KEY} \ -Dcrypto.secret.key=\${CRYPTO_SECRET_KEY} \ + -Dauthentication.token.expire.after=\${AUTH_TOKEN_TTL} \ + -Dauthentication.refresh.token.expire.after=\${REFRESH_AUTH_TOKEN_TTL} \ \${JETTY_DEBUG_OPTS} \ \${JETTY_JMX_OPTS}" diff --git a/commons-rest/errors/src/main/java/org/eclipse/kapua/commons/rest/errors/KapuaAuthenticationExceptionMapper.java b/commons-rest/errors/src/main/java/org/eclipse/kapua/commons/rest/errors/KapuaAuthenticationExceptionMapper.java index 7cd94c6c6e4..aa161f44314 100644 --- a/commons-rest/errors/src/main/java/org/eclipse/kapua/commons/rest/errors/KapuaAuthenticationExceptionMapper.java +++ b/commons-rest/errors/src/main/java/org/eclipse/kapua/commons/rest/errors/KapuaAuthenticationExceptionMapper.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.eclipse.kapua.commons.rest.errors; +import org.eclipse.kapua.commons.rest.model.errors.ExceptionInfo; import org.eclipse.kapua.commons.rest.model.errors.MfaRequiredExceptionInfo; import org.eclipse.kapua.service.authentication.exception.KapuaAuthenticationErrorCodes; import org.eclipse.kapua.service.authentication.exception.KapuaAuthenticationException; @@ -47,6 +48,13 @@ public Response toResponse(KapuaAuthenticationException kapuaAuthenticationExcep .build(); } + if (kapuaAuthenticationException.getCode().equals(KapuaAuthenticationErrorCodes.REFRESH_ERROR)) { + return Response + .status(Status.UNAUTHORIZED) + .entity(new ExceptionInfo(Status.UNAUTHORIZED.getStatusCode(), kapuaAuthenticationException, showStackTrace)) + .build(); + } + return Response.status(Status.UNAUTHORIZED).build(); } diff --git a/commons-rest/filters/src/main/java/org/eclipse/kapua/commons/rest/filters/CORSResponseFilter.java b/commons-rest/filters/src/main/java/org/eclipse/kapua/commons/rest/filters/CORSResponseFilter.java index dde9e1fe490..83bc0c0b3dc 100644 --- a/commons-rest/filters/src/main/java/org/eclipse/kapua/commons/rest/filters/CORSResponseFilter.java +++ b/commons-rest/filters/src/main/java/org/eclipse/kapua/commons/rest/filters/CORSResponseFilter.java @@ -135,7 +135,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } int errorCode = httpResponse.getStatus(); if (errorCode >= 400) { - // if there's an error code at this point, return it and stop the chain + if (errorMessage == null) { //CORS filter passed...show propagated error message to the client + errorMessage = httpResponse.getHeader("exceptionMessagePropagatedToCORS"); + } + httpResponse.setHeader("exceptionMessagePropagatedToCORS", null); //no more needed, delete from response header httpResponse.sendError(errorCode, errorMessage); return; } diff --git a/deployment/docker/compose/docker-compose.yml b/deployment/docker/compose/docker-compose.yml index eaad2cc9ea9..2a6cb95c6ab 100644 --- a/deployment/docker/compose/docker-compose.yml +++ b/deployment/docker/compose/docker-compose.yml @@ -108,6 +108,8 @@ services: - KAPUA_DISABLE_DATASTORE - LOGBACK_LOG_LEVEL - SWAGGER=${KAPUA_SWAGGER_ENABLE:-true} + - AUTH_TOKEN_TTL=${AUTH_TOKEN_TTL:-1800000} + - REFRESH_AUTH_TOKEN_TTL=${REFRESH_AUTH_TOKEN_TTL:-18000000} job-engine: container_name: job-engine image: kapua/kapua-job-engine:${IMAGE_VERSION} diff --git a/deployment/docker/compose/extras/docker-compose.api-dev.yml b/deployment/docker/compose/extras/docker-compose.api-dev.yml new file mode 100644 index 00000000000..94f9930af4c --- /dev/null +++ b/deployment/docker/compose/extras/docker-compose.api-dev.yml @@ -0,0 +1,7 @@ +version: '3.1' + +services: + kapua-api: + environment: + - AUTH_TOKEN_TTL=180000000 + - REFRESH_AUTH_TOKEN_TTL=180000000 diff --git a/deployment/docker/unix/docker-deploy.sh b/deployment/docker/unix/docker-deploy.sh index 6e946591ee6..2b691fb27f2 100755 --- a/deployment/docker/unix/docker-deploy.sh +++ b/deployment/docker/unix/docker-deploy.sh @@ -54,6 +54,7 @@ docker_compose() { echo "Dev mode enabled!" COMPOSE_FILES+=(-f "${SCRIPT_DIR}/../compose/extras/docker-compose.db-dev.yml") COMPOSE_FILES+=(-f "${SCRIPT_DIR}/../compose/extras/docker-compose.es-dev.yml") + COMPOSE_FILES+=(-f "${SCRIPT_DIR}/../compose/extras/docker-compose.api-dev.yml") fi # SSL diff --git a/qa/common/src/main/java/org/eclipse/kapua/qa/common/BasicSteps.java b/qa/common/src/main/java/org/eclipse/kapua/qa/common/BasicSteps.java index 00f4aee494d..0e1798f4750 100644 --- a/qa/common/src/main/java/org/eclipse/kapua/qa/common/BasicSteps.java +++ b/qa/common/src/main/java/org/eclipse/kapua/qa/common/BasicSteps.java @@ -75,6 +75,7 @@ public class BasicSteps extends TestBase { public static final String TELEMETRY_CONSUMER_CONTAINER_NAME = "telemetry-consumer"; public static final String LIFECYCLE_CONSUMER_CONTAINER_NAME = "lifecycle-consumer"; public static final String AUTH_SERVICE_CONTAINER_NAME = "auth-service"; + public static final String API_CONTAINER_NAME = "rest-api"; public static final int JOB_ENGINE_CONTAINER_PORT = 8080; public static final String LAST_CREDENTIAL_ID = "LastCredentialId"; diff --git a/qa/common/src/main/java/org/eclipse/kapua/qa/common/TestJAXBContextProvider.java b/qa/common/src/main/java/org/eclipse/kapua/qa/common/TestJAXBContextProvider.java index d43c4110cef..7d176045afa 100644 --- a/qa/common/src/main/java/org/eclipse/kapua/qa/common/TestJAXBContextProvider.java +++ b/qa/common/src/main/java/org/eclipse/kapua/qa/common/TestJAXBContextProvider.java @@ -33,6 +33,7 @@ import org.eclipse.kapua.model.config.metatype.KapuaTocd; import org.eclipse.kapua.model.config.metatype.KapuaToption; import org.eclipse.kapua.model.config.metatype.MetatypeXmlRegistry; +import org.eclipse.kapua.service.authentication.token.AccessToken; import org.eclipse.kapua.service.device.call.kura.model.bundle.KuraBundle; import org.eclipse.kapua.service.device.call.kura.model.bundle.KuraBundles; import org.eclipse.kapua.service.device.call.kura.model.configuration.KuraDeviceComponentConfiguration; @@ -85,6 +86,8 @@ import org.eclipse.kapua.service.job.Job; import org.eclipse.kapua.service.job.JobListResult; import org.eclipse.kapua.service.job.JobXmlRegistry; +import org.eclipse.kapua.service.user.User; +import org.eclipse.kapua.service.user.UserListResult; import org.eclipse.persistence.jaxb.JAXBContextFactory; import org.eclipse.persistence.jaxb.MarshallerProperties; import org.slf4j.Logger; @@ -191,7 +194,11 @@ public JAXBContext getJAXBContext() throws KapuaException { DeviceConfiguration.class, DevicePackages.class, DevicePackageDownloadRequest.class, - DevicePackageUninstallRequest.class + DevicePackageUninstallRequest.class, + + AccessToken.class, + User.class, + UserListResult.class }; try { Map properties = new HashMap<>(1); diff --git a/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/DockerSteps.java b/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/DockerSteps.java index f34ee62e79e..6920f95c8eb 100644 --- a/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/DockerSteps.java +++ b/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/DockerSteps.java @@ -73,6 +73,7 @@ public class DockerSteps { private static final String LIFECYCLE_CONSUMER_IMAGE = "kapua-consumer-lifecycle"; private static final String TELEMETRY_CONSUMER_IMAGE = "kapua-consumer-telemetry"; private static final String AUTH_SERVICE_IMAGE = "kapua-service-authentication"; + private static final String API_IMAGE = "kapua-api"; private static final List DEFAULT_DEPLOYMENT_CONTAINERS_NAME; private static final List DEFAULT_BASE_DEPLOYMENT_CONTAINERS_NAME; private static final int WAIT_COUNT = 120;//total wait time = 240 secs (120 * 2000ms) @@ -104,6 +105,7 @@ public class DockerSteps { DEFAULT_DEPLOYMENT_CONTAINERS_NAME.add(BasicSteps.LIFECYCLE_CONSUMER_CONTAINER_NAME); DEFAULT_DEPLOYMENT_CONTAINERS_NAME.add(BasicSteps.AUTH_SERVICE_CONTAINER_NAME); DEFAULT_DEPLOYMENT_CONTAINERS_NAME.add(BasicSteps.MESSAGE_BROKER_CONTAINER_NAME); + DEFAULT_DEPLOYMENT_CONTAINERS_NAME.add(BasicSteps.API_CONTAINER_NAME); DEFAULT_BASE_DEPLOYMENT_CONTAINERS_NAME = new ArrayList<>(); DEFAULT_BASE_DEPLOYMENT_CONTAINERS_NAME.add(BasicSteps.JOB_ENGINE_CONTAINER_NAME); DEFAULT_BASE_DEPLOYMENT_CONTAINERS_NAME.add(BasicSteps.EVENTS_BROKER_CONTAINER_NAME); @@ -271,6 +273,41 @@ private void startBaseDockerEnvironmentInternal() throws Exception { synchronized (this) { this.wait(WAIT_FOR_JOB_ENGINE); } + + } catch (Exception e) { + logger.error("Error while starting base docker environment: {}", e.getMessage(), e); + throw e; + } + } + + @Given("start rest-API container and dependencies with auth token TTL {string}ms and refresh token TTL {string}ms") + public void startApiDockerEnvironment(String tokenTTL, String refreshTokenTTL) throws Exception { + logger.info("Starting rest-api docker environment..."); + stopFullDockerEnvironmentInternal(); + try { + removeNetwork(); + createNetwork(); + + startDBContainer(BasicSteps.DB_CONTAINER_NAME); + synchronized (this) { + this.wait(WAIT_FOR_DB); + } + + startEventBrokerContainer(BasicSteps.EVENTS_BROKER_CONTAINER_NAME); + synchronized (this) { + this.wait(WAIT_FOR_EVENTS_BROKER); + } + + startJobEngineContainer(BasicSteps.JOB_ENGINE_CONTAINER_NAME); + synchronized (this) { + this.wait(WAIT_FOR_JOB_ENGINE); + } + + startAPIContainer(BasicSteps.API_CONTAINER_NAME, tokenTTL, refreshTokenTTL); + synchronized (this) { + this.wait(WAIT_FOR_JOB_ENGINE); + } + } catch (Exception e) { logger.error("Error while starting base docker environment: {}", e.getMessage(), e); throw e; @@ -481,6 +518,19 @@ public void startDBContainer(String name) throws DockerException, InterruptedExc logger.info("DB container started: {}", containerId); } + @And("Start API container with name {string}") + public void startAPIContainer(String name, String tokenTTL, String refreshTokenTTL) throws DockerException, InterruptedException { + logger.info("Starting API container..."); + ContainerConfig dbConfig = getApiContainerConfig(tokenTTL, refreshTokenTTL); + ContainerCreation dbContainerCreation = DockerUtil.getDockerClient().createContainer(dbConfig, name); + String containerId = dbContainerCreation.id(); + + DockerUtil.getDockerClient().startContainer(containerId); + DockerUtil.getDockerClient().connectToNetwork(containerId, networkId); + containerMap.put("api", containerId); + logger.info("API container started: {}", containerId); + } + @And("Start ES container with name {string}") public void startESContainer(String name) throws DockerException, InterruptedException { logger.info("Starting ES container..."); @@ -770,6 +820,32 @@ private ContainerConfig getDbContainerConfig() { .build(); } + private ContainerConfig getApiContainerConfig(String tokenTTL, String refreshTokenTTL) { + final Map> portBindings = new HashMap<>(); + addHostPort(ALL_IP, portBindings, 8080, 8081); + addHostPort(ALL_IP, portBindings, 8443, 8443); + final HostConfig hostConfig = HostConfig.builder().portBindings(portBindings).build(); + + String[] ports = { + String.valueOf(8080), + String.valueOf(8443) + }; + + return ContainerConfig.builder() + .hostConfig(hostConfig) + .exposedPorts(ports) + .env( + "CRYPTO_SECRET_KEY=kapuaTestsKey!!!", + "KAPUA_DISABLE_DATASTORE=false", + //now I set very little TTL access token to help me in the test scenarios + "AUTH_TOKEN_TTL=" + tokenTTL, + "REFRESH_AUTH_TOKEN_TTL=" + refreshTokenTTL, + "SWAGGER=true" + ) + .image("kapua/" + API_IMAGE + ":" + KAPUA_VERSION) + .build(); + } + /** * Creation of docker container configuration for telemetry consumer. * diff --git a/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/RestClientSteps.java b/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/RestClientSteps.java index 74b28dd9b6d..5b473c0a0ea 100644 --- a/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/RestClientSteps.java +++ b/qa/integration-steps/src/main/java/org/eclipse/kapua/qa/integration/steps/RestClientSteps.java @@ -12,7 +12,11 @@ *******************************************************************************/ package org.eclipse.kapua.qa.integration.steps; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import io.cucumber.guice.ScenarioScoped; +import io.cucumber.java.en.And; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; @@ -23,6 +27,7 @@ import org.eclipse.kapua.service.authentication.token.AccessToken; import org.eclipse.kapua.service.user.User; import org.eclipse.kapua.service.user.UserListResult; +import org.jose4j.json.internal.json_simple.JSONObject; import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +38,11 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.util.List; @ScenarioScoped @@ -41,7 +50,8 @@ public class RestClientSteps { private static final Logger logger = LoggerFactory.getLogger(RestClientSteps.class); - private static final String TOKEN_ID = "tokenId"; + private static final String TOKEN_ID = "tokenId"; //jwt + private static final String REFRESH_TOKEN = "refreshToken"; private static final String REST_RESPONSE = "restResponse"; private static final String REST_RESPONSE_CODE = "restResponseCode"; @@ -130,6 +140,93 @@ public void restPostCallWithJson(String resource, String json) throws Exception } } + @When("REST {string} call at {string} with JSON {string}") + public void restCall(String method, String resource, String json) throws Exception { + restCallInternal(method, resource, json, true); + } + + public void restCallInternal(String method, String resource, String json, boolean authenticateCall) throws Exception { + // Create an instance of HttpClient + HttpClient httpClient = HttpClient.newHttpClient(); + String host = (String) stepData.get("host"); + String port = (String) stepData.get("port"); + + resource = insertStepData(resource); + // Define the URL you want to send the GET request to + String url = "http://" + host + ":" + port + resource; + + HttpRequest.Builder baseBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept-Language", "UTF-8") + .header("accept", "application/json"); + + if (authenticateCall) { + String tokenId = (String) stepData.get(TOKEN_ID); + baseBuilder.setHeader("Authorization", "Bearer " + tokenId); + } + + if (method.equals("POST")) { + baseBuilder.setHeader("Content-Type", "application/json"); + baseBuilder.POST(HttpRequest.BodyPublishers.ofString(json)); + } + else if (method.equals("GET")) { + baseBuilder.GET(); + } + + // Create an HttpRequest object + HttpRequest request = baseBuilder + .build(); + + try { + // Send the request and retrieve the response + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + // Print out the response status code + System.out.println("Response Code: " + response.statusCode()); + stepData.put(REST_RESPONSE_CODE, response.statusCode()); + + // Print out the response body + System.out.println("Response Body: " + response.body()); + stepData.put(REST_RESPONSE, response.body()); + + } catch (Exception e) { + // Handle exceptions + logger.error("Exception on REST POST call execution: " + resource); + throw e; + } + } + + @When("I refresh last access token") + public void refreshToken() throws Exception { + String tokenId = (String) stepData.get(TOKEN_ID); + String refreshToken = (String) stepData.get(REFRESH_TOKEN); + String resource = "/v1/authentication/refresh"; + JSONObject jsonObject = new JSONObject(); + jsonObject.put("refreshToken", refreshToken); + jsonObject.put("tokenId", tokenId); + restCallInternal("POST", resource, jsonObject.toString(), false); + } + + @When("I refresh access token using refresh token {string} and jwt {string}") + public void refreshPreciseToken(String refreshToken, String jwt) throws Exception { + if (!jwt.isEmpty()) { + stepData.put(TOKEN_ID, jwt); + } + if (!refreshToken.isEmpty()) { + stepData.put(REFRESH_TOKEN, refreshToken); + } + refreshToken(); + } + + @And("I extract {string} from the response and I save it in the key {string}") + public void exctractFromResponse(String field, String key) throws JsonProcessingException { + String response = (String) stepData.get(REST_RESPONSE); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonResponse = objectMapper.readTree(response); + stepData.put(key,jsonResponse.get(field).toString().replace("\"", "")); + String test = (String) stepData.get(key); + } + @Then("REST response containing text {string}") public void restResponseContaining(String checkStr) throws Exception { String restResponse = (String) stepData.get(REST_RESPONSE); @@ -137,6 +234,7 @@ public void restResponseContaining(String checkStr) throws Exception { restResponse.contains(checkStr)); } + @Then("REST response containing Account") public void restResponseContainingAccount() throws Exception { String restResponse = (String) stepData.get(REST_RESPONSE); @@ -161,6 +259,7 @@ public void restResponseContainingAccessToken() throws Exception { AccessToken token = XmlUtil.unmarshalJson(restResponse, AccessToken.class); Assert.assertTrue("Token is null.", token.getTokenId() != null); stepData.put(TOKEN_ID, token.getTokenId()); + stepData.put(REFRESH_TOKEN, token.getRefreshToken()); } @Then("REST response containing User") @@ -199,8 +298,8 @@ public void restResponseContainsLimitExceedValueWithValue(String value) throws E @Given("^An authenticated user$") public void anAuthenticationToken() throws Exception { - restPostCallWithJson("/v1/authentication/user", - "{\"password\": \"kapua-password\", \"username\": \"kapua-sys\"}"); + restCallInternal("POST", "/v1/authentication/user", + "{\"password\": \"kapua-password\", \"username\": \"kapua-sys\"}", false); restResponseContainingAccessToken(); } diff --git a/qa/integration/src/test/java/org/eclipse/kapua/integration/rest/RunRestAuthTest.java b/qa/integration/src/test/java/org/eclipse/kapua/integration/rest/RunRestAuthTest.java new file mode 100644 index 00000000000..1a3b651be13 --- /dev/null +++ b/qa/integration/src/test/java/org/eclipse/kapua/integration/rest/RunRestAuthTest.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2018, 2022 Eurotech and/or its affiliates and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eurotech + *******************************************************************************/ +package org.eclipse.kapua.integration.rest; + +import io.cucumber.junit.Cucumber; +import io.cucumber.junit.CucumberOptions; + +import org.junit.runner.RunWith; + +@RunWith(Cucumber.class) +@CucumberOptions( + features = "classpath:features/rest/authentication/RestAuth.feature", + glue = {"org.eclipse.kapua.qa.common", + "org.eclipse.kapua.qa.integration.steps", + "org.eclipse.kapua.service.account.steps", + "org.eclipse.kapua.service.user.steps" + }, + plugin = { "pretty", + "html:target/cucumber/RestAuth", + "json:target/RestAuth_cucumber.json" + }, + monochrome = true) + +public class RunRestAuthTest { +} diff --git a/qa/integration/src/test/resources/features/rest/authentication/RestAuth.feature b/qa/integration/src/test/resources/features/rest/authentication/RestAuth.feature new file mode 100644 index 00000000000..3369f262df4 --- /dev/null +++ b/qa/integration/src/test/resources/features/rest/authentication/RestAuth.feature @@ -0,0 +1,107 @@ +############################################################################### +# Copyright (c) 2018, 2022 Eurotech and/or its affiliates and others +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# Eurotech - initial API and implementation +############################################################################### +@env_docker_base +@rest_auth + +Feature: REST API tests for User + REST API tests for authentication via login-pw and access token. Refresh access token feature is also covered + + @setup + Scenario: Initialize Jaxb and security context, then start rest-api container and dependencies + Given Init Jaxb Context + And Init Security Context + #NB: be aware that sub-sequent tests depends on the value of TTL that you set here + And start rest-API container and dependencies with auth token TTL "3000"ms and refresh token TTL "2000"ms + + Scenario: Simple login with username-pw works and I can call another API endpoint without errors + First, the authentication via login-pw is tested. + Then, the access token auth. is tested trough the call to the "get user" api call using the previous generated access token + + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + When REST "GET" call at "/v1/_/users?offset=0&limit=50" with JSON "" + Then REST response code is 200 + Then REST response contains list of Users + + Scenario: 'Refresh token' feature is working properly and returns a token that I can use to login + + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + When I refresh last access token + Then REST response code is 200 + # now I extract the jwt created by the refresh and I substitute the old token with this one. Doing so, I use that jwt to perform the next api call + And I extract "tokenId" from the response and I save it in the key "tokenId" + When REST "GET" call at "/v1/_/users?offset=0&limit=50" with JSON "" + Then REST response code is 200 + Then REST response contains list of Users + + Scenario: Refresh token, then try to call another api endpoint with the previous "refreshed", now invalidated, token + The call should fail because you are using an invalidated token for auth. when calling the "get user" api + + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + When I refresh last access token + Then REST response code is 200 + When REST "GET" call at "/v1/_/users?offset=0&limit=50" with JSON "" + Then REST response code is 401 + And REST response containing text "The provided access token has been invalidated in the past" + + Scenario: Auth. with access token fails when I wait the token TTL + + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + When REST "GET" call at "/v1/_/users?offset=0&limit=50" with JSON "" + Then REST response code is 200 + Then REST response contains list of Users + Then I wait 3 seconds + When REST "GET" call at "/v1/_/users?offset=0&limit=50" with JSON "" + Then REST response code is 401 + + Scenario: Refresh token using wrong parameters - wrong refresh token, previous jwt (right) + + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + When I refresh access token using refresh token "foo" and jwt "" + Then REST response code is 401 + And REST response containing text "The provided refresh token doesn't match the one for this jwt" + + Scenario: Refresh token using wrong parameters - previous refresh token (right), wrong jwt + + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + When I refresh access token using refresh token "" and jwt "foo" + Then REST response code is 401 + And REST response containing text "Error while refreshing the AccessToken" + + Scenario: Refresh a token when I wait the token refresh TTL + + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + Then I wait 2 seconds + When I refresh last access token + Then REST response code is 401 + And REST response containing text "The provided refresh token is expired" + + Scenario: Refresh a token that has been invalidated in the past + + Given Server with host "127.0.0.1" on port "8081" + Given An authenticated user + When I refresh last access token + #now in step data I have the previous token that is invalidated on db + When I refresh last access token + Then REST response code is 401 + And REST response containing text "The provided access token has been invalidated" + + @teardown + Scenario: Stop full docker environment + Given Stop full docker environment diff --git a/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/auth/KapuaTokenAuthenticationFilter.java b/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/auth/KapuaTokenAuthenticationFilter.java index c6ea4c4d832..49313d0cb21 100644 --- a/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/auth/KapuaTokenAuthenticationFilter.java +++ b/rest-api/core/src/main/java/org/eclipse/kapua/app/api/core/auth/KapuaTokenAuthenticationFilter.java @@ -20,6 +20,9 @@ import org.eclipse.kapua.locator.KapuaLocator; import org.eclipse.kapua.service.authentication.AccessTokenCredentials; import org.eclipse.kapua.service.authentication.CredentialsFactory; +import org.eclipse.kapua.service.authentication.shiro.exceptions.ExpiredAccessTokenException; +import org.eclipse.kapua.service.authentication.shiro.exceptions.InvalidatedAccessTokenException; +import org.eclipse.kapua.service.authentication.shiro.exceptions.MalformedAccessTokenException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; @@ -69,12 +72,31 @@ protected AuthenticationToken createToken(ServletRequest request, ServletRespons return (AuthenticationToken) accessTokenCredentials; } - @Override - protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { + protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, + ServletRequest request, ServletResponse response) { HttpServletResponse httpResponse = WebUtils.toHttp(response); httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - // Continue with the filter chain, because CORS headers are still needed in the case when token is not authenticated or expired + //now I set a dummy header to propagate the error message to the CORSResponseFilter Class, that eventually will send this error message if CORS filter passes + httpResponse.setHeader("exceptionMessagePropagatedToCORS", handleAuthException(e)); + return false; + } + + @Override + protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { + // Continue with the filter chain, because CORS headers are still needed return true; } + + //with this method we choose what exceptions we want to hide in the response and what we want to show as an error message + private String handleAuthException(AuthenticationException ae) { + String errorMessageInResponse = "An error occurred during the authentication process with the provided access token"; + if (ae instanceof MalformedAccessTokenException || + ae instanceof InvalidatedAccessTokenException || + ae instanceof ExpiredAccessTokenException) { + errorMessageInResponse = ae.getMessage(); + } + return errorMessageInResponse; + } + } diff --git a/rest-api/core/src/test/java/org/eclipse/kapua/app/api/core/auth/KapuaTokenAuthenticationFilterTest.java b/rest-api/core/src/test/java/org/eclipse/kapua/app/api/core/auth/KapuaTokenAuthenticationFilterTest.java index 1595d2f8bfd..a7f9911c85d 100644 --- a/rest-api/core/src/test/java/org/eclipse/kapua/app/api/core/auth/KapuaTokenAuthenticationFilterTest.java +++ b/rest-api/core/src/test/java/org/eclipse/kapua/app/api/core/auth/KapuaTokenAuthenticationFilterTest.java @@ -57,9 +57,4 @@ public void onAccessDeniedTest() throws Exception { public void onAccessDeniedNullRequestTest() throws Exception { Assert.assertTrue("True expected.", kapuaTokenAuthenticationFilter.onAccessDenied(null, response)); } - - @Test(expected = NullPointerException.class) - public void onAccessDeniedNullResponseTest() throws Exception { - Assert.assertTrue("True expected.", kapuaTokenAuthenticationFilter.onAccessDenied(request, null)); - } } diff --git a/service/api/src/main/java/org/eclipse/kapua/KapuaErrorCodes.java b/service/api/src/main/java/org/eclipse/kapua/KapuaErrorCodes.java index 8aadd8fdc3f..75a07354d13 100644 --- a/service/api/src/main/java/org/eclipse/kapua/KapuaErrorCodes.java +++ b/service/api/src/main/java/org/eclipse/kapua/KapuaErrorCodes.java @@ -134,4 +134,10 @@ public enum KapuaErrorCodes implements KapuaErrorCode { * The service has been disabled */ SERVICE_DISABLED, + + /** + * Some parsing failed for some reason + * @since 2.0.0 + */ + PARSING_ERROR } diff --git a/service/api/src/main/resources/kapua-service-error-messages.properties b/service/api/src/main/resources/kapua-service-error-messages.properties index 29fa4b9c38b..d0e01cf24e9 100644 --- a/service/api/src/main/resources/kapua-service-error-messages.properties +++ b/service/api/src/main/resources/kapua-service-error-messages.properties @@ -37,4 +37,5 @@ SERVICE_DISABLED=The Service is disabled: {0} UNAUTHENTICATED=No authenticated Subject found in context. # Deprecated codes USER_ALREADY_RESERVED_BY_ANOTHER_CONNECTION=This user is already reserved for another connection. Please select different user for this connection. -DEVICE_NOT_FOUND=The selected devices were not found. Please refresh device list. \ No newline at end of file +DEVICE_NOT_FOUND=The selected devices were not found. Please refresh device list. +PARSING_ERROR=Error while parsing: {0} \ No newline at end of file diff --git a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/AuthenticationService.java b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/AuthenticationService.java index 8109ef64111..e114b4b25f0 100644 --- a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/AuthenticationService.java +++ b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/AuthenticationService.java @@ -103,11 +103,11 @@ public interface AuthenticationService extends KapuaService { /** * Gets the {@link AccessToken} identified by its {@link AccessToken#getTokenId()}. - * Expired {@link AccessToken}s are excluded. + * In other words, given the JWT it returns the respective Access Token entity * * @param tokenId The {@link AccessToken#getTokenId()} to look for. * @return The found {@link AccessToken} or {@code null} if not present. - * @throws KapuaException + * @throws KapuaException if the provided tokenId (JWT) is not valid in its header or payload content * @since 1.0.0 */ AccessToken findAccessToken(String tokenId) throws KapuaException; diff --git a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessToken.java b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessToken.java index 5f0e5681dfc..dcee70ce1ea 100644 --- a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessToken.java +++ b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessToken.java @@ -22,6 +22,7 @@ import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlType; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import java.io.Serializable; @@ -41,7 +42,7 @@ "refreshToken", // "refreshExpiresOn", // "invalidatedOn", // - "trustKey" // + "trustKey" // }, // factoryClass = AccessTokenXmlRegistry.class, // factoryMethod = "newAccessToken") @@ -56,6 +57,7 @@ default String getType() { /** * Return the token identifier + * This represents the content of the JWT token * * @return the token identifier * @since 1.0.0 @@ -181,4 +183,22 @@ default String getType() { */ void setTrustKey(String trustKey); + /** + * Gets the token identifier + * This represents an id for the JWT token and is meant to be inserted inside its payload + * + * @return The token id + * @since 2.0 + */ + @XmlTransient + String getTokenIdentifier(); + + /** + * Sets the token identifier + * + * @param tokenId the token id to set + * @since 2.0 + */ + void setTokenIdentifier(String tokenId); + } diff --git a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenAttributes.java b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenAttributes.java index 9579e97c8d1..3a81f080592 100644 --- a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenAttributes.java +++ b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenAttributes.java @@ -21,10 +21,12 @@ */ public class AccessTokenAttributes extends KapuaUpdatableEntityAttributes { - public static final String TOKEN_ID = "tokenId"; + public static final String TOKEN_ID = "tokenId"; //This is the content of the JWT token + public static final String TOKEN_IDENTIFIER = "tokenIdentifier"; //This is an identifier of the JWT token that is meant to be inserted inside its payload public static final String USER_ID = "userId"; public static final String EXPIRES_ON = "expiresOn"; public static final String INVALIDATED_ON = "invalidatedOn"; public static final String REFRESH_EXPIRES_ON = "refreshExpiresOn"; + } diff --git a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenCreator.java b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenCreator.java index 6295ba99376..91d1703c670 100644 --- a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenCreator.java +++ b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenCreator.java @@ -33,7 +33,8 @@ "userId", "expiresOn", "refreshToken", - "refreshExpiresOn" + "refreshExpiresOn", + "tokenIdentifier" }, // factoryClass = AccessTokenXmlRegistry.class, // factoryMethod = "newAccessTokenCreator") @@ -41,6 +42,7 @@ public interface AccessTokenCreator extends KapuaEntityCreator { /** * Gets the token id + * This represents the content of the JWT token * * @return The token id * @since 1.0 @@ -121,4 +123,24 @@ public interface AccessTokenCreator extends KapuaEntityCreator { * @since 1.0 */ void setRefreshExpiresOn(Date refreshExpiresOn); + + /** + * Gets the token identifier + * This represents an id for the JWT token and is meant to be inserted inside its payload + * + * @return The token id + * @since 2.0 + */ + @XmlElement(name = "tokenIdentifier") + String getTokenIdentifier(); + + /** + * Sets the token identifier + * + * @param tokenId the token id to set + * @since 2.0 + */ + void setTokenIdentifier(String tokenId); + + } diff --git a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenFactory.java b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenFactory.java index 9f285461665..5aabab8123b 100644 --- a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenFactory.java +++ b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenFactory.java @@ -36,7 +36,7 @@ public interface AccessTokenFactory extends KapuaEntityFactory { + + /** + * Finds by token IDENTIFIER the access token + * + * @return The Access Token identified by the provided tokenId + */ Optional findByTokenId(TxContext tx, String tokenId); } diff --git a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenXmlRegistry.java b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenXmlRegistry.java index 08b3fc699df..8c313ac1d35 100644 --- a/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenXmlRegistry.java +++ b/service/security/authentication/api/src/main/java/org/eclipse/kapua/service/authentication/token/AccessTokenXmlRegistry.java @@ -26,6 +26,6 @@ public AccessToken newAccessToken() { } public AccessTokenCreator newAccessTokenCreator() { - return accessTokenFactory.newCreator(null, null, null, null, null, null); + return accessTokenFactory.newCreator(null, null, null, null, null, null, null); } } diff --git a/service/security/authentication/api/src/main/resources/authentication-error-messages.properties b/service/security/authentication/api/src/main/resources/authentication-error-messages.properties index 9d8fc5e6bd4..2654efe747f 100644 --- a/service/security/authentication/api/src/main/resources/authentication-error-messages.properties +++ b/service/security/authentication/api/src/main/resources/authentication-error-messages.properties @@ -29,10 +29,11 @@ EXPIRED_SESSION_CREDENTIALS=The provided SessionCredentials are expired. LOCKED_SESSION_CREDENTIAL=The provided SessionCredentials are locked. DISABLED_SESSION_CREDENTIAL=The provided SessionCredentials are DISABLED. JWK_FILE_ERROR=This code is deprecated. -REFRESH_ERROR=Error while refreshing the AccessToken. +REFRESH_ERROR=Error while refreshing the AccessToken. {0} JWK_GENERATION_ERROR=This code is deprecated. JWT_CERTIFICATE_NOT_FOUND=Cannot find a JWT Certificate to sign the AccessToken JWT PASSWORD_CANNOT_BE_CHANGED=This code is deprecated. REQUIRE_MFA_CREDENTIALS=This User has MFA enabled and the authorization code must be provided. INCORRECT_CURRENT_PASSWORD=Incorrect current password +MALFORMED_ACCESS_TOKEN=The provided access token is malformed MFA_ERROR=An error occurred while performing MFA check for the User: {0} diff --git a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/AuthenticationServiceShiroImpl.java b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/AuthenticationServiceShiroImpl.java index e626b128fd0..b8cd774106b 100644 --- a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/AuthenticationServiceShiroImpl.java +++ b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/AuthenticationServiceShiroImpl.java @@ -15,6 +15,7 @@ import com.google.common.collect.Sets; import org.apache.shiro.SecurityUtils; import org.apache.shiro.ShiroException; +import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.ExpiredCredentialsException; @@ -34,7 +35,7 @@ import org.eclipse.kapua.commons.security.KapuaSession; import org.eclipse.kapua.commons.util.KapuaDelayUtil; import org.eclipse.kapua.model.query.predicate.AndPredicate; -import org.eclipse.kapua.model.query.predicate.AttributePredicate.Operator; +import org.eclipse.kapua.model.query.predicate.AttributePredicate; import org.eclipse.kapua.service.authentication.AuthenticationCredentials; import org.eclipse.kapua.service.authentication.AuthenticationService; import org.eclipse.kapua.service.authentication.LoginCredentials; @@ -90,7 +91,12 @@ import org.jose4j.jws.AlgorithmIdentifiers; import org.jose4j.jws.JsonWebSignature; import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.MalformedClaimException; import org.jose4j.jwt.NumericDate; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.jwt.consumer.JwtContext; import org.jose4j.lang.JoseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -99,6 +105,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import java.util.Date; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -134,6 +141,8 @@ public class AuthenticationServiceShiroImpl implements AuthenticationService { private final Set credentialsHandlers; private final KapuaAuthenticationSetting kapuaAuthenticationSetting; + private final JwtConsumer jwtConsumer; + @Inject public AuthenticationServiceShiroImpl( CredentialService credentialService, @@ -168,6 +177,11 @@ public AuthenticationServiceShiroImpl( this.userService = userService; this.credentialsHandlers = credentialsHandlers; this.kapuaAuthenticationSetting = kapuaAuthenticationSetting; + this.jwtConsumer = new JwtConsumerBuilder() + .setSkipAllValidators() + .setDisableRequireSignature() + .setSkipSignatureVerification() + .build(); } @Override @@ -244,7 +258,7 @@ public void authenticate(SessionCredentials sessionCredentials) throws KapuaExce currentUser.login(shiroAuthenticationToken); // Retrieve token - AccessToken accessToken = findAccessToken((String) shiroAuthenticationToken.getCredentials()); + AccessToken accessToken = findAccessTokenSession((String) shiroAuthenticationToken.getCredentials()); // Enstablish session establishSession(currentUser, accessToken, null); @@ -327,8 +341,7 @@ public void logout() } } - @Override - public AccessToken findAccessToken(String tokenId) throws KapuaException { + public AccessToken findAccessTokenSession(String jwt) throws KapuaException { AccessToken accessToken = null; try { KapuaSession kapuaSession = KapuaSecurityUtils.getSession(); @@ -336,15 +349,7 @@ public AccessToken findAccessToken(String tokenId) throws KapuaException { accessToken = kapuaSession.getAccessToken(); if (accessToken == null) { - AccessTokenQuery accessTokenQuery = accessTokenFactory.newQuery(null); - AndPredicate andPredicate = accessTokenQuery.andPredicate( - accessTokenQuery.attributePredicate(AccessTokenAttributes.EXPIRES_ON, new java.sql.Timestamp(new Date().getTime()), Operator.GREATER_THAN_OR_EQUAL), - accessTokenQuery.attributePredicate(AccessTokenAttributes.INVALIDATED_ON, null, Operator.IS_NULL), - accessTokenQuery.attributePredicate(AccessTokenAttributes.TOKEN_ID, tokenId) - ); - accessTokenQuery.setPredicate(andPredicate); - accessTokenQuery.setLimit(1); - accessToken = accessTokenService.query(accessTokenQuery).getFirstItem(); + accessToken = findAccessToken(jwt); } } } finally { @@ -354,12 +359,23 @@ public AccessToken findAccessToken(String tokenId) throws KapuaException { return accessToken; } + public AccessToken findAccessToken(String jwt) throws KapuaException { + try { + final JwtContext jwtContext = jwtConsumer.process(jwt); + final String tokenIdentifier = Optional.ofNullable(jwtContext.getJwtClaims().getClaimValue(AccessTokenAttributes.TOKEN_IDENTIFIER, String.class)) + .orElseThrow(() -> new AuthenticationException()); + return KapuaSecurityUtils.doPrivileged(() -> accessTokenService.findByTokenId(tokenIdentifier)); + } catch (InvalidJwtException | MalformedClaimException e) { + throw new AuthenticationException(); + } + } + @Override public AccessToken findRefreshableAccessToken(String tokenId) throws KapuaException { AccessTokenQuery accessTokenQuery = accessTokenFactory.newQuery(null); AndPredicate andPredicate = accessTokenQuery.andPredicate( - accessTokenQuery.attributePredicate(AccessTokenAttributes.REFRESH_EXPIRES_ON, new java.sql.Timestamp(new Date().getTime()), Operator.GREATER_THAN_OR_EQUAL), - accessTokenQuery.attributePredicate(AccessTokenAttributes.INVALIDATED_ON, null, Operator.IS_NULL), + accessTokenQuery.attributePredicate(AccessTokenAttributes.REFRESH_EXPIRES_ON, new java.sql.Timestamp(new Date().getTime()), AttributePredicate.Operator.GREATER_THAN_OR_EQUAL), + accessTokenQuery.attributePredicate(AccessTokenAttributes.INVALIDATED_ON, null, AttributePredicate.Operator.IS_NULL), accessTokenQuery.attributePredicate(AccessTokenAttributes.TOKEN_ID, tokenId) ); accessTokenQuery.setPredicate(andPredicate); @@ -370,12 +386,20 @@ public AccessToken findRefreshableAccessToken(String tokenId) throws KapuaExcept @Override public AccessToken refreshAccessToken(String tokenId, String refreshToken) throws KapuaException { Date now = new Date(); - AccessToken expiredAccessToken = KapuaSecurityUtils.doPrivileged(() -> findRefreshableAccessToken(tokenId)); - if (expiredAccessToken == null || - expiredAccessToken.getInvalidatedOn() != null && now.after(expiredAccessToken.getInvalidatedOn()) || - !expiredAccessToken.getRefreshToken().equals(refreshToken) || - expiredAccessToken.getRefreshExpiresOn() != null && now.after(expiredAccessToken.getRefreshExpiresOn())) { - throw new KapuaAuthenticationException(KapuaAuthenticationErrorCodes.REFRESH_ERROR); + AccessToken expiredAccessToken; + try { + expiredAccessToken = findAccessToken(tokenId); + } catch (AuthenticationException e) { + throw new KapuaAuthenticationException(KapuaAuthenticationErrorCodes.REFRESH_ERROR, ""); + } + if (expiredAccessToken == null) { + throw new KapuaAuthenticationException(KapuaAuthenticationErrorCodes.REFRESH_ERROR, ""); + } else if (expiredAccessToken.getInvalidatedOn() != null && now.after(expiredAccessToken.getInvalidatedOn())) { + throw new KapuaAuthenticationException(KapuaAuthenticationErrorCodes.REFRESH_ERROR, "The provided access token has been invalidated"); + } else if (!expiredAccessToken.getRefreshToken().equals(refreshToken)) { + throw new KapuaAuthenticationException(KapuaAuthenticationErrorCodes.REFRESH_ERROR, "The provided refresh token doesn't match the one for this jwt"); + } else if (expiredAccessToken.getRefreshExpiresOn() != null && now.after(expiredAccessToken.getRefreshExpiresOn())) { + throw new KapuaAuthenticationException(KapuaAuthenticationErrorCodes.REFRESH_ERROR, "The provided refresh token is expired"); } KapuaSecurityUtils.doPrivileged(() -> { try { @@ -535,7 +559,8 @@ private AccessToken createAccessToken(KapuaEid scopeId, KapuaEid userId) throws // Generate token Date now = new Date(); - String jwt = generateJwt(scopeId, userId, now, tokenTtl); + String tokenId = UUID.randomUUID().toString(); + String jwt = generateJwt(scopeId, userId, now, tokenTtl, tokenId); // Persist token AccessTokenCreator accessTokenCreator = accessTokenFactory.newCreator(scopeId, @@ -543,7 +568,8 @@ private AccessToken createAccessToken(KapuaEid scopeId, KapuaEid userId) throws jwt, new Date(now.getTime() + tokenTtl), UUID.randomUUID().toString(), - new Date(now.getTime() + refreshTokenTtl)); + new Date(now.getTime() + refreshTokenTtl), + tokenId); AccessToken accessToken; try { @@ -591,7 +617,7 @@ private void establishSession(Subject subject, AccessToken accessToken, String o MDC.put(LoggingMdcKeys.USER_NAME, (String) subjectSession.getAttribute(ShiroSessionKeys.USER_NAME)); } - private String generateJwt(KapuaEid scopeId, KapuaEid userId, Date now, long ttl) { + private String generateJwt(KapuaEid scopeId, KapuaEid userId, Date now, long ttl, String tokenId) { // Build claims JwtClaims claims = new JwtClaims(); @@ -610,6 +636,7 @@ private String generateJwt(KapuaEid scopeId, KapuaEid userId, Date now, long ttl // .setSubject(userId.getShortId()).claims.setClaim("sId", scopeId.getShortId()); claims.setSubject(userId.toCompactId()); claims.setClaim("sId", scopeId.toCompactId()); + claims.setStringClaim(AccessTokenAttributes.TOKEN_IDENTIFIER, tokenId); String jwt = null; try { diff --git a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/exceptions/ExpiredAccessTokenException.java b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/exceptions/ExpiredAccessTokenException.java new file mode 100644 index 00000000000..f2f7629caf0 --- /dev/null +++ b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/exceptions/ExpiredAccessTokenException.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2020, 2022 Eurotech and/or its affiliates and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eurotech - initial API and implementation + *******************************************************************************/ +package org.eclipse.kapua.service.authentication.shiro.exceptions; + +import org.apache.shiro.authc.ExpiredCredentialsException; + +public class ExpiredAccessTokenException extends ExpiredCredentialsException { + + private static final long serialVersionUID = 3423802102975119979L; + + public ExpiredAccessTokenException() { + super("The provided access token is expired"); + } + + +} diff --git a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/exceptions/InvalidatedAccessTokenException.java b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/exceptions/InvalidatedAccessTokenException.java new file mode 100644 index 00000000000..bd413ba7aa9 --- /dev/null +++ b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/exceptions/InvalidatedAccessTokenException.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2020, 2022 Eurotech and/or its affiliates and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eurotech - initial API and implementation + *******************************************************************************/ +package org.eclipse.kapua.service.authentication.shiro.exceptions; + +import org.apache.shiro.authc.ExpiredCredentialsException; + +public class InvalidatedAccessTokenException extends ExpiredCredentialsException { + + private static final long serialVersionUID = 3922802106975119679L; + + public InvalidatedAccessTokenException() { + super("The provided access token has been invalidated in the past"); + } + +} diff --git a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/exceptions/MalformedAccessTokenException.java b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/exceptions/MalformedAccessTokenException.java new file mode 100644 index 00000000000..ceaf45ff2f0 --- /dev/null +++ b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/exceptions/MalformedAccessTokenException.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2020, 2022 Eurotech and/or its affiliates and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eurotech - initial API and implementation + *******************************************************************************/ +package org.eclipse.kapua.service.authentication.shiro.exceptions; + +import org.apache.shiro.authc.CredentialsException; + +public class MalformedAccessTokenException extends CredentialsException { + + private static final long serialVersionUID = 3962802706975119699L; + + public MalformedAccessTokenException() { + super("The provided access token is malformed"); + } + +} diff --git a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/AccessTokenAuthenticatingRealm.java b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/AccessTokenAuthenticatingRealm.java index 56d2a4384a9..ef7457fd944 100644 --- a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/AccessTokenAuthenticatingRealm.java +++ b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/AccessTokenAuthenticatingRealm.java @@ -17,28 +17,45 @@ import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; -import org.apache.shiro.authc.ExpiredCredentialsException; import org.apache.shiro.authc.UnknownAccountException; -import org.apache.shiro.authc.credential.CredentialsMatcher; +import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; import org.apache.shiro.realm.AuthenticatingRealm; import org.apache.shiro.subject.Subject; +import org.eclipse.kapua.KapuaException; import org.eclipse.kapua.commons.security.KapuaSecurityUtils; import org.eclipse.kapua.commons.security.KapuaSession; import org.eclipse.kapua.locator.KapuaLocator; -import org.eclipse.kapua.model.query.predicate.AndPredicate; -import org.eclipse.kapua.model.query.predicate.AttributePredicate.Operator; +import org.eclipse.kapua.model.query.SortOrder; import org.eclipse.kapua.service.account.Account; import org.eclipse.kapua.service.authentication.AccessTokenCredentials; import org.eclipse.kapua.service.authentication.shiro.AccessTokenCredentialsImpl; +import org.eclipse.kapua.service.authentication.shiro.exceptions.ExpiredAccessTokenException; +import org.eclipse.kapua.service.authentication.shiro.exceptions.InvalidatedAccessTokenException; +import org.eclipse.kapua.service.authentication.shiro.exceptions.JwtCertificateNotFoundException; +import org.eclipse.kapua.service.authentication.shiro.exceptions.MalformedAccessTokenException; +import org.eclipse.kapua.service.authentication.shiro.setting.KapuaAuthenticationSetting; +import org.eclipse.kapua.service.authentication.shiro.setting.KapuaAuthenticationSettingKeys; import org.eclipse.kapua.service.authentication.token.AccessToken; import org.eclipse.kapua.service.authentication.token.AccessTokenAttributes; -import org.eclipse.kapua.service.authentication.token.AccessTokenFactory; -import org.eclipse.kapua.service.authentication.token.AccessTokenQuery; import org.eclipse.kapua.service.authentication.token.AccessTokenService; +import org.eclipse.kapua.service.certificate.CertificateAttributes; +import org.eclipse.kapua.service.certificate.CertificateStatus; +import org.eclipse.kapua.service.certificate.info.CertificateInfo; +import org.eclipse.kapua.service.certificate.info.CertificateInfoFactory; +import org.eclipse.kapua.service.certificate.info.CertificateInfoQuery; +import org.eclipse.kapua.service.certificate.info.CertificateInfoService; +import org.eclipse.kapua.service.certificate.util.CertificateUtils; import org.eclipse.kapua.service.user.User; import org.eclipse.kapua.service.user.UserService; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.consumer.ErrorCodes; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.jwt.consumer.JwtContext; import java.util.Date; +import java.util.Optional; /** * {@link AccessTokenCredentials} based {@link AuthenticatingRealm} implementation. @@ -51,11 +68,11 @@ public class AccessTokenAuthenticatingRealm extends KapuaAuthenticatingRealm { * Realm name. */ public static final String REALM_NAME = "accessTokenAuthenticatingRealm"; - - + private final CertificateInfoFactory certificateInfoFactory = KapuaLocator.getInstance().getFactory(CertificateInfoFactory.class); + private final CertificateInfoService certificateInfoService = KapuaLocator.getInstance().getService(CertificateInfoService.class); private final AccessTokenService accessTokenService = KapuaLocator.getInstance().getService(AccessTokenService.class); - private final AccessTokenFactory accessTokenFactory = KapuaLocator.getInstance().getFactory(AccessTokenFactory.class); private final UserService userService = KapuaLocator.getInstance().getService(UserService.class); + private final KapuaAuthenticationSetting authenticationSetting = KapuaLocator.getInstance().getComponent(KapuaAuthenticationSetting.class); /** * Constructor @@ -64,9 +81,7 @@ public class AccessTokenAuthenticatingRealm extends KapuaAuthenticatingRealm { */ public AccessTokenAuthenticatingRealm() { setName(REALM_NAME); - - CredentialsMatcher credentialsMatcher = new AccessTokenCredentialsMatcher(); - setCredentialsMatcher(credentialsMatcher); + setCredentialsMatcher(new AllowAllCredentialsMatcher()); } @Override @@ -74,25 +89,61 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authent throws AuthenticationException { // Extract credentials AccessTokenCredentialsImpl token = (AccessTokenCredentialsImpl) authenticationToken; - String tokenTokenId = token.getTokenId(); + // Token data + String jwt = token.getTokenId(); + + //verify validity of this token + final JwtClaims jwtClaims; + try { + String issuer = authenticationSetting.getString(KapuaAuthenticationSettingKeys.AUTHENTICATION_SESSION_JWT_ISSUER); + + CertificateInfoQuery certificateInfoQuery = certificateInfoFactory.newQuery(null); + certificateInfoQuery.setPredicate( + certificateInfoQuery.andPredicate( + certificateInfoQuery.attributePredicate(CertificateAttributes.USAGE_NAME, "JWT"), + certificateInfoQuery.attributePredicate(CertificateAttributes.STATUS, CertificateStatus.VALID) + ) + ); + certificateInfoQuery.setSortCriteria(certificateInfoQuery.fieldSortCriteria(CertificateAttributes.CREATED_BY, SortOrder.DESCENDING)); + certificateInfoQuery.setIncludeInherited(true); + certificateInfoQuery.setLimit(1); + + CertificateInfo certificateInfo = KapuaSecurityUtils.doPrivileged(() -> certificateInfoService.query(certificateInfoQuery)).getFirstItem(); + + if (certificateInfo == null) { + throw new JwtCertificateNotFoundException(); + } + // Set validator + JwtConsumer jwtConsumer = new JwtConsumerBuilder() + .setVerificationKey(CertificateUtils.stringToCertificate(certificateInfo.getCertificate()).getPublicKey()) // Set public key + .setExpectedIssuer(issuer) // Set expected issuer + .setRequireIssuedAt() // Set require reserved claim: iat + .setRequireExpirationTime() // Set require reserved claim: exp + .setRequireSubject() // // Set require reserved claim: sub + .build(); + // This validates JWT + final JwtContext jwtContext = jwtConsumer.process(jwt); + jwtClaims = jwtContext.getJwtClaims(); + // FIXME: JWT cert. could be cached to speed-up validation process + } catch (KapuaException ke) { + throw new AuthenticationException(); + } catch (InvalidJwtException e) { + if (e.hasErrorCode(ErrorCodes.EXPIRED)) { + throw new ExpiredAccessTokenException(); + } else { + throw new MalformedAccessTokenException(); + } + } - Date now = new Date(); // Find accessToken final AccessToken accessToken; try { - AccessTokenQuery accessTokenQuery = accessTokenFactory.newQuery(null); - AndPredicate andPredicate = accessTokenQuery.andPredicate( - accessTokenQuery.attributePredicate(AccessTokenAttributes.EXPIRES_ON, new java.sql.Timestamp(now.getTime()), Operator.GREATER_THAN_OR_EQUAL), - accessTokenQuery.attributePredicate(AccessTokenAttributes.INVALIDATED_ON, null, Operator.IS_NULL), - accessTokenQuery.attributePredicate(AccessTokenAttributes.TOKEN_ID, tokenTokenId) - ); - accessTokenQuery.setPredicate(andPredicate); - accessTokenQuery.setLimit(1); - accessToken = KapuaSecurityUtils.doPrivileged(() -> accessTokenService.query(accessTokenQuery).getFirstItem()); - } catch (AuthenticationException ae) { - throw ae; - } catch (Exception e) { - throw new ShiroException("Unexpected error while looking for the access token!", e); + final String tokenIdentifier = Optional.ofNullable(jwtClaims.getClaimValue(AccessTokenAttributes.TOKEN_IDENTIFIER)) + .map(s -> (String) s) + .orElseThrow(() -> new ShiroException("Missing tokenIdentifier in jwt token")); + accessToken = KapuaSecurityUtils.doPrivileged(() -> accessTokenService.findByTokenId(tokenIdentifier)); + } catch (KapuaException ke) { + throw new AuthenticationException(); } // Check existence @@ -101,10 +152,11 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authent } // Check validity - if ((accessToken.getExpiresOn() != null && accessToken.getExpiresOn().before(now)) || - (accessToken.getInvalidatedOn() != null && accessToken.getInvalidatedOn().before(now))) { - throw new ExpiredCredentialsException(); + Date now = new Date(); + if (accessToken.getInvalidatedOn() != null && accessToken.getInvalidatedOn().before(now)) { + throw new InvalidatedAccessTokenException(); } + // Get the associated user by name final User user; try { @@ -119,7 +171,9 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authent // Check account Account account = checkAccount(user.getScopeId()); // BuildAuthenticationInfo - return new SessionAuthenticationInfo(getName(), + return new SessionAuthenticationInfo( + jwtClaims, + getName(), account, user, accessToken); @@ -144,4 +198,5 @@ protected void assertCredentialsMatch(AuthenticationToken authcToken, Authentica public boolean supports(AuthenticationToken authenticationToken) { return authenticationToken instanceof AccessTokenCredentialsImpl; } + } diff --git a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/AccessTokenCredentialsMatcher.java b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/AccessTokenCredentialsMatcher.java index d1885512060..2d182d7d659 100644 --- a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/AccessTokenCredentialsMatcher.java +++ b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/AccessTokenCredentialsMatcher.java @@ -23,7 +23,6 @@ import org.eclipse.kapua.service.authentication.shiro.exceptions.JwtCertificateNotFoundException; import org.eclipse.kapua.service.authentication.shiro.setting.KapuaAuthenticationSetting; import org.eclipse.kapua.service.authentication.shiro.setting.KapuaAuthenticationSettingKeys; -import org.eclipse.kapua.service.authentication.token.AccessToken; import org.eclipse.kapua.service.certificate.CertificateAttributes; import org.eclipse.kapua.service.certificate.CertificateStatus; import org.eclipse.kapua.service.certificate.info.CertificateInfo; @@ -54,48 +53,41 @@ public class AccessTokenCredentialsMatcher implements CredentialsMatcher { public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) { // Token data String jwt = (String) authenticationToken.getCredentials(); - // Info data - SessionAuthenticationInfo info = (SessionAuthenticationInfo) authenticationInfo; - AccessToken infoCredential = info.getAccessToken(); - // Match token with info boolean credentialMatch = false; - if (jwt.equals(infoCredential.getTokenId())) { - try { - String issuer = kapuaAuthenticationSetting.getString(KapuaAuthenticationSettingKeys.AUTHENTICATION_SESSION_JWT_ISSUER); + try { + String issuer = kapuaAuthenticationSetting.getString(KapuaAuthenticationSettingKeys.AUTHENTICATION_SESSION_JWT_ISSUER); - CertificateInfoQuery certificateInfoQuery = certificateInfoFactory.newQuery(null); - certificateInfoQuery.setPredicate( - certificateInfoQuery.andPredicate( - certificateInfoQuery.attributePredicate(CertificateAttributes.USAGE_NAME, "JWT"), - certificateInfoQuery.attributePredicate(CertificateAttributes.STATUS, CertificateStatus.VALID) - ) - ); - certificateInfoQuery.setSortCriteria(certificateInfoQuery.fieldSortCriteria(CertificateAttributes.CREATED_BY, SortOrder.DESCENDING)); - certificateInfoQuery.setIncludeInherited(true); - certificateInfoQuery.setLimit(1); + CertificateInfoQuery certificateInfoQuery = certificateInfoFactory.newQuery(null); + certificateInfoQuery.setPredicate( + certificateInfoQuery.andPredicate( + certificateInfoQuery.attributePredicate(CertificateAttributes.USAGE_NAME, "JWT"), + certificateInfoQuery.attributePredicate(CertificateAttributes.STATUS, CertificateStatus.VALID) + ) + ); + certificateInfoQuery.setSortCriteria(certificateInfoQuery.fieldSortCriteria(CertificateAttributes.CREATED_BY, SortOrder.DESCENDING)); + certificateInfoQuery.setIncludeInherited(true); + certificateInfoQuery.setLimit(1); - CertificateInfo certificateInfo = KapuaSecurityUtils.doPrivileged(() -> certificateInfoService.query(certificateInfoQuery)).getFirstItem(); - - if (certificateInfo == null) { - throw new JwtCertificateNotFoundException(); - } - // Set validator - JwtConsumer jwtConsumer = new JwtConsumerBuilder() - .setVerificationKey(CertificateUtils.stringToCertificate(certificateInfo.getCertificate()).getPublicKey()) // Set public key - .setExpectedIssuer(issuer) // Set expected issuer - .setRequireIssuedAt() // Set require reserved claim: iat - .setRequireExpirationTime() // Set require reserved claim: exp - .setRequireSubject() // // Set require reserved claim: sub - .build(); - // This validates JWT - jwtConsumer.processToClaims(jwt); + CertificateInfo certificateInfo = KapuaSecurityUtils.doPrivileged(() -> certificateInfoService.query(certificateInfoQuery)).getFirstItem(); + if (certificateInfo == null) { + throw new JwtCertificateNotFoundException(); + } + // Set validator + JwtConsumer jwtConsumer = new JwtConsumerBuilder() + .setVerificationKey(CertificateUtils.stringToCertificate(certificateInfo.getCertificate()).getPublicKey()) // Set public key + .setExpectedIssuer(issuer) // Set expected issuer + .setRequireIssuedAt() // Set require reserved claim: iat + .setRequireExpirationTime() // Set require reserved claim: exp + .setRequireSubject() // // Set require reserved claim: sub + .build(); + // This validates JWT + jwtConsumer.processToClaims(jwt); - credentialMatch = true; + credentialMatch = true; - // FIXME: if true cache token password for authentication performance improvement - } catch (InvalidJwtException | KapuaException e) { - LOG.error("Error while validating JWT access token", e); - } + // FIXME: if true cache token password for authentication performance improvement + } catch (InvalidJwtException | KapuaException e) { + LOG.error("Error while validating JWT access token", e); } return credentialMatch; diff --git a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/SessionAuthenticationInfo.java b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/SessionAuthenticationInfo.java index 2bad88c7206..ea1578b7c4e 100644 --- a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/SessionAuthenticationInfo.java +++ b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/shiro/realm/SessionAuthenticationInfo.java @@ -18,17 +18,18 @@ import org.eclipse.kapua.service.account.Account; import org.eclipse.kapua.service.authentication.token.AccessToken; import org.eclipse.kapua.service.user.User; +import org.jose4j.jwt.JwtClaims; /** * Kapua {@link AuthenticationInfo} implementation * * @since 1.0 - * */ public class SessionAuthenticationInfo implements AuthenticationInfo { private static final long serialVersionUID = -8682457531010599453L; + private final JwtClaims jwtClaims; private String realmName; private Account account; private User user; @@ -42,16 +43,23 @@ public class SessionAuthenticationInfo implements AuthenticationInfo { * @param user * @param accessToken */ - public SessionAuthenticationInfo(String realmName, + public SessionAuthenticationInfo( + JwtClaims jwtClaims, + String realmName, Account account, User user, AccessToken accessToken) { + this.jwtClaims = jwtClaims; this.realmName = realmName; this.account = account; this.user = user; this.accessToken = accessToken; } + public JwtClaims getJwtClaims() { + return jwtClaims; + } + /** * Return the user * diff --git a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenCreatorImpl.java b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenCreatorImpl.java index 7163ed7d5f9..3bee25a8045 100644 --- a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenCreatorImpl.java +++ b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenCreatorImpl.java @@ -34,6 +34,7 @@ public class AccessTokenCreatorImpl extends AbstractKapuaEntityCreator findByTokenId(TxContext tx, String tokenId) { - return doFindByField(tx, KapuaId.ANY, AccessTokenAttributes.TOKEN_ID, tokenId); + return doFindByField(tx, KapuaId.ANY, AccessTokenAttributes.TOKEN_IDENTIFIER, tokenId); } } diff --git a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenServiceImpl.java b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenServiceImpl.java index 0e1137e4447..1a4c4d1d105 100644 --- a/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenServiceImpl.java +++ b/service/security/shiro/src/main/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenServiceImpl.java @@ -75,6 +75,7 @@ public AccessToken create(AccessTokenCreator accessTokenCreator) throws KapuaExc ArgumentValidator.notNull(accessTokenCreator.getTokenId(), "accessTokenCreator.tokenId"); ArgumentValidator.notNull(accessTokenCreator.getUserId(), "accessTokenCreator.userId"); ArgumentValidator.notNull(accessTokenCreator.getExpiresOn(), "accessTokenCreator.expiresOn"); + ArgumentValidator.notNull(accessTokenCreator.getTokenIdentifier(), "accessTokenCreator.tokenIdentifier"); // Check access authorizationService.checkPermission(permissionFactory.newPermission(Domains.ACCESS_TOKEN, Actions.write, accessTokenCreator.getScopeId())); @@ -86,6 +87,7 @@ public AccessToken create(AccessTokenCreator accessTokenCreator) throws KapuaExc at.setExpiresOn(accessTokenCreator.getExpiresOn()); at.setRefreshToken(accessTokenCreator.getRefreshToken()); at.setRefreshExpiresOn(accessTokenCreator.getRefreshExpiresOn()); + at.setTokenIdentifier(accessTokenCreator.getTokenIdentifier()); return txManager.execute(tx -> accessTokenRepository.create(tx, at)); } diff --git a/service/security/shiro/src/main/resources/liquibase/2.0.0/atht-newTokenIdentifier.xml b/service/security/shiro/src/main/resources/liquibase/2.0.0/atht-newTokenIdentifier.xml new file mode 100644 index 00000000000..34edd9addb6 --- /dev/null +++ b/service/security/shiro/src/main/resources/liquibase/2.0.0/atht-newTokenIdentifier.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/service/security/shiro/src/main/resources/liquibase/changelog-authentication-master.xml b/service/security/shiro/src/main/resources/liquibase/changelog-authentication-master.xml index 3480a333214..9f138c59cbd 100644 --- a/service/security/shiro/src/main/resources/liquibase/changelog-authentication-master.xml +++ b/service/security/shiro/src/main/resources/liquibase/changelog-authentication-master.xml @@ -21,6 +21,7 @@ + diff --git a/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/shiro/realm/SessionAuthenticationInfoTest.java b/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/shiro/realm/SessionAuthenticationInfoTest.java index 8918232aab8..3181442c5ce 100644 --- a/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/shiro/realm/SessionAuthenticationInfoTest.java +++ b/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/shiro/realm/SessionAuthenticationInfoTest.java @@ -42,7 +42,7 @@ public void initialize() { @Test public void sessionAuthenticationInfoTest() { for (String realmName : realmNames) { - SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(realmName, account, user, accessToken); + SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(null, realmName, account, user, accessToken); Assert.assertEquals("Expected and actual values should be the same.", user, sessionAuthenticationInfo.getUser()); Assert.assertEquals("Expected and actual values should be the same.", account, sessionAuthenticationInfo.getAccount()); Assert.assertEquals("Expected and actual values should be the same.", realmName, sessionAuthenticationInfo.getRealmName()); @@ -54,7 +54,7 @@ public void sessionAuthenticationInfoTest() { @Test public void sessionAuthenticationInfoNullNameTest() { - SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(null, account, user, accessToken); + SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(null, null, account, user, accessToken); Assert.assertEquals("Expected and actual values should be the same.", user, sessionAuthenticationInfo.getUser()); Assert.assertEquals("Expected and actual values should be the same.", account, sessionAuthenticationInfo.getAccount()); Assert.assertNull("Null expected.", sessionAuthenticationInfo.getRealmName()); @@ -71,7 +71,7 @@ public void sessionAuthenticationInfoNullNameTest() { @Test public void sessionAuthenticationInfoNullAccountTest() { for (String realmName : realmNames) { - SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(realmName, null, user, accessToken); + SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(null, realmName, null, user, accessToken); Assert.assertEquals("Expected and actual values should be the same.", user, sessionAuthenticationInfo.getUser()); Assert.assertNull("Null expected.", sessionAuthenticationInfo.getAccount()); Assert.assertEquals("Expected and actual values should be the same.", realmName, sessionAuthenticationInfo.getRealmName()); @@ -84,7 +84,7 @@ public void sessionAuthenticationInfoNullAccountTest() { @Test public void sessionAuthenticationInfoNullUserTest() { for (String realmName : realmNames) { - SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(realmName, account, null, accessToken); + SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(null, realmName, account, null, accessToken); Assert.assertNull("Null expected.", sessionAuthenticationInfo.getUser()); Assert.assertEquals("Expected and actual values should be the same.", account, sessionAuthenticationInfo.getAccount()); Assert.assertEquals("Expected and actual values should be the same.", realmName, sessionAuthenticationInfo.getRealmName()); @@ -102,7 +102,7 @@ public void sessionAuthenticationInfoNullUserTest() { @Test public void sessionAuthenticationInfoNullAccessTokenTest() { for (String realmName : realmNames) { - SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(realmName, account, user, null); + SessionAuthenticationInfo sessionAuthenticationInfo = new SessionAuthenticationInfo(null, realmName, account, user, null); Assert.assertEquals("Expected and actual values should be the same.", user, sessionAuthenticationInfo.getUser()); Assert.assertEquals("Expected and actual values should be the same.", account, sessionAuthenticationInfo.getAccount()); Assert.assertEquals("Expected and actual values should be the same.", realmName, sessionAuthenticationInfo.getRealmName()); diff --git a/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenFactoryImplTest.java b/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenFactoryImplTest.java index ee76a291a98..d1659e85aca 100644 --- a/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenFactoryImplTest.java +++ b/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenFactoryImplTest.java @@ -36,7 +36,7 @@ public class AccessTokenFactoryImplTest { AccessTokenFactoryImpl accessTokenFactoryImpl; KapuaId[] scopeIds; KapuaEid[] userIds; - String[] tokenIds, refreshTokens; + String[] tokenIds, refreshTokens, tokenIdentifiers; Date[] expiresOnDates, refreshExpiresOnDates; AccessToken accessToken; Date modifiedOn, createdOn, invalidatedOn; @@ -54,6 +54,7 @@ public void initialize() { modifiedOn = new Date(); createdOn = new Date(); invalidatedOn = new Date(); + tokenIdentifiers = new String[]{"a2fe104f-5d03-4a09-a28d-817ebbc85901", "8e075aeb-be2a-49a7-8dec-346760375d19", "e71b2f52-e02e-4e24-9147-96674e3bf599"}; } @Test @@ -64,13 +65,16 @@ public void newCreatorMultipleParametersTest() { for (Date expiresOnDate : expiresOnDates) { for (String refreshToken : refreshTokens) { for (Date refreshExpiresOnDate : refreshExpiresOnDates) { - AccessTokenCreatorImpl accessTokenCreatorImpl = accessTokenFactoryImpl.newCreator(scopeId, userId, tokenId, expiresOnDate, refreshToken, refreshExpiresOnDate); - Assert.assertEquals("Expected and actual values should be the same.", scopeId, accessTokenCreatorImpl.getScopeId()); - Assert.assertEquals("Expected and actual values should be the same.", userId, accessTokenCreatorImpl.getUserId()); - Assert.assertEquals("Expected and actual values should be the same.", tokenId, accessTokenCreatorImpl.getTokenId()); - Assert.assertEquals("Expected and actual values should be the same.", expiresOnDate, accessTokenCreatorImpl.getExpiresOn()); - Assert.assertEquals("Expected and actual values should be the same.", refreshToken, accessTokenCreatorImpl.getRefreshToken()); - Assert.assertEquals("Expected and actual values should be the same.", refreshExpiresOnDate, accessTokenCreatorImpl.getRefreshExpiresOn()); + for (String tokenIdenfier : tokenIdentifiers) { + AccessTokenCreatorImpl accessTokenCreatorImpl = accessTokenFactoryImpl.newCreator(scopeId, userId, tokenId, expiresOnDate, refreshToken, refreshExpiresOnDate, tokenIdenfier); + Assert.assertEquals("Expected and actual values should be the same.", scopeId, accessTokenCreatorImpl.getScopeId()); + Assert.assertEquals("Expected and actual values should be the same.", userId, accessTokenCreatorImpl.getUserId()); + Assert.assertEquals("Expected and actual values should be the same.", tokenId, accessTokenCreatorImpl.getTokenId()); + Assert.assertEquals("Expected and actual values should be the same.", expiresOnDate, accessTokenCreatorImpl.getExpiresOn()); + Assert.assertEquals("Expected and actual values should be the same.", refreshToken, accessTokenCreatorImpl.getRefreshToken()); + Assert.assertEquals("Expected and actual values should be the same.", refreshExpiresOnDate, accessTokenCreatorImpl.getRefreshExpiresOn()); + Assert.assertEquals("Expected and actual values should be the same.", tokenIdenfier, accessTokenCreatorImpl.getTokenIdentifier()); + } } } } diff --git a/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenImplTest.java b/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenImplTest.java index 8a8d67deaa7..b1aaa4ade94 100644 --- a/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenImplTest.java +++ b/service/security/shiro/src/test/java/org/eclipse/kapua/service/authentication/token/shiro/AccessTokenImplTest.java @@ -31,7 +31,7 @@ public class AccessTokenImplTest { KapuaId[] scopeIds; KapuaEid[] userIds; - String[] tokenIds, refreshTokens; + String[] tokenIds, refreshTokens, tokenIdentifiers; Date[] expiresOnDates, refreshExpiresOnDates; AccessToken accessToken; Date expiresOn, refreshExpiresOn, invalidatedOn, modifiedOn, createdOn; @@ -51,6 +51,7 @@ public void initialize() throws KapuaException { invalidatedOn = new Date(); modifiedOn = new Date(); createdOn = new Date(); + tokenIdentifiers = new String[]{"a2fe104f-5d03-4a09-a28d-817ebbc85901", "8e075aeb-be2a-49a7-8dec-346760375d19", "e71b2f52-e02e-4e24-9147-96674e3bf599"}; Mockito.when(accessToken.getId()).thenReturn(KapuaId.ANY); Mockito.when(accessToken.getScopeId()).thenReturn(KapuaId.ONE); @@ -67,7 +68,7 @@ public void initialize() throws KapuaException { Mockito.when(accessToken.getOptlock()).thenReturn(10); accessTokenImpl1 = new AccessTokenImpl(); - accessTokenImpl2 = new AccessTokenImpl(KapuaId.ONE, new KapuaEid(KapuaId.ONE), "tokenId", expiresOn, "refreshToken", refreshExpiresOn); + accessTokenImpl2 = new AccessTokenImpl(KapuaId.ONE, new KapuaEid(KapuaId.ONE), "tokenId", expiresOn, "refreshToken", refreshExpiresOn, "id"); accessTokenImpl3 = new AccessTokenImpl(KapuaId.ONE); accessTokenImpl4 = new AccessTokenImpl(accessToken); } @@ -91,13 +92,16 @@ public void accessTokenImplMultipleParametersTest() { for (Date expiresOnDate : expiresOnDates) { for (String refreshToken : refreshTokens) { for (Date refreshExpiresOnDate : refreshExpiresOnDates) { - AccessTokenImpl accessTokenImpl = new AccessTokenImpl(scopeId, userId, tokenId, expiresOnDate, refreshToken, refreshExpiresOnDate); - Assert.assertEquals("Expected and actual values should be the same.", scopeId, accessTokenImpl.getScopeId()); - Assert.assertEquals("Expected and actual values should be the same.", userId, accessTokenImpl.getUserId()); - Assert.assertEquals("Expected and actual values should be the same.", tokenId, accessTokenImpl.getTokenId()); - Assert.assertEquals("Expected and actual values should be the same.", expiresOnDate, accessTokenImpl.getExpiresOn()); - Assert.assertEquals("Expected and actual values should be the same.", refreshToken, accessTokenImpl.getRefreshToken()); - Assert.assertEquals("Expected and actual values should be the same.", refreshExpiresOnDate, accessTokenImpl.getRefreshExpiresOn()); + for (String tokenIdentifier : tokenIdentifiers) { + AccessTokenImpl accessTokenImpl = new AccessTokenImpl(scopeId, userId, tokenId, expiresOnDate, refreshToken, refreshExpiresOnDate, tokenIdentifier); + Assert.assertEquals("Expected and actual values should be the same.", scopeId, accessTokenImpl.getScopeId()); + Assert.assertEquals("Expected and actual values should be the same.", userId, accessTokenImpl.getUserId()); + Assert.assertEquals("Expected and actual values should be the same.", tokenId, accessTokenImpl.getTokenId()); + Assert.assertEquals("Expected and actual values should be the same.", expiresOnDate, accessTokenImpl.getExpiresOn()); + Assert.assertEquals("Expected and actual values should be the same.", refreshToken, accessTokenImpl.getRefreshToken()); + Assert.assertEquals("Expected and actual values should be the same.", refreshExpiresOnDate, accessTokenImpl.getRefreshExpiresOn()); + Assert.assertEquals("Expected and actual values should be the same.", tokenIdentifier, accessTokenImpl.getTokenIdentifier()); + } } } }