Skip to content

Commit

Permalink
feat: make ktor-authnz config and data polymorphically deserializable
Browse files Browse the repository at this point in the history
  • Loading branch information
waltkb committed Oct 11, 2024
1 parent 4ef8fcb commit 1d9aa06
Show file tree
Hide file tree
Showing 40 changed files with 192 additions and 125 deletions.
4 changes: 2 additions & 2 deletions waltid-libraries/auth/waltid-ktor-authnz/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dependencies {

// JWT
implementation(project(":waltid-libraries:crypto:waltid-crypto"))
implementation("com.nimbusds:nimbus-jose-jwt:9.41.1")
implementation("com.nimbusds:nimbus-jose-jwt:9.41.2")

// Ktor server
implementation("io.ktor:ktor-server-core-jvm")
Expand Down Expand Up @@ -70,7 +70,7 @@ dependencies {
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")

// Ktor server external
implementation("io.github.smiley4:ktor-swagger-ui:3.4.0")
implementation("io.github.smiley4:ktor-swagger-ui:3.5.0")

// JSON
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ data class AuthContext(
/**
* For implicit session start the auth flow is necessary
*/
val initialFlow: AuthFlow? = null
val initialFlow: AuthFlow? = null,
) {

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package id.walt.ktorauthnz.accounts

data class Account(
val id: String,
val name: String? = null
val name: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import id.walt.ktorauthnz.methods.AuthenticationMethod
import id.walt.ktorauthnz.methods.TOTP
import id.walt.ktorauthnz.methods.UserPass
import id.walt.ktorauthnz.methods.data.AuthMethodStoredData
import id.walt.ktorauthnz.methods.data.TOTPStoredData
import id.walt.ktorauthnz.methods.data.UserPassStoredData
import id.walt.ktorauthnz.sessions.AuthSession

object ExampleAccountStore : EditableAccountStore {
Expand All @@ -29,8 +31,8 @@ object ExampleAccountStore : EditableAccountStore {
val accountIdentifier = UsernameIdentifier("alice1")
addAccountIdentifierToAccount(newAccount, accountIdentifier)

addAccountStoredData(newAccount.id, UserPass to UserPass.UserPassStoredData("123456"))
addAccountStoredData(newAccount.id, TOTP to TOTP.TOTPStoredData("JBSWY3DPEHPK3PXP")) // https://totp.danhersam.com/
addAccountStoredData(newAccount.id, UserPass to UserPassStoredData("123456"))
addAccountStoredData(newAccount.id, TOTP to TOTPStoredData("JBSWY3DPEHPK3PXP")) // https://totp.danhersam.com/
}

override fun registerAccount(newAccount: Account) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ data class AuthenticationFailureException(override val message: String) : Illega
inline fun authFailure(message: String): Nothing = throw AuthenticationFailureException(message)

@OptIn(ExperimentalContracts::class)
public inline fun authCheck(value: Boolean, lazyMessage: () -> Any): Unit {
inline fun authCheck(value: Boolean, lazyMessage: () -> Any): Unit {
contract {
returns() implies value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ data class AuthFlow(

if (continueWith != null) {
check(continueWith.isNotEmpty()) { "Next flow list (`continueWith`) is empty at method $method" }
check(continueWith.methods().toSet().size == continueWith.size) { "Duplicated method in same flow in next flow list (`continueWith`) at method $method" }
check(
continueWith.methods().toSet().size == continueWith.size
) { "Duplicated method in same flow in next flow list (`continueWith`) at method $method" }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,4 @@ package id.walt.ktorauthnz.flows
/**
* Store for Flow stored data (account unspecific data)
*/
object FlowStore {



}
object FlowStore
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ abstract class AuthenticationMethod(open val id: String) {
}

inline fun <reified V : AuthMethodStoredData> lookupStoredMultiData(session: AuthSession): V {
val storedData = KtorAuthnzManager.accountStore.lookupStoredMultiDataForAccount(session, this) ?: error("No stored data for method: $id")
val storedData =
KtorAuthnzManager.accountStore.lookupStoredMultiDataForAccount(session, this) ?: error("No stored data for method: $id")
return (storedData as? V) ?: error("${storedData::class.simpleName} is not requested ${V::class.simpleName}")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import id.walt.ktorauthnz.AuthContext
import id.walt.ktorauthnz.accounts.identifiers.AccountIdentifier
import id.walt.ktorauthnz.accounts.identifiers.EmailIdentifier
import id.walt.ktorauthnz.exceptions.authCheck
import id.walt.ktorauthnz.methods.data.AuthMethodStoredData
import id.walt.ktorauthnz.methods.data.EmailPassStoredData
import id.walt.ktorauthnz.sessions.AuthSession
import id.walt.ktorauthnz.sessions.AuthSessionInformation
import io.github.smiley4.ktorswaggerui.dsl.routing.post
Expand All @@ -15,26 +15,21 @@ import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import kotlinx.serialization.Serializable

@Serializable
data class EmailPassCredentials(val email: String, val password: String)

object EmailPass : UserPassBasedAuthMethod("email", usernameName = "email") {

@Serializable
data class EmailPassStoredData(
val password: String,
) : AuthMethodStoredData

override suspend fun auth(session: AuthSession, credential: UserPasswordCredential, context: ApplicationCall): AccountIdentifier {
val identifier = EmailIdentifier(credential.name)

val storedData: EmailPassStoredData = lookupStoredData(identifier /*context()*/)

authCheck(credential.password == storedData.password) { "Invalid password" }

return identifier
return identifier
}

@Serializable
data class EmailPassCredentials(val email: String, val password: String)

override fun Route.register(authContext: PipelineContext<Unit, ApplicationCall>.() -> AuthContext) {
post("emailpass", {
request { body<EmailPassCredentials>() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,17 @@ import com.nimbusds.jose.crypto.MACVerifier
import id.walt.ktorauthnz.AuthContext
import id.walt.ktorauthnz.accounts.identifiers.JWTIdentifier
import id.walt.ktorauthnz.exceptions.authCheck
import id.walt.ktorauthnz.methods.config.AuthMethodConfiguration
import id.walt.ktorauthnz.methods.config.JwtAuthConfiguration
import id.walt.ktorauthnz.sessions.AuthSessionInformation
import io.github.smiley4.ktorswaggerui.dsl.routing.post
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import kotlinx.serialization.Serializable

object JWT : AuthenticationMethod("jwt") {

@Serializable
data class JwtAuthConfiguration(
val verifyKey: String,
val identifyClaim: String = "sub",
) : AuthMethodConfiguration

fun auth(jwt: String, config: JwtAuthConfiguration): JWTIdentifier {
// todo: handle others
val parsedJws = JWSObject.parse(jwt)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import id.walt.ktorauthnz.AuthContext
import id.walt.ktorauthnz.accounts.identifiers.AccountIdentifier
import id.walt.ktorauthnz.accounts.identifiers.LDAPIdentifier
import id.walt.ktorauthnz.exceptions.authFailure
import id.walt.ktorauthnz.methods.config.AuthMethodConfiguration
import id.walt.ktorauthnz.methods.config.LDAPConfiguration
import id.walt.ktorauthnz.sessions.AuthSession
import id.walt.ktorauthnz.sessions.AuthSessionInformation
import io.github.smiley4.ktorswaggerui.dsl.routing.post
Expand All @@ -13,19 +13,12 @@ import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import kotlinx.serialization.Serializable
import org.apache.directory.api.ldap.model.exception.LdapException
import org.apache.directory.ldap.client.api.LdapConnection
import org.apache.directory.ldap.client.api.LdapNetworkConnection

object LDAP : UserPassBasedAuthMethod("ldap") {

@Serializable
data class LDAPConfiguration(
val ldapServerUrl: String,
val userDNFormat: String,
) : AuthMethodConfiguration

override suspend fun auth(session: AuthSession, credential: UserPasswordCredential, context: ApplicationCall): AccountIdentifier {
val config = session.lookupConfiguration<LDAPConfiguration>(this)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package id.walt.ktorauthnz.methods

import id.walt.ktorauthnz.AuthContext
import id.walt.ktorauthnz.accounts.identifiers.OIDCIdentifier
import id.walt.ktorauthnz.methods.config.AuthMethodConfiguration
import id.walt.ktorauthnz.methods.config.OidcAuthConfiguration
import io.github.smiley4.ktorswaggerui.dsl.routing.route
import io.ktor.client.*
import io.ktor.client.call.*
Expand All @@ -17,7 +17,6 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.util.*
import io.ktor.util.pipeline.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
Expand All @@ -29,7 +28,6 @@ import kotlin.uuid.Uuid

object OIDC : AuthenticationMethod("oidc") {


// override val config = OidcAuthConfiguration::class

/*
Expand All @@ -44,33 +42,6 @@ object OIDC : AuthenticationMethod("oidc") {
3. Validate returned ID token, retrieve claims
*/

@Serializable
data class OidcAuthConfiguration(
val openIdConfigurationUrl: String? = null,
var openIdConfiguration: OpenIdConfiguration = OpenIdConfiguration.INVALID,

val clientId: String,
val clientSecret: String,

val accountIdentifierClaim: String = "sub"
): AuthMethodConfiguration {
fun check() {
require(openIdConfiguration != OpenIdConfiguration.INVALID || openIdConfigurationUrl != null) { "Either openIdConfiguration or openIdConfigurationUrl have to be provided!" }
}

suspend fun init() {
check()

if (openIdConfiguration == OpenIdConfiguration.INVALID) {
// TODO: move to cache
openIdConfiguration = resolveConfiguration(openIdConfigurationUrl!!)
}
}

init {
runBlocking { init() }
}
}

/**
* minimal open id configuration - unused fields are omitted, use Json with ignoreUnknownKeys = true when deserializing into this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import id.walt.ktorauthnz.AuthContext
import id.walt.ktorauthnz.accounts.identifiers.AccountIdentifier
import id.walt.ktorauthnz.accounts.identifiers.RADIUSIdentifier
import id.walt.ktorauthnz.exceptions.authCheck
import id.walt.ktorauthnz.methods.config.AuthMethodConfiguration
import id.walt.ktorauthnz.methods.config.RADIUSConfiguration
import id.walt.ktorauthnz.sessions.AuthSession
import id.walt.ktorauthnz.sessions.AuthSessionInformation
import io.github.smiley4.ktorswaggerui.dsl.routing.post
Expand All @@ -13,7 +13,6 @@ import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*
import kotlinx.serialization.Serializable
import org.aaa4j.radius.client.RadiusClient
import org.aaa4j.radius.client.clients.UdpRadiusClient
import org.aaa4j.radius.core.attribute.StringData
Expand All @@ -28,15 +27,6 @@ import java.net.InetSocketAddress

object RADIUS : UserPassBasedAuthMethod("radius") {

@Serializable
data class RADIUSConfiguration(
val radiusServerHost: String,
val radiusServerPort: Int,
val radiusServerSecret: String,
val radiusNasIdentifier: String,
) : AuthMethodConfiguration


override suspend fun auth(session: AuthSession, credential: UserPasswordCredential, context: ApplicationCall): AccountIdentifier {
val config = session.lookupConfiguration<RADIUSConfiguration>(this)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.atlassian.onetime.model.TOTPSecret
import com.atlassian.onetime.service.DefaultTOTPService
import id.walt.ktorauthnz.AuthContext
import id.walt.ktorauthnz.exceptions.authCheck
import id.walt.ktorauthnz.methods.data.AuthMethodStoredData
import id.walt.ktorauthnz.methods.data.TOTPStoredData
import id.walt.ktorauthnz.sessions.AuthSession
import id.walt.ktorauthnz.sessions.AuthSessionInformation
import io.github.smiley4.ktorswaggerui.dsl.routing.post
Expand All @@ -21,10 +21,6 @@ import kotlinx.serialization.Serializable

object TOTP : AuthenticationMethod("totp") {

@Serializable
data class TOTPStoredData(
val secret: String,
) : AuthMethodStoredData

fun auth(session: AuthSession, code: String) {
val storedData = lookupStoredMultiData<TOTPStoredData>(session /* context() */)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import id.walt.ktorauthnz.AuthContext
import id.walt.ktorauthnz.accounts.identifiers.AccountIdentifier
import id.walt.ktorauthnz.accounts.identifiers.UsernameIdentifier
import id.walt.ktorauthnz.exceptions.authCheck
import id.walt.ktorauthnz.methods.data.AuthMethodStoredData
import id.walt.ktorauthnz.methods.data.UserPassStoredData
import id.walt.ktorauthnz.sessions.AuthSession
import id.walt.ktorauthnz.sessions.AuthSessionInformation
import io.github.smiley4.ktorswaggerui.dsl.routing.post
Expand All @@ -20,10 +20,6 @@ data class UserPassCredentials(val username: String, val password: String)

object UserPass : UserPassBasedAuthMethod("userpass") {

@Serializable
data class UserPassStoredData(
val password: String,
) : AuthMethodStoredData

override suspend fun auth(session: AuthSession, credential: UserPasswordCredential, context: ApplicationCall): AccountIdentifier {
val identifier = UsernameIdentifier(credential.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ abstract class UserPassBasedAuthMethod(
ContentType.Application.FormUrlEncoded -> {
val form = receiveParameters()

val username = form[usernameName] ?: form[DEFAULT_USER_NAME] ?: error("Invalid or missing $usernameName in form post request. $EXPLANATION_MESSAGE")
val password = form[passwordName] ?: form[DEFAULT_PASSWORD_NAME] ?: error("Invalid or missing $passwordName in form post request. $EXPLANATION_MESSAGE")
val username = form[usernameName] ?: form[DEFAULT_USER_NAME]
?: error("Invalid or missing $usernameName in form post request. $EXPLANATION_MESSAGE")
val password = form[passwordName] ?: form[DEFAULT_PASSWORD_NAME]
?: error("Invalid or missing $passwordName in form post request. $EXPLANATION_MESSAGE")

return UserPasswordCredential(username, password)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package id.walt.ktorauthnz.methods
import com.nfeld.jsonpathkt.JsonPath
import com.nfeld.jsonpathkt.kotlinx.resolveAsStringOrNull
import id.walt.ktorauthnz.AuthContext
import id.walt.ktorauthnz.methods.config.AuthMethodConfiguration
import id.walt.ktorauthnz.methods.config.VerifiableCredentialAuthConfiguration
import io.github.smiley4.ktorswaggerui.dsl.routing.route
import io.ktor.client.*
import io.ktor.client.call.*
Expand All @@ -27,12 +27,6 @@ import kotlin.io.encoding.ExperimentalEncodingApi

object VerifiableCredential : AuthenticationMethod("vc") {

@Serializable
data class VerifiableCredentialAuthConfiguration(
val verification: Map<String, JsonElement>,
//val claimMappings: Map<String, String>? = null,
//val redirectUrl: String? = null,
) : AuthMethodConfiguration

// TODO:
val verifierUrl = "http://localhost:7003"
Expand Down Expand Up @@ -88,7 +82,11 @@ object Verifier {
val state: String,
)

suspend fun verify(verifierUrl: String, verificationRequest: Map<String, JsonElement>, redirectUrl: String? = null): VerificationSessionResponse {
suspend fun verify(
verifierUrl: String,
verificationRequest: Map<String, JsonElement>,
redirectUrl: String? = null,
): VerificationSessionResponse {
val response: HttpResponse = client.post("$verifierUrl/openid4vc/verify") {
setBody(verificationRequest)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package id.walt.ktorauthnz.methods.config

interface AuthMethodConfiguration
import kotlinx.serialization.Serializable

@Serializable
sealed interface AuthMethodConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package id.walt.ktorauthnz.methods.config

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
@SerialName("jwt-config")
data class JwtAuthConfiguration(
val verifyKey: String,
val identifyClaim: String = "sub",
) : AuthMethodConfiguration
Loading

0 comments on commit 1d9aa06

Please sign in to comment.