Browse Source

SONAR-13472 Fix SSF-113

* SONAR-13472 Create 'SESSION_TOKENS' table
* SONAR-13472 Remove 'SESSION_TOKENS' from user when disabling an user
* SONAR-13472 Replace JwtSession expiration duration by a time
* SONAR-13472 Create, update and delete SessionToken during authentication lifecycle
* SONAR-13472 Purge expired session tokens at start-up and every day
* SONAR-13472 Improve log during session tokens cleaning
* Add example to start a Keycloak server already configured
tags/8.4.0.35506
Julien Lancelot 4 years ago
parent
commit
46a49f0b5e
31 changed files with 1154 additions and 155 deletions
  1. 1
    0
      server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
  2. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java
  3. 7
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java
  4. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
  5. 74
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenDto.java
  6. 40
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenMapper.java
  7. 72
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokensDao.java
  8. 59
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/user/SessionTokenMapper.xml
  9. 10
    0
      server/sonar-db-dao/src/schema/schema-sq.ddl
  10. 164
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/user/SessionTokensDaoTest.java
  11. 16
    0
      server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
  12. 7
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java
  13. 13
    12
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java
  14. 0
    1
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java
  15. 76
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTable.java
  16. 2
    2
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java
  17. 59
    0
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTableTest.java
  18. 18
    14
      server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
  19. 58
    18
      server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java
  20. 28
    28
      server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java
  21. 74
    0
      server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java
  22. 27
    0
      server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java
  23. 42
    0
      server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java
  24. 1
    1
      server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java
  25. 111
    24
      server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java
  26. 36
    39
      server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java
  27. 127
    0
      server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java
  28. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java
  29. 2
    7
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
  30. 8
    8
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java
  31. 17
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java

+ 1
- 0
server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java View File

@@ -112,6 +112,7 @@ public final class SqTables {
"rules_profiles",
"rule_repositories",
"schema_migrations",
"session_tokens",
"snapshots",
"users",
"user_properties",

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java View File

@@ -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,

+ 7
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java View File

@@ -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;
}

}

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java View File

@@ -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,

+ 74
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenDto.java View File

@@ -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;
}
}

+ 40
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokenMapper.java View File

@@ -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);

}

+ 72
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/SessionTokensDao.java View File

@@ -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);
}
}

+ 59
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/user/SessionTokenMapper.xml View File

@@ -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>

+ 10
- 0
server/sonar-db-dao/src/schema/schema-sq.ddl View File

@@ -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,

+ 164
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/user/SessionTokensDaoTest.java View File

@@ -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);
}
}

+ 16
- 0
server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java View File

@@ -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;
}

}

+ 7
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/def/VarcharColumnDef.java View File

@@ -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;


+ 13
- 12
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v00/CreateInitialSchema.java View File

@@ -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();

+ 0
- 1
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v83/DbVersion83.java View File

@@ -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)

;
}
}

+ 76
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTable.java View File

@@ -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());
}
}

+ 2
- 2
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java View File

@@ -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)
;
}
}

+ 59
- 0
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSessionTokensTableTest.java View File

@@ -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);
}

}

+ 18
- 14
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java View File

@@ -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);
}
}

+ 58
- 18
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtHttpHandler.java View File

@@ -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) {

+ 28
- 28
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/JwtSerializer.java View File

@@ -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() {

+ 74
- 0
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java View File

@@ -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
}

}

+ 27
- 0
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java View File

@@ -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 {
}

+ 42
- 0
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java View File

@@ -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;
}));
}
}

+ 1
- 1
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/AuthenticationModuleTest.java View File

@@ -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);
}

}

+ 111
- 24
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtHttpHandlerTest.java View File

@@ -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));

+ 36
- 39
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/JwtSerializerTest.java View File

@@ -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) {

+ 127
- 0
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java View File

@@ -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;
}

}
}

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/authentication/ws/LogoutAction.java View File

@@ -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());

+ 2
- 7
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java View File

@@ -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);

+ 8
- 8
server/sonar-webserver-webapi/src/test/java/org/sonar/server/authentication/ws/LogoutActionTest.java View File

@@ -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() {

+ 17
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java View File

@@ -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;
@@ -259,6 +260,22 @@ public class DeactivateActionTest {
assertThat(db.getDbClient().almPatDao().selectByUserAndAlmSetting(dbSession, anotherUser.getUuid(), almSettingDto)).isNotNull();
}

@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();

Loading…
Cancel
Save