Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ DEBUG=false
ENABLE_OPENAPI=true
# Examples: oidc,some-other-feature
FEATURES=oidc
# Ratelimits
RATE_LIMITS_ENABLED=true

# Authentication
ENABLE_LOCAL_REGISTRATION=true
Expand Down
5 changes: 3 additions & 2 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,10 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
// OIDC
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
// SRP
implementation("com.nimbusds:srp6a:2.1.0")

// Local auth
implementation("org.springframework.security:spring-security-crypto")
// Algorithm Provider & Encryption primitives
implementation("org.bouncycastle:bcprov-jdk18on:1.83")

// Serialization
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
package app.cliq.backend.auth

import app.cliq.backend.auth.annotation.AuthController
import app.cliq.backend.auth.params.LoginParams
import app.cliq.backend.auth.params.RegistrationParams
import app.cliq.backend.auth.service.JwtService
import app.cliq.backend.auth.view.TokenResponse
import app.cliq.backend.config.properties.AuthProperties
import app.cliq.backend.exception.EmailNotVerifiedException
import app.cliq.backend.exception.InvalidEmailOrPasswordException
import app.cliq.backend.exception.LocalLoginDisabledException
import app.cliq.backend.exception.LocalRegistrationDisabledException
import app.cliq.backend.user.UserRepository
import app.cliq.backend.user.factory.UserFactory
import app.cliq.backend.user.view.UserResponse
import io.swagger.v3.oas.annotations.Operation
Expand All @@ -22,18 +15,14 @@ import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping

@AuthController
@RequestMapping("/api/auth")
class LocalAuthController(
private val userRepository: UserRepository,
private val userFactory: UserFactory,
private val passwordEncoder: PasswordEncoder,
private val jwtService: JwtService,
private val authProperties: AuthProperties,
) {
@PostMapping("/register")
Expand Down Expand Up @@ -69,55 +58,4 @@ class LocalAuthController(

return ResponseEntity.status(HttpStatus.CREATED).body(UserResponse.fromUser(user))
}

@PostMapping("/login")
@Operation(summary = "Logs in a User using local authentication.")
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "Successfully logged in.",
content = [
Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = TokenResponse::class),
),
],
),
ApiResponse(
responseCode = "400",
description = "Invalid input",
content = [Content()],
),
ApiResponse(
responseCode = "403",
description = "Login with local authentication has been disabled.",
content = [Content()],
),
],
)
private fun login(
@Valid @RequestBody loginParams: LoginParams,
): ResponseEntity<TokenResponse> {
if (!authProperties.local.login) {
throw LocalLoginDisabledException()
}

val user = userRepository.findByEmail(loginParams.email) ?: throw InvalidEmailOrPasswordException()

if (!user.isUsable()) throw EmailNotVerifiedException()

if (!passwordEncoder.matches(
loginParams.password,
user.password,
)
) {
throw InvalidEmailOrPasswordException()
}

val tokenPair = jwtService.generateJwtTokenPair(loginParams, user)
val response = TokenResponse.fromTokenPair(tokenPair)

return ResponseEntity.ok(response)
}
}
120 changes: 120 additions & 0 deletions backend/src/main/kotlin/app/cliq/backend/auth/LocalLoginController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package app.cliq.backend.auth

import app.cliq.backend.auth.annotation.AuthController
import app.cliq.backend.auth.params.login.LoginFinishParams
import app.cliq.backend.auth.params.login.LoginStartParams
import app.cliq.backend.auth.service.JwtService
import app.cliq.backend.auth.service.SrpService
import app.cliq.backend.auth.view.TokenResponse
import app.cliq.backend.auth.view.login.LoginFinishResponse
import app.cliq.backend.auth.view.login.LoginStartResponse
import app.cliq.backend.config.properties.AuthProperties
import app.cliq.backend.exception.EmailNotVerifiedException
import app.cliq.backend.exception.InvalidEmailException
import app.cliq.backend.exception.LocalLoginDisabledException
import app.cliq.backend.exception.TriedLocalLoginWithOidcUserException
import app.cliq.backend.user.UserRepository
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import jakarta.validation.Valid
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping

@AuthController
@RequestMapping("/api/auth/login")
class LocalLoginController(
private val authProperties: AuthProperties,
private val srpService: SrpService,
private val userRepository: UserRepository,
private val jwtService: JwtService,
) {
@PostMapping("/start")
@Operation(summary = "Starts the login process")
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "Started log in process.",
content = [
Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = LoginStartResponse::class),
),
],
),
ApiResponse(
responseCode = "400",
description = "Invalid input",
content = [Content()],
),
ApiResponse(
responseCode = "403",
description = "Login with local authentication has been disabled or OIDC User tried.",
content = [Content()],
),
],
)
fun startLogin(
@Valid @RequestBody loginStartParams: LoginStartParams,
): ResponseEntity<LoginStartResponse> {
if (!authProperties.local.login) {
throw LocalLoginDisabledException()
}

val user = userRepository.findByEmail(loginStartParams.email) ?: throw InvalidEmailException()
if (!user.isUsable()) throw EmailNotVerifiedException()
if (user.isOidcUser()) throw TriedLocalLoginWithOidcUserException()

val view = srpService.startAuthenticationProcess(user, loginStartParams)

return ResponseEntity.ok(view)
}

