Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Password reuse prevention #162

Merged
merged 11 commits into from
Oct 4, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ data class Account(
}

@JvmInline
value class Email(val content: String) {}
value class Email(val content: String)

data class UserLocale(val locale: Locale) {
fun formattedLocale(): String = this.locale.toLanguageTag()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.vauthenticator.server.account.Account
import com.vauthenticator.server.account.repository.DynamoAccountConverter.fromDynamoToDomain
import com.vauthenticator.server.account.repository.DynamoAccountQueryFactory.findAccountQueryForUserName
import com.vauthenticator.server.account.repository.DynamoAccountQueryFactory.storeAccountQueryFor
import com.vauthenticator.server.extentions.filterEmptyAccountMetadata
import com.vauthenticator.server.extentions.filterEmptyMetadata
import com.vauthenticator.server.role.RoleRepository
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException
Expand All @@ -19,7 +19,7 @@ class DynamoDbAccountRepository(

override fun accountFor(username: String): Optional<Account> =
ofNullable(findAccountFor(username))
.flatMap { it.filterEmptyAccountMetadata() }
.flatMap { it.filterEmptyMetadata() }
.map(::fromDynamoToDomain)
.map(::stealRoleCleanUpFor)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,49 @@
package com.vauthenticator.server.account.signup

import com.vauthenticator.server.account.Account
import com.vauthenticator.server.account.Email
import com.vauthenticator.server.account.mailverification.SendVerifyMailChallenge
import com.vauthenticator.server.account.repository.AccountRepository
import com.vauthenticator.server.account.welcome.SayWelcome
import com.vauthenticator.server.events.SignUpEvent
import com.vauthenticator.server.events.VAuthenticatorEventsDispatcher
import com.vauthenticator.server.oauth2.clientapp.ClientAppId
import com.vauthenticator.server.oauth2.clientapp.ClientApplicationRepository
import com.vauthenticator.server.password.Password
import com.vauthenticator.server.password.PasswordPolicy
import com.vauthenticator.server.password.VAuthenticatorPasswordEncoder
import java.time.Instant

open class SignUpUse(
private val passwordPolicy: PasswordPolicy,
private val clientAccountRepository: ClientApplicationRepository,
private val accountRepository: AccountRepository,
private val sendVerifyMailChallenge: SendVerifyMailChallenge,
private val vAuthenticatorPasswordEncoder: VAuthenticatorPasswordEncoder,
private val sayWelcome: SayWelcome
private val sayWelcome: SayWelcome,
private val eventsDispatcher: VAuthenticatorEventsDispatcher
) {
open fun execute(clientAppId: ClientAppId, account: Account) {
passwordPolicy.accept(account.password)
passwordPolicy.accept(account.email, account.password)
clientAccountRepository.findOne(clientAppId)
.map {
val encodedPassword = vAuthenticatorPasswordEncoder.encode(account.password)
val registeredAccount = account.copy(
authorities = it.authorities.content.map { it.content }.toSet(),
password = vAuthenticatorPasswordEncoder.encode(account.password)
password = encodedPassword
)
accountRepository.create(registeredAccount)
sayWelcome.welcome(registeredAccount.email)
sendVerifyMailChallenge.sendVerifyMail(account.email)

eventsDispatcher.dispatch(
SignUpEvent(
Email(account.email),
clientAppId,
Instant.now(),
Password(encodedPassword)
)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.vauthenticator.server.account.tiket

import com.vauthenticator.server.extentions.asDynamoAttribute
import com.vauthenticator.server.extentions.filterEmptyAccountMetadata
import com.vauthenticator.server.extentions.filterEmptyMetadata
import com.vauthenticator.server.extentions.valueAsLongFor
import com.vauthenticator.server.extentions.valueAsStringFor
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
Expand Down Expand Up @@ -46,7 +46,7 @@ class DynamoDbTicketRepository(private val dynamoDbClient: DynamoDbClient,
.build()
).item()
)
.flatMap { it.filterEmptyAccountMetadata() }
.flatMap { it.filterEmptyMetadata() }
.map {
Ticket(
VerificationTicket(it.valueAsStringFor("ticket")),
Expand Down
14 changes: 12 additions & 2 deletions src/main/kotlin/com/vauthenticator/server/config/EventsConfig.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.vauthenticator.server.config

import com.vauthenticator.server.events.*
import com.vauthenticator.server.password.UpdatePasswordHistoryUponSignUpEventConsumer
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand All @@ -17,9 +18,18 @@ class EventsConfig(private val eventConsumerConfig: EventConsumerConfig) {
VAuthenticatorEventsDispatcher(publisher)

@Bean
fun eventsCollector() =
fun eventsCollector(
updatePasswordHistoryUponSignUpEventConsumer: UpdatePasswordHistoryUponSignUpEventConsumer,
loggerEventConsumer: EventConsumer
) =
SpringEventsCollector(
listOf(LoggerEventConsumer(eventConsumerConfig))
listOf(
loggerEventConsumer,
updatePasswordHistoryUponSignUpEventConsumer
)
)

@Bean
fun loggerEventConsumer() = LoggerEventConsumer(eventConsumerConfig)

}
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
package com.vauthenticator.server.config

import com.vauthenticator.server.password.CompositePasswordPolicy
import com.vauthenticator.server.password.MinimumCharacterPasswordPolicy
import com.vauthenticator.server.password.PasswordPolicyConfigProp
import com.vauthenticator.server.password.SpecialCharacterPasswordPolicy
import com.vauthenticator.server.password.*
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import java.time.Clock

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PasswordPolicyConfigProp::class)
class PasswordPolicyConfig {

@Bean
fun passwordPolicy(passwordPolicyConfigProp: PasswordPolicyConfigProp) =
CompositePasswordPolicy(
setOf(
MinimumCharacterPasswordPolicy(passwordPolicyConfigProp.minSize),
SpecialCharacterPasswordPolicy(passwordPolicyConfigProp.minSpecialSymbol)
)
fun passwordPolicy(
reusePreventionPasswordPolicy: PasswordPolicy,
passwordPolicyConfigProp: PasswordPolicyConfigProp
): CompositePasswordPolicy {
val passwordPolicies = mutableSetOf(
MinimumCharacterPasswordPolicy(passwordPolicyConfigProp.minSize),
SpecialCharacterPasswordPolicy(passwordPolicyConfigProp.minSpecialSymbol)
)
if (passwordPolicyConfigProp.enablePasswordReusePrevention) {
passwordPolicies.add(reusePreventionPasswordPolicy)
}

return CompositePasswordPolicy(
passwordPolicies
)
}

@Bean
fun reusePreventionPasswordPolicy(
passwordEncoder: VAuthenticatorPasswordEncoder,
passwordHistoryRepository: PasswordHistoryRepository
) = ReusePreventionPasswordPolicy(
passwordEncoder,
passwordHistoryRepository
)


@Bean
fun passwordHistoryRepository(
@Value("\${vauthenticator.dynamo-db.password-history.table-name}") dynamoPasswordHistoryTableName: String,
dynamoDbClient: DynamoDbClient
): DynamoPasswordHistoryRepository = DynamoPasswordHistoryRepository(
Clock.systemUTC(),
dynamoPasswordHistoryTableName,
dynamoDbClient
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.vauthenticator.server.account.mailverification.SendVerifyMailChalleng
import com.vauthenticator.server.account.repository.AccountRepository
import com.vauthenticator.server.account.signup.SignUpUse
import com.vauthenticator.server.account.welcome.SayWelcome
import com.vauthenticator.server.events.VAuthenticatorEventsDispatcher
import com.vauthenticator.server.mail.MailSenderService
import com.vauthenticator.server.oauth2.clientapp.ClientApplicationRepository
import com.vauthenticator.server.password.PasswordPolicy
Expand All @@ -23,14 +24,16 @@ class SingUpConfig {
sendVerifyMailChallenge: SendVerifyMailChallenge,
vAuthenticatorPasswordEncoder: VAuthenticatorPasswordEncoder,
sayWelcome: SayWelcome,
vAuthenticatorEventsDispatcher : VAuthenticatorEventsDispatcher
): SignUpUse =
SignUpUse(
passwordPolicy,
clientAccountRepository,
accountRepository,
sendVerifyMailChallenge,
vAuthenticatorPasswordEncoder,
sayWelcome
sayWelcome,
vAuthenticatorEventsDispatcher
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,5 @@ class WebSecurityConfig(
fun failureHandler(): AuthenticationFailureHandler {
return SimpleUrlAuthenticationFailureHandler("/login?error")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class LoggerEventConsumer(private val eventConsumerConfig: EventConsumerConfig)
private val logger = LoggerFactory.getLogger(LoggerEventConsumer::class.java)

override fun accept(event: VAuthenticatorEvent) {
if (handleable()) {
if (handleable(event)) {
val logLine = """
The user ${event.userName.content}
with the client id ${event.clientAppId.content}
Expand All @@ -21,5 +21,5 @@ class LoggerEventConsumer(private val eventConsumerConfig: EventConsumerConfig)
}
}

override fun handleable() = eventConsumerConfig.enable[LOGGER_EVENT_CONSUMER] ?: false
}
override fun handleable(event: VAuthenticatorEvent) = eventConsumerConfig.enable[LOGGER_EVENT_CONSUMER] ?: false
}
19 changes: 18 additions & 1 deletion src/main/kotlin/com/vauthenticator/server/events/Events.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.vauthenticator.server.events

import com.vauthenticator.server.account.Email
import com.vauthenticator.server.oauth2.clientapp.ClientAppId
import com.vauthenticator.server.password.Password
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.security.authentication.event.AbstractAuthenticationEvent
import java.time.Instant
Expand All @@ -17,7 +18,7 @@ interface EventsCollector {

interface EventConsumer {
fun accept(event: VAuthenticatorEvent)
fun handleable(): Boolean
fun handleable(event: VAuthenticatorEvent): Boolean

}

Expand Down Expand Up @@ -48,3 +49,19 @@ class VAuthenticatorAuthEvent(
return javaClass.hashCode()
}
}

class SignUpEvent(
userName: Email,
clientAppId: ClientAppId,
timeStamp: Instant,
password : Password) : VAuthenticatorEvent(userName, clientAppId, timeStamp, password) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}

override fun hashCode(): Int {
return javaClass.hashCode()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fun MutableMap<String, AttributeValue>.valueAsStringSetFor(key: String): Set<Str
fun MutableMap<String, AttributeValue>.valueAsLongFor(key: String): Long =
this[key]?.n()!!.toLong()

fun MutableMap<String, AttributeValue>.filterEmptyAccountMetadata() =
fun MutableMap<String, AttributeValue>.filterEmptyMetadata() =
if (this.isEmpty()) {
Optional.empty()
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.vauthenticator.server.oauth2.clientapp

import com.vauthenticator.server.extentions.asDynamoAttribute
import com.vauthenticator.server.extentions.filterEmptyAccountMetadata
import com.vauthenticator.server.extentions.filterEmptyMetadata
import com.vauthenticator.server.oauth2.clientapp.DynamoClientApplicationConverter.fromDomainToDynamo
import com.vauthenticator.server.oauth2.clientapp.DynamoClientApplicationConverter.fromDynamoToDomain
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
Expand Down Expand Up @@ -31,7 +31,7 @@ class DynamoDbClientApplicationRepository(
)
.item()
}
.flatMap { it.filterEmptyAccountMetadata() }
.flatMap { it.filterEmptyMetadata() }
.map { fromDynamoToDomain(it) }
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.vauthenticator.server.password

@JvmInline
value class Password(val content: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.vauthenticator.server.password

import com.vauthenticator.server.extentions.asDynamoAttribute
import com.vauthenticator.server.extentions.valueAsStringFor
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest
import software.amazon.awssdk.services.dynamodb.model.QueryRequest
import java.time.Clock
import java.time.LocalDateTime
import java.time.ZoneOffset

interface PasswordHistoryRepository {

fun store(userName : String, password: Password)
fun load(userName : String, ): List<Password>

}

class DynamoPasswordHistoryRepository(
private val clock: Clock,
private val dynamoPasswordHistoryTableName: String,
private val dynamoDbClient: DynamoDbClient
) : PasswordHistoryRepository {
override fun store(userName : String, password: Password) {
dynamoDbClient.putItem(
PutItemRequest.builder()
.tableName(dynamoPasswordHistoryTableName)
.item(
mapOf(
"user_name" to userName.asDynamoAttribute(),
"created_at" to createdAt().asDynamoAttribute(),
"password" to password.content.asDynamoAttribute()
)
)
.build()
)
}

private fun createdAt() =
LocalDateTime.now(clock)
.toInstant(ZoneOffset.UTC)
.toEpochMilli()

override fun load(userName : String, ): List<Password> {
return dynamoDbClient.query(
QueryRequest.builder()
.tableName(dynamoPasswordHistoryTableName)
.scanIndexForward(false)
.keyConditionExpression("user_name=:email")
.expressionAttributeValues(mapOf(":email" to userName.asDynamoAttribute())).build()
).items()
.map { Password(it.valueAsStringFor("password")) }
}

}
Loading