Skip to content

Commit

Permalink
Merge pull request #182 from walt-id/increase-oidc-configurability
Browse files Browse the repository at this point in the history
OIDC Unique Subject Authentication Strategy, alternative authentication header (see description for more)
  • Loading branch information
philpotisk committed Mar 4, 2024
2 parents 687a4cd + 87ba2c5 commit ed8d276
Show file tree
Hide file tree
Showing 42 changed files with 1,545 additions and 75 deletions.
8 changes: 7 additions & 1 deletion waltid-wallet-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ dependencies {
// Cache
implementation("io.github.reactivecircus.cache4k:cache4k:0.13.0")

// Webauthn
implementation("com.webauthn4j:webauthn4j-core:0.22.1.RELEASE") {
exclude("ch.qos.logback")
}

// DB
implementation("org.jetbrains.exposed:exposed-core:0.47.0")
implementation("org.jetbrains.exposed:exposed-jdbc:0.47.0")
Expand All @@ -132,7 +137,8 @@ dependencies {
//implementation("org.flywaydb:flyway-core:9.22.2")

// Web push
implementation("nl.martijndwars:web-push:5.1.1") // todo: replace with https://github.com/interaso/webpush
// implementation("dev.blanke.webpush:webpush:6.1.1") // alternative
implementation("com.interaso:webpush:1.1.1")

// Config
implementation("com.sksamuel.hoplite:hoplite-core:2.8.0.RC3")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import id.walt.webwallet.service.issuers.IssuersService
import id.walt.webwallet.service.settings.SettingsService
import id.walt.webwallet.service.settings.WalletSetting
import id.walt.webwallet.web.controllers.generateToken
import id.walt.webwallet.web.model.AccountRequest
import id.walt.webwallet.web.model.AddressAccountRequest
import id.walt.webwallet.web.model.EmailAccountRequest
import id.walt.webwallet.web.model.OidcAccountRequest
import id.walt.webwallet.web.model.*
import kotlinx.datetime.toKotlinInstant
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
Expand All @@ -35,6 +32,7 @@ object AccountsService {
is EmailAccountRequest -> EmailAccountStrategy.register(tenant, request)
is AddressAccountRequest -> Web3WalletAccountStrategy.register(tenant, request)
is OidcAccountRequest -> OidcAccountStrategy.register(tenant, request)
is OidcUniqueSubjectRequest -> OidcUniqueSubjectStrategy.register(tenant, request)
}.onSuccess { registrationResult ->
val registeredUserId = registrationResult.id

Expand Down Expand Up @@ -69,6 +67,7 @@ object AccountsService {
is EmailAccountRequest -> EmailAccountStrategy.authenticate(tenant, request)
is AddressAccountRequest -> Web3WalletAccountStrategy.authenticate(tenant, request)
is OidcAccountRequest -> OidcAccountStrategy.authenticate(tenant, request)
is OidcUniqueSubjectRequest -> OidcUniqueSubjectStrategy.authenticate(tenant, request)
}
}.fold(onSuccess = {
EventService.add(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
package id.walt.webwallet.service.account

import com.auth0.jwk.Jwk
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.interfaces.DecodedJWT
import id.walt.webwallet.db.models.Accounts
import id.walt.webwallet.db.models.OidcLogins
import id.walt.webwallet.service.OidcLoginService
import id.walt.webwallet.utils.JwkUtils
import id.walt.webwallet.utils.JwkUtils.verifyToken
import id.walt.webwallet.web.model.OidcAccountRequest
import kotlinx.datetime.Clock
import kotlinx.datetime.toJavaInstant
import kotlinx.uuid.UUID
import kotlinx.uuid.generateUUID
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction
import java.security.interfaces.ECPublicKey
import java.security.interfaces.RSAPublicKey

object OidcAccountStrategy : AccountStrategy<OidcAccountRequest>("oidc") {
override suspend fun register(tenant: String, request: OidcAccountRequest): Result<RegistrationResult> {
val jwt = verifiedToken(request.token)
val jwt = verifyToken(request.token)

if (AccountsService.hasAccountOidcId(jwt.subject)) {
throw IllegalArgumentException("Account already exists with OIDC id: ${request.token}")
Expand All @@ -46,31 +41,9 @@ object OidcAccountStrategy : AccountStrategy<OidcAccountRequest>("oidc") {
return Result.success(RegistrationResult(createdAccountId))
}

private fun verifiedToken(token: String): DecodedJWT {
val decoded = JWT.decode(token)

val verifier = JWT.require(OidcLoginService.jwkProvider.get(decoded.keyId).makeAlgorithm())
.withIssuer(OidcLoginService.oidcRealm)
.build()

return verifier.verify(decoded)!!
}

internal fun Jwk.makeAlgorithm(): Algorithm = when (algorithm) {
"RS256" -> Algorithm.RSA256(publicKey as RSAPublicKey, null)
"RS384" -> Algorithm.RSA384(publicKey as RSAPublicKey, null)
"RS512" -> Algorithm.RSA512(publicKey as RSAPublicKey, null)
"ES256" -> Algorithm.ECDSA256(publicKey as ECPublicKey, null)
"ES384" -> Algorithm.ECDSA384(publicKey as ECPublicKey, null)
"ES512" -> Algorithm.ECDSA512(publicKey as ECPublicKey, null)
null -> Algorithm.RSA256(publicKey as RSAPublicKey, null)
else -> throw IllegalArgumentException("Unsupported algorithm $algorithm")
}

override suspend fun authenticate(tenant: String, request: OidcAccountRequest): AuthenticatedUser {
println("OIDC LOGIN REQUEST: ${request.token}")

val jwt = verifiedToken(request.token)
val jwt = JwkUtils.verifyToken(request.token)

val registeredUserId = if (AccountsService.hasAccountOidcId(jwt.subject)) {
AccountsService.getAccountByOidcId(jwt.subject)!!.id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package id.walt.webwallet.service.account

import id.walt.webwallet.db.models.Accounts
import id.walt.webwallet.db.models.OidcLogins
import id.walt.webwallet.utils.JwkUtils.verifyToken
import id.walt.webwallet.web.model.OidcUniqueSubjectRequest
import kotlinx.datetime.Clock
import kotlinx.datetime.toJavaInstant
import kotlinx.uuid.UUID
import kotlinx.uuid.generateUUID
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction

object OidcUniqueSubjectStrategy : AccountStrategy<OidcUniqueSubjectRequest>("oidc-unique-subject") {
override suspend fun register(tenant: String, request: OidcUniqueSubjectRequest): Result<RegistrationResult> {
val jwt = verifyToken(request.token)
val sub = jwt.subject

if (AccountsService.hasAccountOidcId(sub)) {
throw IllegalArgumentException("Account already exists with OIDC id: ${request.token}")
}

val createdAccountId = transaction {
val accountId = Accounts.insert {
it[Accounts.tenant] = tenant
it[id] = UUID.generateUUID()
it[name] = sub
it[email] = sub
it[createdOn] = Clock.System.now().toJavaInstant()
}[Accounts.id]

OidcLogins.insert {
it[OidcLogins.tenant] = tenant
it[OidcLogins.accountId] = accountId
it[oidcId] = jwt.subject
}

accountId
}

return Result.success(RegistrationResult(createdAccountId))
}

override suspend fun authenticate(tenant: String, request: OidcUniqueSubjectRequest): AuthenticatedUser {
val jwt = verifyToken(request.token)

val registeredUserId = if (AccountsService.hasAccountOidcId(jwt.subject)) {
AccountsService.getAccountByOidcId(jwt.subject)!!.id
} else {
AccountsService.register(tenant, request).getOrThrow().id
}
return AuthenticatedUser(registeredUserId, jwt.subject)
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package id.walt.webwallet.service.push

import com.interaso.webpush.VapidKeys
import com.interaso.webpush.WebPushService
import id.walt.webwallet.config.ConfigManager
import id.walt.webwallet.config.PushConfig
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import nl.martijndwars.webpush.Notification
import nl.martijndwars.webpush.PushAsyncService

object PushManager {

val pushConfig by lazy { ConfigManager.getConfig<PushConfig>() }
private val log = KotlinLogging.logger { }

val pushService = PushAsyncService(
pushConfig.pushPublicKey, pushConfig.pushPrivateKey.value, "mailto:dev@walt.id"
)
private val pushConfig by lazy { ConfigManager.getConfig<PushConfig>() }

// subject: "mailto:dev@walt.id"
private val pushService = WebPushService(pushConfig.pushSubject, VapidKeys.create(pushConfig.pushPublicKey, pushConfig.pushPrivateKey.value))

val subscriptions = ArrayList<Subscription>()

Expand All @@ -31,7 +33,9 @@ object PushManager {
val payload = Json.encodeToString(data).toByteArray()

subscriptions.forEach { subscription ->
val notification = Notification(
val res = pushService.send(payload, subscription.endpoint, subscription.userPublicKey().encoded, subscription.authAsBytes())
log.debug { "Push result for ${subscription.endpoint} (${subscription.userPublicKey().encoded}): $res" }
/*val notification = Notification(
subscription.endpoint,
subscription.userPublicKey(),
subscription.authAsBytes(),
Expand All @@ -40,7 +44,9 @@ object PushManager {
val pushResponse = pushService.send(notification)
.get()
println("Push send response: $pushResponse")
*/
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package id.walt.webwallet.service.webauthn

import com.webauthn4j.WebAuthnManager
import com.webauthn4j.authenticator.Authenticator
import com.webauthn4j.authenticator.AuthenticatorImpl
import com.webauthn4j.converter.exception.DataConversionException
import com.webauthn4j.data.*
import com.webauthn4j.data.client.Origin
import com.webauthn4j.data.client.challenge.Challenge
import com.webauthn4j.data.client.challenge.DefaultChallenge
import com.webauthn4j.server.ServerProperty
import com.webauthn4j.validator.exception.ValidationException


object WebauthnService {

private val webAuthnManager = WebAuthnManager.createNonStrictWebAuthnManager()

fun register() {
val randomChallenge = DefaultChallenge()
}

fun attestationVerification() {
// Client properties
val attestationObject: ByteArray? = null /* set attestationObject */
val clientDataJSON: ByteArray? = null /* set clientDataJSON */
val clientExtensionJSON: String? = null /* set clientExtensionJSON */
val transports: Set<String>? = null /* set transports */


// Server properties
val origin: Origin? = null /* set origin */
val rpId: String? = null /* set rpId */
val challenge: Challenge? = null /* set challenge */


val tokenBindingId: ByteArray? = null /* set tokenBindingId */
val serverProperty: ServerProperty = ServerProperty(origin!!, rpId!!, challenge, tokenBindingId)


// expectations
val userVerificationRequired = false
val userPresenceRequired = true

val registrationRequest = RegistrationRequest(attestationObject, clientDataJSON, clientExtensionJSON, transports)
val registrationParameters = RegistrationParameters(serverProperty, null, userVerificationRequired, userPresenceRequired)
val registrationData: RegistrationData

try {
registrationData = webAuthnManager.parse(registrationRequest)
} catch (e: DataConversionException) {
// If you would like to handle WebAuthn data structure parse error, please catch DataConversionException
throw e
}

try {
webAuthnManager.validate(registrationData, registrationParameters)
} catch (e: ValidationException) {
// If you would like to handle WebAuthn data validation error, please catch ValidationException
throw e
}


// please persist Authenticator object, which will be used in the authentication process.
val authenticator: Authenticator =
AuthenticatorImpl( // You may create your own Authenticator implementation to save friendly authenticator name
registrationData.attestationObject!!.authenticatorData.attestedCredentialData!!,
registrationData.attestationObject!!.attestationStatement,
registrationData.attestationObject!!.authenticatorData.signCount
)

save(authenticator) // please persist authenticator in your manner
}

private val TEMPORARY_STORE = HashMap<ByteArray, Authenticator>()

fun save(authenticator: Authenticator) {
TEMPORARY_STORE[authenticator.attestedCredentialData.credentialId] = authenticator
}

fun load(credentialId: ByteArray): Authenticator? {
return TEMPORARY_STORE[credentialId]
}

fun updateCounter(credentialId: ByteArray, signCount: Long) {
TEMPORARY_STORE[credentialId]!!.counter = signCount
}

fun assertionVerification() {
// Client properties
val credentialId: ByteArray? = null /* set credentialId */
val userHandle: ByteArray? = null /* set userHandle */
val authenticatorData: ByteArray? = null /* set authenticatorData */
val clientDataJSON: ByteArray? = null /* set clientDataJSON */
val clientExtensionJSON: String? = null /* set clientExtensionJSON */
val signature: ByteArray? = null /* set signature */


// Server properties
val origin: Origin? = null /* set origin */
val rpId: String? = null /* set rpId */
val challenge: Challenge? = null /* set challenge */
val tokenBindingId: ByteArray? = null /* set tokenBindingId */
val serverProperty = ServerProperty(origin!!, rpId!!, challenge, tokenBindingId)


// expectations
val allowCredentials: List<ByteArray>? = null
val userVerificationRequired = true
val userPresenceRequired = true

val authenticator: Authenticator =
load(credentialId!!)!! // please load authenticator object persisted in the registration process in your manner

val authenticationRequest =
AuthenticationRequest(
credentialId,
userHandle,
authenticatorData,
clientDataJSON,
clientExtensionJSON,
signature
)
val authenticationParameters =
AuthenticationParameters(
serverProperty,
authenticator,
allowCredentials,
userVerificationRequired,
userPresenceRequired
)

val authenticationData: AuthenticationData
try {
authenticationData = webAuthnManager.parse(authenticationRequest)
} catch (e: DataConversionException) {
// If you would like to handle WebAuthn data structure parse error, please catch DataConversionException
throw e
}
try {
webAuthnManager.validate(authenticationData, authenticationParameters)
} catch (e: ValidationException) {
// If you would like to handle WebAuthn data validation error, please catch ValidationException
throw e
}

// please update the counter of the authenticator record
updateCounter(
authenticationData.credentialId,
authenticationData.authenticatorData!!.signCount
)
}
}
Loading

0 comments on commit ed8d276

Please sign in to comment.