@PostMapping("/finish")
@Operation(summary = "Finishes the login process")
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "Finished log in process.",
content = [
Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
),
],
),
ApiResponse(
responseCode = "400",
description = "Invalid input",
content = [Content()],
),
ApiResponse(
responseCode = "403",
description = "Login with local authentication has been disabled.",
content = [Content()],
),
],
)
fun finishLogin(
@Valid @RequestBody loginFinishParams: LoginFinishParams,
): ResponseEntity<LoginFinishResponse> {
if (!authProperties.local.login) {
throw LocalLoginDisabledException()
}

val (email, publicM2) = srpService.finishAuthenticationProcess(loginFinishParams)
val user = userRepository.findByEmail(email) ?: throw InvalidEmailException()

val tokenPair = jwtService.generateJwtTokenPair(loginFinishParams, user)
val tokenResponse = TokenResponse.fromTokenPair(tokenPair)
val loginResponse = LoginFinishResponse(publicM2, tokenResponse)

return ResponseEntity.ok(loginResponse)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package app.cliq.backend.auth.params

import app.cliq.backend.constants.EXAMPLE_EMAIL
import app.cliq.backend.constants.EXAMPLE_PASSWORD
import app.cliq.backend.constants.EXAMPLE_SRP_SALT
import app.cliq.backend.constants.EXAMPLE_SRP_VERIFIER
import app.cliq.backend.constants.EXAMPLE_USERNAME
import app.cliq.backend.constants.MAX_PASSWORD_LENGTH
import app.cliq.backend.constants.MIN_PASSWORD_LENGTH
import app.cliq.backend.user.DEFAULT_LOCALE
import app.cliq.backend.user.validator.EmailOccupiedConstraint
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotEmpty
import jakarta.validation.constraints.Size

@Schema
data class RegistrationParams(
Expand All @@ -19,19 +17,19 @@ data class RegistrationParams(
@field:NotEmpty
@field:EmailOccupiedConstraint
val email: String,
@field:Schema(example = EXAMPLE_PASSWORD)
@field:NotEmpty
@field:Size(
min = MIN_PASSWORD_LENGTH,
max = MAX_PASSWORD_LENGTH,
)
val password: String,
@field:Schema(
description = "An arbitrary username. Can be the user's full name",
example = EXAMPLE_USERNAME,
)
@field:NotEmpty
val username: String,
@field:Schema(description = "The data encryption key encoded in Base64")
val dataEncryptionKey: String,
@field:Schema(example = EXAMPLE_SRP_SALT, description = "The salt hex encoded")
val srpSalt: String,
@field:Schema(example = EXAMPLE_SRP_VERIFIER, description = "The verifier hex encoded")
@field:NotEmpty
val srpVerifier: String,
@field:Schema(example = DEFAULT_LOCALE, defaultValue = DEFAULT_LOCALE)
val locale: String = DEFAULT_LOCALE,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package app.cliq.backend.auth.params.login

import app.cliq.backend.constants.EXAMPLE_SESSION_NAME
import io.swagger.v3.oas.annotations.media.Schema

@Schema
data class LoginFinishParams(
// SRP
val authenticationSessionToken: String,
@field:Schema(description = "The A parameter hex encoded")
val publicA: String,
@field:Schema(description = "The M1 parameter hex encoded")
val publicM1: String,
// Session Data
@field:Schema(example = EXAMPLE_SESSION_NAME)
val sessionName: String? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app.cliq.backend.auth.params.login

import app.cliq.backend.constants.EXAMPLE_EMAIL
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotEmpty

@Schema
data class LoginStartParams(
@field:Schema(example = EXAMPLE_EMAIL)
@field:Email
@field:NotEmpty
val email: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.cliq.backend.auth.service
import app.cliq.backend.auth.jwt.JwtClaims
import app.cliq.backend.session.Session
import app.cliq.backend.session.SessionRepository
import app.cliq.backend.utils.TokenUtils
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.stereotype.Service
Expand All @@ -13,6 +14,7 @@ class JwtResolver(
private val jwtDecoder: JwtDecoder,
private val sessionRepository: SessionRepository,
private val refreshTokenService: RefreshTokenService,
private val tokenUtils: TokenUtils,
) {
fun resolveSessionFromJwt(jwtAccessToken: String): Session {
val jwt = jwtDecoder.decode(jwtAccessToken)
Expand All @@ -25,7 +27,7 @@ class JwtResolver(
}

fun resolveSessionFromRefreshToken(refreshToken: String): Session? {
val hashedRefreshToken = refreshTokenService.hashRefreshToken(refreshToken)
val hashedRefreshToken = tokenUtils.hashTokenUsingSha512(refreshToken)

return sessionRepository.findByRefreshToken(hashedRefreshToken)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package app.cliq.backend.auth.service

import app.cliq.backend.auth.factory.JwtFactory
import app.cliq.backend.auth.jwt.TokenPair
import app.cliq.backend.auth.params.LoginParams
import app.cliq.backend.auth.params.login.LoginFinishParams
import app.cliq.backend.user.User
import org.springframework.stereotype.Service
import java.time.Clock
Expand All @@ -15,9 +15,9 @@ class JwtService(
private val clock: Clock,
) {
fun generateJwtTokenPair(
loginParams: LoginParams,
loginFinishParams: LoginFinishParams,
user: User,
): TokenPair = generateJwtTokenPair(loginParams.name, user)
): TokenPair = generateJwtTokenPair(loginFinishParams.sessionName, user)

fun generateJwtTokenPair(
sessionName: String?,
Expand Down
Loading
Loading