Compare commits

..

No commits in common. "master" and "feature/auth" have entirely different histories.

391 changed files with 229 additions and 9783 deletions

328
README.md
View File

@ -19,19 +19,6 @@
implementation("ru.touchin:common")
}
```
## Project Gradle tasks
### Detekt:
- `gradle $project:detekt` - detect not formatted, complex code.
Reports are stored in "$pwd/build/reports/kotlin-detekt-${project.name}.html".
### DiKTat:
- `gradle :diktatCheck` - detect not formatted code of "kt", "kts" files;
- `gradle :diktatFix` - if possible, fix not formatted code. Known issues: full fix may require 1+ launch in order to apply all rules; some rules potentially may break code syntax.
By setting environment variable `TASKS_FILE_REPORT_ENABLED`(true, false) you may configure raw console output or html file as report.
Reports are stored in "$pwd/build/reports/diktat-report.html".
## common
@ -81,106 +68,6 @@ Reports are stored in "$pwd/build/reports/diktat-report.html".
Утилиты для тестирования репозиториев
## codestyle-archunit
Набор правил для поддержки оформления архитектуры.
#### Список доступных правил
- `ru.touchin.codestyle.archunit.rules.ClassNamingArchRules`
- `ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules`
#### Gradle plugin
Настройка и применение совместно с [ArchUnit Gradle Plugin](https://github.com/societe-generale/arch-unit-gradle-plugin).
Действие `checkRules` для проверки соответствие правилам запускается при операциях сборки, по умолчанию.
Вручную можно вызвать командой ``gradle :checkRules`` для нужного модуля.
Добавить его можно следующим образом на примере установки в рутовый gradle.build проекта:
Groovy DSL:
```groovy
buildscript {
dependencies {
classpath "com.societegenerale.commons:arch-unit-gradle-plugin:3.0.0"
}
}
subprojects {
dependencyManagement {
dependencies {
dependency "com.tngtech.archunit:archunit:1.0.1"
}
}
apply plugin: "java"
apply plugin: "com.societegenerale.commons.arch-unit-gradle-plugin"
archUnit {
mainScopePath = "/classes/kotlin/main" // or "/classes/java/main"
testScopePath = "/classes/kotlin/test" // or "/classes/java/test"
var applyType = applyOn("ru.touchin", "main")
configurableRules = [
configurableRule(
"ru.touchin.codestyle.archunit.rules.ClassNamingArchRules",
applyType,
),
configurableRule(
"ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules",
applyType,
),
]
}
dependencies {
archUnitExtraLib "ru.touchin:codestyle-archunit" // or archUnitExtraLib project(":codestyle-archunit")
}
}
```
Kotlin DSL:
```kotlin
plugins {
id("com.societegenerale.commons.arch-unit-gradle-plugin") version "3.0.0"
}
subprojects {
configure<DependencyManagementExtension> {
dependencies {
dependency("com.tngtech.archunit:archunit:1.0.1")
}
}
apply(plugin = "java")
apply(plugin = "com.societegenerale.commons.arch-unit-gradle-plugin")
archUnit {
mainScopePath = "/classes/kotlin/main" // or "/classes/java/main"
testScopePath = "/classes/kotlin/test" // or "/classes/java/test"
configurableRules = listOf(
"ru.touchin.codestyle.archunit.rules.ClassNamingArchRules",
"ru.touchin.codestyle.archunit.rules.ClassPackagingArchRules"
).map { package ->
configurableRule(
package,
applyOn("ru.touchin", "main")
)
}
}
dependencies {
archUnitExtraLib("ru.touchin:codestyle-archunit") // or archUnitExtraLib(project(":codestyle-archunit"))
}
}
```
Отключить проверки на таске помимо конфигурирования `configurableRule` можно также таким образом:
```kotlin
// clear action launch for root project to avoid exception
tasks.checkRules.configure {
actions.clear()
}
```
## logger
Основные компоненты логирования:
@ -202,17 +89,11 @@ Interceptor для логирования запросов/ответов.
## exception-handler-spring-web
Перехватывает ошибки сервера, определяет код ошибки и возвращает их в правильный `response`.
Подключается с помощью аннотации `@EnableSpringExceptionHandler`
Перехватывает ошибки сервера, определяет код ошибки и возвращает их в правильный `response`
## exception-handler-logger-spring-web
Добавляет логирование в обработку ошибок.
Подключается с помощью аннотации `@EnableSpringExceptionHandlerLogger` до подключения основного модуля.
## validation-spring
Добавляет аннотации для валидации запросов.
Добавляет логирование в обработку ошибок
## version-spring-web
@ -242,11 +123,11 @@ Interceptor для логирования запросов/ответов.
Модуль для хранения настроек
## security-authorization-server-core
## auth-core
Модуль авторизации
## security-authorization-server-jwt-core
## auth-jwt-core
Добавляет поддержку jwt-токенов (создание/хранение). Для работы этого модуля требуется прописать в пропертях:
@ -267,204 +148,3 @@ token.refresh:
prefix: RT-
timeToLive: PT2H # 2 hours
```
Генерация ключей:
```bash
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -pubout -out public.pem
openssl pkcs8 -topk8 -inform PEM -in private.pem -out private_key.pem -nocrypt
cat private_key.pem
cat public.pem
```
## security-authorization-server-oauth2-metadata
OAuth2 metadata support.
## security-jwt-common
JWT related utilities.
## security-resource-server-default-configuration
Default configuration for the Spring OAuth2 resource server with JWT auth.
## security-resource-server-custom-configuration
Custom configuration for the Spring OAuth2 resource server with JWT auth. Requires the following properties:
``` yaml
token.access:
issuer: ${app.issuer}
signatureAlgorithm: RS256
keyPair:
public: |
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
```
## security-resource-server-test-configuration
Disables Spring OAuth2 resource server for testing.
## s3-storage
Amazon S3 support.
## server-info-spring-web
Allow include headers with information about the server in responses
To get started you need:
1) Add annotation to configuration
2) Add property to yml/properties file:
```
server.info:
buildVersion: ${buildVersion}
```
3) Implement ServerInfoService (optional. If you want to add other headers)
4) Add dir with impl ServerInfoService in ComponentScan annotation
## push-message-provider
Интерфейсы и компоненты для модулей по обеспечению интеграции с сервисами отправки пуш-уведомлений. Является необходимой зависимостью для использования провайдеров.
Далее рассматривается пример использования подключаемых модулей-провайдеров.
``` kotlin
@Service
class PushSendingService(
private val pushMessageProviderServiceFactory: PushMessageProviderServiceFactory
) {
fun sendPushMessage() {
val yourPushToken = "pushTokenForChecking"
val platform = PlatformType.ANDROID_GOOGLE
val pushMessageProvider: PushMessageProviderService = pushMessageProviderServiceFactory.get(platform)
val result = pushMessageProvider.check( // Проверка валидности токена для обозначения целесообразности отправки
PushTokenCheck(
pushToken = yourPushToken
)
)
if (result.status == PushTokenStatus.VALID) { // Токен валиден, PushMessageProviderService интегрирован в систему
// Отправка пуш-уведомления
pushMessageProvider.send(
PushTokenMessage(
token = yourPushToken,
pushMessageNotification = PushMessageNotification(
title = "Your PushMessage",
description = "Provided by PushMessageProviderService",
imageUrl = null
),
data = mapOf(
"customKey" to "customData"
)
)
)
}
}
}
```
## push-message-provider-fcm
Модуль по обеспечению интеграции с Firebase Cloud Messaging.
1) Подключение компонентов Spring осуществляется при помощи аннотации `@EnablePushMessageProviderFcm`.
2) Необходимо добавление конфигурации для модуля с выбранным способом хранения данных для авторизации. Пример файла конфигурации в формате yaml:
``` yaml
push-message-provider:
platformProviders:
ANDROID_GOOGLE:
- FCM
IOS:
- FCM
fcm:
appName: yourAppName
auth:
# Выбранный тип авторизации
client:
readTimeout: 10s
connectionTimeout: 1s
```
3) Настраивается способ предоставления авторизации для Firebase Cloud Messaging.
А) Токен доступа из консоли Google, добавляемый в конфигурацию настроек:
``` yaml
auth:
token:
value: testValue
expiresAt: 2023-01-01 23:59:59 +00:00
```
B) Данные в файле из консоли Firebase, добавляемые в resources с обозначением пути в конфигурации настроек:
``` yaml
auth:
credentialsFile:
path: credentials/firebase-admin.json
```
C) Данные из файла консоли Firebase, добавляемые в конфигурацию настроек:
``` yaml
auth:
credentialsData:
type: service_account
projectId: yourProjectId
privateKeyId: yourPrivateKeyId
privateKey: |
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
clientEmail: yourClientEmail
clientId: yourClientId
authUri: yourAuthUri
tokenUri: yourTokenUri
authProviderX509CertUrl: yourAuthProviderX509CertUrl
clientX509CertUrl: yourClientX509CertUrl
```
## push-message-provider-hpk
Модуль по обеспечению интеграции с Huawei Push Kit.
1) Подключение нового провайдера осуществляется при помощи аннотации `@EnablePushMessageProviderHpk`.
2) Для логирования запросов к сервису HPK нужно встроить в контейнер Spring собственный `WebClientLogger` из модуля `logger-spring-web` или же использовать стандартный посредством импорта конфигурации:
``` kotlin
@Import(
SpringLoggerConfiguration::class,
SpringLoggerWebConfiguration::class
)
class YourConfiguration
```
3) Нужно добавить конфигурацию для считывания модулем. Пример файла в формате yaml:
``` yaml
push-message-provider:
platformProviders:
ANDROID_HUAWEI:
- HPK
hpk:
web-services:
client-id: yourClientId
oauth:
client-secret: yourClientSecret
url: https://oauth-login.cloud.huawei.com/oauth2/v3/
http:
connection-timeout: 1s
read-timeout: 10s
write-timeout: 10s
ssl: # Опциональная структура
handshake-timeout: 1s
notify-read-timeout: 1s
notify-flush-timeout: 1s
hpk:
url: https://push-api.cloud.huawei.com/v1/
http:
connection-timeout: 1s
read-timeout: 10s
write-timeout: 10s
ssl: # Опциональная структура
handshake-timeout: 1s
notify-read-timeout: 1s
notify-flush-timeout: 1s
```

