diff --git a/arlas-iam-core/pom.xml b/arlas-iam-core/pom.xml index b05bbcb..eaf5fcb 100644 --- a/arlas-iam-core/pom.xml +++ b/arlas-iam-core/pom.xml @@ -5,7 +5,7 @@ arlas-iam-parent io.arlas.iam - 24.1.0-rc.1 + 24.1.1-rc.2 4.0.0 diff --git a/arlas-iam-core/src/main/java/io/arlas/iam/core/AuthService.java b/arlas-iam-core/src/main/java/io/arlas/iam/core/AuthService.java index f3e4525..baf8cbb 100644 --- a/arlas-iam-core/src/main/java/io/arlas/iam/core/AuthService.java +++ b/arlas-iam-core/src/main/java/io/arlas/iam/core/AuthService.java @@ -17,7 +17,7 @@ public interface AuthService { LoginSession login(String email, String password, String issuer) throws ArlasException; DecodedJWT verifyToken(String token); void logout(UUID userId); - LoginSession refresh(String authHeader, String refreshToken, String issuer) throws ArlasException; + LoginSession refresh(String userId, String refreshToken, String issuer) throws ArlasException; String createPermissionToken(String subject, String orgFilter, String issuer, Date iat) throws ArlasException; String createPermissionToken(String keyId, String keySecret, String issuer) throws ArlasException; String createPermissionToken(HttpHeaders headers, String orgFilter) throws ArlasException; diff --git a/arlas-iam-core/src/main/java/io/arlas/iam/impl/HibernateAuthService.java b/arlas-iam-core/src/main/java/io/arlas/iam/impl/HibernateAuthService.java index a1480f2..60460c0 100644 --- a/arlas-iam-core/src/main/java/io/arlas/iam/impl/HibernateAuthService.java +++ b/arlas-iam-core/src/main/java/io/arlas/iam/impl/HibernateAuthService.java @@ -297,18 +297,8 @@ public void logout(UUID userId) { } @Override - public LoginSession refresh(String authHeader, String refreshToken, String issuer) throws ArlasException { - if (authHeader == null || !authHeader.toLowerCase().startsWith("bearer ")) { - throw new InvalidTokenException("Invalid access token: " + authHeader); - } - User user; - try { - String accessToken = authHeader.substring(7); - DecodedJWT t = JWT.decode(accessToken); - user = readUser(UUID.fromString(t.getSubject()), true); - } catch (JWTDecodeException e) { - throw new InvalidTokenException("Invalid access token: " + authHeader, e); - } + public LoginSession refresh(String userId, String refreshToken, String issuer) throws ArlasException { + var user = readUser(UUID.fromString(userId), true); RefreshToken token = tokenDao.read(refreshToken).orElseThrow(() -> new InvalidTokenException("Invalid refresh token.")); if (user.is(token.getUserId()) && token.getExpiryDate() >= System.currentTimeMillis() / 1000) { diff --git a/arlas-iam-rest/pom.xml b/arlas-iam-rest/pom.xml index dec8514..2756730 100644 --- a/arlas-iam-rest/pom.xml +++ b/arlas-iam-rest/pom.xml @@ -5,7 +5,7 @@ arlas-iam-parent io.arlas.iam - 24.1.0-rc.1 + 24.1.1-rc.2 4.0.0 @@ -15,7 +15,7 @@ io.arlas.iam arlas-iam-core - 24.1.0-rc.1 + 24.1.1-rc.2 diff --git a/arlas-iam-rest/src/main/java/io/arlas/iam/rest/model/output/LoginData.java b/arlas-iam-rest/src/main/java/io/arlas/iam/rest/model/output/LoginData.java index 08dda19..63e24c7 100644 --- a/arlas-iam-rest/src/main/java/io/arlas/iam/rest/model/output/LoginData.java +++ b/arlas-iam-rest/src/main/java/io/arlas/iam/rest/model/output/LoginData.java @@ -2,18 +2,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.arlas.iam.model.LoginSession; -import io.arlas.iam.model.RefreshToken; public class LoginData { @JsonProperty("access_token") public String accessToken; // JWT - @JsonProperty("refresh_token") - public RefreshToken refreshToken; public UserData user; public LoginData(LoginSession loginSession) { this.accessToken = loginSession.accessToken; - this.refreshToken = loginSession.refreshToken; this.user = new UserData(loginSession.user, true); } } diff --git a/arlas-iam-rest/src/main/java/io/arlas/iam/rest/model/output/RefreshTokenCookie.java b/arlas-iam-rest/src/main/java/io/arlas/iam/rest/model/output/RefreshTokenCookie.java new file mode 100644 index 0000000..85a0d66 --- /dev/null +++ b/arlas-iam-rest/src/main/java/io/arlas/iam/rest/model/output/RefreshTokenCookie.java @@ -0,0 +1,33 @@ +package io.arlas.iam.rest.model.output; + +import io.arlas.iam.exceptions.InvalidTokenException; +import io.arlas.iam.model.RefreshToken; + +public class RefreshTokenCookie { + public String userId; + public String refreshToken; + + public RefreshTokenCookie(RefreshToken rt) { + this.userId = rt.getUserId().toString(); + this.refreshToken = rt.getValue(); + } + + public RefreshTokenCookie(String cookieValue) throws InvalidTokenException { + String[] v = parseCookieValue(cookieValue); + this.userId = v[0]; + this.refreshToken = v[1]; + } + + + public String getCookieValue() { + return String.join("/", this.userId, this.refreshToken); + } + + public String[] parseCookieValue(String cookieValue) throws InvalidTokenException { + if (cookieValue.contains("/")) { + return cookieValue.split("/"); + } else { + throw new InvalidTokenException("Refresh Token format invalid."); + } + } +} diff --git a/arlas-iam-rest/src/main/java/io/arlas/iam/rest/service/IAMRestService.java b/arlas-iam-rest/src/main/java/io/arlas/iam/rest/service/IAMRestService.java index bcc0250..0a058c3 100644 --- a/arlas-iam-rest/src/main/java/io/arlas/iam/rest/service/IAMRestService.java +++ b/arlas-iam-rest/src/main/java/io/arlas/iam/rest/service/IAMRestService.java @@ -10,10 +10,7 @@ import io.arlas.filter.core.IdentityParam; import io.arlas.iam.core.AuthService; import io.arlas.iam.exceptions.*; -import io.arlas.iam.model.ApiKey; -import io.arlas.iam.model.ForbiddenOrganisation; -import io.arlas.iam.model.OrganisationMember; -import io.arlas.iam.model.User; +import io.arlas.iam.model.*; import io.arlas.iam.rest.model.input.*; import io.arlas.iam.rest.model.output.*; import io.arlas.iam.util.ArlasAuthServerConfiguration; @@ -56,10 +53,12 @@ public class IAMRestService { protected final AuthService authService; private final ArlasAuthConfiguration configuration; + private final long refreshTokenTtl; public IAMRestService(AuthService authService, ArlasAuthServerConfiguration configuration) { this.authService = authService; this.configuration = configuration.arlasAuthConfiguration; + this.refreshTokenTtl = configuration.arlasAuthConfiguration.refreshTokenTTL / 1000L; } private void logUAM(HttpServletRequest request, HttpHeaders headers, String action, String log) { @@ -144,9 +143,16 @@ public Response login( @ApiParam(name = "loginDef", required = true) @NotNull @Valid LoginDef loginDef ) throws ArlasException { - LoginData loginData = new LoginData(authService.login(loginDef.email, loginDef.password, uriInfo.getBaseUri().getHost())); + LoginSession loginSession = authService.login(loginDef.email, loginDef.password, uriInfo.getBaseUri().getHost()); + LoginData loginData = new LoginData(loginSession); + String refreshTokenCookieValue = String.format( + "refresh_token=%s; Path=/; Max-Age=%s; Secure; HttpOnly; SameSite=Strict", + new RefreshTokenCookie(loginSession.refreshToken).getCookieValue(), + refreshTokenTtl); Response response = Response.ok(uriInfo.getRequestUriBuilder().build()) .entity(loginData) + // setting manually as we can't use the NewCookie object because it doesn't accept SameSite attribute before jax-rs version 3.1 + .header("Set-Cookie", refreshTokenCookieValue) .type(MediaType.APPLICATION_JSON_TYPE) .build(); MDC.put(USER_ID, loginData.user.id.toString()); @@ -178,12 +184,13 @@ public Response logout( logUAM(request, headers, "session", "user-logout"); return Response.ok(uriInfo.getRequestUriBuilder().build()) .entity("Session deleted.") + .header("Set-Cookie", "refresh_token=; Max-Age=0") .type(MediaType.TEXT_PLAIN_TYPE) .build(); } @Timed - @Path("session/{refreshToken}") + @Path("session/refresh") @PUT @Produces(UTF8JSON) @Consumes(UTF8JSON) @@ -201,13 +208,16 @@ public Response logout( public Response refresh( @Context UriInfo uriInfo, @Context HttpHeaders headers, - @Context HttpServletRequest request, + @Context HttpServletRequest request, - @ApiParam(name = "refreshToken", required = true) - @PathParam(value = "refreshToken") String refreshToken + @CookieParam("refresh_token") Cookie refreshToken ) throws ArlasException { + if (refreshToken == null) { + throw new InvalidTokenException("Missing refresh token in cookie"); + } + RefreshTokenCookie rt = new RefreshTokenCookie(refreshToken.getValue()); return Response.ok(uriInfo.getRequestUriBuilder().build()) - .entity(new LoginData(authService.refresh(headers.getHeaderString(HttpHeaders.AUTHORIZATION), refreshToken, uriInfo.getBaseUri().getHost()))) + .entity(new LoginData(authService.refresh(rt.userId, rt.refreshToken, uriInfo.getBaseUri().getHost()))) .type(MediaType.APPLICATION_JSON_TYPE) .build(); } diff --git a/arlas-iam-server/pom.xml b/arlas-iam-server/pom.xml index d145991..f13a08d 100644 --- a/arlas-iam-server/pom.xml +++ b/arlas-iam-server/pom.xml @@ -5,7 +5,7 @@ arlas-iam-parent io.arlas.iam - 24.1.0-rc.1 + 24.1.1-rc.2 4.0.0 @@ -15,7 +15,7 @@ io.arlas.iam arlas-iam-rest - 24.1.0-rc.1 + 24.1.1-rc.2 diff --git a/arlas-iam-tests/pom.xml b/arlas-iam-tests/pom.xml index c872fe3..3f9b80d 100644 --- a/arlas-iam-tests/pom.xml +++ b/arlas-iam-tests/pom.xml @@ -5,7 +5,7 @@ arlas-iam-parent io.arlas.iam - 24.1.0-rc.1 + 24.1.1-rc.2 4.0.0 diff --git a/arlas-iam-tests/src/test/java/io/arlas/iam/test/AuthEndpoints.java b/arlas-iam-tests/src/test/java/io/arlas/iam/test/AuthEndpoints.java index ee99c87..d109c0e 100644 --- a/arlas-iam-tests/src/test/java/io/arlas/iam/test/AuthEndpoints.java +++ b/arlas-iam-tests/src/test/java/io/arlas/iam/test/AuthEndpoints.java @@ -91,12 +91,11 @@ protected Response login(String email, String password) { .post(arlasAppPath.concat("session")); } - protected Response refreshToken(String userId, String refreshToken) { + protected Response refreshToken(String refreshToken) { return given() - .header(AUTH_HEADER, getToken(userId)) + .cookie("refresh_token", refreshToken) .contentType("application/json") - .pathParam("refreshToken", refreshToken) - .put(arlasAppPath.concat("session/{refreshToken}")); + .put(arlasAppPath.concat("session/refresh")); } protected Response logout(String userId) { diff --git a/arlas-iam-tests/src/test/java/io/arlas/iam/test/AuthITUser.java b/arlas-iam-tests/src/test/java/io/arlas/iam/test/AuthITUser.java index 2315e35..def8b30 100644 --- a/arlas-iam-tests/src/test/java/io/arlas/iam/test/AuthITUser.java +++ b/arlas-iam-tests/src/test/java/io/arlas/iam/test/AuthITUser.java @@ -1,6 +1,8 @@ package io.arlas.iam.test; import io.restassured.path.json.JsonPath; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import io.restassured.response.ValidatableResponse; import org.junit.FixMethodOrder; import org.junit.Test; @@ -35,9 +37,10 @@ public void test002CreateUserInvalidEmail() { @Test public void test010Login() { - JsonPath json = login(USER1).then().statusCode(200).extract().jsonPath(); + ExtractableResponse response = login(USER1).then().statusCode(200).extract(); + JsonPath json = response.jsonPath(); token1 = json.get("access_token"); - refreshToken1 = json.get("refresh_token.value"); + refreshToken1 = response.cookie("refresh_token"); token2 = login(USER2).then().statusCode(200) .extract().jsonPath().get("access_token"); tokenAdmin = login(ADMIN, ADMIN_PASSWORD).then().statusCode(200) @@ -46,7 +49,7 @@ public void test010Login() { @Test public void test011RefreshToken() { - token1 = refreshToken(userId1, refreshToken1).then().statusCode(200) + token1 = refreshToken(refreshToken1).then().statusCode(200) .extract().jsonPath().get("access_token"); } @@ -67,6 +70,8 @@ public void test015ChangePassword() { logout(USER1).then().statusCode(200); + refreshToken(refreshToken1).then().statusCode(401); + token1 = login(USER1, "newsecret").then().statusCode(200) .extract().jsonPath().get("access_token"); diff --git a/conf/configuration.yaml b/conf/configuration.yaml index e591624..5238e59 100644 --- a/conf/configuration.yaml +++ b/conf/configuration.yaml @@ -41,10 +41,10 @@ swagger: # Configuration of SWAGGER for generating documentation and APIs arlas_cors: enabled: ${ARLAS_CORS_ENABLED:-true} allowed_origins: ${ARLAS_CORS_ALLOWED_ORIGINS:-"*"} - allowed_headers: ${ARLAS_CORS_ALLOWED_HEADERS:-"arlas-user,arlas-groups,arlas-organization,arlas-org-filter,X-Requested-With,Content-Type,Accept,Origin,Authorization,WWW-Authenticate"} + allowed_headers: ${ARLAS_CORS_ALLOWED_HEADERS:-"arlas-user,arlas-groups,arlas-organization,arlas-org-filter,X-Requested-With,Content-Type,Accept,Origin,Authorization,WWW-Authenticate,Set-Cookie"} allowed_methods: ${ARLAS_CORS_ALLOWED_METHODS:-"OPTIONS,GET,PUT,POST,DELETE,HEAD"} allowed_credentials: ${ARLAS_CORS_ALLOWED_CREDENTIALS:-true} - exposed_headers: ${ARLAS_CORS_EXPOSED_HEADERS:-"Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,Location,WWW-Authenticate"} + exposed_headers: ${ARLAS_CORS_EXPOSED_HEADERS:-"Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,Location,WWW-Authenticate,Set-Cookie"} arlas_auth: # Access token time to live in millisecond @@ -53,7 +53,7 @@ arlas_auth: refresh_token_ttl: ${ARLAS_REFRESH_TOKEN_TTL:-600000} # Verify token time to live in millisecond verify_token_ttl: ${ARLAS_VERIFY_TOKEN_TTL:-86400000} - public_uris: [${ARLAS_AUTH_PUBLIC_URIS:-swagger,swagger.*,session:POST,session/.*:PUT,users:POST,users/.*:POST,organisations/check:GET}] + public_uris: [${ARLAS_AUTH_PUBLIC_URIS:-swagger,swagger.*,session:POST,session/refresh:PUT,users:POST,users/.*:POST,organisations/check:GET}] header_user: ${ARLAS_HEADER_USER:-arlas-user} header_group: ${ARLAS_HEADER_GROUP:-arlas-groups} anonymous_value: ${ARLAS_ANONYMOUS_VALUE:-anonymous} diff --git a/docker/docker-files/docker-compose.yml b/docker/docker-files/docker-compose.yml index 159d57e..308d73e 100644 --- a/docker/docker-files/docker-compose.yml +++ b/docker/docker-files/docker-compose.yml @@ -45,7 +45,7 @@ services: - ARLAS_IAM_PREFIX="${ARLAS_IAM_PREFIX:-/arlas_iam_server}" - ARLAS_IAM_APP_PATH="${ARLAS_IAM_APP_PATH:-/}" - ARLAS_ANONYMOUS_VALUE="${ARLAS_ANONYMOUS_VALUE:-anonymous}" - - ARLAS_AUTH_PUBLIC_URIS=${ARLAS_AUTH_PUBLIC_URIS:-swagger,swagger.*,session:POST,session/.*:PUT,users:POST,users/.*:POST} + - ARLAS_AUTH_PUBLIC_URIS=${ARLAS_AUTH_PUBLIC_URIS:-swagger,swagger.*,session:POST,session/refresh:PUT,users:POST,users/.*:POST} - ARLAS_IAM_PRIVATE_ORG="${ARLAS_IAM_PRIVATE_ORG:-false}" - ARLAS_IAM_HIBERNATE_URL="${ARLAS_IAM_HIBERNATE_URL:-jdbc:postgresql://db:5432/arlas_iam}" - ARLAS_IAM_HIBERNATE_USER="${ARLAS_IAM_HIBERNATE_USER:-pg-user}" diff --git a/pom.xml b/pom.xml index aee312e..1fc3a8e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.arlas.iam arlas-iam-parent - 24.1.0-rc.1 + 24.1.1-rc.2 arlas-iam-core arlas-iam-rest