Skip to content

Implement Webauthn #8393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 113 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
cd974e6
add WebAuthn credentials to database schema
robert-oleynik Feb 11, 2025
47f52ac
setup WebAuthnCredentials DAO
robert-oleynik Feb 11, 2025
c522883
Implement WebAuthn backend
robert-oleynik Feb 12, 2025
8938d9e
add frontend webauthn support lib
Feb 12, 2025
f5130f6
use proper file extension in some files
Feb 12, 2025
09afa8a
WIP: Add passkey management page
Feb 12, 2025
86260d8
WIP: add some frontend part for passkey registration
Feb 13, 2025
1fe36d9
setup tls proxy
robert-oleynik Feb 18, 2025
391ec47
fix webauthn registration start
robert-oleynik Feb 18, 2025
aba3d99
fix webauthn registration finalize
robert-oleynik Feb 18, 2025
4f03669
fix webauthn auth start
robert-oleynik Feb 18, 2025
b9014a0
fix key id and authentication process
robert-oleynik Feb 18, 2025
3447565
fix frontend redirect
robert-oleynik Feb 18, 2025
3cf61e6
restyle login form
robert-oleynik Feb 20, 2025
dcff035
move user key id to separate datbase field
robert-oleynik Feb 20, 2025
898abda
fix authentication failures
robert-oleynik Feb 20, 2025
f68ab95
display and remove webauthn keys
robert-oleynik Feb 20, 2025
7ba0238
wrap blocking calls
robert-oleynik Feb 20, 2025
dc53bd0
increment schema version
robert-oleynik Feb 20, 2025
dcf027d
fix schema versioning
robert-oleynik Feb 20, 2025
a42a10d
fix schema field typo
robert-oleynik Feb 20, 2025
636de42
add reversion for database evolution
robert-oleynik Feb 20, 2025
5069e9f
fix compiler errors
robert-oleynik Feb 20, 2025
ff9fb7b
fix future box handling
robert-oleynik Feb 20, 2025
23a9591
fix frontend lints
robert-oleynik Feb 20, 2025
7592b13
fix api usage
robert-oleynik Feb 20, 2025
96cf3d6
add trailing ;
robert-oleynik Feb 20, 2025
ab95204
apply format to backend
robert-oleynik Feb 20, 2025
bce5a1d
fix uri
robert-oleynik Feb 20, 2025
e28d477
fix typecheck errors
robert-oleynik Feb 20, 2025
6838401
fixed frontend
robert-oleynik Feb 20, 2025
ce831fb
read origin from configuration
robert-oleynik Feb 25, 2025
dbccd1f
apply format
robert-oleynik Feb 25, 2025
e652f78
fix future exception handling
robert-oleynik Feb 25, 2025
3bdd978
rename PassKeys to Passkeys and add missing await
robert-oleynik Feb 25, 2025
f7734e4
merge manage passkeys and change password view
robert-oleynik Feb 26, 2025
c827ee5
fix frontend
robert-oleynik Feb 26, 2025
01198f5
catch WebAuthn exception on log in
robert-oleynik Feb 26, 2025
8a495fd
minor fixes
robert-oleynik Feb 26, 2025
d263bec
fix frontend
robert-oleynik Feb 26, 2025
69885d7
rework webAuthnRegistrationStart
robert-oleynik Mar 26, 2025
9987c58
add json format for WebAuthn types
robert-oleynik Mar 27, 2025
699ac9d
setup webauthn registration
robert-oleynik Mar 27, 2025
9157c18
Add webauthn4j
robert-oleynik Apr 9, 2025
e65a551
Merge branch 'master' into webauthn2
robert-oleynik Apr 10, 2025
8e4771e
Merge branch 'master' into webauthn2
robert-oleynik Apr 10, 2025
881df01
Merge branch 'webauthn2' of github.com:scalableminds/webknossos into …
robert-oleynik Apr 10, 2025
7b95eb9
Add WebAuthnPublicKeyCredentialRequestOptions
robert-oleynik Apr 10, 2025
a6ac15f
rework webauthn assertion
robert-oleynik Apr 15, 2025
5c53f6d
remove old webauthn dependencies
robert-oleynik Apr 15, 2025
6cd7b7f
Merge branch 'master' into webauthn2
robert-oleynik Apr 15, 2025
1c16b1a
Merge branch 'master' into webauthn2
robert-oleynik May 27, 2025
35db834
fix merge artifacts
robert-oleynik May 27, 2025
8814f2f
fix registration flow
robert-oleynik May 28, 2025
bf2fa71
move encoding to backend
robert-oleynik Jun 3, 2025
9034377
fix frontend
robert-oleynik Jun 3, 2025
da5c092
apply review comments
robert-oleynik Jun 4, 2025
4663fee
Merge branch 'master' into webauthn2
robert-oleynik Jun 4, 2025
50b830a
fix frontend
robert-oleynik Jun 4, 2025
1f1b0b3
apply some fixes
robert-oleynik Jun 4, 2025
2c32d15
format and lint backend
robert-oleynik Jun 4, 2025
fa78c96
use CollectionConverters
robert-oleynik Jun 4, 2025
48b5f83
apply format
robert-oleynik Jun 4, 2025
5dd64dc
fix application conf
robert-oleynik Jun 4, 2025
c449e89
add coderabbit suggestions
robert-oleynik Jun 4, 2025
0b8e657
apply suggestions
robert-oleynik Jun 10, 2025
8cf40ca
fix lints
robert-oleynik Jun 11, 2025
8225850
fix types
robert-oleynik Jun 11, 2025
7e9fa24
apply format links
robert-oleynik Jun 11, 2025
8caa46b
fix start-tls webpack
robert-oleynik Jun 11, 2025
290b60f
improve login form
robert-oleynik Jun 11, 2025
37139e2
proxy.js improve packed args array
robert-oleynik Jun 11, 2025
a97b217
improve change password view
robert-oleynik Jun 11, 2025
60e3162
create WebAuthnCredentials view
robert-oleynik Jun 11, 2025
48a0ccd
fix format
robert-oleynik Jun 11, 2025
66e3db7
pass webauthn credential id in url for deletion
robert-oleynik Jun 11, 2025
1292025
add option to disable passkeys
robert-oleynik Jun 17, 2025
566d037
fix types
robert-oleynik Jun 17, 2025
98d638a
useEffect run once
robert-oleynik Jun 17, 2025
435beb2
disable passkeys if feature is missing
robert-oleynik Jun 17, 2025
828e6ea
update snapshots
robert-oleynik Jun 17, 2025
c6b704b
update docs, changelog and migrations
robert-oleynik Jun 17, 2025
22aeaed
fix feature toggle
robert-oleynik Jun 17, 2025
f4f8e19
add missing authentication failed
robert-oleynik Jun 17, 2025
a0280ec
validate sign count
robert-oleynik Jun 18, 2025
7edc631
use webauthn timeout for assertion store
robert-oleynik Jun 18, 2025
0507ef2
Merge branch 'master' into webauthn2
robert-oleynik Jun 18, 2025
428ffcc
Set cookie SameSite policy to strict
robert-oleynik Jun 18, 2025
92e1d69
Set cookie SameSite policy to strict
robert-oleynik Jun 18, 2025
e2fb57d
apply suggestions
robert-oleynik Jul 1, 2025
b71c7cd
fix remove webauthn key
robert-oleynik Jul 1, 2025
bc91c41
Merge branch 'master' into webauthn2
robert-oleynik Jul 1, 2025
2fca534
cleanup merge artificats
robert-oleynik Jul 1, 2025
560d107
fix build and format issues
robert-oleynik Jul 1, 2025
3b12222
fix evolution name
robert-oleynik Jul 1, 2025
70a702c
fix dependency overrides
robert-oleynik Jul 1, 2025
1434c02
update snapshots
robert-oleynik Jul 1, 2025
2eb5c53
apply suggestions and fix comments
robert-oleynik Jul 8, 2025
065fd1e
fix passkeys view and authentication process
robert-oleynik Jul 8, 2025
4b11e93
enforce HTTPS connection for passkeys
robert-oleynik Jul 8, 2025
b839a2b
Merge branch 'master' into webauthn2
robert-oleynik Jul 8, 2025
7070012
fix frontend
robert-oleynik Jul 8, 2025
9d6956c
fix tsx extension
robert-oleynik Jul 9, 2025
f321d1c
obfuscate authentication errors
robert-oleynik Jul 9, 2025
f59e16f
serialize attestation statement and some flags
robert-oleynik Jul 9, 2025
a983204
apply suggestions
robert-oleynik Jul 9, 2025
eba46f8
format Fox to scala
robert-oleynik Jul 9, 2025
dea2135
Merge branch 'master' into webauthn2
robert-oleynik Jul 9, 2025
4fac665
apply suggestions
robert-oleynik Jul 15, 2025
5f7b577
Merge branch 'webauthn2' of github.com:scalableminds/webknossos into …
robert-oleynik Jul 15, 2025
60ef914
update webauthn4j
robert-oleynik Jul 15, 2025
766ea06
format backend
robert-oleynik Jul 15, 2025
c64692f
fix application.conf
robert-oleynik Jul 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions app/controllers/AuthenticationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,8 @@ class AuthenticationController @Inject()(
.toFox ?~> "Timeout during authentication. Please try again." ~> UNAUTHORIZED
authData <- tryo(webAuthnManager.parseAuthenticationResponseJSON(Json.stringify(request.body.key))).toFox ??~> "Passkey Authentication Failed" ~> UNAUTHORIZED
credentialId = authData.getCredentialId
multiUserId <- ObjectId.fromString(new String(authData.getUserHandle)) ??~> "Passkey Authentication Failed" ~> UNAUTHORIZED
multiUserId <- ObjectId
.fromString(new String(authData.getUserHandle)) ??~> "Passkey Authentication Failed" ~> UNAUTHORIZED
multiUser <- multiUserDAO
.findOneById(multiUserId)(GlobalAccessContext) ??~> "Passkey Authentication Failed" ~> UNAUTHORIZED
credential <- webAuthnCredentialDAO
Expand Down Expand Up @@ -709,18 +710,13 @@ class AuthenticationController @Inject()(
new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, COSEAlgorithmIdentifier.create(k.alg)))
registrationParams = new RegistrationParameters(serverProperty, publicKeyParams.toList.asJava, false, true)
_ <- tryo(webAuthnManager.verify(registrationData, registrationParams)).toFox
attestation = registrationData.getAttestationObject
attestationObject = registrationData.getAttestationObject
_ = println(attestationObject)
credentialRecord = new WebAuthnCredentialRecord(
attestation.getAttestationStatement,
null, // User Verification not used
null, // Backup not used
null, // Backup not used
0, // Counter is initially 0
attestation.getAuthenticatorData.getAttestedCredentialData,
attestation.getAuthenticatorData.getExtensions,
null, // We do not need the challenge
null, // Client extensions are not used
null // Transports are not used
attestationObject,
null, // clientData - Client data is not collected.
null, // clientExtensions - Client extensions are ignored.
null // transports - All transports are allowed.
)
_ = registrationData
credential = WebAuthnCredential(
Expand Down
76 changes: 60 additions & 16 deletions app/models/user/WebAuthnCredentials.scala
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package models.user

import com.fasterxml.jackson.core.`type`.TypeReference
import com.fasterxml.jackson.annotation._
import com.scalableminds.util.accesscontext.DBAccessContext
import com.scalableminds.util.objectid.ObjectId
import com.scalableminds.util.tools.Fox
import com.scalableminds.util.tools.{JsonHelper, Fox, FoxImplicits}
import com.scalableminds.webknossos.schema.Tables._
import com.webauthn4j.converter.AttestedCredentialDataConverter
import com.webauthn4j.converter.util.ObjectConverter
import com.webauthn4j.credential.{CredentialRecordImpl => WebAuthnCredentialRecord}
import com.webauthn4j.data.attestation.statement.NoneAttestationStatement
import com.webauthn4j.data.attestation.statement._
import com.webauthn4j.data.extension.authenticator.{
AuthenticationExtensionsAuthenticatorOutputs,
RegistrationExtensionAuthenticatorOutput
}
import com.scalableminds.util.tools.Box.tryo
import slick.lifted.Rep
import utils.sql.{SQLDAO, SqlClient}
import play.api.libs.json._

import javax.inject.Inject
import scala.concurrent.ExecutionContext
Expand All @@ -26,14 +28,48 @@ case class WebAuthnCredential(
name: String,
credentialRecord: WebAuthnCredentialRecord,
isDeleted: Boolean,
) {
) extends FoxImplicits {
def serializeAttestedCredential(objectConverter: ObjectConverter): Array[Byte] = {
val converter = new AttestedCredentialDataConverter(objectConverter);
converter.convert(credentialRecord.getAttestedCredentialData)
}

def serializedExtensions(converter: ObjectConverter): String =
converter.getJsonConverter.writeValueAsString(credentialRecord.getAuthenticatorExtensions)
def serializeAttestationStatement(objectConverter: ObjectConverter)(implicit ec: ExecutionContext): Fox[JsObject] = {
val envelope = new AttestationStatementEnvelope()
envelope.fmt = credentialRecord.getAttestationStatement.getFormat
envelope.attestationStatement = credentialRecord.getAttestationStatement
val rawJson = objectConverter.getJsonConverter.writeValueAsString(envelope)
JsonHelper.parseAs[JsObject](rawJson).toFox
}

def serializedExtensions(converter: ObjectConverter)(implicit ec: ExecutionContext): Fox[JsObject] = {
val rawJson = converter.getJsonConverter.writeValueAsString(credentialRecord.getAuthenticatorExtensions)
JsonHelper.parseAs[JsObject](rawJson).toFox
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
class AttestationStatementEnvelope {

@JsonProperty("fmt")
var fmt: String = _

@JsonProperty("attestationStatement")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "fmt")
@JsonSubTypes(
Array(
new JsonSubTypes.Type(value = classOf[NoneAttestationStatement], name = "none"),
new JsonSubTypes.Type(value = classOf[PackedAttestationStatement], name = "packed"),
new JsonSubTypes.Type(value = classOf[AndroidKeyAttestationStatement], name = "android-key"),
new JsonSubTypes.Type(value = classOf[AndroidSafetyNetAttestationStatement], name = "android-safetynet"),
new JsonSubTypes.Type(value = classOf[AppleAnonymousAttestationStatement], name = "apple"),
new JsonSubTypes.Type(value = classOf[FIDOU2FAttestationStatement], name = "fido-u2f"),
new JsonSubTypes.Type(value = classOf[TPMAttestationStatement], name = "tpm")
))
var attestationStatement: AttestationStatement = _

def getFormat: String = fmt
def getAttestationStatement: AttestationStatement = attestationStatement
}

class WebAuthnCredentialDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext)
Expand All @@ -53,17 +89,19 @@ class WebAuthnCredentialDAO @Inject()(sqlClient: SqlClient)(implicit ec: Executi
converter.readValue(r.serializedextensions,
new TypeReference[AuthenticationExtensionsAuthenticatorOutputs[
RegistrationExtensionAuthenticatorOutput]] {})).toFox
attestationStatement <- tryo(
converter.readValue(r.serializedattestationstatement, new TypeReference[AttestationStatementEnvelope] {})).toFox
record = new WebAuthnCredentialRecord(
new NoneAttestationStatement(),
null, // attestationData - document why null is safe
null, // clientData - document why null is safe
null, // clientDataJSON - document why null is safe
attestationStatement.getAttestationStatement,
r.userverified,
r.backupeligible,
r.backupstate,
r.signaturecount.toLong,
attestedCredential,
authenticatorExtensions,
null, // attestationObject - document why null is safe
null, // transports - document why null is safe
null // largeBlob - document why null is safe
null, // clientData - No client data is collected during registration.
null, // clientExtensions - Client extensions are ignored.
null // transports - All transport methods are allowed.
)
} yield WebAuthnCredential(ObjectId(r._Id), ObjectId(r._Multiuser), r.name, record, r.isdeleted)
}
Expand All @@ -90,13 +128,19 @@ class WebAuthnCredentialDAO @Inject()(sqlClient: SqlClient)(implicit ec: Executi
def insertOne(c: WebAuthnCredential): Fox[Unit] = {
val converter = new ObjectConverter()
val serializedAttestedCredential = c.serializeAttestedCredential(converter)
val serializedAuthenticatorExtensions = c.serializedExtensions(converter)
val credentialId = c.credentialRecord.getAttestedCredentialData.getCredentialId
val userVerified = c.credentialRecord.isUvInitialized.booleanValue
val backupEligible = c.credentialRecord.isBackupEligible.booleanValue
val backupState = c.credentialRecord.isBackedUp.booleanValue
for {
serializedAuthenticatorExtensions <- c.serializedExtensions(converter)
attestationStatement <- c.serializeAttestationStatement(converter)
_ = println(attestationStatement)
_ <- run(
q"""INSERT INTO $existingCollectionName (_id, _multiUser, credentialId, name, serializedAttestedCredential, serializedExtensions, signatureCount)
VALUES(${c._id}, ${c._multiUser}, ${credentialId}, ${c.name}, ${serializedAttestedCredential},
${serializedAuthenticatorExtensions}, ${c.credentialRecord.getCounter.toInt})""".asUpdate)
q"""INSERT INTO $existingCollectionName (_id, _multiUser, credentialId, name, userVerified, backupEligible, backupState,
serializedAttestationStatement, serializedAttestedCredential, serializedExtensions, signatureCount)
VALUES(${c._id}, ${c._multiUser}, ${credentialId}, ${c.name}, ${userVerified}, ${backupEligible}, ${backupState}, ${attestationStatement},
${serializedAttestedCredential}, ${serializedAuthenticatorExtensions}, ${c.credentialRecord.getCounter.toInt})""".asUpdate)
} yield ()
}