View File

@ -8,7 +8,6 @@ dependencies {
runtimeOnly("org.postgresql:postgresql")
api(project(":common"))
api(project(":common-device"))
api(project(":common-spring-jpa"))

View File

@ -8,7 +8,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
@ComponentScan("ru.touchin.auth.core")
@ConfigurationPropertiesScan("ru.touchin.auth.core")
@ConfigurationPropertiesScan
class AuthCoreConfiguration {
@Bean

View File

@ -0,0 +1,13 @@
@file:Suppress("unused")
package ru.touchin.auth.core.configurations
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.cache.annotation.EnableCaching
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import ru.touchin.common.spring.jpa.EnableJpaAuditingExtra
@EntityScan("ru.touchin.auth.core")
@EnableJpaRepositories("ru.touchin.auth.core")
@EnableCaching
@EnableJpaAuditingExtra
class AuthCoreDatabaseConfiguration

View File

@ -1,6 +1,6 @@
package ru.touchin.auth.core.device.dto
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import java.util.*
data class Device(

View File

@ -0,0 +1,5 @@
package ru.touchin.auth.core.device.dto.enums
enum class DevicePlatform {
Android, Huawei, Apple
}

View File

@ -1,8 +1,7 @@
package ru.touchin.auth.core.device.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import ru.touchin.auth.core.user.models.UserEntity
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
import javax.persistence.Entity
import javax.persistence.JoinColumn
@ -11,7 +10,7 @@ import javax.persistence.ManyToMany
import javax.persistence.Table
@Entity
@Table(name = "devices", schema = SCHEMA)
@Table(name = "devices")
class DeviceEntity: AuditableUuidIdEntity() {
lateinit var platform: DevicePlatform
@ -19,26 +18,11 @@ class DeviceEntity: AuditableUuidIdEntity() {
@ManyToMany
@JoinTable(
name = "devices_users",
schema = SCHEMA,
joinColumns = [JoinColumn(name = "device_id")],
inverseJoinColumns = [JoinColumn(name = "user_id")]
)
lateinit var users: Set<UserEntity>
override fun equals(other: Any?): Boolean {
if (other == null || this.javaClass != other.javaClass) {
return false
}
other as DeviceEntity
return this.id == other.id
}
override fun hashCode(): Int {
return this.id.hashCode()
}
companion object {
fun create(platform: DevicePlatform): DeviceEntity {
return DeviceEntity().apply {

View File

@ -1,7 +1,7 @@
package ru.touchin.auth.core.device.services
import ru.touchin.auth.core.device.dto.Device
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import java.util.UUID
interface DeviceCoreService {

View File

@ -6,9 +6,9 @@ import org.springframework.transaction.annotation.Transactional
import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
import ru.touchin.auth.core.device.dto.Device
import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.auth.core.device.repository.findByIdOrThrow
import ru.touchin.common.devices.enums.DevicePlatform
import java.util.*
@Service

View File

@ -1,5 +1,6 @@
package ru.touchin.auth.core.policy.services
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.stereotype.Service
import ru.touchin.auth.core.policy.dto.RegistrationPolicy

View File

@ -0,0 +1,15 @@
package ru.touchin.auth.core.scope.models
import ru.touchin.common.spring.jpa.models.BaseEntity
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.Table
@Entity
@Table(name = "scopes")
class ScopeEntity : BaseEntity() {
@Id
lateinit var name: String
}

View File

@ -1,8 +1,5 @@
@file:Suppress("unused")
package ru.touchin.auth.core.scope.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA
import ru.touchin.common.spring.jpa.models.BaseUuidIdEntity
import javax.persistence.Entity
import javax.persistence.JoinColumn
@ -10,7 +7,7 @@ import javax.persistence.ManyToOne
import javax.persistence.Table
@Entity
@Table(name = "scope_groups", schema = SCHEMA)
@Table(name = "scope_groups")
class ScopeGroupEntity : BaseUuidIdEntity() {
lateinit var groupName: String

View File

@ -1,8 +1,8 @@
package ru.touchin.auth.core.user.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA
import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
import java.time.ZonedDateTime
import javax.persistence.Entity
import javax.persistence.EnumType
import javax.persistence.Enumerated
@ -11,7 +11,7 @@ import javax.persistence.ManyToOne
import javax.persistence.Table
@Entity
@Table(name = "user_accounts", schema = SCHEMA)
@Table(name = "user_accounts")
class UserAccountEntity: AuditableUuidIdEntity() {
lateinit var username: String

View File

@ -1,6 +1,5 @@
package ru.touchin.auth.core.user.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA
import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.common.spring.jpa.models.AuditableUuidIdEntity
@ -12,8 +11,8 @@ import javax.persistence.ManyToMany
import javax.persistence.Table
@Entity
@Table(name = "users", schema = SCHEMA)
class UserEntity : AuditableUuidIdEntity() {
@Table(name = "users")
class UserEntity: AuditableUuidIdEntity() {
var anonymous: Boolean = true
@ -22,7 +21,6 @@ class UserEntity : AuditableUuidIdEntity() {
@ManyToMany
@JoinTable(
name = "devices_users",
schema = SCHEMA,
joinColumns = [JoinColumn(name = "user_id")],
inverseJoinColumns = [JoinColumn(name = "device_id")]
)
@ -31,14 +29,9 @@ class UserEntity : AuditableUuidIdEntity() {
@ManyToMany
@JoinTable(
name = "users_scopes",
schema = SCHEMA,
joinColumns = [JoinColumn(name = "user_id")],
inverseJoinColumns = [JoinColumn(name = "scope_name")]
)
lateinit var scopes: MutableSet<ScopeEntity>
fun addScopes(scopes: Collection<ScopeEntity>) {
this.scopes.addAll(scopes)
}
lateinit var scopes: Set<ScopeEntity>
}

View File

@ -4,7 +4,6 @@ package ru.touchin.auth.core.user.repositories
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.findByIdOrNull
import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.auth.core.user.exceptions.UserAccountNotFoundException
import ru.touchin.auth.core.user.models.UserAccountEntity
@ -19,26 +18,9 @@ interface UserAccountRepository: JpaRepository<UserAccountEntity, UUID> {
""")
fun findByUsername(username: String, identifierType: IdentifierType): UserAccountEntity?
@Query("""
SELECT ua
FROM UserAccountEntity ua
WHERE ua.user.id = :userId AND identifierType = :identifierType
""")
fun findByUserId(userId: UUID, identifierType: IdentifierType): UserAccountEntity?
}
fun UserAccountRepository.findByIdOrThrow(userAccountId: UUID): UserAccountEntity {
return findByIdOrNull(userAccountId)
?: throw UserAccountNotFoundException(userAccountId.toString())
}
fun UserAccountRepository.findByUsernameOrThrow(username: String, identifierType: IdentifierType): UserAccountEntity {
return findByUsername(username, identifierType)
?: throw UserAccountNotFoundException(username)
}
fun UserAccountRepository.findByUserIdOrThrow(userId: UUID, identifierType: IdentifierType): UserAccountEntity {
return findByUserId(userId, identifierType)
?: throw UserAccountNotFoundException(userId.toString())
}

View File

@ -1,28 +1,17 @@
package ru.touchin.auth.core.user.services
import ru.touchin.auth.core.user.dto.User
import ru.touchin.auth.core.user.dto.UserAccount
import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.auth.core.user.services.dto.AddUserScopes
import ru.touchin.auth.core.user.services.dto.GetUserAccount
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser
import ru.touchin.auth.core.user.services.dto.NewUser
import ru.touchin.auth.core.user.services.dto.UserLogin
import ru.touchin.auth.core.user.services.dto.UserLogout
import ru.touchin.auth.core.user.services.dto.UserSetPassword
import ru.touchin.auth.core.user.services.dto.UserUpdatePassword
interface UserCoreService {
fun create(newAnonymousUser: NewAnonymousUser): User
fun create(newUser: NewUser): User
fun get(username: String, identifierType: IdentifierType): User
fun getUserAccount(userAccount: GetUserAccount): UserAccount
fun getOrNull(username: String, identifierType: IdentifierType): User?
fun login(userLogin: UserLogin): User
fun logout(userLogout: UserLogout)
fun updatePassword(update: UserUpdatePassword)
fun setPassword(userSetPassword: UserSetPassword)
fun addScopes(addUserScopes: AddUserScopes)
}

View File

@ -1,8 +1,6 @@
@file:Suppress("unused")
package ru.touchin.auth.core.user.services
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@ -11,13 +9,10 @@ import ru.touchin.auth.core.device.exceptions.DeviceAlreadyLinkedException
import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.auth.core.device.repository.findByIdWithLockOrThrow
import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.scope.models.ScopeGroupEntity
import ru.touchin.auth.core.scope.repositories.ScopeRepository
import ru.touchin.auth.core.user.converters.UserAccountConverter.toDto
import ru.touchin.auth.core.user.converters.UserConverter.toDto
import ru.touchin.auth.core.user.dto.User
import ru.touchin.auth.core.user.dto.UserAccount
import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.auth.core.user.exceptions.UserAccountNotFoundException
import ru.touchin.auth.core.user.exceptions.UserAlreadyRegisteredException
@ -26,17 +21,10 @@ import ru.touchin.auth.core.user.models.UserAccountEntity
import ru.touchin.auth.core.user.models.UserEntity
import ru.touchin.auth.core.user.repositories.UserAccountRepository
import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.auth.core.user.repositories.findByIdOrThrow
import ru.touchin.auth.core.user.repositories.findByUserIdOrThrow
import ru.touchin.auth.core.user.repositories.findByUsernameOrThrow
import ru.touchin.auth.core.user.services.dto.AddUserScopes
import ru.touchin.auth.core.user.services.dto.GetUserAccount
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser
import ru.touchin.auth.core.user.services.dto.NewUser
import ru.touchin.auth.core.user.services.dto.UserLogin
import ru.touchin.auth.core.user.services.dto.UserLogout
import ru.touchin.auth.core.user.services.dto.UserSetPassword
import ru.touchin.auth.core.user.services.dto.UserUpdatePassword
@Service
class UserCoreServiceImpl(
@ -58,7 +46,7 @@ class UserCoreServiceImpl(
val user = UserEntity().apply {
anonymous = true
devices = hashSetOf(device)
scopes = mutableSetOf()
scopes = emptySet()
}
return userRepository.save(user)
@ -80,7 +68,7 @@ class UserCoreServiceImpl(
.apply {
anonymous = false
devices = hashSetOf(device)
scopes = defaultScopes.toMutableSet()
scopes = defaultScopes.toSet()
}
.also(userRepository::save)
@ -112,85 +100,19 @@ class UserCoreServiceImpl(
val user = userAccount.user
.apply {
devices = devices + device
devices = hashSetOf(device)
}
.also(userRepository::save)
return user.toDto(device.toDto())
}
@Transactional
override fun logout(userLogout: UserLogout) {
val device = deviceRepository.findByIdWithLockOrThrow(userLogout.deviceId)
resetDeviceUsers(device)
userRepository.findByIdOrThrow(userLogout.userId)
.apply {
devices = devices - device
}
.also(userRepository::save)
}
@Transactional
override fun updatePassword(update: UserUpdatePassword) {
val userAccount = userAccountRepository.findByIdOrThrow(update.userAccountId)
if (userAccount.password != null) {
if (!passwordEncoder.matches(update.oldPassword, userAccount.password!!)) {
throw WrongPasswordException("userAccountId=${update.userAccountId}")
}
}
userAccount.apply {
password = update.newPassword?.let(passwordEncoder::encode)
}.also(userAccountRepository::save)
}
@Transactional
override fun setPassword(userSetPassword: UserSetPassword) {
val userAccount = userAccountRepository.findByIdOrThrow(userSetPassword.userAccountId)
userAccount.apply {
password = userSetPassword.newPassword.let(passwordEncoder::encode)
}.also(userAccountRepository::save)
}
@Transactional
override fun addScopes(addUserScopes: AddUserScopes) {
val user = userRepository.findByIdOrThrow(addUserScopes.userId)
val newScopes = addUserScopes.scopes.map { scope ->
scopeRepository.findByIdOrNull(scope)
?: createNewScope(scope)
}
user.addScopes(newScopes)
userRepository.save(user)
}
private fun createNewScope(scope: String): ScopeEntity {
val newScope = ScopeEntity().apply {
name = scope
users = mutableSetOf()
}
return scopeRepository.save(newScope)
}
@Transactional(readOnly = true)
override fun get(username: String, identifierType: IdentifierType): User {
return getOrNull(username, identifierType)
?: throw UserAccountNotFoundException(username)
}
@Transactional(readOnly = true)
override fun getUserAccount(userAccount: GetUserAccount): UserAccount {
return userAccountRepository.findByUserIdOrThrow(userAccount.userId, userAccount.identifierType)
.toDto()
}
@Transactional(readOnly = true)
override fun getOrNull(username: String, identifierType: IdentifierType): User? {
return userAccountRepository.findByUsername(username, identifierType)

View File

@ -3,10 +3,10 @@ databaseChangeLog:
id: 202105011900__create_table__devices
author: touchin
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName: devices
onFail: MARK_RAN
not:
tableExists:
tableName: devices
changes:
- createTable:
tableName: devices

View File

@ -3,10 +3,10 @@ databaseChangeLog:
id: 202105052300__create_table__users
author: touchin
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName: users
onFail: MARK_RAN
not:
tableExists:
tableName: users
changes:
- createTable:
tableName: users

View File

@ -3,10 +3,10 @@ databaseChangeLog:
id: 202105052310__create_table__user_accounts
author: touchin
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName: user_accounts
onFail: MARK_RAN
not:
tableExists:
tableName: user_accounts
changes:
- createTable:
tableName: user_accounts

View File

@ -3,10 +3,10 @@ databaseChangeLog:
id: 202105052331__create_table__devices_users
author: touchin
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName: devices_users
onFail: MARK_RAN
not:
tableExists:
tableName: devices_users
changes:
- createTable:
tableName: devices_users

View File

@ -3,10 +3,10 @@ databaseChangeLog:
id: 202105081411__create_table__scopes
author: touchin
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName: scopes
onFail: MARK_RAN
not:
tableExists:
tableName: scopes
changes:
- createTable:
tableName: scopes

View File

@ -3,10 +3,10 @@ databaseChangeLog:
id: 202105081431__create_table__scope_groups
author: touchin
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName: scope_groups
onFail: MARK_RAN
not:
tableExists:
tableName: scope_groups
changes:
- createTable:
tableName: scope_groups

View File

@ -3,10 +3,10 @@ databaseChangeLog:
id: 202105081531__create_table__users_scopes
author: touchin
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName: users_scopes
onFail: MARK_RAN
not:
tableExists:
tableName: users_scopes
changes:
- createTable:
tableName: users_scopes

View File

@ -8,12 +8,12 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.dao.DataAccessException
import ru.touchin.auth.core.device.exceptions.DeviceNotFoundException
import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.auth.core.device.repository.findByIdOrThrow
import ru.touchin.auth.core.device.repository.findByIdWithLockOrThrow
import ru.touchin.auth.core.user.models.UserEntity
import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
import java.time.Duration
import java.util.*

View File

@ -14,8 +14,8 @@ import org.springframework.test.context.ActiveProfiles
import ru.touchin.auth.core.device.dto.Device
import ru.touchin.auth.core.device.exceptions.DeviceNotFoundException
import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.common.devices.enums.DevicePlatform
import java.util.*
@ActiveProfiles("test")

View File

@ -48,7 +48,7 @@ internal class ScopeRepositoryTest {
val savedScope = scopeRepository.findByIdOrThrow(scope.name)
assertTrue(
ReflectionEquals(scope, "createdAt", "users").matches(savedScope)
ReflectionEquals(scope, "createdAt").matches(savedScope)
)
}

View File

@ -9,12 +9,12 @@ import org.mockito.internal.matchers.apachecommons.ReflectionEquals
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.dao.DataAccessException
import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.scope.repositories.ScopeRepository
import ru.touchin.auth.core.user.exceptions.UserNotFoundException
import ru.touchin.auth.core.user.models.UserEntity
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
import java.time.ZonedDateTime
import java.util.*
@ -43,7 +43,7 @@ internal class UserRepositoryTest {
anonymous = false
confirmedAt = ZonedDateTime.now()
devices = emptySet()
scopes = mutableSetOf()
scopes = emptySet()
}
)
@ -51,7 +51,7 @@ internal class UserRepositoryTest {
val savedUser = userRepository.findByIdOrThrow(user.id!!)
assertTrue(ReflectionEquals(user, "createdAt", "confirmedAt").matches(savedUser))
assertTrue(ReflectionEquals(user, "createdAt").matches(savedUser))
}
@ -87,7 +87,7 @@ internal class UserRepositoryTest {
val user = UserEntity()
.apply {
scopes = mutableSetOf(scope)
scopes = setOf(scope)
}
userRepository.saveAndFlush(user)
@ -117,7 +117,7 @@ internal class UserRepositoryTest {
fun shouldBeErrorIfScopeNew() {
val user = UserEntity()
.apply {
scopes = mutableSetOf(ScopeEntity().apply { name = "admin" })
scopes = setOf(ScopeEntity().apply { name = "admin" })
}
assertThrows(DataAccessException::class.java) {

View File

@ -2,7 +2,6 @@ package ru.touchin.auth.core.user.services
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
@ -11,11 +10,10 @@ import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.crypto.password.PasswordEncoder
import ru.touchin.auth.core.device.dto.Device
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.auth.core.device.repository.findByIdOrThrow
import ru.touchin.auth.core.device.services.DeviceCoreService
import ru.touchin.auth.core.scope.dto.Scope
import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.user.dto.User
import ru.touchin.auth.core.user.dto.enums.IdentifierType
import ru.touchin.auth.core.user.exceptions.UserAlreadyRegisteredException
@ -23,14 +21,11 @@ import ru.touchin.auth.core.user.exceptions.WrongPasswordException
import ru.touchin.auth.core.user.repositories.UserAccountRepository
import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.auth.core.user.repositories.findByIdOrThrow
import ru.touchin.auth.core.user.services.dto.AddUserScopes
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser
import ru.touchin.auth.core.user.services.dto.NewUser
import ru.touchin.auth.core.user.services.dto.UserLogin
import ru.touchin.auth.core.user.services.dto.UserLogout
import ru.touchin.auth.core.user.services.dto.UserUpdatePassword
import ru.touchin.common.devices.enums.DevicePlatform
import ru.touchin.common.spring.test.jpa.repository.RepositoryTest
import java.lang.IllegalArgumentException
import java.util.*
import javax.persistence.EntityManager
@ -184,7 +179,7 @@ internal class UserCoreServiceImplSlowTest {
fun loginShouldBeOk() {
val device = createDevice()
val regUser = createNewUser(deviceId = device.id, password = "qwerty", username = "employee")
val regUser = createNewUser(deviceId = device.id, password = "qwerty", username = "employee" )
val logingUser = userCoreService.login(
UserLogin(
@ -287,128 +282,4 @@ internal class UserCoreServiceImplSlowTest {
assertEquals(loginUserE2.id, actualDeviceE2.users.first().id!!)
}
@Test
@DisplayName("При логауте пользователь отлинковывается от устройства")
fun usersShouldBeUnbindedAfterLogout() {
val device = createDevice()
createNewUser(deviceId = device.id, password = "qwerty", username = "employee1")
val regUserE2 = createNewUser(deviceId = device.id, password = "qwerty", username = "employee2")
val actualDeviceE2 = deviceRepository.findByIdOrThrow(deviceId = device.id)
assertEquals(1, actualDeviceE2.users.size)
assertEquals(regUserE2.id, actualDeviceE2.users.first().id!!)
userCoreService.logout(
UserLogout(
deviceId = device.id,
userId = regUserE2.id
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualDevice = deviceRepository.findByIdOrThrow(deviceId = device.id)
assertTrue(actualDevice.users.isEmpty())
}
@Test
@DisplayName("Пользователь может залогиниться на нескольких устройствах")
fun usersShouldOkTwoDevicesLogin() {
val deviceD1 = createDevice()
val deviceD2 = createDevice()
val regUser = createNewUser(deviceId = deviceD1.id, password = "qwerty", username = "employee")
userCoreService.login(
UserLogin(
deviceId = deviceD2.id,
username = "employee",
password = "qwerty",
identifierType = IdentifierType.Email,
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualUser = userRepository.findByIdOrThrow(regUser.id)
assertEquals(2, actualUser.devices.size)
userCoreService.logout(
UserLogout(
deviceId = deviceD1.id,
userId = regUser.id
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualUser2 = userRepository.findByIdOrThrow(regUser.id)
assertEquals(1, actualUser2.devices.size)
assertEquals(deviceD2.id, actualUser2.devices.first().id!!)
}
@Test
@DisplayName("Пользователь может сменить пароль")
fun userCanChangePassword() {
val device = createDevice()
val regUser = createNewUser(deviceId = device.id, password = "qwerty", username = "employee1")
val userAccount = userAccountRepository.findByUserId(regUser.id, IdentifierType.Email)
assertNotNull(userAccount)
userCoreService.updatePassword(
UserUpdatePassword(
userAccountId = userAccount?.id!!,
oldPassword = "qwerty",
newPassword = "QWERTY1234"
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualUserAccount = userAccountRepository.findByUsername("employee1", IdentifierType.Email)
assertNotNull(actualUserAccount)
assertFalse(passwordEncoder.matches(userAccount.password!!, actualUserAccount?.password!!))
assertTrue(passwordEncoder.matches("QWERTY1234", actualUserAccount.password!!))
}
@Test
@DisplayName("Пользователю можно добавить скоупы")
fun userCanAddScope() {
val device = createDevice()
val regUser = createNewUser(deviceId = device.id, password = "qwerty", username = "employee1")
val newScopes = listOf("admin", "moderator", "robot")
userCoreService.addScopes(
AddUserScopes(
userId = regUser.id,
scopes = newScopes
)
).also {
entityManager.flush()
entityManager.clear()
}
val actualUser = userRepository.findByIdOrThrow(regUser.id)
val expectedScopes: List<String> = regUser.scopes.map(Scope::name) + newScopes
assertTrue(actualUser.scopes.map(ScopeEntity::name).containsAll(expectedScopes))
assertEquals(expectedScopes.size, actualUser.scopes.size)
}
}

View File

@ -18,7 +18,10 @@ import org.springframework.test.context.ActiveProfiles
import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
import ru.touchin.auth.core.device.exceptions.DeviceAlreadyLinkedException
import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.device.dto.enums.DevicePlatform
import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.auth.core.policy.dto.RegistrationPolicy
import ru.touchin.auth.core.policy.services.PolicyService
import ru.touchin.auth.core.scope.dto.Scope
import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.scope.repositories.ScopeRepository
@ -29,7 +32,6 @@ import ru.touchin.auth.core.user.repositories.UserAccountRepository
import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.auth.core.user.services.dto.NewAnonymousUser
import ru.touchin.auth.core.user.services.dto.NewUser
import ru.touchin.common.devices.enums.DevicePlatform
import java.util.*
@ActiveProfiles("test")

View File

@ -2,3 +2,5 @@ databaseChangeLog:
- changeset:
id: 1
author: test
- includeAll:
path: classpath*:/db/changelog/auth/core

View File

@ -5,9 +5,7 @@ plugins {
}
dependencies {
implementation(project(":security-authorization-server-core"))
implementation(project(":security-resource-server-custom-jwt-configuration"))
implementation(project(":security-jwt-common"))
implementation(project(":auth-core"))
implementation("com.auth0:java-jwt")
implementation("org.springframework.security:spring-security-oauth2-jose")

View File

@ -1,11 +1,11 @@
@file:Suppress("unused")
package ru.touchin.auth.core.configurations
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
@Configuration
@Import(AuthCoreConfiguration::class)
@ComponentScan("ru.touchin.auth.core.tokens")
@ConfigurationPropertiesScan("ru.touchin.auth.core.tokens")

View File

@ -1,10 +1,10 @@
@file:Suppress("unused")
package ru.touchin.auth.core.configurations
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
@Configuration
@Import(AuthCoreDatabaseConfiguration::class)
@EntityScan("ru.touchin.auth.core.tokens")
class AuthTokenDatabaseConfiguration

View File

@ -1,33 +1,35 @@
package ru.touchin.auth.core.tokens.access.config
import com.auth0.jwt.algorithms.Algorithm
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import ru.touchin.auth.core.tokens.access.properties.AccessTokenProperties
import ru.touchin.auth.security.jwt.configurations.JwtConfiguration
import ru.touchin.auth.security.jwt.utils.JwtUtils.getKeySpec
import ru.touchin.common.string.StringUtils.emptyString
import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
@Configuration
@Import(JwtConfiguration::class)
class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProperties) {
@Bean
fun accessTokenSigningAlgorithm(
@Qualifier("accessTokenPublicKey")
accessTokenPublicKey: RSAPublicKey
): Algorithm {
fun accessTokenSigningAlgorithm(): Algorithm {
return Algorithm.RSA256(
accessTokenPublicKey,
accessTokenPublicKey(),
accessTokenPrivateKey()
)
}
@Bean("accessTokenPublicKey")
fun accessTokenPublicKey(): RSAPublicKey {
val keySpecX509 = getKeySpec(accessTokenProperties.keyPair.public, ::X509EncodedKeySpec)
return keyFactory.generatePublic(keySpecX509) as RSAPublicKey
}
@Bean("accessTokenPrivateKey")
fun accessTokenPrivateKey(): RSAPrivateKey {
val keySpecPKCS8 = getKeySpec(accessTokenProperties.keyPair.private, ::PKCS8EncodedKeySpec)
@ -35,6 +37,22 @@ class AccessTokenBeanConfig(private val accessTokenProperties: AccessTokenProper
return keyFactory.generatePrivate(keySpecPKCS8) as RSAPrivateKey
}
private fun <T> getKeySpec(key: String, keySpecFn: (ByteArray) -> T): T {
val rawKey = getRawKey(key)
return Base64.getDecoder()
.decode(rawKey)
.let(keySpecFn)
}
private fun getRawKey(key: String): String {
return key
.replace("-----BEGIN .+KEY-----".toRegex(), emptyString())
.replace("-----END .+KEY-----".toRegex(), emptyString())
.replace("\n", emptyString())
.trim()
}
companion object {
val keyFactory: KeyFactory = KeyFactory.getInstance("RSA")
}

View File

@ -3,7 +3,7 @@ package ru.touchin.auth.core.tokens.access.services
import ru.touchin.auth.core.tokens.access.dto.AccessToken
import ru.touchin.auth.core.tokens.access.dto.AccessTokenRequest
interface AccessTokenCoreService {
interface AccessTokenService {
fun create(accessTokenRequest: AccessTokenRequest): AccessToken

View File

@ -13,10 +13,10 @@ import java.time.ZoneId
import java.util.*
@Service
class JwtAccessTokenCoreServiceImpl(
class JwtAccessTokenServiceImpl(
private val accessTokenProperties: AccessTokenProperties,
private val accessTokenSigningAlgorithm: Algorithm
) : AccessTokenCoreService {
) : AccessTokenService {
private fun sign(builder: JWTCreator.Builder) = builder.sign(accessTokenSigningAlgorithm)

View File

@ -6,6 +6,5 @@ import java.time.ZonedDateTime
data class RefreshToken(
val value: String,
val expiresAt: ZonedDateTime,
val usedAt: ZonedDateTime?,
val user: User,
)

View File

@ -1,8 +1,5 @@
@file:Suppress("unused")
package ru.touchin.auth.core.tokens.refresh.models
import ru.touchin.auth.core.configurations.AuthCoreDatabaseConfiguration.Companion.SCHEMA
import ru.touchin.auth.core.device.models.DeviceEntity
import ru.touchin.auth.core.scope.models.ScopeEntity
import ru.touchin.auth.core.tokens.refresh.exceptions.RefreshTokenExpiredException
@ -18,15 +15,13 @@ import javax.persistence.ManyToOne
import javax.persistence.Table
@Entity
@Table(name = "refresh_tokens", schema = SCHEMA)
@Table(name = "refresh_tokens")
class RefreshTokenEntity : AuditableUuidIdEntity() {
lateinit var value: String
lateinit var expiresAt: ZonedDateTime
var usedAt: ZonedDateTime? = null
@ManyToOne
@JoinColumn(name = "user_id")
lateinit var user: UserEntity
@ -38,14 +33,13 @@ class RefreshTokenEntity : AuditableUuidIdEntity() {
@ManyToMany
@JoinTable(
name = "refresh_token_scopes",
schema = SCHEMA,
joinColumns = [JoinColumn(name = "refresh_token_id")],
inverseJoinColumns = [JoinColumn(name = "scope_name")]
)
lateinit var scopes: Set<ScopeEntity>
fun validate(): RefreshTokenEntity = this.apply {
if (expiresAt.isExpired() || usedAt != null) {
if (expiresAt.isExpired()) {
throw RefreshTokenExpiredException(value)
}
}

View File

@ -3,10 +3,9 @@ package ru.touchin.auth.core.tokens.refresh.services
import ru.touchin.auth.core.tokens.refresh.dto.RefreshToken
import ru.touchin.auth.core.tokens.refresh.services.dto.NewRefreshToken
interface RefreshTokenCoreService {
interface RefreshTokenService {
fun get(value: String): RefreshToken
fun create(token: NewRefreshToken): RefreshToken
fun refresh(value: String): RefreshToken
}

View File

@ -7,6 +7,8 @@ import ru.touchin.auth.core.device.converters.DeviceConverter.toDto
import ru.touchin.auth.core.device.repository.DeviceRepository
import ru.touchin.auth.core.scope.dto.Scope
import ru.touchin.auth.core.scope.repositories.ScopeRepository
import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.auth.core.user.repositories.findByIdOrThrow
import ru.touchin.auth.core.tokens.refresh.dto.RefreshToken
import ru.touchin.auth.core.tokens.refresh.models.RefreshTokenEntity
import ru.touchin.auth.core.tokens.refresh.properties.RefreshTokenProperties
@ -14,29 +16,22 @@ import ru.touchin.auth.core.tokens.refresh.repositories.RefreshTokenRepository
import ru.touchin.auth.core.tokens.refresh.repositories.findByValueOrThrow
import ru.touchin.auth.core.tokens.refresh.services.dto.NewRefreshToken
import ru.touchin.auth.core.user.converters.UserConverter.toDto
import ru.touchin.auth.core.user.repositories.UserRepository
import ru.touchin.auth.core.user.repositories.findByIdOrThrow
import ru.touchin.common.byte.ByteUtils.toHex
import ru.touchin.common.random.SecureRandomStringGenerator
import ru.touchin.common.security.hash.HashUtils
import ru.touchin.common.security.hash.HashUtils.calculateHash
import java.time.ZonedDateTime
@Service
class RefreshTokenCoreServiceImpl(
class RefreshTokenServiceImpl(
private val refreshTokenProperties: RefreshTokenProperties,
private val refreshTokenRepository: RefreshTokenRepository,
private val userRepository: UserRepository,
private val deviceRepository: DeviceRepository,
private val scopeRepository: ScopeRepository,
) : RefreshTokenCoreService {
) : RefreshTokenService {
@Transactional(readOnly = true)
override fun get(value: String): RefreshToken {
val valueHash = getTokenHash(value)
return refreshTokenRepository.findByValueOrThrow(valueHash)
.toDto(value)
return refreshTokenRepository.findByValueOrThrow(value)
.toDto()
}
@Transactional
@ -44,44 +39,17 @@ class RefreshTokenCoreServiceImpl(
val user = userRepository.findByIdOrThrow(token.userId)
val device = token.deviceId?.let(deviceRepository::findByIdOrNull)
val scopes = scopeRepository.findAllById(token.scopes.map(Scope::name))
val value = generateTokenValue()
val model = RefreshTokenEntity().apply {
expiresAt = getExpirationDate()
this.value = getTokenHash(value)
value = generateTokenValue()
this.user = user
this.device = device
this.scopes = scopes.toSet()
}
return refreshTokenRepository.save(model)
.toDto(value)
}
@Transactional
override fun refresh(value: String): RefreshToken {
val valueHash = getTokenHash(value)
val oldToken = refreshTokenRepository.findByValueOrThrow(valueHash)
.validate()
.apply {
usedAt = ZonedDateTime.now()
}
refreshTokenRepository.save(oldToken)
val newValue = generateTokenValue()
val model = RefreshTokenEntity().apply {
this.value = getTokenHash(newValue)
expiresAt = getExpirationDate()
user = oldToken.user
device = oldToken.device
scopes = oldToken.scopes.toSet()
}
return refreshTokenRepository.save(model)
.toDto(newValue)
.toDto()
}
private fun getExpirationDate(): ZonedDateTime {
@ -94,20 +62,13 @@ class RefreshTokenCoreServiceImpl(
}
}
private fun getTokenHash(value: String): String {
return value.calculateHash(HashUtils.HashAlgorithm.SHA256)
.toHex()
}
companion object {
fun RefreshTokenEntity.toDto(rawValue: String): RefreshToken {
fun RefreshTokenEntity.toDto(): RefreshToken {
val device = device?.toDto()
return RefreshToken(
value = rawValue,
value = value,
expiresAt = expiresAt,
usedAt = usedAt,
user = user.toDto(device)
)
}

View File

@ -3,10 +3,10 @@ databaseChangeLog:
id: 202105201851__create_table__refresh_tokens
author: touchin
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName: refresh_tokens
onFail: MARK_RAN
not:
tableExists:
tableName: refresh_tokens
changes:
- createTable:
tableName: refresh_tokens

View File

@ -3,10 +3,10 @@ databaseChangeLog:
id: 202105201901__create_table__refresh_token_scopes
author: touchin
preConditions:
- onFail: MARK_RAN
not:
tableExists:
tableName: refresh_token_scopes
onFail: MARK_RAN
not:
tableExists:
tableName: refresh_token_scopes
changes:
- createTable:
tableName: refresh_token_scopes

View File

@ -1,11 +1,9 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.jetbrains.kotlin.cli.common.toBooleanLenient
plugins {
kotlin("jvm")
id("org.springframework.boot") apply false
id ("org.springframework.boot") apply false
// IntelliJ
idea
@ -17,9 +15,6 @@ plugins {
// A Gradle plugin that provides Maven-like dependency management and exclusions
// https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/
id("io.spring.dependency-management")
id("io.gitlab.arturbosch.detekt")
id("org.cqfn.diktat.diktat-gradle-plugin")
}
allprojects {
@ -35,30 +30,6 @@ allprojects {
apply(plugin = "idea")
}
diktat {
inputs {
include(
"**/src/**/*.kt",
"*.kts",
"**/*.kts",
"**/src/**/*.kts",
)
exclude(
"**/build/**",
"build/**",
)
}
val tasksFileReportEnabled = project.properties["TASKS_FILE_REPORT_ENABLED"]
?.let { it.toString().toBooleanLenient() }
?: true
reporter = if (tasksFileReportEnabled) "html" else "plain"
output = if (tasksFileReportEnabled) "${project.buildDir}/reports/diktat-report.html" else String()
diktatConfigFile = file("$rootDir/diktat-analysis.yml")
ignoreFailures = true
debug = false
}
subprojects {
println("Enabling Kotlin JVM plugin in project ${project.name}...")
apply(plugin = "org.jetbrains.kotlin.jvm")
@ -69,26 +40,6 @@ subprojects {
println("Enabling Spring Boot Dependency Management in project ${project.name}...")
apply(plugin = "io.spring.dependency-management")
println("Enabling Detekt support in project ${project.name}...")
apply(plugin = "io.gitlab.arturbosch.detekt")
detekt {
config = files("$rootDir/detekt-config.yml")
source = files(
DetektExtension.Companion.DEFAULT_SRC_DIR_JAVA,
DetektExtension.Companion.DEFAULT_SRC_DIR_KOTLIN,
)
reports {
txt.enabled = false
xml.enabled = false
html {
enabled = true
destination = file("${project.buildDir}/reports/kotlin-detekt-${project.name}.html")
}
}
}
configure<DependencyManagementExtension> {
imports {
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
@ -99,17 +50,13 @@ subprojects {
dependency("ch.qos.logback.contrib:logback-json-classic:0.1.5")
dependency("ch.qos.logback.contrib:logback-jackson:0.1.5")
dependency("org.testcontainers:testcontainers:1.18.3")
dependency("org.testcontainers:postgresql:1.18.3")
dependency("org.testcontainers:junit-jupiter:1.18.3")
dependency("org.testcontainers:testcontainers:1.15.1")
dependency("org.testcontainers:postgresql:1.15.1")
dependency("org.testcontainers:junit-jupiter:1.15.1")
dependency("org.junit.jupiter:junit-jupiter-api:5.4.2")
dependency("org.junit.jupiter:junit-jupiter-params:5.4.2")
dependency("org.junit.jupiter:junit-jupiter-engine:5.4.2")
dependency("com.tngtech.archunit:archunit:1.0.1")
dependency("org.liquibase:liquibase-core:4.4.0")
dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
dependency("org.mockito:mockito-inline:3.11.0")
@ -121,19 +68,13 @@ subprojects {
dependency("org.locationtech.spatial4j:spatial4j:0.8")
dependency("com.auth0:java-jwt:3.10.3")
dependency("software.amazon.awssdk:s3:2.10.11")
dependency("com.google.firebase:firebase-admin:9.0.0")
dependency("com.github.ua-parser:uap-java:1.5.3")
}
}
dependencies {
// use for @ConstructorBinding
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation ("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}

View File

@ -1,15 +0,0 @@
plugins {
id("kotlin")
id("kotlin-spring")
}
dependencies {
implementation(project(":common"))
implementation(project(":common-spring-web"))
implementation(project(":logger-spring"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-aop")
}

View File

@ -1,9 +0,0 @@
package ru.touchin.captcha.annotations
import ru.touchin.captcha.dto.enums.CaptchaScore
@Target(AnnotationTarget.FUNCTION)
annotation class Captcha(
val action: String,
val minScore: CaptchaScore = CaptchaScore.AVERAGE
)

View File

@ -1,39 +0,0 @@
package ru.touchin.captcha.aspects
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import ru.touchin.captcha.annotations.Captcha
import ru.touchin.captcha.exceptions.CaptchaResponseMissingException
import ru.touchin.captcha.services.CaptchaService
@Aspect
@Component
@ConditionalOnProperty(prefix = "captcha", name = ["enabled"], havingValue = "true", matchIfMissing = true)
class CaptchaSiteVerifyAspect(private val captchaService: CaptchaService) {
@Throws(Throwable::class)
@Before("@annotation(captcha)")
fun captchaSiteVerify(captcha: Captcha) {
val currentRequestAttributes = RequestContextHolder.currentRequestAttributes()
as? ServletRequestAttributes
?: throw IllegalStateException("unable to get current request attributes")
val captchaResponse = currentRequestAttributes.request.getHeader(CAPTCHA_RESPONSE_HEADER_NAME)
?: throw CaptchaResponseMissingException()
captchaService.verify(captchaResponse)
.validateAction(captcha.action)
.validateScore(captcha.minScore.value)
}
companion object {
private const val CAPTCHA_RESPONSE_HEADER_NAME = "X-Captcha-Response"
}
}

View File

@ -1,8 +0,0 @@
package ru.touchin.captcha.configurations
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.context.annotation.ComponentScan
@ComponentScan("ru.touchin.captcha")
@ConfigurationPropertiesScan("ru.touchin.captcha")
class CaptchaConfiguration

View File

@ -1,24 +0,0 @@
package ru.touchin.captcha.dto
import ru.touchin.captcha.exceptions.CaptchaActionMismatchException
import ru.touchin.captcha.exceptions.CaptchaScoreBelowMinimumException
data class CaptchaVerificationResult(val score: Double, val action: String) {
fun validateScore(minScore: Double): CaptchaVerificationResult {
if (score < minScore) {
throw CaptchaScoreBelowMinimumException(score)
}
return this
}
fun validateAction(actionToValidate: String): CaptchaVerificationResult {
if (action != actionToValidate) {
throw CaptchaActionMismatchException(actionToValidate)
}
return this
}
}

View File

@ -1,9 +0,0 @@
package ru.touchin.captcha.dto.enums
enum class CaptchaScore(val value: Double) {
WEAK(0.2),
AVERAGE(0.5),
STRONG(0.8)
}

View File

@ -1,12 +0,0 @@
package ru.touchin.captcha.dto.response
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.ZonedDateTime
data class CaptchaSiteVerifyResponse(
val score: Double,
val action: String,
val success: Boolean,
@JsonProperty("challenge_ts")
val challengeTs: ZonedDateTime
)

View File

@ -1,7 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaActionMismatchException(
action: String
) : CommonException("invalid captcha action $action")

View File

@ -1,5 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaResponseMissingException : CommonException("missing captcha response header")

View File

@ -1,7 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaScoreBelowMinimumException(
score: Double,
) : CommonException("captcha score below minimum $score")

View File

@ -1,7 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaSiteVerifyFailureException(
description: String?
) : CommonException(description)

View File

@ -1,7 +0,0 @@
package ru.touchin.captcha.exceptions
import ru.touchin.common.exceptions.CommonException
class CaptchaUnknownActionException(
action: String
) : CommonException("unknown captcha action $action")

View File

@ -1,13 +0,0 @@
package ru.touchin.captcha.properties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import java.net.URI
@ConstructorBinding
@ConfigurationProperties(prefix = "captcha")
data class CaptchaProperties(
val uri: URI,
val secret: String,
val actions: List<String>
)

View File

@ -1,9 +0,0 @@
package ru.touchin.captcha.services
import ru.touchin.captcha.dto.CaptchaVerificationResult
interface CaptchaService {
fun verify(response: String): CaptchaVerificationResult
}

View File

@ -1,33 +0,0 @@
package ru.touchin.captcha.services.impl
import org.springframework.stereotype.Service
import ru.touchin.captcha.dto.CaptchaVerificationResult
import ru.touchin.captcha.exceptions.CaptchaUnknownActionException
import ru.touchin.captcha.properties.CaptchaProperties
import ru.touchin.captcha.services.CaptchaService
import ru.touchin.captcha.webclients.CaptchaWebClient
import ru.touchin.logger.spring.annotations.AutoLogging
import ru.touchin.logger.spring.annotations.LogValue
@Service
class CaptchaServiceImpl(
private val captchaWebClient: CaptchaWebClient,
private val captchaProperties: CaptchaProperties,
) : CaptchaService {
@LogValue
@AutoLogging(tags = ["CAPTCHA", "CAPTCHA_VERIFICATION"])
override fun verify(response: String): CaptchaVerificationResult {
val siteVerifyResponse = captchaWebClient.siteVerify(response)
if (siteVerifyResponse.action !in captchaProperties.actions) {
throw CaptchaUnknownActionException(siteVerifyResponse.action)
}
return CaptchaVerificationResult(
score = siteVerifyResponse.score,
action = siteVerifyResponse.action,
)
}
}

View File

@ -1,55 +0,0 @@
package ru.touchin.captcha.webclients
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import ru.touchin.captcha.properties.CaptchaProperties
import ru.touchin.captcha.dto.response.CaptchaSiteVerifyResponse
import ru.touchin.captcha.exceptions.CaptchaSiteVerifyFailureException
import ru.touchin.common.spring.web.webclient.BaseLogWebClient
import ru.touchin.common.spring.web.webclient.dto.RequestLogData
import ru.touchin.common.spring.web.webclient.logger.WebClientLogger
private const val SITE_VERIFY_PATH = "/recaptcha/api/siteverify"
@Component
class CaptchaWebClient(
webClientLogger: WebClientLogger,
webClientBuilder: WebClient.Builder,
private val captchaProperties: CaptchaProperties,
) : BaseLogWebClient(webClientLogger, webClientBuilder) {
override fun getWebClient(): WebClient {
return getWebClientBuilder(captchaProperties.uri.toString()).build()
}
fun siteVerify(response: String): CaptchaSiteVerifyResponse {
return getWebClient().post()
.uri {
it.path(SITE_VERIFY_PATH)
it.queryParam("response", response)
it.queryParam("secret", captchaProperties.secret)
it.build()
}
.accept(MediaType.APPLICATION_JSON)
.exchange(
clazz = CaptchaSiteVerifyResponse::class.java,
requestLogData = RequestLogData(
uri = SITE_VERIFY_PATH,
logTags = listOf("CAPTCHA_SITEVERIFY"),
method = HttpMethod.POST,
)
)
.block()!!
.also(this::validateSuccess)
}
private fun validateSuccess(captchaSiteVerifyResponse: CaptchaSiteVerifyResponse) {
if (!captchaSiteVerifyResponse.success) {
throw CaptchaSiteVerifyFailureException("Captcha siteverify request did not succeed")
}
}
}

View File

@ -1,8 +0,0 @@
plugins {
id("kotlin")
}
dependencies {
implementation(project(":common"))
implementation("com.tngtech.archunit:archunit")
}

View File

@ -1,53 +0,0 @@
package ru.touchin.codestyle.archunit.rules
import com.tngtech.archunit.lang.ArchRule
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods
import ru.touchin.common.exceptions.CommonException
/**
* Set of rules that defines constraint for classes naming.
*/
@Suppress("unused")
object ClassNamingArchRules {
val CLASSES_WHICH_HAVE_SCHEDULED_METHODS_MUST_HAVE_SUFFIX: ArchRule = methods()
.that()
.areAnnotatedWith("org.springframework.scheduling.annotation.Scheduled")
.should().beDeclaredInClassesThat().haveSimpleNameEndingWith("Job")
.allowEmptyShould(true)
.because("Classes that use 'Scheduled' annotation must have name with 'Job' suffix")
val ANNOTATED_SERVICE_CLASSES_MUST_HAVE_SUFFIX: ArchRule = methods()
.that().areAnnotatedWith("org.springframework.stereotype.Service")
.should().beDeclaredInClassesThat().haveSimpleNameContaining("Service")
.allowEmptyShould(true)
.because("Classes that use 'Service' annotation are assignable to class with `Service` suffix")
val ANNOTATED_ENTITY_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().areAnnotatedWith("javax.persistence.Entity")
.should().haveSimpleNameEndingWith("Entity")
.allowEmptyShould(true)
.because("Hibernate 'Entity' classes must have name with 'Entity' suffix")
val EXCEPTION_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().areAssignableTo(Exception::class.java)
.or().areAssignableTo(CommonException::class.java)
.should().haveSimpleNameEndingWith("Exception")
.allowEmptyShould(true)
.because("'Exception' classes must have name with 'Exception' suffix")
val JPAREPOSITORY_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().areAssignableTo("org.springframework.data.jpa.repository.JpaRepository")
.should().haveSimpleNameEndingWith("Repository")
.allowEmptyShould(true)
.because("Classes that use 'JpaRepository' must have name with 'Repository' suffix")
val WEBCLIENT_CLASSES_MUST_HAVE_SUFFIX: ArchRule = classes()
.that().haveFullyQualifiedName("org.springframework.web.reactive.valction.client.WebClient")
.should().onlyBeAccessed().byClassesThat().haveSimpleNameEndingWith("WebClient")
.allowEmptyShould(true)
.because("Classes that use Spring 'WebClient' must have name with 'WebClient' suffix")
}

View File

@ -1,56 +0,0 @@
package ru.touchin.codestyle.archunit.rules
import com.tngtech.archunit.lang.ArchRule
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes
/**
* Set of rules that defines constraint for classes placement.
*/
@Suppress("unused")
object ClassPackagingArchRules {
val IMPL_CLASSES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Impl")
.should().resideInAPackage("..impl..")
.allowEmptyShould(true)
.because("Implementations of interfaces must reside in package 'impl'")
val ENTITIES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Entity")
.should().resideInAPackage("..models..")
.allowEmptyShould(true)
.because("'Entity' must reside in package 'models'")
val REPOSITORIES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Repository")
.should().resideInAPackage("..repositories..")
.allowEmptyShould(true)
.because("'Repository' must reside in package 'repositories'")
val CORE_SERVICES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("CoreService")
.should().resideInAPackage("..services..")
.allowEmptyShould(true)
.because("'CoreService' must reside in package 'services'")
val CONVERTERS_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Converter")
.should().resideInAPackage("..converters..")
.allowEmptyShould(true)
.because("'Converter' must reside in package 'converters'")
val EXCEPTIONS_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().haveSimpleNameEndingWith("Exception")
.should().resideInAPackage("..exceptions..")
.allowEmptyShould(true)
.because("'Exception' must reside in package 'exceptions'")
val ENUM_CLASSES_ARE_IN_CORRESPONDING_PACKAGE: ArchRule = classes()
.that().areEnums()
.should().resideInAPackage("..enums..")
.allowEmptyShould(true)
.because("'Enum' must reside in package 'enums'")
}

Some files were not shown because too many files have changed in this diff Show More