Skip to content

Commit

Permalink
mfa association use case redesign
Browse files Browse the repository at this point in the history
  • Loading branch information
mrFlick72 committed Jul 17, 2024
1 parent a1eec1f commit 7041b09
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,39 @@ package com.vauthenticator.server.account.emailverification

import com.vauthenticator.server.account.Account
import com.vauthenticator.server.account.repository.AccountRepository
import com.vauthenticator.server.mfa.domain.*
import com.vauthenticator.server.mfa.domain.InvalidTicketException
import com.vauthenticator.server.mfa.domain.MfaMethod
import com.vauthenticator.server.mfa.domain.MfaMethodsEnrollmentAssociation
import com.vauthenticator.server.mfa.domain.VerificationTicket
import com.vauthenticator.server.mfa.repository.TicketRepository

class VerifyEMailChallenge(
private val accountRepository: AccountRepository,
private val ticketRepository: TicketRepository,
private val mfaMethodsEnrolmentAssociation: MfaMethodsEnrolmentAssociation
private val accountRepository: AccountRepository,
private val mfaMethodsEnrollmentAssociation: MfaMethodsEnrollmentAssociation
) {


fun verifyMail(ticket: String) {
ticketRepository.loadFor(VerificationTicket(ticket))
.map { ticket ->
enableAccountForEnabledClientAppFrom(ticket)
revoke(ticket)
.map {
mfaMethodsEnrollmentAssociation.associate(ticket, MfaMethod.EMAIL_MFA_METHOD)
enableAccountFrom(it.email)
}
.orElseThrow { throw InvalidTicketException("The ticket $ticket is not a valid ticket, it seems to be expired") }
}


private fun enableAccountForEnabledClientAppFrom(ticket: Ticket) {
val account = enableAccountFrom(ticket)
mfaMethodsEnrolmentAssociation.associate(account, MfaMethod.EMAIL_MFA_METHOD)
}


private fun enableAccountFrom(ticket: Ticket): Account =
accountRepository.accountFor(ticket.email)
private fun enableAccountFrom(email: String): Account =
accountRepository.accountFor(email)
.map { account ->
val enabledAccount = makeAnAccountEnableForm(account)
accountRepository.save(enabledAccount)
enabledAccount
}
.orElseThrow { throw InvalidTicketException("The ticket ${ticket.verificationTicket.content} is not a valid ticket") }
.orElseThrow { throw InvalidTicketException("The ticket associated with the username $email is not a valid ticket") }


private fun makeAnAccountEnableForm(account: Account) =
account.copy(accountNonLocked = true, enabled = true, emailVerified = true)

private fun revoke(ticket: Ticket) =
ticketRepository.delete(ticket.verificationTicket)

}

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.vauthenticator.server.account.emailverification.SendVerifyEMailChalle
import com.vauthenticator.server.account.emailverification.VerifyEMailChallenge
import com.vauthenticator.server.account.repository.AccountRepository
import com.vauthenticator.server.email.*
import com.vauthenticator.server.mfa.domain.MfaMethodsEnrolmentAssociation
import com.vauthenticator.server.mfa.domain.MfaMethodsEnrollmentAssociation
import com.vauthenticator.server.mfa.domain.VerificationTicketFactory
import com.vauthenticator.server.mfa.repository.TicketRepository
import com.vauthenticator.server.oauth2.clientapp.ClientApplicationRepository
Expand Down Expand Up @@ -38,12 +38,12 @@ class EMailVerificationConfig {
fun verifyMailChallengeSent(
accountRepository: AccountRepository,
ticketRepository: TicketRepository,
mfaMethodsEnrolmentAssociation: MfaMethodsEnrolmentAssociation
mfaMethodsEnrollmentAssociation: MfaMethodsEnrollmentAssociation
) =
VerifyEMailChallenge(
accountRepository,
ticketRepository,
mfaMethodsEnrolmentAssociation
accountRepository,
mfaMethodsEnrollmentAssociation
)

@Bean
Expand Down
6 changes: 4 additions & 2 deletions src/main/kotlin/com/vauthenticator/server/mfa/MfaConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.vauthenticator.server.mask.SensitiveEmailMasker
import com.vauthenticator.server.mfa.domain.*
import com.vauthenticator.server.mfa.repository.DynamoMfaAccountMethodsRepository
import com.vauthenticator.server.mfa.repository.MfaAccountMethodsRepository
import com.vauthenticator.server.mfa.repository.TicketRepository
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
Expand Down Expand Up @@ -39,8 +40,9 @@ class MfaConfig {
fun sensitiveEmailMasker() = SensitiveEmailMasker()

@Bean
fun mfaMethodsEnrolmentAssociation(mfaAccountMethodsRepository: MfaAccountMethodsRepository) =
MfaMethodsEnrolmentAssociation(mfaAccountMethodsRepository)
fun mfaMethodsEnrolmentAssociation(ticketRepository: TicketRepository,
mfaAccountMethodsRepository: MfaAccountMethodsRepository) =
MfaMethodsEnrollmentAssociation(ticketRepository , mfaAccountMethodsRepository)

@Bean
fun otpMfa(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,34 @@ package com.vauthenticator.server.mfa.domain

import com.vauthenticator.server.account.Account
import com.vauthenticator.server.mfa.repository.MfaAccountMethodsRepository
import com.vauthenticator.server.mfa.repository.TicketRepository

class MfaMethodsEnrolmentAssociation(private val mfaAccountMethodsRepository: MfaAccountMethodsRepository) {
class MfaMethodsEnrollmentAssociation(
private val ticketRepository: TicketRepository,
private val mfaAccountMethodsRepository: MfaAccountMethodsRepository
) {

fun associate(account: Account, emailMfaMethod: MfaMethod) {
val email = account.email
val mfaAccountMethods = mfaAccountMethodsRepository.findAll(email)
if (!mfaAccountMethods.any { it.method == emailMfaMethod}) {
mfaAccountMethodsRepository.save(email, emailMfaMethod)
}
//todo mfaMethod: MfaMethod can be encoded in the ticket itself
//todo ticket can be an higher abstraction like RawTicket
fun associate(ticket: String, mfaMethod: MfaMethod) {
ticketRepository.loadFor(VerificationTicket(ticket))
.map { ticket ->
val email = ticket.email
val mfaAccountMethods = mfaAccountMethodsRepository.findAll(email)
if (!mfaAccountMethods.any { it.method == mfaMethod }) {
mfaAccountMethodsRepository.save(email, mfaMethod)
}

revoke(ticket)
ticket.email
}
.orElseThrow { throw InvalidTicketException("The ticket $ticket is not a valid ticket, it seems to be expired") }
}

private fun revoke(ticket: Ticket) =
ticketRepository.delete(ticket.verificationTicket)
}

class MfaMethodsEnrollment(private val mfaAccountMethodsRepository: MfaAccountMethodsRepository) {

fun enroll(account: Account, emailMfaMethod: MfaMethod) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.vauthenticator.server.account.emailverification
import com.vauthenticator.server.account.repository.AccountRepository
import com.vauthenticator.server.mfa.domain.InvalidTicketException
import com.vauthenticator.server.mfa.domain.MfaMethod
import com.vauthenticator.server.mfa.domain.MfaMethodsEnrolmentAssociation
import com.vauthenticator.server.mfa.domain.MfaMethodsEnrollmentAssociation
import com.vauthenticator.server.mfa.domain.VerificationTicket
import com.vauthenticator.server.mfa.repository.TicketRepository
import com.vauthenticator.server.oauth2.clientapp.ClientAppId
Expand Down Expand Up @@ -31,16 +31,17 @@ internal class VerifyEMailChallengeTest {
lateinit var ticketRepository: TicketRepository

@MockK
lateinit var mfaMethodsEnrolmentAssociation: MfaMethodsEnrolmentAssociation
lateinit var mfaMethodsEnrollmentAssociation: MfaMethodsEnrollmentAssociation


private lateinit var underTest: VerifyEMailChallenge

@BeforeEach
fun setup() {
underTest = VerifyEMailChallenge(
accountRepository,
ticketRepository,
mfaMethodsEnrolmentAssociation
accountRepository,
mfaMethodsEnrollmentAssociation
)
}

Expand All @@ -57,13 +58,13 @@ internal class VerifyEMailChallengeTest {
ClientAppId.empty().content
)
)
every { mfaMethodsEnrolmentAssociation.associate(enabledAccount, MfaMethod.EMAIL_MFA_METHOD) } just runs
every { mfaMethodsEnrollmentAssociation.associate("A_TICKET", MfaMethod.EMAIL_MFA_METHOD) } just runs
every { accountRepository.accountFor(account.email) } returns Optional.of(account)
every { accountRepository.save(enabledAccount) } just runs
every { ticketRepository.delete(verificationTicket) } just runs

underTest.verifyMail("A_TICKET")
verify(exactly = 1) { mfaMethodsEnrolmentAssociation.associate(enabledAccount, MfaMethod.EMAIL_MFA_METHOD) }
verify(exactly = 1) { mfaMethodsEnrollmentAssociation.associate("A_TICKET", MfaMethod.EMAIL_MFA_METHOD) }
}

@Test
Expand All @@ -79,6 +80,7 @@ internal class VerifyEMailChallengeTest {
)
)
every { accountRepository.accountFor(account.email) } returns Optional.empty()
every { mfaMethodsEnrollmentAssociation.associate("A_TICKET", MfaMethod.EMAIL_MFA_METHOD) } just runs

assertThrows(InvalidTicketException::class.java) { underTest.verifyMail("A_TICKET") }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ package com.vauthenticator.server.mfa.domain

import com.vauthenticator.server.keys.Kid
import com.vauthenticator.server.mfa.repository.MfaAccountMethodsRepository
import com.vauthenticator.server.mfa.repository.TicketRepository
import com.vauthenticator.server.oauth2.clientapp.ClientAppId
import com.vauthenticator.server.support.AccountTestFixture.anAccount
import com.vauthenticator.server.support.TicketFixture
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import io.mockk.just
import io.mockk.runs
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import java.util.*

private const val RAW_TICKET = "A_TICKET"

@ExtendWith(MockKExtension::class)
class MfaMethodsEnrollmentAssociationTest {
Expand All @@ -21,35 +29,57 @@ class MfaMethodsEnrollmentAssociationTest {
Kid(""),
MfaMethod.EMAIL_MFA_METHOD
)
private val ticket = TicketFixture.ticketFor(
RAW_TICKET,
account.email,
ClientAppId.empty().content
)
private val verificationTicket = VerificationTicket(RAW_TICKET)

@MockK
lateinit var ticketRepository: TicketRepository

@MockK
lateinit var mfaAccountMethodsRepository: MfaAccountMethodsRepository

lateinit var underTest: MfaMethodsEnrolmentAssociation
lateinit var underTest: MfaMethodsEnrollmentAssociation

@BeforeEach
fun setUp() {
underTest = MfaMethodsEnrolmentAssociation(mfaAccountMethodsRepository)
underTest = MfaMethodsEnrollmentAssociation(ticketRepository, mfaAccountMethodsRepository)
}

@Test
fun `when an email association goes fine`() {
fun `when an email association can be associated`() {
every { ticketRepository.loadFor(verificationTicket) } returns Optional.of(
ticket
)
every { mfaAccountMethodsRepository.findAll(email) } returns emptyList()
every { mfaAccountMethodsRepository.save(email, MfaMethod.EMAIL_MFA_METHOD) } returns mfaAccountMethod
every { ticketRepository.delete(ticket.verificationTicket) } just runs


underTest.associate(account, MfaMethod.EMAIL_MFA_METHOD)
underTest.associate(RAW_TICKET, MfaMethod.EMAIL_MFA_METHOD)

verify { ticketRepository.loadFor(verificationTicket) }
verify { mfaAccountMethodsRepository.findAll(email) }
verify { mfaAccountMethodsRepository.save(email, MfaMethod.EMAIL_MFA_METHOD) }
verify { ticketRepository.delete(ticket.verificationTicket) }
}

@Test
fun `when an email association is already done`() {
every { mfaAccountMethodsRepository.findAll(email) } returns listOf( mfaAccountMethod)
fun `when an email is already associated`() {
every { ticketRepository.loadFor(verificationTicket) } returns Optional.of(
ticket
)
every { mfaAccountMethodsRepository.findAll(email) } returns listOf(mfaAccountMethod)
every { ticketRepository.delete(ticket.verificationTicket) } just runs

underTest.associate(account, MfaMethod.EMAIL_MFA_METHOD)
underTest.associate(RAW_TICKET, MfaMethod.EMAIL_MFA_METHOD)

verify { ticketRepository.loadFor(verificationTicket) }
verify { mfaAccountMethodsRepository.findAll(email) }
verify(exactly = 0) { mfaAccountMethodsRepository.save(email, MfaMethod.EMAIL_MFA_METHOD) }
verify { ticketRepository.delete(ticket.verificationTicket) }
}
}

0 comments on commit 7041b09

Please sign in to comment.