Skip to content

Commit

Permalink
UCP WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Brutus5000 authored and Sheikah45 committed Aug 11, 2023
1 parent a16f9ae commit ef32c82
Show file tree
Hide file tree
Showing 13 changed files with 375 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ data class User(
val id: Int? = null,
@Column(name = "login")
val username: String,
val password: String,
@Column(name = "password")
val passwordHash: String,
val email: String,
val ip: String?,
) : PanacheEntityBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.faforever.userservice.backend.hydra

import com.faforever.userservice.backend.domain.IpAddress
import com.faforever.userservice.backend.domain.UserRepository
import com.faforever.userservice.backend.login.LoginResult
import com.faforever.userservice.backend.login.LoginService
import com.faforever.userservice.backend.security.LoginResult
import com.faforever.userservice.backend.security.LoginService
import com.faforever.userservice.backend.security.OAuthScope
import jakarta.enterprise.context.ApplicationScoped
import jakarta.enterprise.inject.Produces
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ class MetricHelper(meterRegistry: MeterRegistry) {
"steamLinkFailed",
)

// Username Change Counters
// User Change Counters
val userNameChangeCounter: Counter = meterRegistry.counter("user.name.change.count")
val userPasswordChangeCounter: Counter = meterRegistry.counter("user.password.change.count")

