aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java1
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java2
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java7
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java2
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenDto.java74
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenMapper.java40
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokensDao.java72
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/user/SessionTokenMapper.xml59
-rw-r--r--server/sonar-db-dao/src/schema/schema-sq.ddl10
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/user/SessionTokensDaoTest.java164
-rw-r--r--server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java16
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java7
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java25
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java1
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTable.java76
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java4
-rw-r--r--server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTableTest.java59
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java32
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java76
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java56
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java74
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java27
-rw-r--r--server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java42
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java2
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java135
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java75
-rw-r--r--server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java127
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java2
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java9
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java16
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java17
31 files changed, 1154 insertions, 155 deletions
diff --git a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
index 5dd185823aa..5c899412dca 100644
--- a/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
+++ b/server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
@@ -112,6 +112,7 @@ public final class SqTables {
"rules_profiles",
"rule_repositories",
"schema_migrations",
+ "session_tokens",
"snapshots",
"users",
"user_properties",
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java
index 05637e31fc6..bba7ecd2731 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java
@@ -84,6 +84,7 @@ import org.sonar.db.source.FileSourceDao;
import org.sonar.db.user.GroupDao;
import org.sonar.db.user.GroupMembershipDao;
import org.sonar.db.user.RoleDao;
+import org.sonar.db.user.SessionTokensDao;
import org.sonar.db.user.UserDao;
import org.sonar.db.user.UserGroupDao;
import org.sonar.db.user.UserPropertiesDao;
@@ -156,6 +157,7 @@ public class DaoModule extends Module {
RuleRepositoryDao.class,
SnapshotDao.class,
SchemaMigrationDao.class,
+ SessionTokensDao.class,
UserDao.class,
UserGroupDao.class,
UserPermissionDao.class,
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java
index 1ca65d852ae..2d3f79aad57 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java
@@ -82,6 +82,7 @@ import org.sonar.db.source.FileSourceDao;
import org.sonar.db.user.GroupDao;
import org.sonar.db.user.GroupMembershipDao;
import org.sonar.db.user.RoleDao;
+import org.sonar.db.user.SessionTokensDao;
import org.sonar.db.user.UserDao;
import org.sonar.db.user.UserGroupDao;
import org.sonar.db.user.UserPropertiesDao;
@@ -162,6 +163,7 @@ public class DbClient {
private final OrganizationAlmBindingDao organizationAlmBindingDao;
private final NewCodePeriodDao newCodePeriodDao;
private final ProjectDao projectDao;
+ private final SessionTokensDao sessionTokensDao;
public DbClient(Database database, MyBatis myBatis, DBSessions dbSessions, Dao... daos) {
this.database = database;
@@ -239,6 +241,7 @@ public class DbClient {
internalComponentPropertiesDao = getDao(map, InternalComponentPropertiesDao.class);
newCodePeriodDao = getDao(map, NewCodePeriodDao.class);
projectDao = getDao(map, ProjectDao.class);
+ sessionTokensDao = getDao(map, SessionTokensDao.class);
}
public DbSession openSession(boolean batch) {
@@ -527,4 +530,8 @@ public class DbClient {
return newCodePeriodDao;
}
+ public SessionTokensDao sessionTokensDao() {
+ return sessionTokensDao;
+ }
+
}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
index c56a9646c00..ad5d5310b2f 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
@@ -140,6 +140,7 @@ import org.sonar.db.user.GroupMapper;
import org.sonar.db.user.GroupMembershipDto;
import org.sonar.db.user.GroupMembershipMapper;
import org.sonar.db.user.RoleMapper;
+import org.sonar.db.user.SessionTokenMapper;
import org.sonar.db.user.UserDto;
import org.sonar.db.user.UserGroupDto;
import org.sonar.db.user.UserGroupMapper;
@@ -286,6 +287,7 @@ public class MyBatis implements Startable {
RuleMapper.class,
RuleRepositoryMapper.class,
SchemaMigrationMapper.class,
+ SessionTokenMapper.class,
SnapshotMapper.class,
UserGroupMapper.class,
UserMapper.class,
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenDto.java
new file mode 100644
index 00000000000..40c7c73e4f3
--- /dev/null
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenDto.java
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+public class SessionTokenDto {
+
+ private String uuid;
+ private String userUuid;
+ private long expirationDate;
+ private long createdAt;
+ private long updatedAt;
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ SessionTokenDto setUuid(String uuid) {
+ this.uuid = uuid;
+ return this;
+ }
+
+ public String getUserUuid() {
+ return userUuid;
+ }
+
+ public SessionTokenDto setUserUuid(String userUuid) {
+ this.userUuid = userUuid;
+ return this;
+ }
+
+ public long getExpirationDate() {
+ return expirationDate;
+ }
+
+ public SessionTokenDto setExpirationDate(long expirationDate) {
+ this.expirationDate = expirationDate;
+ return this;
+ }
+
+ public long getCreatedAt() {
+ return createdAt;
+ }
+
+ SessionTokenDto setCreatedAt(long createdAt) {
+ this.createdAt = createdAt;
+ return this;
+ }
+
+ public long getUpdatedAt() {
+ return updatedAt;
+ }
+
+ SessionTokenDto setUpdatedAt(long updatedAt) {
+ this.updatedAt = updatedAt;
+ return this;
+ }
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenMapper.java
new file mode 100644
index 00000000000..4aad8e8e6f8
--- /dev/null
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenMapper.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+import javax.annotation.CheckForNull;
+import org.apache.ibatis.annotations.Param;
+
+public interface SessionTokenMapper {
+
+ @CheckForNull
+ SessionTokenDto selectByUuid(String uuid);
+
+ void insert(@Param("dto") SessionTokenDto dto);
+
+ void update(@Param("dto") SessionTokenDto dto);
+
+ void deleteByUuid(@Param("uuid") String uuid);
+
+ void deleteByUserUuid(@Param("userUuid") String userUuid);
+
+ int deleteExpired(@Param("now") long now);
+
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokensDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokensDao.java
new file mode 100644
index 00000000000..b0142cba32f
--- /dev/null
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokensDao.java
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+import java.util.Optional;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.Dao;
+import org.sonar.db.DbSession;
+
+public class SessionTokensDao implements Dao {
+
+ private final System2 system2;
+ private final UuidFactory uuidFactory;
+
+ public SessionTokensDao(System2 system2, UuidFactory uuidFactory) {
+ this.system2 = system2;
+ this.uuidFactory = uuidFactory;
+ }
+
+ public Optional<SessionTokenDto> selectByUuid(DbSession session, String uuid) {
+ return Optional.ofNullable(mapper(session).selectByUuid(uuid));
+ }
+
+ public SessionTokenDto insert(DbSession session, SessionTokenDto dto) {
+ long now = system2.now();
+ mapper(session).insert(dto
+ .setUuid(uuidFactory.create())
+ .setCreatedAt(now)
+ .setUpdatedAt(now));
+ return dto;
+ }
+
+ public SessionTokenDto update(DbSession session, SessionTokenDto dto) {
+ long now = system2.now();
+ mapper(session).update(dto.setUpdatedAt(now));
+ return dto;
+ }
+
+ public void deleteByUuid(DbSession dbSession, String uuid) {
+ mapper(dbSession).deleteByUuid(uuid);
+ }
+
+ public void deleteByUser(DbSession dbSession, UserDto user) {
+ mapper(dbSession).deleteByUserUuid(user.getUuid());
+ }
+
+ public int deleteExpired(DbSession dbSession) {
+ return mapper(dbSession).deleteExpired(system2.now());
+ }
+
+ private static SessionTokenMapper mapper(DbSession session) {
+ return session.getMapper(SessionTokenMapper.class);
+ }
+}
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/SessionTokenMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/SessionTokenMapper.xml
new file mode 100644
index 00000000000..4a655eac015
--- /dev/null
+++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/SessionTokenMapper.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "mybatis-3-mapper.dtd">
+
+<mapper namespace="org.sonar.db.user.SessionTokenMapper">
+
+ <sql id="columns">
+ st.uuid as uuid,
+ st.user_uuid as "userUuid",
+ st.expiration_date as "expirationDate",
+ st.created_at as "createdAt",
+ st.updated_at as "updatedAt"
+ </sql>
+
+ <select id="selectByUuid" parameterType="String" resultType="org.sonar.db.user.SessionTokenDto">
+ select
+ <include refid="columns"/>
+ from session_tokens st
+ where st.uuid=#{uuid, jdbcType=VARCHAR}
+ </select>
+
+ <insert id="insert" parameterType="Map" useGeneratedKeys="false">
+ insert into session_tokens
+ (
+ uuid,
+ user_uuid,
+ expiration_date,
+ created_at,
+ updated_at
+ )
+ values (
+ #{dto.uuid, jdbcType=VARCHAR},
+ #{dto.userUuid, jdbcType=VARCHAR},
+ #{dto.expirationDate, jdbcType=BIGINT},
+ #{dto.createdAt, jdbcType=BIGINT},
+ #{dto.updatedAt, jdbcType=BIGINT}
+ )
+ </insert>
+
+ <update id="update" parameterType="Map">
+ update session_tokens set
+ expiration_date = #{dto.expirationDate, jdbcType=BIGINT},
+ updated_at = #{dto.updatedAt, jdbcType=BIGINT}
+ where
+ uuid = #{dto.uuid, jdbcType=VARCHAR}
+ </update>
+
+ <delete id="deleteByUuid" parameterType="String">
+ delete from session_tokens where uuid = #{uuid, jdbcType=VARCHAR}
+ </delete>
+
+ <delete id="deleteByUserUuid" parameterType="String">
+ delete from session_tokens where user_uuid = #{userUuid, jdbcType=VARCHAR}
+ </delete>
+
+ <delete id="deleteExpired" parameterType="Long" >
+ delete from session_tokens where expiration_date &lt; #{now, jdbcType=BIGINT}
+ </delete>
+
+</mapper>
diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl
index 4690b546381..fc2acf81abc 100644
--- a/server/sonar-db-dao/src/schema/schema-sq.ddl
+++ b/server/sonar-db-dao/src/schema/schema-sq.ddl
@@ -871,6 +871,16 @@ CREATE TABLE "RULES_PROFILES"(
);
ALTER TABLE "RULES_PROFILES" ADD CONSTRAINT "PK_RULES_PROFILES" PRIMARY KEY("UUID");
+CREATE TABLE "SESSION_TOKENS"(
+ "UUID" VARCHAR(40) NOT NULL,
+ "USER_UUID" VARCHAR(255) NOT NULL,
+ "EXPIRATION_DATE" BIGINT NOT NULL,
+ "CREATED_AT" BIGINT NOT NULL,
+ "UPDATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "SESSION_TOKENS" ADD CONSTRAINT "PK_SESSION_TOKENS" PRIMARY KEY("UUID");
+CREATE INDEX "SESSION_TOKENS_USER_UUID" ON "SESSION_TOKENS"("USER_UUID");
+
CREATE TABLE "SNAPSHOTS"(
"UUID" VARCHAR(50) NOT NULL,
"COMPONENT_UUID" VARCHAR(50) NOT NULL,
diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/user/SessionTokensDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/user/SessionTokensDaoTest.java
new file mode 100644
index 00000000000..63763b264c1
--- /dev/null
+++ b/server/sonar-db-dao/src/test/java/org/sonar/db/user/SessionTokensDaoTest.java
@@ -0,0 +1,164 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.db.user;
+
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.core.util.SequenceUuidFactory;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SessionTokensDaoTest {
+
+ private static final long NOW = 1_000_000_000L;
+
+ private TestSystem2 system2 = new TestSystem2().setNow(NOW);
+ @Rule
+ public DbTester db = DbTester.create(system2);
+
+ private DbSession dbSession = db.getSession();
+ private UuidFactory uuidFactory = new SequenceUuidFactory();
+
+ private SessionTokensDao underTest = new SessionTokensDao(system2, uuidFactory);
+
+ @Test
+ public void selectByUuid() {
+ SessionTokenDto dto = new SessionTokenDto()
+ .setUserUuid("ABCD")
+ .setExpirationDate(15_000_000_000L);
+ underTest.insert(dbSession, dto);
+
+ Optional<SessionTokenDto> result = underTest.selectByUuid(dbSession, dto.getUuid());
+
+ assertThat(result.isPresent());
+ assertThat(result.get().getUserUuid()).isEqualTo("ABCD");
+ assertThat(result.get().getExpirationDate()).isEqualTo(15_000_000_000L);
+ assertThat(result.get().getCreatedAt()).isEqualTo(NOW);
+ assertThat(result.get().getUpdatedAt()).isEqualTo(NOW);
+ }
+
+ @Test
+ public void uuid_created_at_and_updated_at_are_ignored_during_insert() {
+ SessionTokenDto dto = new SessionTokenDto()
+ .setUserUuid("ABCD")
+ .setExpirationDate(15_000_000_000L)
+ // Following fields should be ignored
+ .setUuid("SHOULD_NOT_BE_USED")
+ .setCreatedAt(8_000_000_000L)
+ .setUpdatedAt(9_000_000_000L);
+ underTest.insert(dbSession, dto);
+
+ Optional<SessionTokenDto> result = underTest.selectByUuid(dbSession, dto.getUuid());
+
+ assertThat(result.isPresent());
+ assertThat(result.get().getUuid()).isNotEqualTo("SHOULD_NOT_BE_USED");
+ assertThat(result.get().getCreatedAt()).isEqualTo(NOW);
+ assertThat(result.get().getUpdatedAt()).isEqualTo(NOW);
+ }
+
+ @Test
+ public void update() {
+ SessionTokenDto dto = new SessionTokenDto()
+ .setUserUuid("ABCD")
+ .setExpirationDate(15_000_000_000L);
+ underTest.insert(dbSession, dto);
+ system2.setNow(NOW + 10_000_000_000L);
+ underTest.update(dbSession, dto
+ .setExpirationDate(45_000_000_000L));
+
+ Optional<SessionTokenDto> result = underTest.selectByUuid(dbSession, dto.getUuid());
+
+ assertThat(result.get().getExpirationDate()).isEqualTo(45_000_000_000L);
+ assertThat(result.get().getCreatedAt()).isEqualTo(NOW);
+ assertThat(result.get().getUpdatedAt()).isEqualTo(NOW + 10_000_000_000L);
+ }
+
+ @Test
+ public void only_update_fields_that_makes_sense() {
+ SessionTokenDto dto = new SessionTokenDto()
+ .setUserUuid("ABCD")
+ .setExpirationDate(15_000_000_000L);
+ underTest.insert(dbSession, dto);
+ system2.setNow(NOW + 10_000_000_000L);
+ underTest.update(dbSession, dto
+ .setExpirationDate(45_000_000_000L)
+ // Following fields are ignored
+ .setUserUuid("ANOTHER USER UUID")
+ .setCreatedAt(NOW -10_000_000_000L)
+ );
+
+ Optional<SessionTokenDto> result = underTest.selectByUuid(dbSession, dto.getUuid());
+
+ assertThat(result.get().getExpirationDate()).isEqualTo(45_000_000_000L);
+ assertThat(result.get().getUserUuid()).isEqualTo("ABCD");
+ assertThat(result.get().getCreatedAt()).isEqualTo(NOW);
+ assertThat(result.get().getUpdatedAt()).isEqualTo(NOW + 10_000_000_000L);
+ }
+
+ @Test
+ public void deleteByUuid() {
+ UserDto user = db.users().insertUser();
+ SessionTokenDto sessionToken1 = db.users().insertSessionToken(user);
+ SessionTokenDto sessionToken2 = db.users().insertSessionToken(user);
+ UserDto anotherUser = db.users().insertUser();
+ SessionTokenDto anotherSessionToken = db.users().insertSessionToken(anotherUser);
+
+ underTest.deleteByUuid(dbSession, sessionToken1.getUuid());
+
+ assertThat(underTest.selectByUuid(dbSession, sessionToken1.getUuid())).isNotPresent();
+ assertThat(underTest.selectByUuid(dbSession, sessionToken2.getUuid())).isPresent();
+ assertThat(underTest.selectByUuid(dbSession, anotherSessionToken.getUuid())).isPresent();
+ }
+
+ @Test
+ public void deleteByUser() {
+ UserDto user = db.users().insertUser();
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user);
+ // Creation another session token linked on another user, it should not be removed
+ UserDto anotherUser = db.users().insertUser();
+ SessionTokenDto anotherSessionToken = db.users().insertSessionToken(anotherUser);
+
+ underTest.deleteByUser(dbSession, user);
+
+ assertThat(underTest.selectByUuid(dbSession, sessionToken.getUuid())).isNotPresent();
+ assertThat(underTest.selectByUuid(dbSession, anotherSessionToken.getUuid())).isPresent();
+ }
+
+ @Test
+ public void deleteExpired() {
+ UserDto user = db.users().insertUser();
+ SessionTokenDto expiredSessionToken1 = db.users().insertSessionToken(user, st -> st.setExpirationDate(NOW - 1_000_000_000L));
+ SessionTokenDto expiredSessionToken2 = db.users().insertSessionToken(user, st -> st.setExpirationDate(NOW - 1_000_000_000L));
+ SessionTokenDto validSessionToken = db.users().insertSessionToken(user);
+
+ int result = underTest.deleteExpired(dbSession);
+
+ assertThat(underTest.selectByUuid(dbSession, expiredSessionToken1.getUuid())).isNotPresent();
+ assertThat(underTest.selectByUuid(dbSession, expiredSessionToken2.getUuid())).isNotPresent();
+ assertThat(underTest.selectByUuid(dbSession, validSessionToken.getUuid())).isPresent();
+ assertThat(result).isEqualTo(2);
+ }
+}
diff --git a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
index 93bc6d09710..079e15fe109 100644
--- a/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
+++ b/server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
@@ -41,6 +41,7 @@ import org.sonar.db.permission.UserPermissionDto;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static java.util.Arrays.stream;
+import static org.apache.commons.lang.math.RandomUtils.nextLong;
import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
public class UserDbTester {
@@ -378,6 +379,8 @@ public class UserDbTester {
.collect(MoreCollectors.toList());
}
+ // USER TOKEN
+
@SafeVarargs
public final UserTokenDto insertToken(UserDto user, Consumer<UserTokenDto>... populators) {
UserTokenDto dto = UserTokenTesting.newUserToken().setUserUuid(user.getUuid());
@@ -387,4 +390,17 @@ public class UserDbTester {
return dto;
}
+ // SESSION TOKENS
+
+ @SafeVarargs
+ public final SessionTokenDto insertSessionToken(UserDto user, Consumer<SessionTokenDto>... populators) {
+ SessionTokenDto dto = new SessionTokenDto()
+ .setUserUuid(user.getUuid())
+ .setExpirationDate(nextLong());
+ stream(populators).forEach(p -> p.accept(dto));
+ db.getDbClient().sessionTokensDao().insert(db.getSession(), dto);
+ db.commit();
+ return dto;
+ }
+
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java
index a4f62523460..41d45667c74 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java
@@ -43,6 +43,13 @@ public class VarcharColumnDef extends AbstractColumnDef {
public static final int UUID_VARCHAR_SIZE = 50;
public static final int UUID_SIZE = 40;
+ /**
+ * UUID length of the USERS table is not using the standard UUID length.
+ * The reason of this is because when the UUID column was introduced in the USERS table, existing rows were fed with the login, which has a length of 255.
+ * @see <a https://jira.sonarsource.com/browse/SONAR-10597>SONAR-10597</a>
+ */
+ public static final int USER_UUID_SIZE = 255;
+
private final int columnSize;
private final boolean ignoreOracleUnit;
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java
index c22374418c1..2f8b95aa499 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java
@@ -44,6 +44,7 @@ import static org.sonar.server.platform.db.migration.def.IntegerColumnDef.newInt
import static org.sonar.server.platform.db.migration.def.TimestampColumnDef.newTimestampColumnDefBuilder;
import static org.sonar.server.platform.db.migration.def.TinyIntColumnDef.newTinyIntColumnDefBuilder;
import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.MAX_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.USER_UUID_SIZE;
import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
import static org.sonar.server.platform.db.migration.sql.CreateTableBuilder.PRIMARY_KEY_PREFIX;
@@ -241,7 +242,7 @@ public class CreateInitialSchema extends DdlChange {
.addColumn(mainIsLastKeyCol)
.addColumn(isLastCol)
.addColumn(isLastKeyCol)
- .addColumn(newLenientVarcharBuilder("submitter_uuid").setLimit(255).setIsNullable(true).build())
+ .addColumn(newLenientVarcharBuilder("submitter_uuid").setLimit(USER_UUID_SIZE).setIsNullable(true).build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("submitted_at").setIsNullable(false).build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("started_at").setIsNullable(true).build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("executed_at").setIsNullable(true).build())
@@ -277,7 +278,7 @@ public class CreateInitialSchema extends DdlChange {
.addColumn(mainComponentUuidCol)
.addColumn(componentUuidCol)
.addColumn(newLenientVarcharBuilder("status").setLimit(15).setIsNullable(true).build())
- .addColumn(newLenientVarcharBuilder("submitter_uuid").setLimit(255).setIsNullable(true).build())
+ .addColumn(newLenientVarcharBuilder("submitter_uuid").setLimit(USER_UUID_SIZE).setIsNullable(true).build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("started_at").setIsNullable(true).build())
.addColumn(newVarcharColumnBuilder("worker_uuid").setLimit(UUID_SIZE).setIsNullable(true).build())
.addColumn(newIntegerColumnDefBuilder().setColumnName("execution_count").setIsNullable(false).build())
@@ -558,7 +559,7 @@ public class CreateInitialSchema extends DdlChange {
.addPkColumn(newBigIntegerColumnDefBuilder().setColumnName("id").setIsNullable(false).build(), AUTO_INCREMENT)
.addColumn(keeCol)
.addColumn(issueKeyCol)
- .addColumn(newLenientVarcharBuilder("user_login").setLimit(255).build())
+ .addColumn(newLenientVarcharBuilder("user_login").setLimit(USER_UUID_SIZE).build())
.addColumn(newLenientVarcharBuilder("change_type").setLimit(20).build())
.addColumn(newClobColumnDefBuilder().setColumnName("change_data").build())
.addColumn(NULLABLE_TECHNICAL_CREATED_AT_COL)
@@ -570,7 +571,7 @@ public class CreateInitialSchema extends DdlChange {
}
private void createIssues(Context context) {
- VarcharColumnDef assigneeCol = newLenientVarcharBuilder("assignee").setLimit(255).build();
+ VarcharColumnDef assigneeCol = newLenientVarcharBuilder("assignee").setLimit(USER_UUID_SIZE).build();
VarcharColumnDef componentUuidCol = newLenientVarcharBuilder(COMPONENT_UUID_COL_NAME).setLimit(50).build();
BigIntegerColumnDef issueCreationDateCol = newBigIntegerColumnDefBuilder().setColumnName("issue_creation_date").build();
VarcharColumnDef keeCol = newLenientVarcharBuilder("kee").setLimit(50).setIsNullable(false).build();
@@ -592,7 +593,7 @@ public class CreateInitialSchema extends DdlChange {
.addColumn(newLenientVarcharBuilder("status").setLimit(20).build())
.addColumn(resolutionCol)
.addColumn(newLenientVarcharBuilder("checksum").setLimit(1000).build())
- .addColumn(newLenientVarcharBuilder("reporter").setLimit(255).build())
+ .addColumn(newLenientVarcharBuilder("reporter").setLimit(USER_UUID_SIZE).build())
.addColumn(assigneeCol)
.addColumn(newLenientVarcharBuilder("author_login").setLimit(255).build())
.addColumn(newLenientVarcharBuilder("action_plan_key").setLimit(50).build())
@@ -651,7 +652,7 @@ public class CreateInitialSchema extends DdlChange {
.addColumn(newIntegerColumnDefBuilder().setColumnName(METRIC_ID_COL_NAME).setIsNullable(false).build())
.addColumn(newDecimalColumnDefBuilder().setColumnName("value").setPrecision(38).setScale(20).build())
.addColumn(newLenientVarcharBuilder("text_value").setLimit(MAX_SIZE).build())
- .addColumn(newLenientVarcharBuilder(USER_UUID_COL_NAME).setLimit(255).build())
+ .addColumn(newLenientVarcharBuilder(USER_UUID_COL_NAME).setLimit(USER_UUID_SIZE).build())
.addColumn(newLenientVarcharBuilder(DESCRIPTION_COL_NAME).setLimit(MAX_SIZE).build())
.addColumn(NULLABLE_TECHNICAL_CREATED_AT_COL)
.addColumn(NULLABLE_TECHNICAL_UPDATED_AT_COL)
@@ -737,7 +738,7 @@ public class CreateInitialSchema extends DdlChange {
.addColumn(almAppInstallUuidCol)
.addColumn(newVarcharColumnBuilder("alm_id").setIsNullable(false).setLimit(UUID_SIZE).build())
.addColumn(newVarcharColumnBuilder("url").setIsNullable(false).setLimit(2000).build())
- .addColumn(newVarcharColumnBuilder(USER_UUID_COL_NAME).setIsNullable(false).setLimit(255).build())
+ .addColumn(newVarcharColumnBuilder(USER_UUID_COL_NAME).setIsNullable(false).setLimit(USER_UUID_SIZE).build())
.addColumn(newBooleanColumnDefBuilder().setColumnName("members_sync_enabled").setIsNullable(true).build())
.addColumn(TECHNICAL_CREATED_AT_COL)
.build());
@@ -1049,7 +1050,7 @@ public class CreateInitialSchema extends DdlChange {
.addPkColumn(newLenientVarcharBuilder("kee").setLimit(UUID_SIZE).setIsNullable(false).build())
.addColumn(rulesProfileUuidCol)
.addColumn(newLenientVarcharBuilder("change_type").setLimit(20).setIsNullable(false).build())
- .addColumn(newLenientVarcharBuilder(USER_UUID_COL_NAME).setLimit(255).setIsNullable(true).build())
+ .addColumn(newLenientVarcharBuilder(USER_UUID_COL_NAME).setLimit(USER_UUID_SIZE).setIsNullable(true).build())
.addColumn(newClobColumnDefBuilder().setColumnName("change_data").setIsNullable(true).build())
.addColumn(TECHNICAL_CREATED_AT_COL)
.build());
@@ -1164,7 +1165,7 @@ public class CreateInitialSchema extends DdlChange {
.addPkColumn(newIntegerColumnDefBuilder().setColumnName("rule_id").setIsNullable(false).build())
.addPkColumn(newVarcharColumnBuilder(ORGANIZATION_UUID_COL_NAME).setLimit(UUID_SIZE).setIsNullable(false).build())
.addColumn(newClobColumnDefBuilder().setColumnName("note_data").setIsNullable(true).build())
- .addColumn(newVarcharColumnBuilder("note_user_uuid").setLimit(255).setIsNullable(true).build())
+ .addColumn(newVarcharColumnBuilder("note_user_uuid").setLimit(USER_UUID_SIZE).setIsNullable(true).build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("note_created_at").setIsNullable(true).build())
.addColumn(newBigIntegerColumnDefBuilder().setColumnName("note_updated_at").setIsNullable(true).build())
.addColumn(newVarcharColumnBuilder("remediation_function").setLimit(20).setIsNullable(true).build())
@@ -1253,7 +1254,7 @@ public class CreateInitialSchema extends DdlChange {
private void createUserProperties(Context context) {
String tableName = "user_properties";
- VarcharColumnDef userUuidCol = newVarcharColumnBuilder(USER_UUID_COL_NAME).setLimit(255).setIsNullable(false).build();
+ VarcharColumnDef userUuidCol = newVarcharColumnBuilder(USER_UUID_COL_NAME).setLimit(USER_UUID_SIZE).setIsNullable(false).build();
VarcharColumnDef keyCol = newVarcharColumnBuilder("kee").setLimit(100).setIsNullable(false).build();
context.execute(newTableBuilder(tableName)
.addPkColumn(newVarcharColumnBuilder("uuid").setLimit(UUID_SIZE).setIsNullable(false).build())
@@ -1284,7 +1285,7 @@ public class CreateInitialSchema extends DdlChange {
private void createUserTokens(Context context) {
String tableName = "user_tokens";
- VarcharColumnDef userUuidCol = newVarcharColumnBuilder(USER_UUID_COL_NAME).setLimit(255).setIsNullable(false).build();
+ VarcharColumnDef userUuidCol = newVarcharColumnBuilder(USER_UUID_COL_NAME).setLimit(USER_UUID_SIZE).setIsNullable(false).build();
VarcharColumnDef nameCol = newVarcharColumnBuilder("name").setLimit(100).setIsNullable(false).build();
VarcharColumnDef tokenHashCol = newVarcharColumnBuilder("token_hash").setLimit(255).setIsNullable(false).build();
context.execute(
@@ -1302,7 +1303,7 @@ public class CreateInitialSchema extends DdlChange {
private void createUsers(Context context) {
String tableName = "users";
- VarcharColumnDef uuidCol = newVarcharColumnBuilder("uuid").setLimit(255).setIsNullable(false).build();
+ VarcharColumnDef uuidCol = newVarcharColumnBuilder("uuid").setLimit(USER_UUID_SIZE).setIsNullable(false).build();
VarcharColumnDef loginCol = newLenientVarcharBuilder("login").setLimit(255).setIsNullable(false).build();
BigIntegerColumnDef updatedAtCol = NULLABLE_TECHNICAL_UPDATED_AT_COL;
VarcharColumnDef externalLoginCol = newLenientVarcharBuilder("external_login").setLimit(255).setIsNullable(false).build();
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java
index 000f8bcf457..35918f7e5bb 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java
@@ -47,7 +47,6 @@ public class DbVersion83 implements DbVersion {
.add(3309, "Migrate 'resource_id' to 'component_uuid' in 'user_roles'", MigrateResourceIdToUuidInUserRoles.class)
.add(3310, "Remove column 'resource_id' in 'user_roles'", DropResourceIdFromUserRolesTable.class)
.add(3311, "Remove column 'id' in 'components'", DropIdFromComponentsTable.class)
-
;
}
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTable.java
new file mode 100644
index 00000000000..e4abbad3ae3
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTable.java
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v84;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.USER_UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+public class CreateSessionTokensTable extends DdlChange {
+
+ private static final String TABLE_NAME = "session_tokens";
+ private static final VarcharColumnDef USER_UUID_COLUMN = VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName("user_uuid")
+ .setLimit(USER_UUID_SIZE)
+ .setIsNullable(false)
+ .build();
+
+ public CreateSessionTokensTable(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ context.execute(new CreateTableBuilder(getDialect(), TABLE_NAME)
+ .addPkColumn(VarcharColumnDef.newVarcharColumnDefBuilder()
+ .setColumnName("uuid")
+ .setLimit(UUID_SIZE)
+ .setIsNullable(false)
+ .build())
+ .addColumn(USER_UUID_COLUMN)
+ .addColumn(newBigIntegerColumnDefBuilder()
+ .setColumnName("expiration_date")
+ .setIsNullable(false)
+ .build())
+ .addColumn(newBigIntegerColumnDefBuilder()
+ .setColumnName("created_at")
+ .setIsNullable(false)
+ .build())
+ .addColumn(newBigIntegerColumnDefBuilder()
+ .setColumnName("updated_at")
+ .setIsNullable(false)
+ .build())
+ .build());
+
+ context.execute(new CreateIndexBuilder()
+ .setTable(TABLE_NAME)
+ .setName("session_tokens_user_uuid")
+ .addColumn(USER_UUID_COLUMN)
+ .setUnique(false)
+ .build());
+ }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java
index 72702ae7791..1d4eac7aed2 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java
@@ -780,8 +780,8 @@ public class DbVersion84 implements DbVersion {
.add(3710, "Add primary key on 'UUID' column of 'RULES' table", AddPrimaryKeyOnUuidColumnOfRulesTable.class)
.add(3711, "Drop column 'ID' of 'RULES' table", DropIdColumnOfRulesTable.class)
- .add(3800, "Remove favourites for components with qualifiers 'DIR', 'FIL', 'UTS'", RemoveFilesFavouritesFromProperties.class);
-
+ .add(3800, "Remove favourites for components with qualifiers 'DIR', 'FIL', 'UTS'", RemoveFilesFavouritesFromProperties.class)
+ .add(3801, "Create 'SESSION_TOKENS' table", CreateSessionTokensTable.class)
;
}
}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTableTest.java
new file mode 100644
index 00000000000..c66d83b9f0b
--- /dev/null
+++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTableTest.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.server.platform.db.migration.version.v84;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.VARCHAR;
+
+public class CreateSessionTokensTableTest {
+
+ private static final String TABLE_NAME = "session_tokens";
+
+ @Rule
+ public CoreDbTester dbTester = CoreDbTester.createEmpty();
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private CreateSessionTokensTable underTest = new CreateSessionTokensTable(dbTester.database());
+
+ @Test
+ public void table_has_been_created() throws SQLException {
+ underTest.execute();
+
+ dbTester.assertTableExists(TABLE_NAME);
+ dbTester.assertPrimaryKey(TABLE_NAME, "pk_session_tokens", "uuid");
+ dbTester.assertIndex(TABLE_NAME, "session_tokens_user_uuid", "user_uuid");
+
+ dbTester.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, 40, false);
+ dbTester.assertColumnDefinition(TABLE_NAME, "user_uuid", VARCHAR, 255, false);
+ dbTester.assertColumnDefinition(TABLE_NAME, "expiration_date", BIGINT, 20, false);
+ dbTester.assertColumnDefinition(TABLE_NAME, "updated_at", BIGINT, 20, false);
+ dbTester.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, 20, false);
+ }
+
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
index 35e49f833e0..c2619579720 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
@@ -21,30 +21,34 @@ package org.sonar.server.authentication;
import org.sonar.core.platform.Module;
import org.sonar.server.authentication.event.AuthenticationEventImpl;
+import org.sonar.server.authentication.purge.SessionTokensCleaner;
+import org.sonar.server.authentication.purge.SessionTokensCleanerExecutorServiceImpl;
public class AuthenticationModule extends Module {
@Override
protected void configureModule() {
add(
AuthenticationEventImpl.class,
- InitFilter.class,
- OAuth2CallbackFilter.class,
- IdentityProviderRepository.class,
BaseContextFactory.class,
- OAuth2ContextFactory.class,
- UserRegistrarImpl.class,
- OAuthCsrfVerifier.class,
- UserSessionInitializer.class,
- JwtSerializer.class,
- JwtHttpHandler.class,
- JwtCsrfVerifier.class,
- OAuth2AuthenticationParametersImpl.class,
+ BasicAuthentication.class,
CredentialsAuthentication.class,
- CredentialsLocalAuthentication.class,
CredentialsExternalAuthentication.class,
- BasicAuthentication.class,
+ CredentialsLocalAuthentication.class,
HttpHeadersAuthentication.class,
+ IdentityProviderRepository.class,
+ InitFilter.class,
+ JwtCsrfVerifier.class,
+ JwtHttpHandler.class,
+ JwtSerializer.class,
+ OAuth2AuthenticationParametersImpl.class,
+ OAuth2CallbackFilter.class,
+ OAuth2ContextFactory.class,
+ OAuthCsrfVerifier.class,
RequestAuthenticatorImpl.class,
- UserLastConnectionDatesUpdaterImpl.class);
+ SessionTokensCleaner.class,
+ SessionTokensCleanerExecutorServiceImpl.class,
+ UserLastConnectionDatesUpdaterImpl.class,
+ UserRegistrarImpl.class,
+ UserSessionInitializer.class);
}
}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
index a46245a0ab0..699c771d901 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
@@ -34,6 +34,7 @@ import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
+import org.sonar.db.user.SessionTokenDto;
import org.sonar.db.user.UserDto;
import static com.google.common.base.Preconditions.checkArgument;
@@ -79,10 +80,13 @@ public class JwtHttpHandler {
public void generateToken(UserDto user, Map<String, Object> properties, HttpServletRequest request, HttpServletResponse response) {
String csrfState = jwtCsrfVerifier.generateState(request, response, sessionTimeoutInSeconds);
+ long expirationTime = system2.now() + sessionTimeoutInSeconds * 1000L;
+ SessionTokenDto sessionToken = createSessionToken(user, expirationTime);
String token = jwtSerializer.encode(new JwtSerializer.JwtSession(
user.getUuid(),
- sessionTimeoutInSeconds,
+ sessionToken.getUuid(),
+ expirationTime,
ImmutableMap.<String, Object>builder()
.putAll(properties)
.put(LAST_REFRESH_TIME_PARAM, system2.now())
@@ -91,16 +95,24 @@ public class JwtHttpHandler {
response.addCookie(createCookie(request, JWT_COOKIE, token, sessionTimeoutInSeconds));
}
+ private SessionTokenDto createSessionToken(UserDto user, long expirationTime) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ SessionTokenDto sessionToken = new SessionTokenDto()
+ .setUserUuid(user.getUuid())
+ .setExpirationDate(expirationTime);
+ dbClient.sessionTokensDao().insert(dbSession, sessionToken);
+ dbSession.commit();
+ return sessionToken;
+ }
+ }
+
public void generateToken(UserDto user, HttpServletRequest request, HttpServletResponse response) {
generateToken(user, Collections.emptyMap(), request, response);
}
public Optional<UserDto> validateToken(HttpServletRequest request, HttpServletResponse response) {
Optional<Token> token = getToken(request, response);
- if (token.isPresent()) {
- return Optional.of(token.get().getUserDto());
- }
- return Optional.empty();
+ return token.map(Token::getUserDto);
}
public Optional<Token> getToken(HttpServletRequest request, HttpServletResponse response) {
@@ -108,7 +120,9 @@ public class JwtHttpHandler {
if (!encodedToken.isPresent()) {
return Optional.empty();
}
- return validateToken(encodedToken.get(), request, response);
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ return validateToken(dbSession, encodedToken.get(), request, response);
+ }
}
private static Optional<String> getTokenFromCookie(HttpServletRequest request) {
@@ -124,24 +138,32 @@ public class JwtHttpHandler {
return Optional.of(token);
}
- private Optional<Token> validateToken(String tokenEncoded, HttpServletRequest request, HttpServletResponse response) {
+ private Optional<Token> validateToken(DbSession dbSession, String tokenEncoded, HttpServletRequest request, HttpServletResponse response) {
Optional<Claims> claims = jwtSerializer.decode(tokenEncoded);
if (!claims.isPresent()) {
return Optional.empty();
}
+ Claims token = claims.get();
+ Optional<SessionTokenDto> sessionToken = dbClient.sessionTokensDao().selectByUuid(dbSession, token.getId());
+ if (!sessionToken.isPresent()) {
+ return Optional.empty();
+ }
+ // Check on expiration is already done when decoding the JWT token, but here is done a double check with the expiration date from DB.
Date now = new Date(system2.now());
- Claims token = claims.get();
+ if (now.getTime() > sessionToken.get().getExpirationDate()) {
+ return Optional.empty();
+ }
if (now.after(addSeconds(token.getIssuedAt(), SESSION_DISCONNECT_IN_SECONDS))) {
return Optional.empty();
}
jwtCsrfVerifier.verifyState(request, (String) token.get(CSRF_JWT_PARAM), token.getSubject());
if (now.after(addSeconds(getLastRefreshDate(token), SESSION_REFRESH_IN_SECONDS))) {
- refreshToken(token, request, response);
+ refreshToken(dbSession, sessionToken.get(), token, request, response);
}
- Optional<UserDto> user = selectUserFromUuid(token.getSubject());
+ Optional<UserDto> user = selectUserFromUuid(dbSession, token.getSubject());
return user.map(userDto -> new Token(userDto, claims.get()));
}
@@ -151,26 +173,44 @@ public class JwtHttpHandler {
return new Date(lastFreshTime);
}
- private void refreshToken(Claims token, HttpServletRequest request, HttpServletResponse response) {
- String refreshToken = jwtSerializer.refresh(token, sessionTimeoutInSeconds);
+ private void refreshToken(DbSession dbSession, SessionTokenDto tokenFromDb, Claims tokenFromCookie, HttpServletRequest request, HttpServletResponse response) {
+ long expirationTime = system2.now() + sessionTimeoutInSeconds * 1000L;
+ String refreshToken = jwtSerializer.refresh(tokenFromCookie, expirationTime);
response.addCookie(createCookie(request, JWT_COOKIE, refreshToken, sessionTimeoutInSeconds));
- jwtCsrfVerifier.refreshState(request, response, (String) token.get(CSRF_JWT_PARAM), sessionTimeoutInSeconds);
+ jwtCsrfVerifier.refreshState(request, response, (String) tokenFromCookie.get(CSRF_JWT_PARAM), sessionTimeoutInSeconds);
+
+ dbClient.sessionTokensDao().update(dbSession, tokenFromDb.setExpirationDate(expirationTime));
+ dbSession.commit();
}
public void removeToken(HttpServletRequest request, HttpServletResponse response) {
+ removeSessionToken(request);
response.addCookie(createCookie(request, JWT_COOKIE, null, 0));
jwtCsrfVerifier.removeState(request, response);
}
+ private void removeSessionToken(HttpServletRequest request) {
+ Optional<Cookie> jwtCookie = findCookie(JWT_COOKIE, request);
+ if (!jwtCookie.isPresent()) {
+ return;
+ }
+ Optional<Claims> claims = jwtSerializer.decode(jwtCookie.get().getValue());
+ if (!claims.isPresent()) {
+ return;
+ }
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ dbClient.sessionTokensDao().deleteByUuid(dbSession, claims.get().getId());
+ dbSession.commit();
+ }
+ }
+
private static Cookie createCookie(HttpServletRequest request, String name, @Nullable String value, int expirationInSeconds) {
return newCookieBuilder(request).setName(name).setValue(value).setHttpOnly(true).setExpiry(expirationInSeconds).build();
}
- private Optional<UserDto> selectUserFromUuid(String userUuid) {
- try (DbSession dbSession = dbClient.openSession(false)) {
- UserDto user = dbClient.userDao().selectByUuid(dbSession, userUuid);
- return Optional.ofNullable(user != null && user.isActive() ? user : null);
- }
+ private Optional<UserDto> selectUserFromUuid(DbSession dbSession, String userUuid) {
+ UserDto user = dbClient.userDao().selectByUuid(dbSession, userUuid);
+ return Optional.ofNullable(user != null && user.isActive() ? user : null);
}
private static int getSessionTimeoutInSeconds(Configuration config) {
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java
index cfd06edf931..3aba1994598 100644
--- a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java
@@ -25,7 +25,7 @@ import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.SignatureException;
+import io.jsonwebtoken.security.SignatureException;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
@@ -38,7 +38,6 @@ import org.sonar.api.Startable;
import org.sonar.api.config.Configuration;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.System2;
-import org.sonar.core.util.UuidFactory;
import org.sonar.server.authentication.event.AuthenticationEvent.Source;
import org.sonar.server.authentication.event.AuthenticationException;
@@ -57,14 +56,12 @@ public class JwtSerializer implements Startable {
private final Configuration config;
private final System2 system2;
- private final UuidFactory uuidFactory;
private SecretKey secretKey;
- public JwtSerializer(Configuration config, System2 system2, UuidFactory uuidFactory) {
+ public JwtSerializer(Configuration config, System2 system2) {
this.config = config;
this.system2 = system2;
- this.uuidFactory = uuidFactory;
}
@VisibleForTesting
@@ -75,22 +72,19 @@ public class JwtSerializer implements Startable {
@Override
public void start() {
Optional<String> encodedKey = config.get(AUTH_JWT_SECRET.getKey());
- if (encodedKey.isPresent()) {
- this.secretKey = decodeSecretKeyProperty(encodedKey.get());
- } else {
- this.secretKey = generateSecretKey();
- }
+ this.secretKey = encodedKey
+ .map(JwtSerializer::decodeSecretKeyProperty)
+ .orElseGet(JwtSerializer::generateSecretKey);
}
String encode(JwtSession jwtSession) {
checkIsStarted();
- long now = system2.now();
JwtBuilder jwtBuilder = Jwts.builder()
- .setId(uuidFactory.create())
+ .setId(jwtSession.getSessionTokenUuid())
.setSubject(jwtSession.getUserLogin())
- .setIssuedAt(new Date(now))
- .setExpiration(new Date(now + jwtSession.getExpirationTimeInSeconds() * 1000))
- .signWith(SIGNATURE_ALGORITHM, secretKey);
+ .setIssuedAt(new Date(system2.now()))
+ .setExpiration(new Date(jwtSession.getExpirationTime()))
+ .signWith(secretKey, SIGNATURE_ALGORITHM);
for (Map.Entry<String, Object> entry : jwtSession.getProperties().entrySet()) {
jwtBuilder.claim(entry.getKey(), entry.getValue());
}
@@ -101,9 +95,10 @@ public class JwtSerializer implements Startable {
checkIsStarted();
Claims claims = null;
try {
- claims = Jwts.parser()
+ claims = (Claims) Jwts.parserBuilder()
.setSigningKey(secretKey)
- .parseClaimsJws(token)
+ .build()
+ .parse(token)
.getBody();
requireNonNull(claims.getId(), "Token id hasn't been found");
requireNonNull(claims.getSubject(), "Token subject hasn't been found");
@@ -121,15 +116,14 @@ public class JwtSerializer implements Startable {
}
}
- String refresh(Claims token, int expirationTimeInSeconds) {
+ String refresh(Claims token, long expirationTime) {
checkIsStarted();
- long now = system2.now();
JwtBuilder jwtBuilder = Jwts.builder();
for (Map.Entry<String, Object> entry : token.entrySet()) {
jwtBuilder.claim(entry.getKey(), entry.getValue());
}
- jwtBuilder.setExpiration(new Date(now + expirationTimeInSeconds * 1_000L))
- .signWith(SIGNATURE_ALGORITHM, secretKey);
+ jwtBuilder.setExpiration(new Date(expirationTime))
+ .signWith(secretKey, SIGNATURE_ALGORITHM);
return jwtBuilder.compact();
}
@@ -155,16 +149,18 @@ public class JwtSerializer implements Startable {
static class JwtSession {
private final String userLogin;
- private final long expirationTimeInSeconds;
+ private final String sessionTokenUuid;
+ private final long expirationTime;
private final Map<String, Object> properties;
- JwtSession(String userLogin, long expirationTimeInSeconds) {
- this(userLogin, expirationTimeInSeconds, Collections.emptyMap());
+ JwtSession(String userLogin, String sessionTokenUuid, long expirationTime) {
+ this(userLogin, sessionTokenUuid, expirationTime, Collections.emptyMap());
}
- JwtSession(String userLogin, long expirationTimeInSeconds, Map<String, Object> properties) {
+ JwtSession(String userLogin, String sessionTokenUuid, long expirationTime, Map<String, Object> properties) {
this.userLogin = requireNonNull(userLogin, "User login cannot be null");
- this.expirationTimeInSeconds = expirationTimeInSeconds;
+ this.sessionTokenUuid = requireNonNull(sessionTokenUuid, "Session token UUID cannot be null");
+ this.expirationTime = expirationTime;
this.properties = properties;
}
@@ -172,8 +168,12 @@ public class JwtSerializer implements Startable {
return userLogin;
}
- long getExpirationTimeInSeconds() {
- return expirationTimeInSeconds;
+ String getSessionTokenUuid() {
+ return sessionTokenUuid;
+ }
+
+ long getExpirationTime() {
+ return expirationTime;
}
Map<String, Object> getProperties() {
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java
new file mode 100644
index 00000000000..d5764bf3fd8
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.server.authentication.purge;
+
+import java.util.concurrent.TimeUnit;
+import org.sonar.api.Startable;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.util.GlobalLockManager;
+
+public class SessionTokensCleaner implements Startable {
+
+ private static final Logger LOG = Loggers.get(SessionTokensCleaner.class);
+
+ private static final String PURGE_DELAY_CONFIGURATION = "sonar.authentication.session.tokens.purge.delay";
+ private static final long DEFAULT_PURGE_DELAY_IN_SECONDS = 24 * 60 * 60L;
+ private static final String LOCK_NAME = "SessionCleaner";
+
+ private final SessionTokensCleanerExecutorService executorService;
+ private final DbClient dbClient;
+ private final Configuration configuration;
+ private final GlobalLockManager lockManager;
+
+ public SessionTokensCleaner(SessionTokensCleanerExecutorService executorService, DbClient dbClient, Configuration configuration, GlobalLockManager lockManager) {
+ this.executorService = executorService;
+ this.dbClient = dbClient;
+ this.configuration = configuration;
+ this.lockManager = lockManager;
+ }
+
+ @Override
+ public void start() {
+ this.executorService.scheduleAtFixedRate(this::executePurge, 0, configuration.getLong(PURGE_DELAY_CONFIGURATION).orElse(DEFAULT_PURGE_DELAY_IN_SECONDS), TimeUnit.SECONDS);
+ }
+
+ private void executePurge() {
+ if (!lockManager.tryLock(LOCK_NAME)) {
+ return;
+ }
+ LOG.debug("Start of cleaning expired session tokens");
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ int deletedSessionTokens = dbClient.sessionTokensDao().deleteExpired(dbSession);
+ dbSession.commit();
+ LOG.info("Purge of expired session tokens has removed {} elements", deletedSessionTokens);
+ }
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java
new file mode 100644
index 00000000000..551363c6944
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.authentication.purge;
+
+import java.util.concurrent.ScheduledExecutorService;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+public interface SessionTokensCleanerExecutorService extends ScheduledExecutorService {
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java
new file mode 100644
index 00000000000..3a0bbadb7a6
--- /dev/null
+++ b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.authentication.purge;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import org.sonar.server.util.AbstractStoppableScheduledExecutorServiceImpl;
+
+import static java.lang.Thread.MIN_PRIORITY;
+
+public class SessionTokensCleanerExecutorServiceImpl
+ extends AbstractStoppableScheduledExecutorServiceImpl<ScheduledExecutorService>
+ implements SessionTokensCleanerExecutorService {
+
+ public SessionTokensCleanerExecutorServiceImpl() {
+ super(
+ Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread thread = Executors.defaultThreadFactory().newThread(r);
+ thread.setName("SessionTokensCleaner-%d");
+ thread.setPriority(MIN_PRIORITY);
+ thread.setDaemon(false);
+ return thread;
+ }));
+ }
+}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java
index 34f83b73dd7..d3dccc5a21c 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java
@@ -31,7 +31,7 @@ public class AuthenticationModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new AuthenticationModule().configure(container);
- assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 20);
+ assertThat(container.size()).isGreaterThan(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER);
}
}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java
index f267d9400d4..0e6430c1ae8 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java
@@ -22,6 +22,7 @@ package org.sonar.server.authentication;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.impl.DefaultClaims;
import java.util.Date;
+import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.servlet.http.Cookie;
@@ -38,18 +39,21 @@ import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
+import org.sonar.db.user.SessionTokenDto;
import org.sonar.db.user.UserDto;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.db.user.UserTesting.newUserDto;
@@ -63,9 +67,10 @@ public class JwtHttpHandlerTest {
private static final long SIX_MINUTES_AGO = NOW - 6 * 60 * 1000L;
private static final long TEN_DAYS_AGO = NOW - 10 * 24 * 60 * 60 * 1000L;
+ private static final long IN_FIVE_MINUTES = NOW + 5 * 60 * 1000L;
+
@Rule
public ExpectedException expectedException = ExpectedException.none();
-
@Rule
public DbTester db = DbTester.create();
@@ -102,6 +107,7 @@ public class JwtHttpHandlerTest {
verify(jwtSerializer).encode(jwtArgumentCaptor.capture());
verifyToken(jwtArgumentCaptor.getValue(), user, 3 * 24 * 60 * 60, NOW);
+ verifySessionTokenInDb(jwtArgumentCaptor.getValue());
}
@Test
@@ -142,7 +148,7 @@ public class JwtHttpHandlerTest {
settings.setProperty("sonar.web.sessionTimeoutInMinutes", 15);
underTest.generateToken(user, request, response);
verify(jwtSerializer, times(2)).encode(jwtArgumentCaptor.capture());
- verifyToken(jwtArgumentCaptor.getAllValues().get(0), user,firstSessionTimeoutInMinutes * 60, NOW);
+ verifyToken(jwtArgumentCaptor.getAllValues().get(0), user, firstSessionTimeoutInMinutes * 60, NOW);
verifyToken(jwtArgumentCaptor.getAllValues().get(1), user, firstSessionTimeoutInMinutes * 60, NOW);
}
@@ -180,7 +186,8 @@ public class JwtHttpHandlerTest {
public void validate_token() {
UserDto user = db.users().insertUser();
addJwtCookie();
- Claims claims = createToken(user.getUuid(), NOW);
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+ Claims claims = createToken(sessionToken, NOW);
when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
assertThat(underTest.validateToken(request, response).isPresent()).isTrue();
@@ -193,13 +200,17 @@ public class JwtHttpHandlerTest {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 10 days ago and refreshed 6 minutes ago
- Claims claims = createToken(user.getUuid(), TEN_DAYS_AGO);
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+ Claims claims = createToken(sessionToken, TEN_DAYS_AGO);
claims.put("lastRefreshTime", SIX_MINUTES_AGO);
when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
assertThat(underTest.validateToken(request, response).isPresent()).isTrue();
- verify(jwtSerializer).refresh(any(Claims.class), eq(3 * 24 * 60 * 60));
+ verify(jwtSerializer).refresh(any(Claims.class), eq(NOW + 3 * 24 * 60 * 60 * 1000L));
+ assertThat(dbClient.sessionTokensDao().selectByUuid(dbSession, sessionToken.getUuid()).get().getExpirationDate())
+ .isNotEqualTo(IN_FIVE_MINUTES)
+ .isEqualTo(NOW + 3 * 24 * 60 * 60 * 1000L);
}
@Test
@@ -207,7 +218,8 @@ public class JwtHttpHandlerTest {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 10 days ago and refreshed 4 minutes ago
- Claims claims = createToken(user.getUuid(), TEN_DAYS_AGO);
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+ Claims claims = createToken(sessionToken, TEN_DAYS_AGO);
claims.put("lastRefreshTime", FOUR_MINUTES_AGO);
when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
@@ -221,8 +233,8 @@ public class JwtHttpHandlerTest {
UserDto user = db.users().insertUser();
addJwtCookie();
// Token was created 4 months ago, refreshed 4 minutes ago, and it expired in 5 minutes
- Claims claims = createToken(user.getUuid(), NOW - (4L * 30 * 24 * 60 * 60 * 1000));
- claims.setExpiration(new Date(NOW + 5 * 60 * 1000));
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+ Claims claims = createToken(sessionToken, NOW - (4L * 30 * 24 * 60 * 60 * 1000));
claims.put("lastRefreshTime", FOUR_MINUTES_AGO);
when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
@@ -233,8 +245,8 @@ public class JwtHttpHandlerTest {
public void validate_token_does_not_refresh_session_when_user_is_disabled() {
addJwtCookie();
UserDto user = addUser(false);
-
- Claims claims = createToken(user.getLogin(), NOW);
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+ Claims claims = createToken(sessionToken, NOW);
when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
@@ -253,7 +265,7 @@ public class JwtHttpHandlerTest {
public void validate_token_does_nothing_when_no_jwt_cookie() {
underTest.validateToken(request, response);
- verifyZeroInteractions(httpSession, jwtSerializer);
+ verifyNoInteractions(httpSession, jwtSerializer);
assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
}
@@ -263,7 +275,7 @@ public class JwtHttpHandlerTest {
underTest.validateToken(request, response);
- verifyZeroInteractions(httpSession, jwtSerializer);
+ verifyNoInteractions(httpSession, jwtSerializer);
assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
}
@@ -271,7 +283,8 @@ public class JwtHttpHandlerTest {
public void validate_token_verify_csrf_state() {
UserDto user = db.users().insertUser();
addJwtCookie();
- Claims claims = createToken(user.getUuid(), NOW);
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+ Claims claims = createToken(sessionToken, NOW);
claims.put("xsrfToken", CSRF_STATE);
when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
@@ -281,31 +294,95 @@ public class JwtHttpHandlerTest {
}
@Test
- public void validate_token_refresh_state_when_refreshing_token() {
+ public void validate_token_does_nothing_when_no_session_token_in_db() {
UserDto user = db.users().insertUser();
addJwtCookie();
+ // No SessionToken in DB
+ Claims claims = createToken("ABCD", user.getUuid(), NOW, IN_FIVE_MINUTES);
+ claims.put("lastRefreshTime", SIX_MINUTES_AGO);
+ when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
+
+ underTest.validateToken(request, response);
+
+ assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
+ }
+ @Test
+ public void validate_token_does_nothing_when_expiration_date_from_session_token_is_expired() {
+ UserDto user = db.users().insertUser();
+ addJwtCookie();
+ // In SessionToken, the expiration date is expired...
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(FOUR_MINUTES_AGO));
+ // ...whereas in the cookie, the expiration date is not expired
+ Claims claims = createToken(sessionToken.getUuid(), user.getUuid(), NOW, IN_FIVE_MINUTES);
+ claims.put("lastRefreshTime", SIX_MINUTES_AGO);
+ when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
+
+ underTest.validateToken(request, response);
+
+ assertThat(underTest.validateToken(request, response).isPresent()).isFalse();
+ }
+
+ @Test
+ public void validate_token_refresh_state_when_refreshing_token() {
+ UserDto user = db.users().insertUser();
+ addJwtCookie();
// Token was created 10 days ago and refreshed 6 minutes ago
- Claims claims = createToken(user.getUuid(), TEN_DAYS_AGO);
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+ Claims claims = createToken(sessionToken, TEN_DAYS_AGO);
claims.put("xsrfToken", "CSRF_STATE");
when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
underTest.validateToken(request, response);
- verify(jwtSerializer).refresh(any(Claims.class), anyInt());
+ verify(jwtSerializer).refresh(any(Claims.class), anyLong());
verify(jwtCsrfVerifier).refreshState(request, response, "CSRF_STATE", 3 * 24 * 60 * 60);
}
@Test
public void remove_token() {
+ addJwtCookie();
+ UserDto user = db.users().insertUser();
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+ Claims claims = createToken(sessionToken, TEN_DAYS_AGO);
+ claims.put("lastRefreshTime", FOUR_MINUTES_AGO);
+ when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.of(claims));
+
+ underTest.removeToken(request, response);
+
+ verifyCookie(findCookie("JWT-SESSION").get(), null, 0);
+ verify(jwtCsrfVerifier).removeState(request, response);
+ assertThat(dbClient.sessionTokensDao().selectByUuid(dbSession, sessionToken.getUuid())).isNotPresent();
+ }
+
+ @Test
+ public void does_not_remove_token_from_db_when_no_jwt_token_in_cookie() {
+ addJwtCookie();
+ UserDto user = db.users().insertUser();
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+ when(jwtSerializer.decode(JWT_TOKEN)).thenReturn(Optional.empty());
+
underTest.removeToken(request, response);
verifyCookie(findCookie("JWT-SESSION").get(), null, 0);
verify(jwtCsrfVerifier).removeState(request, response);
+ assertThat(dbClient.sessionTokensDao().selectByUuid(dbSession, sessionToken.getUuid())).isPresent();
}
- private void verifyToken(JwtSerializer.JwtSession token, UserDto user, int expectedExpirationTime, long expectedRefreshTime) {
- assertThat(token.getExpirationTimeInSeconds()).isEqualTo(expectedExpirationTime);
+ @Test
+ public void does_not_remove_token_from_db_when_no_cookie() {
+ UserDto user = db.users().insertUser();
+ SessionTokenDto sessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(IN_FIVE_MINUTES));
+
+ underTest.removeToken(request, response);
+
+ verifyCookie(findCookie("JWT-SESSION").get(), null, 0);
+ verify(jwtCsrfVerifier).removeState(request, response);
+ assertThat(dbClient.sessionTokensDao().selectByUuid(dbSession, sessionToken.getUuid())).isPresent();
+ }
+
+ private void verifyToken(JwtSerializer.JwtSession token, UserDto user, long expectedExpirationDuration, long expectedRefreshTime) {
+ assertThat(token.getExpirationTime()).isEqualTo(NOW + expectedExpirationDuration * 1000L);
assertThat(token.getUserLogin()).isEqualTo(user.getUuid());
assertThat(token.getProperties().get("lastRefreshTime")).isEqualTo(expectedRefreshTime);
}
@@ -325,6 +402,17 @@ public class JwtHttpHandlerTest {
assertThat(cookie.getValue()).isEqualTo(value);
}
+ private void verifySessionTokenInDb(JwtSerializer.JwtSession jwtSession) {
+ Map<String, Object> map = db.selectFirst(dbSession, "select st.uuid as \"uuid\", " +
+ "st.user_uuid as \"userUuid\", " +
+ "st.expiration_date as \"expirationDate\" " +
+ "from session_tokens st ");
+ assertThat(map)
+ .contains(
+ entry("uuid", jwtSession.getSessionTokenUuid()),
+ entry("expirationDate", jwtSession.getExpirationTime()));
+ }
+
private UserDto addUser(boolean active) {
UserDto user = newUserDto()
.setActive(active);
@@ -339,14 +427,13 @@ public class JwtHttpHandlerTest {
return cookie;
}
- private Claims createToken(String userUuid, long createdAt) {
- // Expired in 5 minutes by default
- return createToken(userUuid, createdAt, NOW + 5 * 60 * 1000);
+ private Claims createToken(SessionTokenDto sessionToken, long createdAt) {
+ return createToken(sessionToken.getUuid(), sessionToken.getUserUuid(), createdAt, sessionToken.getExpirationDate());
}
- private Claims createToken(String userUuid, long createdAt, long expiredAt) {
+ private Claims createToken(String uuid, String userUuid, long createdAt, long expiredAt) {
DefaultClaims claims = new DefaultClaims();
- claims.setId("ID");
+ claims.setId(uuid);
claims.setSubject(userUuid);
claims.setIssuedAt(new Date(createdAt));
claims.setExpiration(new Date(expiredAt));
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java
index d5451459a11..e94b9fb8ddc 100644
--- a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java
@@ -22,7 +22,6 @@ package org.sonar.server.authentication;
import com.google.common.collect.ImmutableMap;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClaims;
import java.util.Base64;
import java.util.Date;
@@ -35,11 +34,12 @@ import org.junit.rules.ExpectedException;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.utils.System2;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.core.util.UuidFactoryImpl;
import org.sonar.server.authentication.JwtSerializer.JwtSession;
import org.sonar.server.authentication.event.AuthenticationEvent.Source;
+import static io.jsonwebtoken.SignatureAlgorithm.HS256;
+import static org.apache.commons.lang.time.DateUtils.addMinutes;
+import static org.apache.commons.lang.time.DateUtils.addYears;
import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException;
@@ -47,21 +47,21 @@ public class JwtSerializerTest {
private static final String A_SECRET_KEY = "HrPSavOYLNNrwTY+SOqpChr7OwvbR/zbDLdVXRN0+Eg=";
private static final String USER_LOGIN = "john";
+ private static final String SESSION_TOKEN_UUID = "ABCD";
@Rule
public ExpectedException expectedException = ExpectedException.none();
private MapSettings settings = new MapSettings();
private System2 system2 = System2.INSTANCE;
- private UuidFactory uuidFactory = UuidFactoryImpl.INSTANCE;
- private JwtSerializer underTest = new JwtSerializer(settings.asConfig(), system2, uuidFactory);
+ private JwtSerializer underTest = new JwtSerializer(settings.asConfig(), system2);
@Test
public void generate_token() {
setSecretKey(A_SECRET_KEY);
underTest.start();
- String token = underTest.encode(new JwtSession(USER_LOGIN, 10));
+ String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime()));
assertThat(token).isNotEmpty();
}
@@ -71,29 +71,27 @@ public class JwtSerializerTest {
setSecretKey(A_SECRET_KEY);
underTest.start();
- Date now = new Date();
- long expirationTimeInSeconds = 10L;
- String token = underTest.encode(new JwtSession(USER_LOGIN, expirationTimeInSeconds));
+ String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime()));
assertThat(token).isNotEmpty();
Claims claims = underTest.decode(token).get();
- assertThat(claims.getExpiration().getTime()).isGreaterThanOrEqualTo(now.getTime() + expirationTimeInSeconds * 1000L - 1000L);
+ assertThat(claims.getExpiration().getTime())
+ .isGreaterThanOrEqualTo(addMinutes(new Date(), 19).getTime());
}
@Test
public void generate_token_with_big_expiration_date() {
setSecretKey(A_SECRET_KEY);
underTest.start();
- Date now = new Date();
- long oneYearInSeconds = 12 * 30 * 24 * 60 * 60L;
- String token = underTest.encode(new JwtSession(USER_LOGIN, oneYearInSeconds));
+ long oneYearLater = addYears(new Date(), 1).getTime();
+ String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, oneYearLater));
assertThat(token).isNotEmpty();
Claims claims = underTest.decode(token).get();
// Check expiration date it set to one year in the future
- assertThat(claims.getExpiration().getTime()).isGreaterThanOrEqualTo(now.getTime() + oneYearInSeconds * 1000L - 1000L);
+ assertThat(claims.getExpiration().getTime()).isGreaterThanOrEqualTo(oneYearLater - 1000L);
}
@Test
@@ -101,7 +99,7 @@ public class JwtSerializerTest {
setSecretKey(A_SECRET_KEY);
underTest.start();
- String token = underTest.encode(new JwtSession(USER_LOGIN, 10, ImmutableMap.of("custom", "property")));
+ String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime(), ImmutableMap.of("custom", "property")));
assertThat(token).isNotEmpty();
Claims claims = underTest.decode(token).get();
@@ -112,17 +110,16 @@ public class JwtSerializerTest {
public void decode_token() {
setSecretKey(A_SECRET_KEY);
underTest.start();
- Date now = new Date();
- String token = underTest.encode(new JwtSession(USER_LOGIN, 20 * 60));
+ String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime()));
Claims claims = underTest.decode(token).get();
- assertThat(claims.getId()).isNotEmpty();
+ assertThat(claims.getId()).isEqualTo(SESSION_TOKEN_UUID);
assertThat(claims.getSubject()).isEqualTo(USER_LOGIN);
assertThat(claims.getExpiration()).isNotNull();
assertThat(claims.getIssuedAt()).isNotNull();
// Check expiration date it set to more than 19 minutes in the future
- assertThat(claims.getExpiration()).isAfterOrEqualsTo(new Date(now.getTime() + 19 * 60 * 1000));
+ assertThat(claims.getExpiration()).isAfterOrEqualTo(addMinutes(new Date(), 19));
}
@Test
@@ -133,8 +130,8 @@ public class JwtSerializerTest {
String token = Jwts.builder()
.setId("123")
.setIssuedAt(new Date(system2.now()))
- .setExpiration(new Date(system2.now()))
- .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY))
+ .setExpiration(addMinutes(new Date(), -20))
+ .signWith(decodeSecretKey(A_SECRET_KEY), HS256)
.compact();
assertThat(underTest.decode(token)).isEmpty();
@@ -149,8 +146,8 @@ public class JwtSerializerTest {
.setId("123")
.setSubject(USER_LOGIN)
.setIssuedAt(new Date(system2.now()))
- .setExpiration(new Date(system2.now() + 20 * 60 * 1000))
- .signWith(SignatureAlgorithm.HS256, decodeSecretKey("LyWgHktP0FuHB2K+kMs3KWMCJyFHVZDdDSqpIxAMVaQ="))
+ .setExpiration(addMinutes(new Date(), 20))
+ .signWith(decodeSecretKey("LyWgHktP0FuHB2K+kMs3KWMCJyFHVZDdDSqpIxAMVaQ="), HS256)
.compact();
assertThat(underTest.decode(token)).isEmpty();
@@ -165,8 +162,8 @@ public class JwtSerializerTest {
.setSubject(USER_LOGIN)
.setIssuer("sonarqube")
.setIssuedAt(new Date(system2.now()))
- .setExpiration(new Date(system2.now() + 20 * 60 * 1000))
- .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY))
+ .setExpiration(addMinutes(new Date(), 20))
+ .signWith(decodeSecretKey(A_SECRET_KEY), HS256)
.compact();
expectedException.expect(authenticationException().from(Source.jwt()).withLogin(USER_LOGIN).andNoPublicMessage());
@@ -183,8 +180,8 @@ public class JwtSerializerTest {
.setId("123")
.setIssuer("sonarqube")
.setIssuedAt(new Date(system2.now()))
- .setExpiration(new Date(system2.now() + 20 * 60 * 1000))
- .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY))
+ .setExpiration(addMinutes(new Date(), 20))
+ .signWith(HS256, decodeSecretKey(A_SECRET_KEY))
.compact();
expectedException.expect(authenticationException().from(Source.jwt()).withoutLogin().andNoPublicMessage());
@@ -202,7 +199,7 @@ public class JwtSerializerTest {
.setIssuer("sonarqube")
.setSubject(USER_LOGIN)
.setIssuedAt(new Date(system2.now()))
- .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY))
+ .signWith(decodeSecretKey(A_SECRET_KEY), HS256)
.compact();
expectedException.expect(authenticationException().from(Source.jwt()).withLogin(USER_LOGIN).andNoPublicMessage());
@@ -218,8 +215,8 @@ public class JwtSerializerTest {
String token = Jwts.builder()
.setId("123")
.setSubject(USER_LOGIN)
- .setExpiration(new Date(system2.now() + 20 * 60 * 1000))
- .signWith(SignatureAlgorithm.HS256, decodeSecretKey(A_SECRET_KEY))
+ .setExpiration(addMinutes(new Date(), 20))
+ .signWith(decodeSecretKey(A_SECRET_KEY), HS256)
.compact();
expectedException.expect(authenticationException().from(Source.jwt()).withLogin(USER_LOGIN).andNoPublicMessage());
@@ -234,7 +231,7 @@ public class JwtSerializerTest {
underTest.start();
assertThat(underTest.getSecretKey()).isNotNull();
- assertThat(underTest.getSecretKey().getAlgorithm()).isEqualTo(SignatureAlgorithm.HS256.getJcaName());
+ assertThat(underTest.getSecretKey().getAlgorithm()).isEqualTo(HS256.getJcaName());
}
@Test
@@ -254,7 +251,7 @@ public class JwtSerializerTest {
Date now = new Date();
Date createdAt = DateUtils.parseDate("2016-01-01");
// Expired in 10 minutes
- Date expiredAt = new Date(now.getTime() + 10 * 60 * 1000);
+ Date expiredAt = addMinutes(new Date(), 10);
Claims token = new DefaultClaims()
.setId("id")
.setSubject("subject")
@@ -264,7 +261,7 @@ public class JwtSerializerTest {
token.put("key", "value");
// Refresh the token with a higher expiration time
- String encodedToken = underTest.refresh(token, 20 * 60);
+ String encodedToken = underTest.refresh(token, addMinutes(new Date(), 20).getTime());
Claims result = underTest.decode(encodedToken).get();
assertThat(result.getId()).isEqualTo("id");
@@ -274,17 +271,17 @@ public class JwtSerializerTest {
assertThat(result.get("key")).isEqualTo("value");
// Expiration date has been changed
assertThat(result.getExpiration()).isNotEqualTo(expiredAt)
- .isAfterOrEqualsTo(new Date(now.getTime() + 19 * 1000));
+ .isAfterOrEqualTo(addMinutes(new Date(), 19));
}
@Test
public void refresh_token_generate_a_new_hash() {
setSecretKey(A_SECRET_KEY);
underTest.start();
- String token = underTest.encode(new JwtSession(USER_LOGIN, 30));
+ String token = underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 20).getTime()));
Optional<Claims> claims = underTest.decode(token);
- String newToken = underTest.refresh(claims.get(), 45);
+ String newToken = underTest.refresh(claims.get(), addMinutes(new Date(), 45).getTime());
assertThat(newToken).isNotEqualTo(token);
}
@@ -294,7 +291,7 @@ public class JwtSerializerTest {
expectedException.expect(NullPointerException.class);
expectedException.expectMessage("org.sonar.server.authentication.JwtSerializer not started");
- underTest.encode(new JwtSession(USER_LOGIN, 10));
+ underTest.encode(new JwtSession(USER_LOGIN, SESSION_TOKEN_UUID, addMinutes(new Date(), 10).getTime()));
}
@Test
@@ -310,12 +307,12 @@ public class JwtSerializerTest {
expectedException.expect(NullPointerException.class);
expectedException.expectMessage("org.sonar.server.authentication.JwtSerializer not started");
- underTest.refresh(new DefaultClaims(), 10);
+ underTest.refresh(new DefaultClaims(), addMinutes(new Date(), 10).getTime());
}
private SecretKey decodeSecretKey(String encodedKey) {
byte[] decodedKey = Base64.getDecoder().decode(encodedKey);
- return new SecretKeySpec(decodedKey, 0, decodedKey.length, SignatureAlgorithm.HS256.getJcaName());
+ return new SecretKeySpec(decodedKey, 0, decodedKey.length, HS256.getJcaName());
}
private void setSecretKey(String s) {
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java
new file mode 100644
index 00000000000..08a28f7f5df
--- /dev/null
+++ b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java
@@ -0,0 +1,127 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.server.authentication.purge;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.api.utils.log.LogAndArguments;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.SessionTokenDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.util.AbstractStoppableExecutorService;
+import org.sonar.server.util.GlobalLockManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SessionTokensCleanerTest {
+
+ private static final long NOW = 1_000_000_000L;
+
+ private TestSystem2 system2 = new TestSystem2().setNow(NOW);
+ @Rule
+ public DbTester db = DbTester.create(system2);
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ private GlobalLockManager lockManager = mock(GlobalLockManager.class);
+
+ private final MapSettings settings = new MapSettings();
+ private final Configuration configuration = settings.asConfig();
+
+ private SyncSessionTokensCleanerExecutorService executorService = new SyncSessionTokensCleanerExecutorService();
+
+ private SessionTokensCleaner underTest = new SessionTokensCleaner(executorService, db.getDbClient(), configuration, lockManager);
+
+ @Test
+ public void purge_expired_session_tokens() {
+ when(lockManager.tryLock(anyString())).thenReturn(true);
+ UserDto user = db.users().insertUser();
+ SessionTokenDto validSessionToken = db.users().insertSessionToken(user);
+ SessionTokenDto expiredSessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(NOW - 1_000_000L));
+ underTest.start();
+
+ executorService.runCommand();
+
+ assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), validSessionToken.getUuid())).isPresent();
+ assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), expiredSessionToken.getUuid())).isNotPresent();
+ assertThat(logTester.getLogs(LoggerLevel.INFO))
+ .extracting(LogAndArguments::getFormattedMsg)
+ .containsOnly("Purge of expired session tokens has removed 1 elements");
+ }
+
+ @Test
+ public void do_not_execute_purge_when_fail_to_get_lock() {
+ when(lockManager.tryLock(anyString())).thenReturn(false);
+ SessionTokenDto expiredSessionToken = db.users().insertSessionToken(db.users().insertUser(), st -> st.setExpirationDate(NOW - 1_000_000L));
+ underTest.start();
+
+ executorService.runCommand();
+
+ assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), expiredSessionToken.getUuid())).isPresent();
+ }
+
+ private static class SyncSessionTokensCleanerExecutorService extends AbstractStoppableExecutorService<ScheduledExecutorService> implements SessionTokensCleanerExecutorService {
+
+ private Runnable command;
+
+ public SyncSessionTokensCleanerExecutorService() {
+ super(null);
+ }
+
+ public void runCommand() {
+ command.run();
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
+ this.command = command;
+ return null;
+ }
+
+ @Override
+ public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+ return null;
+ }
+
+ @Override
+ public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+ return null;
+ }
+
+ @Override
+ public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
+ return null;
+ }
+
+ }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java
index 936e1d64baf..6470e483807 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java
@@ -87,7 +87,7 @@ public class LogoutAction extends ServletFilter implements AuthenticationWsActio
private void generateAuthenticationEvent(HttpServletRequest request, HttpServletResponse response) {
try {
Optional<JwtHttpHandler.Token> token = jwtHttpHandler.getToken(request, response);
- String userLogin = token.isPresent() ? token.get().getUserDto().getLogin() : null;
+ String userLogin = token.map(value -> value.getUserDto().getLogin()).orElse(null);
authenticationEvent.logoutSuccess(request, userLogin);
} catch (AuthenticationException e) {
authenticationEvent.logoutFailure(request, e.getMessage());
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
index a5da21e08a4..ac5b47ecbfe 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
@@ -108,19 +108,14 @@ public class DeactivateAction implements UsersWsAction {
dbClient.organizationMemberDao().deleteByUserUuid(dbSession, userUuid);
dbClient.userPropertiesDao().deleteByUser(dbSession, user);
dbClient.almPatDao().deleteByUser(dbSession, user);
- deactivateUser(dbSession, user);
+ dbClient.sessionTokensDao().deleteByUser(dbSession, user);
+ dbClient.userDao().deactivateUser(dbSession, user);
userIndexer.commitAndIndex(dbSession, user);
-
- LOGGER.debug("Deactivate user: {}; by admin: {}", login, userSession.isSystemAdministrator());
}
writeResponse(response, login);
}
- private void deactivateUser(DbSession dbSession, UserDto user) {
- dbClient.userDao().deactivateUser(dbSession, user);
- }
-
private void writeResponse(Response response, String login) {
try (DbSession dbSession = dbClient.openSession(false)) {
UserDto user = dbClient.userDao().selectByLogin(dbSession, login);
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java
index 0cf8985de48..84861bc4607 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java
@@ -40,7 +40,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.db.user.UserTesting.newUserDto;
import static org.sonar.server.authentication.event.AuthenticationEvent.Source.sso;
@@ -91,34 +91,34 @@ public class LogoutActionTest {
underTest.doFilter(request, response, chain);
- verifyZeroInteractions(jwtHttpHandler, chain);
+ verifyNoInteractions(jwtHttpHandler, chain);
verify(response).setStatus(400);
}
@Test
- public void logout_logged_user() throws Exception {
+ public void logout_logged_user() {
setUser(USER);
executeRequest();
verify(jwtHttpHandler).removeToken(request, response);
- verifyZeroInteractions(chain);
+ verifyNoInteractions(chain);
verify(authenticationEvent).logoutSuccess(request, "john");
}
@Test
- public void logout_unlogged_user() throws Exception {
+ public void logout_unlogged_user() {
setNoUser();
executeRequest();
verify(jwtHttpHandler).removeToken(request, response);
- verifyZeroInteractions(chain);
+ verifyNoInteractions(chain);
verify(authenticationEvent).logoutSuccess(request, null);
}
@Test
- public void generate_auth_event_on_failure() throws Exception {
+ public void generate_auth_event_on_failure() {
setUser(USER);
AuthenticationException exception = AuthenticationException.newBuilder().setMessage("error!").setSource(sso()).build();
doThrow(exception).when(jwtHttpHandler).getToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
@@ -127,7 +127,7 @@ public class LogoutActionTest {
verify(authenticationEvent).logoutFailure(request, "error!");
verify(jwtHttpHandler).removeToken(any(HttpServletRequest.class), any(HttpServletResponse.class));
- verifyZeroInteractions(chain);
+ verifyNoInteractions(chain);
}
private void executeRequest() {
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
index 010afb4a438..caaecbb3ac1 100644
--- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
+++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
@@ -38,6 +38,7 @@ import org.sonar.db.property.PropertyDto;
import org.sonar.db.property.PropertyQuery;
import org.sonar.db.qualityprofile.QProfileDto;
import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.SessionTokenDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.BadRequestException;
@@ -260,6 +261,22 @@ public class DeactivateActionTest {
}
@Test
+ public void deactivate_user_deletes_his_session_tokens() {
+ logInAsSystemAdministrator();
+ UserDto user = db.users().insertUser();
+ SessionTokenDto sessionToken1 = db.users().insertSessionToken(user);
+ SessionTokenDto sessionToken2 =db.users().insertSessionToken(user);
+ UserDto anotherUser = db.users().insertUser();
+ SessionTokenDto sessionToken3 =db.users().insertSessionToken(anotherUser);
+
+ deactivate(user.getLogin());
+
+ assertThat(db.getDbClient().sessionTokensDao().selectByUuid(dbSession, sessionToken1.getUuid())).isNotPresent();
+ assertThat(db.getDbClient().sessionTokensDao().selectByUuid(dbSession, sessionToken2.getUuid())).isNotPresent();
+ assertThat(db.getDbClient().sessionTokensDao().selectByUuid(dbSession, sessionToken3.getUuid())).isPresent();
+ }
+
+ @Test
public void user_cannot_deactivate_itself_on_sonarqube() {
UserDto user = db.users().insertUser();
userSession.logIn(user.getLogin()).setSystemAdministrator();