Skip to content

Commit

Permalink
Add TimeBasedOneTimePassword module
Browse files Browse the repository at this point in the history
  • Loading branch information
IRus committed Dec 20, 2024
1 parent 72d1aa6 commit 6bf1b30
Show file tree
Hide file tree
Showing 15 changed files with 189 additions and 225 deletions.
17 changes: 0 additions & 17 deletions komok-app/src/main/kotlin/io/heapy/komok/TimeSourceContext.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@ package io.heapy.komok.business.login

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.heapy.komok.TimeSourceContext
import io.heapy.komok.infra.time.TimeSource
import io.heapy.komok.auth.common.User
import io.heapy.komok.infra.time.TimeSourceModule
import io.heapy.komok.tech.config.ConfigurationModule
import io.heapy.komok.tech.di.lib.Module
import java.time.temporal.ChronoUnit

interface JwtService {
context(TimeSourceContext)
fun createToken(
user: User,
): String
}

private class DefaultJwtService(
private val jwtConfiguration: JwtConfiguration,
private val timeSource: TimeSource,
) : JwtService {
context(TimeSourceContext)
override fun createToken(
user: User,
): String {
Expand All @@ -45,8 +45,9 @@ private class DefaultJwtService(
@Module
open class JwtModule(
private val configurationModule: ConfigurationModule,
private val timeSourceModule: TimeSourceModule,
) {
open val config: JwtConfiguration by lazy {
open val jwtConfiguration: JwtConfiguration by lazy {
configurationModule
.config
.read(
Expand All @@ -62,7 +63,8 @@ open class JwtModule(

open val jwtService: JwtService by lazy {
DefaultJwtService(
jwtConfiguration = config,
jwtConfiguration = jwtConfiguration,
timeSource = timeSourceModule.timeSource,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package io.heapy.komok.business.login

import io.heapy.komok.TimeSourceContext
import io.heapy.komok.business.user.UserDao
import io.heapy.komok.business.user.session.SessionTokenGenerator

class LoginService(
private val userDao: UserDao,
private val sessionTokenGenerator: SessionTokenGenerator
) {
context(TimeSourceContext)
suspend fun login(
email: String,
password: String,
Expand Down
146 changes: 0 additions & 146 deletions komok-app/src/main/kotlin/io/heapy/komok/business/user/totp/TOTP.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ open class ServerApplicationConfigurationModule(
}

open val defaultFeature by lazy {
DefaultFeature(jwtModule.config)
DefaultFeature(jwtModule.jwtConfiguration)
}

open val callLoggingFeature by lazy {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.heapy.komok
package io.heapy.komok.infra.time

import java.time.Clock
import java.time.Instant
Expand All @@ -20,7 +20,7 @@ interface TimeSource {
fun offsetTime(): OffsetTime
}

fun TimeContext(
fun TimeSource(
clock: Clock,
): TimeSource =
DefaultTimeSource(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.heapy.komok.infra.time

import io.heapy.komok.tech.di.lib.Module
import java.time.Clock

@Module
open class TimeSourceModule {
open val timeSource: TimeSource by lazy {
TimeSource(
clock = Clock.systemDefaultZone(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.heapy.komok.infra.totp

import io.heapy.komok.infra.time.TimeSource
import io.heapy.komok.infra.base32.Base32
import java.nio.ByteBuffer
import java.nio.ByteOrder
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.math.pow

class TimeBasedOneTimePassword(
private val base32: Base32,
private val timeSource: TimeSource,
) {
fun generate(
secret: String,
): String {
val key = base32.decode(secret)
val timeWindow = timeSource.instant().epochSecond / TIME_STEP_SECONDS

val data = ByteBuffer
.allocate(8)
.order(ByteOrder.BIG_ENDIAN)
.putLong(timeWindow)
.array()

val mac = Mac.getInstance(HMAC_ALGO)
mac.init(
SecretKeySpec(
key,
HMAC_ALGO
)
)

val hmac = mac.doFinal(data)

val offset = hmac[hmac.size - 1].toInt() and 0x0F
val binary = (hmac[offset].toInt() and 0x7f shl 24) or
(hmac[offset + 1].toInt() and 0xff shl 16) or
(hmac[offset + 2].toInt() and 0xff shl 8) or
(hmac[offset + 3].toInt() and 0xff)

val otp = binary % 10.0.pow(TOTP_DIGITS.toDouble())
.toInt()

return otp
.toString()
.padStart(
TOTP_DIGITS,
'0'
)
}

fun validate(
secret: String,
otp: String,
): Boolean {
val calculatedOtp = generate(secret)

return calculatedOtp == otp
}

private companion object {
private const val HMAC_ALGO = "HmacSHA1"
private const val TIME_STEP_SECONDS = 30L
private const val TOTP_DIGITS = 6
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.heapy.komok.infra.totp

import io.heapy.komok.infra.base32.Base32Module
import io.heapy.komok.infra.time.TimeSourceModule
import io.heapy.komok.tech.di.lib.Module

@Module
open class TimeBasedOneTimePasswordModule(
private val base32Module: Base32Module,
private val timeSourceModule: TimeSourceModule,
) {
open val timeBasedOneTimePassword by lazy {
TimeBasedOneTimePassword(
base32 = base32Module.base32,
timeSource = timeSourceModule.timeSource,
)
}
}
4 changes: 1 addition & 3 deletions komok-app/src/test/kotlin/io/heapy/komok/KomokBaseTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,5 @@ package io.heapy.komok

import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(
TestTimeSourceContextParameterResolver::class,
)
@ExtendWith
interface KomokBaseTest
7 changes: 4 additions & 3 deletions komok-app/src/test/kotlin/io/heapy/komok/TestTimeSource.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.heapy.komok

import io.heapy.komok.infra.time.TimeSource
import java.time.Clock
import java.time.Instant
import java.time.LocalDate
Expand All @@ -14,8 +15,8 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration

class TestTimeSource(
private var advanceBy: Duration = 1.seconds,
var initial: Instant = Instant.now(),
private val advanceBy: Duration = 1.seconds,
val initial: Instant = Instant.now(),
) : TimeSource {
private var current: Instant = initial
private var _calls: Int = 0
Expand All @@ -28,7 +29,7 @@ class TestTimeSource(
instant,
ZoneId.systemDefault(),
)
return TimeContext(clock)
return TimeSource(clock)
}

val calls: Int
Expand Down
Loading

0 comments on commit 6bf1b30

Please sign in to comment.