Skip to content

Commit

Permalink
Drastic improvement to forming auth handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
UnknownJoe796 committed Oct 11, 2023
1 parent 53bb103 commit 9d5507e
Show file tree
Hide file tree
Showing 16 changed files with 378 additions and 229 deletions.
31 changes: 7 additions & 24 deletions demo/src/main/kotlin/Server.kt
Original file line number Diff line number Diff line change
Expand Up @@ -250,38 +250,21 @@ object Server : ServerPathGroup(ServerPath.root) {
path("subject"),
object : Authentication.SubjectHandler<User, UUID> {
override val name: String get() = "User"
override val idProofs: Set<Authentication.ProofMethod> = setOf(proofEmail)
override val authType: AuthType get() = AuthType<User>()
override val additionalProofs: Set<Authentication.ProofMethod> = setOf(proofOtp, proofPassword)
override suspend fun authenticate(vararg proofs: Proof): Authentication.AuthenticateResult<User, UUID>? {
val emailIdentifier = proofs.find { it.of == "email" } ?: return null
val user = userInfo.collection().findOne(condition { it.email eq emailIdentifier.value }) ?: run {
userInfo.collection().insertOne(
User(
email = emailIdentifier.value
)
)
} ?: return null
val options = listOfNotNull(
ProofOption(proofEmail.info, user.email),
proofOtp.proofOption(this, user._id),
proofPassword.proofOption(this, user._id),
)
return Authentication.AuthenticateResult(
id = user._id,
subjectCopy = user,
options = options,
strengthRequired = 20
)
}

override val idSerializer: KSerializer<UUID>
get() = userInfo.serialization.idSerializer
override val subjectSerializer: KSerializer<User>
get() = userInfo.serialization.serializer

override suspend fun fetch(id: UUID): User = userInfo.collection().get(id) ?: throw NotFoundException()
override suspend fun findUser(property: String, value: String): User? = when(property) {
"email" -> userInfo.collection().findOne(condition { it.email eq value })
"_id" -> userInfo.collection().get(uuid(value))
else -> null
}
override val knownCacheTypes: List<RequestAuth.CacheKey<User, UUID, *>> = listOf(EmailCacheKey)

override suspend fun desiredStrengthFor(result: User): Int = if(result.isSuperUser) Int.MAX_VALUE else 5
},
database = database
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ package com.lightningkite.lightningserver.auth

import com.lightningkite.lightningdb.HasId
import com.lightningkite.lightningserver.SetOnce
import com.lightningkite.lightningserver.auth.proof.Proof
import com.lightningkite.lightningserver.auth.proof.ProofEvidence
import com.lightningkite.lightningserver.auth.proof.ProofMethodInfo
import com.lightningkite.lightningserver.auth.proof.ProofOption
import com.lightningkite.lightningserver.auth.proof.*
import com.lightningkite.lightningserver.core.ServerPath
import com.lightningkite.lightningserver.http.Request
import com.lightningkite.lightningserver.serialization.Serialization
import com.lightningkite.lightningserver.serialization.decodeUnwrappingString
import com.lightningkite.lightningserver.typed.ApiEndpoint
import com.lightningkite.lightningserver.typed.TypedServerPath0
import kotlinx.serialization.KSerializer
import kotlinx.serialization.UseContextualSerialization
import kotlinx.datetime.Instant
import kotlinx.serialization.encoding.CompositeDecoder

object Authentication {

Expand All @@ -36,19 +36,29 @@ object Authentication {

interface SubjectHandler<SUBJECT: HasId<ID>, ID: Comparable<ID>> {
val name: String
val idProofs: Set<ProofMethod>
val authType: AuthType
val additionalProofs: Set<ProofMethod>
val applicableProofs: Set<ProofMethod> get() = idProofs + additionalProofs
suspend fun authenticate(vararg proofs: Proof): AuthenticateResult<SUBJECT, ID>?
val knownCacheTypes: List<RequestAuth.CacheKey<SUBJECT, ID, *>> get() = listOf()

suspend fun fetch(id: ID): SUBJECT
suspend fun findUser(property: String, value: String): SUBJECT? {
return if(property == "_id") fetch(Serialization.json.decodeUnwrappingString(idSerializer, value)) else null
}
suspend fun permitMasquerade(
other: SubjectHandler<*, *>,
id: ID,
otherId: Comparable<*>,
): Boolean = false
val knownCacheTypes: List<RequestAuth.CacheKey<SUBJECT, ID, *>> get() = listOf()

suspend fun fetch(id: ID): SUBJECT
suspend fun desiredStrengthFor(result: SUBJECT): Int = 5
fun get(property: String): Boolean = subjectSerializer.descriptor.getElementIndex(property) != CompositeDecoder.UNKNOWN_NAME
fun get(subject: SUBJECT, property: String): String? {
return Serialization.properties.encodeToStringMap(subjectSerializer, subject)[property]
}

val proofMethods: Set<ProofMethod> get() = Authentication.proofMethods.values.filter {
it.info.property == null || get(it.info.property!!)
}.toSet()

val idSerializer: KSerializer<ID>
val subjectSerializer: KSerializer<SUBJECT>
}
Expand All @@ -58,23 +68,23 @@ object Authentication {
_subjects[subjectHandler.authType] = subjectHandler
}

interface ProofMethod {
val name: String
val humanName: String
val validates: String
val strength: Int
val info: ProofMethodInfo get() = ProofMethodInfo(name, validates, strength)
private val _proofMethods: MutableMap<String, ProofMethod> = HashMap()
val proofMethods: Map<String, ProofMethod> get() = _proofMethods
fun register(method: ProofMethod) {
_proofMethods[method.info.via] = method
}

interface EndsWithStringProofMethod : ProofMethod {
val prove: ApiEndpoint<HasId<*>?, TypedServerPath0, ProofEvidence, Proof>
interface ProofMethod {
val info: ProofMethodInfo
suspend fun <SUBJECT: HasId<ID>, ID: Comparable<ID>> established(handler: SubjectHandler<SUBJECT, ID>, item: SUBJECT): Boolean = info.property?.let { handler.get(it) } ?: false
}

interface DirectProofMethod : EndsWithStringProofMethod {
interface DirectProofMethod : ProofMethod {
val prove: ApiEndpoint<HasId<*>?, TypedServerPath0, IdentificationAndPassword, Proof>
}

interface StartedProofMethod : EndsWithStringProofMethod {
interface StartedProofMethod : ProofMethod {
val start: ApiEndpoint<HasId<*>?, TypedServerPath0, String, String>
val prove: ApiEndpoint<HasId<*>?, TypedServerPath0, FinishProof, Proof>
}

interface ExternalProofMethod : ProofMethod {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlinx.serialization.UseContextualSerialization
import kotlin.time.Duration
import kotlinx.datetime.Instant
import com.lightningkite.UUID
import com.lightningkite.lightningserver.serialization.encodeUnwrappingString

data class RequestAuth<SUBJECT : HasId<*>>(
val subject: Authentication.SubjectHandler<SUBJECT, *>,
Expand Down Expand Up @@ -125,6 +126,8 @@ data class RequestAuth<SUBJECT : HasId<*>>(

@Suppress("UNCHECKED_CAST")
inline val <SUBJECT : HasId<ID>, ID : Comparable<ID>> RequestAuth<SUBJECT>.id get() = rawId as ID
@Suppress("UNCHECKED_CAST")
val <SUBJECT : HasId<*>> RequestAuth<SUBJECT>.idString: String get() = Serialization.json.encodeUnwrappingString(subject.idSerializer as KSerializer<Any?>, rawId)

suspend fun Request.authAny(): RequestAuth<*>? = this.cache(RequestAuth.Key)
suspend fun <SUBJECT : HasId<*>> Request.auth(type: AuthType): RequestAuth<SUBJECT>? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,6 @@ open class BaseAuthEndpoints<USER : HasId<ID>, ID : Comparable<ID>>(
override val idSerializer: KSerializer<ID> get() = userAccess.idSerializer
override val subjectSerializer: KSerializer<USER> get() = userAccess.serializer
override suspend fun fetch(id: ID): USER = userAccess.byId(id)

override val idProofs: Set<Authentication.ProofMethod> get() = setOf()
override val additionalProofs: Set<Authentication.ProofMethod> get() = setOf()
override suspend fun authenticate(vararg proofs: Proof): Authentication.AuthenticateResult<USER, ID>? = null

}

init {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.lightningkite.lightningserver.auth.proof

import com.lightningkite.lightningdb.HasId
import com.lightningkite.lightningserver.auth.Authentication
import com.lightningkite.lightningserver.core.ServerPath
import com.lightningkite.lightningserver.core.ServerPathGroup
Expand Down Expand Up @@ -28,19 +29,12 @@ class EmailProofEndpoints(
val emailTemplate: suspend (String, String) -> Email,
proofHasher: () -> SecureHasher = secretBasis.hasher("proof"),
val verifyEmail: suspend (String) -> Boolean = { true },
) : PinBasedProofEndpoints(path, proofHasher, pin) {
) : PinBasedProofEndpoints(path, "email", "email", proofHasher, pin) {
init {
if (path.docName == null) path.docName = "EmailProof"
Authentication.register(this)
}

override val name: String
get() = "email"
override val humanName: String
get() = "Email"
override val strength: Int
get() = 10
override val validates: String
get() = "email"
override val exampleTarget: String
get() = "[email protected]"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ class OauthProofEndpoints(
val continueUiAuthUrl: ()->String
) : ServerPathGroup(path), Authentication.ExternalProofMethod {

override val name: String
get() = provider.identifierName
override val humanName: String
get() = provider.niceName
override val strength: Int
get() = 10
override val validates: String
get() = "email"
init {
Authentication.register(this)
}

override val info: ProofMethodInfo = ProofMethodInfo(
via = provider.identifierName,
property = "email",
strength = 10
)

@Suppress("UNCHECKED_CAST")
val callback = path("callback").oauthCallback<UUID>(
Expand All @@ -53,6 +54,7 @@ class OauthProofEndpoints(
val email = profile.email ?: throw BadRequestException("No email was found for this profile.")
HttpResponse.redirectToGet(continueUiAuthUrl() + "?proof=${Serialization.json.encodeToString(Proof.serializer(), proofHasher().makeProof(
info = info,
property = "email",
value = email,
at = now()
)).encodeURLQueryComponent()}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import com.lightningkite.lightningserver.http.get
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.settings.generalSettings
import com.lightningkite.lightningserver.tasks.Tasks
Expand Down Expand Up @@ -54,14 +53,15 @@ class OneTimePasswordProofEndpoints(
if (path.docName == null) path.docName = "OneTimePasswordProof"
}

override val name: String
get() = "otp"
override val humanName: String
get() = "One-time Password"
override val validates: String
get() = "otp"
override val strength: Int
get() = 5
override val info: ProofMethodInfo = ProofMethodInfo(
via = "otp",
property = null,
strength = 10
)

init {
Authentication.register(this)
}

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

Expand Down Expand Up @@ -114,13 +114,6 @@ class OneTimePasswordProofEndpoints(
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 an One Time Password",
inputType = EstablishOtp.serializer(),
Expand Down Expand Up @@ -156,11 +149,11 @@ class OneTimePasswordProofEndpoints(
@Suppress("UNCHECKED_CAST")
prove.implementation(
AuthAndPathParts(null, null, arrayOf()),
ProofEvidence(
key(
auth.subject as Authentication.SubjectHandler<*, Comparable<Any?>>,
auth.rawId as Comparable<Any?>
), code
IdentificationAndPassword(
auth.subject.name,
"_id",
auth.idString,
code
)
)
Unit
Expand Down Expand Up @@ -200,56 +193,72 @@ class OneTimePasswordProofEndpoints(

override val prove = path("prove").post.api(
authOptions = noAuth,
summary = "Prove $validates ownership",
summary = "Prove OTP",
description = "Logs in to the given account with an OTP code. Limits to 10 attempts per hour.",
errorCases = listOf(),
examples = listOf(
ApiExample(
input = ProofEvidence(
input = IdentificationAndPassword(
"User",
"_id",
"some-id",
"000000"
),
output = Proof(
via = name,
of = validates,
strength = strength,
via = info.via,
property = "_id",
strength = info.strength,
value = "some-id",
at = now(),
signature = "opaquesignaturevalue"
)
)
),
successCode = HttpStatus.OK,
implementation = { input: ProofEvidence ->
implementation = { input: IdentificationAndPassword ->
val postedAt = now()
val cacheKey = "otp-count-${input.key}"
val cacheKey = "otp-count-${input.property}-${input.value}"
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)
val subject = input.type
val handler = Authentication.subjects.values.find { it.name == subject }
?: throw IllegalArgumentException("No subject $subject recognized")
val item = handler.findUser(input.property, input.value)
?: throw BadRequestException("User ID and code do not match")
val id = item._id

@Suppress("UNCHECKED_CAST")
val secret = table(subject).get(id as Comparable<Any>)
val secret = table(handler).get(id as Comparable<Any>)
?: throw BadRequestException("User ID and code do not match")
if (!secret.generator.isValid(
input.password,
postedAt.toJavaInstant()
)
) throw BadRequestException("User ID and code do not match")
if (!secret.active) {
val table = table(subject)
val table = table(handler)
table.updateOneById(id, modification(DataClassPathSelf(table.serializer)) {
it.active assign true
})
}
cache().remove(cacheKey)
proofHasher().makeProof(
info = info,
value = input.key,
property = input.property,
value = input.value,
at = now()
)
}
)

override suspend fun <SUBJECT : HasId<ID>, ID : Comparable<ID>> established(
handler: Authentication.SubjectHandler<SUBJECT, ID>,
item: SUBJECT
): Boolean {
@Suppress("UNCHECKED_CAST")
return table(handler).get(item._id as Comparable<Any>)?.active == true
}
suspend fun <ID : Comparable<ID>> established(handler: Authentication.SubjectHandler<*, ID>, id: ID): Boolean {
@Suppress("UNCHECKED_CAST")
return table(handler).get(id as Comparable<Any>)?.active == true
Expand Down
Loading

0 comments on commit 9d5507e

Please sign in to comment.