Expand Down
6 changes: 5 additions & 1 deletion conf/evolutions/136-add-webauthn-credentials.sql
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ CREATE TABLE webknossos.webauthnCredentials(
_multiUser TEXT NOT NULL,
credentialId BYTEA NOT NULL,
name TEXT NOT NULL,
userVerified BOOLEAN NOT NULL,
backupEligible BOOLEAN NOT NULL,
backupState BOOLEAN NOT NULL,
serializedAttestationStatement JSONB NOT NULL,
serializedAttestedCredential BYTEA NOT NULL,
serializedExtensions TEXT NOT NULL,
serializedExtensions JSONB NOT NULL,
signatureCount INTEGER NOT NULL,
isDeleted BOOLEAN NOT NULL DEFAULT false,
UNIQUE (_id, credentialId)
Expand Down
6 changes: 5 additions & 1 deletion tools/postgres/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,12 @@ CREATE TABLE webknossos.webauthnCredentials(
_multiUser TEXT NOT NULL,
credentialId BYTEA NOT NULL,
name TEXT NOT NULL,
userVerified BOOLEAN NOT NULL,
backupEligible BOOLEAN NOT NULL,
backupState BOOLEAN NOT NULL,
serializedAttestationStatement JSONB NOT NULL,
serializedAttestedCredential BYTEA NOT NULL,
serializedExtensions TEXT NOT NULL,
serializedExtensions JSONB NOT NULL,
signatureCount INTEGER NOT NULL,
isDeleted BOOLEAN NOT NULL DEFAULT false,
UNIQUE (_id, credentialId)
Expand Down
4 changes: 4 additions & 0 deletions util/src/main/scala/com/scalableminds/util/tools/Fox.scala
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ class Fox[+A](val futureBox: Future[Box[A]])(implicit ec: ExecutionContext) {
Fox.fromFutureBox {
futureBox.map {
case Full(value) => Full(value)
case f: Failure => {
println(f)
Failure(s)
}
case _ => Failure(s)
}
}
Expand Down