Skip to content

Commit

Permalink
Password method added
Browse files Browse the repository at this point in the history
  • Loading branch information
UnknownJoe796 committed Oct 9, 2023
1 parent c522c2c commit d32cc39
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.lightningkite.lightningserver.auth.old

import com.lightningkite.lightningdb.HasId
import com.lightningkite.lightningserver.HtmlDefaults
import com.lightningkite.lightningserver.encryption.checkHash
import com.lightningkite.lightningserver.encryption.checkAgainstHash
import com.lightningkite.lightningserver.encryption.secureHash
import com.lightningkite.lightningserver.core.ContentType
import com.lightningkite.lightningserver.core.ServerPathGroup
Expand All @@ -27,7 +27,7 @@ open class PasswordAuthEndpoints<USER : HasId<ID>, ID: Comparable<ID>>(
errorCases = listOf(),
implementation = { anon: Unit, input: PasswordLogin ->
val user = info.byUsername(input.username, input.password)
if (!input.password.checkHash(info.hashedPassword(user)))
if (!input.password.checkAgainstHash(info.hashedPassword(user)))
throw BadRequestException(
detail = "password-incorrect",
message = "Password does not match the account."
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.lightningkite.lightningserver.auth.old

import com.lightningkite.lightningserver.encryption.checkHash
import com.lightningkite.lightningserver.encryption.checkAgainstHash
import com.lightningkite.lightningserver.encryption.secureHash
import com.lightningkite.lightningserver.cache.Cache
import com.lightningkite.lightningserver.cache.get
Expand Down Expand Up @@ -54,7 +54,7 @@ open class PinHandler(
}
cache().add(attemptCacheKey(uniqueIdentifier), 1)
val fixedPin = if(mixedCaseMode) pin else pin.lowercase()
if (!fixedPin.checkHash(hashedPin)) throw BadRequestException(
if (!fixedPin.checkAgainstHash(hashedPin)) throw BadRequestException(
detail = "pin-incorrect",
message = "Incorrect PIN. ${maxAttempts - attempts} attempts remain."
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package com.lightningkite.lightningserver.auth.proof

import com.lightningkite.lightningdb.*
import com.lightningkite.lightningserver.auth.*
import com.lightningkite.lightningserver.cache.Cache
import com.lightningkite.lightningserver.cache.get
import com.lightningkite.lightningserver.core.ServerPath
import com.lightningkite.lightningserver.core.ServerPathGroup
import com.lightningkite.lightningserver.db.modelInfo
import com.lightningkite.lightningserver.db.ModelRestEndpoints
import com.lightningkite.lightningserver.db.ModelSerializationInfo
import com.lightningkite.lightningserver.encryption.*
import com.lightningkite.lightningserver.exceptions.BadRequestException
import com.lightningkite.lightningserver.http.HttpStatus
import com.lightningkite.lightningserver.http.post
import com.lightningkite.lightningserver.routes.docName
import com.lightningkite.lightningserver.serialization.Serialization
import com.lightningkite.lightningserver.serialization.decodeUnwrappingString
import com.lightningkite.lightningserver.serialization.encodeUnwrappingString
import com.lightningkite.lightningserver.tasks.Tasks
import com.lightningkite.lightningserver.typed.*
import com.lightningkite.now
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlin.time.Duration.Companion.hours

@OptIn(InternalSerializationApi::class)
class PasswordProofEndpoints(
path: ServerPath,
val database: () -> Database,
val cache: () -> Cache,
val proofHasher: () -> SecureHasher = secretBasis.hasher("proof"),
val evaluatePassword: (String) -> Unit = { it }
) : ServerPathGroup(path), Authentication.DirectProofMethod {
init {
if (path.docName == null) path.docName = "PasswordProof"
}

override val name: String
get() = "password"
override val humanName: String
get() = "Password"
override val validates: String
get() = "password"
override val strength: Int
get() = 5

private val tables = HashMap<String, FieldCollection<PasswordSecret<Comparable<Any>>>>()

@Suppress("UNCHECKED_CAST")
fun table(subjectHandler: Authentication.SubjectHandler<*, *>) = tables.getOrPut(subjectHandler.name) {
database().collection(
PasswordSecret.serializer(subjectHandler.idSerializer),
"PasswordSecretFor${subjectHandler.name}"
) as FieldCollection<PasswordSecret<Comparable<Any>>>
}

init {
prepareModels()
Tasks.onSettingsReady {
Authentication.subjects.forEach {
@Suppress("UNCHECKED_CAST")
ModelRestEndpoints<HasId<*>, PasswordSecret<Comparable<Any>>, Comparable<Any>>(path("secrets/${it.value.name.lowercase()}"), modelInfo< HasId<*>, PasswordSecret<Comparable<Any>>, Comparable<Any>>(
serialization = ModelSerializationInfo(PasswordSecret.serializer(it.value.idSerializer as KSerializer<Comparable<Any>>), it.value.idSerializer as KSerializer<Comparable<Any>>),
authOptions = Authentication.isSuperUser as AuthOptions<HasId<*>>,
getCollection = { table(it.value).withPermissions(ModelPermissions(
create = Condition.Always(),
read = Condition.Always(),
readMask = Mask(
listOf(
Condition.Never<PasswordSecret<Comparable<Any>>>() to Modification.OnField(
PasswordSecret_hash(it.value.idSerializer as KSerializer<Comparable<Any>>),
Modification.Assign("")
)
)
),
update = Condition.Always(),
delete = Condition.Always(),
)) as FieldCollection<PasswordSecret<Comparable<Any>>> },
modelName = "PasswordSecret For ${it.value.name}"
))
}
}
}

fun <ID : Comparable<ID>> key(subjectHandler: Authentication.SubjectHandler<*, ID>, id: ID): String =
subjectHandler.name + "|" + Serialization.json.encodeUnwrappingString(subjectHandler.idSerializer, id)

fun key(id: String): Pair<Authentication.SubjectHandler<*, *>, Any?> {
val subject = id.substringBefore('|', "")
val handler = Authentication.subjects.values.find { it.name == subject }
?: throw IllegalArgumentException("No subject $subject recognized")
return handler to Serialization.json.decodeUnwrappingString(handler.idSerializer, id.substringAfter('|'))
}

val establish = path("establish").post.api(
summary = "Establish a Password",
inputType = String.serializer(),
outputType = Unit.serializer(),
description = "Generates a new One Time Password configuration.",
authOptions = anyAuth,
errorCases = listOf(),
examples = listOf(),
implementation = { value: String ->
evaluatePassword(value)
val secret = PasswordSecret(
_id = auth.rawId as Comparable<Any>,
hash = value.secureHash()
)
table(auth.subject).deleteOneById(auth.rawId as Comparable<Any>)
table(auth.subject).insertOne(secret)
Unit
}
)

override val prove = path("prove").post.api(
authOptions = noAuth,
summary = "Prove $validates ownership",
description = "Logs in to the given account with a password. Limits to 10 attempts per hour.",
errorCases = listOf(),
examples = listOf(
ApiExample(
input = ProofEvidence(
"User|id",
"my password"
),
output = Proof(
via = name,
of = validates,
strength = strength,
value = "User|id",
at = now(),
signature = "opaquesignaturevalue"
)
)
),
successCode = HttpStatus.OK,
implementation = { input: ProofEvidence ->
val postedAt = now()
val cacheKey = "otp-count-${input.key}"
cache().add(cacheKey, 1, 1.hours)
val ct = (cache().get<Int>(cacheKey) ?: 0)
if (ct > 5) throw BadRequestException("Too many attempts; please wait.")
val (subject, id) = key(input.key)
@Suppress("UNCHECKED_CAST")
val secret = table(subject).get(id as Comparable<Any>)
?: throw BadRequestException("User ID and code do not match")
if (!input.password.checkAgainstHash(secret.hash)) throw BadRequestException("User ID and code do not match")
cache().remove(cacheKey)
proofHasher().makeProof(
info = info,
value = input.key,
at = now()
)
}
)

suspend fun <ID : Comparable<ID>> established(handler: Authentication.SubjectHandler<*, ID>, id: ID): Boolean {
@Suppress("UNCHECKED_CAST")
return table(handler).get(id as Comparable<Any>) != null
}

suspend fun <ID : Comparable<ID>> proofOption(handler: Authentication.SubjectHandler<*, ID>, id: ID): ProofOption? {
return if (established(handler, id)) {
ProofOption(info, key(handler, id))
} else {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.lightningkite.lightningserver.auth.proof

import com.lightningkite.lightningserver.encryption.checkHash
import com.lightningkite.lightningserver.encryption.checkAgainstHash
import com.lightningkite.lightningserver.encryption.secureHash
import com.lightningkite.lightningserver.cache.Cache
import com.lightningkite.lightningserver.cache.get
Expand All @@ -11,7 +11,6 @@ import com.lightningkite.lightningserver.utils.BadWordList
import com.lightningkite.uuid
import java.security.SecureRandom
import kotlin.time.Duration
import java.util.UUID
import kotlin.time.Duration.Companion.minutes

open class PinHandler(
Expand Down Expand Up @@ -61,7 +60,7 @@ open class PinHandler(
}
cache().add(attemptCacheKey(key), 1)
val fixedPin = if(mixedCaseMode) pin else pin.lowercase()
if (!fixedPin.checkHash(hashedPin)) throw BadRequestException(
if (!fixedPin.checkAgainstHash(hashedPin)) throw BadRequestException(
detail = "pin-incorrect",
message = "Incorrect PIN. ${maxAttempts - attempts} attempts remain."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ class AuthEndpointsForSubject<SUBJECT : HasId<ID>, ID : Comparable<ID>>(
input.code != null -> {
val client = OauthClientEndpoints.instance?.modelInfo?.collection()?.get(input.client_id)
?: throw BadRequestException("Client ID/Secret mismatch")
if (client.secrets.none { input.client_secret.checkHash(it.secretHash) }) throw BadRequestException(
if (client.secrets.none { input.client_secret.checkAgainstHash(it.secretHash) }) throw BadRequestException(
"Client ID/Secret mismatch"
)
val future = FutureSession.fromToken(input.code!!)
Expand Down Expand Up @@ -423,7 +423,7 @@ class AuthEndpointsForSubject<SUBJECT : HasId<ID>, ID : Comparable<ID>>(
if (!valid) return null
if (type != handler.name) return null
val session = sessionInfo.collection().get(_id) ?: return null
if (!plainTextSecret.checkHash(session.secretHash)) return null
if (!plainTextSecret.checkAgainstHash(session.secretHash)) return null
if (session.terminated != null) return null
sessionInfo.collection().updateOneById(_id, modification(dataClassPath) {
it.lastUsed assign now()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fun String.secureHash(): String {
/**
* Checks hashes generated by [secureHash].
*/
fun String.checkHash(againstHash: String): Boolean {
fun String.checkAgainstHash(againstHash: String): Boolean {
if (againstHash.isEmpty()) return false
val against = againstHash.removePrefix(prefix)
val salt = against.substringBefore('.').decodeBase64Bytes()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.lightningkite.lightningserver.encryption

import com.lightningkite.lightningserver.encryption.checkHash
import com.lightningkite.lightningserver.encryption.secureHash
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
Expand All @@ -13,10 +11,10 @@ class SecureHashKtTest {
val hash = "asdf".secureHash()
println("Hash is $hash")
measureTimeMillis {
assertTrue("asdf".checkHash(hash))
assertTrue("asdf".checkAgainstHash(hash))
}.also { println(it) }
measureTimeMillis {
assertFalse("asdff".checkHash(hash))
assertFalse("asdff".checkAgainstHash(hash))
}.also { println(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package com.lightningkite.lightningserver.auth.proof
import com.lightningkite.lightningdb.ExperimentalLightningServer
import com.lightningkite.lightningdb.GenerateDataClassPaths
import com.lightningkite.lightningdb.HasId
import com.lightningkite.now
import kotlinx.datetime.Instant
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseContextualSerialization
Expand All @@ -21,3 +23,11 @@ data class OtpSecret<ID : Comparable<ID>>(
val digits: Int,
val algorithm: OtpHashAlgorithm
) : HasId<ID>

@Serializable
@GenerateDataClassPaths
data class PasswordSecret<ID : Comparable<ID>>(
@Contextual override val _id: ID,
val hash: String,
val establishedAt: Instant = now()
) : HasId<ID>

0 comments on commit d32cc39

Please sign in to comment.