// Password Reset Counters
val userPasswordResetRequestCounter: Counter = meterRegistry.counter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class RegistrationService(

val user = User(
username = username,
password = encodedPassword,
passwordHash = encodedPassword,
email = email,
ip = ipAddress.value,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.faforever.userservice.backend.security

import com.faforever.userservice.backend.domain.User
import io.quarkus.security.identity.SecurityIdentity
import jakarta.inject.Singleton
import org.slf4j.Logger
import org.slf4j.LoggerFactory

@Singleton
class CurrentUserService(
private val securityIdentity: SecurityIdentity,
) {
companion object {
private val log: Logger = LoggerFactory.getLogger(CurrentUserService::class.java)
}

// TODO: Implement Vaadin auth mechanism and reload from database or something
fun requireUser(): User = User(
5,
"zep",
"thisshouldnotbehere",
email = "iam@faforever.com",
null,
)

fun invalidate() {
log.debug("Invalidating current user")
// TODO: Invalidate cache
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.faforever.userservice.backend.login
package com.faforever.userservice.backend.security

import com.faforever.userservice.backend.domain.AccountLinkRepository
import com.faforever.userservice.backend.domain.Ban
Expand All @@ -9,30 +9,13 @@ import com.faforever.userservice.backend.domain.LoginLog
import com.faforever.userservice.backend.domain.LoginLogRepository
import com.faforever.userservice.backend.domain.User
import com.faforever.userservice.backend.domain.UserRepository
import com.faforever.userservice.backend.security.PasswordEncoder
import io.smallrye.config.ConfigMapping
import com.faforever.userservice.config.FafProperties
import jakarta.enterprise.context.ApplicationScoped
import jakarta.validation.constraints.NotNull
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.time.LocalDateTime
import java.time.OffsetDateTime

@ConfigMapping(prefix = "security")
interface SecurityProperties {
@NotNull
fun failedLoginAccountThreshold(): Int

@NotNull
fun failedLoginAttemptThreshold(): Int

@NotNull
fun failedLoginThrottlingMinutes(): Long

@NotNull
fun failedLoginDaysToCheck(): Long
}

sealed interface LoginResult {
sealed interface RecoverableLoginFailure : LoginResult
object ThrottlingActive : RecoverableLoginFailure
Expand Down Expand Up @@ -60,7 +43,7 @@ interface LoginService {

@ApplicationScoped
class LoginServiceImpl(
private val securityProperties: SecurityProperties,
private val fafProperties: FafProperties,
private val userRepository: UserRepository,
private val loginLogRepository: LoginLogRepository,
private val accountLinkRepository: AccountLinkRepository,
Expand All @@ -80,7 +63,7 @@ class LoginServiceImpl(
}

val user = userRepository.findByUsernameOrEmail(usernameOrEmail)
if (user == null || !passwordEncoder.matches(password, user.password)) {
if (user == null || !passwordEncoder.matches(password, user.passwordHash)) {
logFailedLogin(usernameOrEmail, ip)
return LoginResult.RecoverableLoginOrCredentialsMismatch
}
Expand Down Expand Up @@ -119,20 +102,20 @@ class LoginServiceImpl(
private fun throttlingRequired(ip: IpAddress): Boolean {
val failedAttemptsSummary = loginLogRepository.findFailedAttemptsByIpAfterDate(
ip.value,
LocalDateTime.now().minusDays(securityProperties.failedLoginDaysToCheck()),
LocalDateTime.now().minusDays(fafProperties.security().failedLoginDaysToCheck()),
) ?: FailedAttemptsSummary(0, 0, null, null)

val accountsAffected = failedAttemptsSummary.accountsAffected
val totalFailedAttempts = failedAttemptsSummary.totalAttempts

LOG.debug("Failed login attempts for IP address '{}': {}", ip, failedAttemptsSummary)

return if (accountsAffected > securityProperties.failedLoginAccountThreshold() ||
totalFailedAttempts > securityProperties.failedLoginAttemptThreshold()
return if (accountsAffected > fafProperties.security().failedLoginAccountThreshold() ||
totalFailedAttempts > fafProperties.security().failedLoginAttemptThreshold()
) {
val lastAttempt = failedAttemptsSummary.lastAttemptAt!!
if (LocalDateTime.now()
.minusMinutes(securityProperties.failedLoginThrottlingMinutes())
.minusMinutes(fafProperties.security().failedLoginThrottlingMinutes())
.isBefore(lastAttempt)
) {
LOG.debug("IP '$ip' is trying again to early -> throttle it")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.faforever.userservice.backend.security

import com.faforever.userservice.backend.domain.User
import com.faforever.userservice.backend.domain.UserRepository
import com.faforever.userservice.backend.metrics.MetricHelper
import com.faforever.userservice.config.FafProperties
import jakarta.enterprise.context.ApplicationScoped
import org.slf4j.Logger
import org.slf4j.LoggerFactory

@ApplicationScoped
class PasswordService(
private val fafProperties: FafProperties,
private val metricHelper: MetricHelper,
private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder,
) {

companion object {
private val log: Logger = LoggerFactory.getLogger(FafTokenService::class.java)
}

enum class ValidatePasswordResult {
VALID,
TOO_SHORT,
}

fun validatePassword(password: String) =
if (password.length < fafProperties.security().minimumPasswordLength()) {
ValidatePasswordResult.TOO_SHORT
} else {
ValidatePasswordResult.VALID
}

enum class ChangePasswordResult {
OK,
PASSWORD_MISMATCH,
}

fun changePassword(user: User, oldPassword: String, newPassword: String): ChangePasswordResult {
if (!passwordEncoder.matches(oldPassword, user.passwordHash)) {
return ChangePasswordResult.PASSWORD_MISMATCH
}

userRepository.persist(
user.copy(
passwordHash = passwordEncoder.encode(newPassword),
),
)
metricHelper.userPasswordChangeCounter.increment()

log.info("Password of user ${user.id} was changed")

return ChangePasswordResult.OK
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/com/faforever/userservice/config/FafProperties.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.faforever.userservice.config
import io.smallrye.config.ConfigMapping
import io.smallrye.config.WithDefault
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import java.util.*

@ConfigMapping(prefix = "faf")
Expand All @@ -16,12 +17,31 @@ interface FafProperties {
@NotBlank
fun realIpHeader(): String

fun security(): Security

fun account(): Account

fun jwt(): Jwt

fun recaptcha(): Recaptcha

interface Security {
@WithDefault("6")
fun minimumPasswordLength(): Int

@NotNull
fun failedLoginAccountThreshold(): Int

@NotNull
fun failedLoginAttemptThreshold(): Int

@NotNull
fun failedLoginThrottlingMinutes(): Long

@NotNull
fun failedLoginDaysToCheck(): Long
}

interface Jwt {
fun secret(): String
}
Expand Down
80 changes: 80 additions & 0 deletions src/main/kotlin/com/faforever/userservice/ui/layout/UcpLayout.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.faforever.userservice.ui.layout

import com.faforever.userservice.backend.i18n.I18n
import com.faforever.userservice.ui.view.ucp.AccountDataView
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.applayout.AppLayout
import com.vaadin.flow.component.applayout.DrawerToggle
import com.vaadin.flow.component.html.Anchor
import com.vaadin.flow.component.html.H1
import com.vaadin.flow.component.html.Span
import com.vaadin.flow.component.icon.Icon
import com.vaadin.flow.component.icon.VaadinIcon
import com.vaadin.flow.component.tabs.Tab
import com.vaadin.flow.component.tabs.Tabs
import com.vaadin.flow.router.RouterLayout
import com.vaadin.flow.router.RouterLink
import kotlin.reflect.KClass

abstract class UcpLayout(
private val i18n: I18n,
) : AppLayout(), RouterLayout {

init {
val toggle = DrawerToggle()

val title = H1("FAF User Control Panel")
title.style.set("font-size", "var(--lumo-font-size-l)")["margin"] = "0"

// createLinks().forEach(::addToDrawer)
addToDrawer(getTabs())
addToNavbar(toggle, title)
}

private fun buildAnchor(href: String, i18nKey: String, icon: VaadinIcon) =
Anchor().apply {
setHref(href)
add(icon.create())
// add(i18n.getTranslation(i18nKey))
add(i18nKey)
}

private fun getTabs(): Tabs {
val tabs = Tabs()
tabs.add(
createTab(VaadinIcon.USER_CARD, "Account Data", AccountDataView::class),
createTab(VaadinIcon.LINK, "Account Links", AccountDataView::class, false),
createTab(VaadinIcon.DESKTOP, "Active Devices", AccountDataView::class, false),
createTab(VaadinIcon.USER_HEART, "Friends & Foes", AccountDataView::class, false),
createTab(VaadinIcon.TROPHY, "Avatars", AccountDataView::class, false),
createTab(VaadinIcon.FILE_ZIP, "Uploaded content", AccountDataView::class, false),
createTab(VaadinIcon.SWORD, "Moderation Reports", AccountDataView::class, false),
createTab(VaadinIcon.KEY_O, "Permissions", AccountDataView::class, false),
createTab(VaadinIcon.BAN, "Ban history", AccountDataView::class, false),
createTab(VaadinIcon.EXIT_O, "Delete Account", AccountDataView::class, false),
)
tabs.orientation = Tabs.Orientation.VERTICAL
return tabs
}

private fun createTab(
viewIcon: VaadinIcon,
viewName: String,
route: KClass<out Component>,
enabled: Boolean = true,
): Tab {
val icon: Icon = viewIcon.create()
icon.getStyle().set("box-sizing", "border-box")
.set("margin-inline-end", "var(--lumo-space-m)")
.set("margin-inline-start", "var(--lumo-space-xs)")
.set("padding", "var(--lumo-space-xs)")
val link = RouterLink()
link.add(icon, Span(viewName))
// Demo has no routes
link.setRoute(route.java)
link.tabIndex = -1
return Tab(link).apply {
isEnabled = enabled
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.faforever.userservice.backend.domain.IpAddress
import com.faforever.userservice.backend.hydra.HydraService
import com.faforever.userservice.backend.hydra.LoginResponse
import com.faforever.userservice.backend.hydra.NoChallengeException
import com.faforever.userservice.backend.login.LoginResult
import com.faforever.userservice.backend.security.LoginResult
import com.faforever.userservice.config.FafProperties
import com.faforever.userservice.ui.component.FontAwesomeIcon
import com.faforever.userservice.ui.component.LogoHeader
Expand Down
Loading

0 comments on commit ef32c82

Please sign in to comment.