-
Notifications
You must be signed in to change notification settings - Fork 29
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
robert-oleynik
wants to merge
113
commits into
master
Choose a base branch
from
webauthn2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Implement Webauthn #8393
Changes from 112 commits
Commits
Show all changes
113 commits
Select commit
Hold shift + click to select a range
cd974e6
add WebAuthn credentials to database schema
robert-oleynik 47f52ac
setup WebAuthnCredentials DAO
robert-oleynik c522883
Implement WebAuthn backend
robert-oleynik 8938d9e
add frontend webauthn support lib
f5130f6
use proper file extension in some files
09afa8a
WIP: Add passkey management page
86260d8
WIP: add some frontend part for passkey registration
1fe36d9
setup tls proxy
robert-oleynik 391ec47
fix webauthn registration start
robert-oleynik aba3d99
fix webauthn registration finalize
robert-oleynik 4f03669
fix webauthn auth start
robert-oleynik b9014a0
fix key id and authentication process
robert-oleynik 3447565
fix frontend redirect
robert-oleynik 3cf61e6
restyle login form
robert-oleynik dcff035
move user key id to separate datbase field
robert-oleynik 898abda
fix authentication failures
robert-oleynik f68ab95
display and remove webauthn keys
robert-oleynik 7ba0238
wrap blocking calls
robert-oleynik dc53bd0
increment schema version
robert-oleynik dcf027d
fix schema versioning
robert-oleynik a42a10d
fix schema field typo
robert-oleynik 636de42
add reversion for database evolution
robert-oleynik 5069e9f
fix compiler errors
robert-oleynik ff9fb7b
fix future box handling
robert-oleynik 23a9591
fix frontend lints
robert-oleynik 7592b13
fix api usage
robert-oleynik 96cf3d6
add trailing ;
robert-oleynik ab95204
apply format to backend
robert-oleynik bce5a1d
fix uri
robert-oleynik e28d477
fix typecheck errors
robert-oleynik 6838401
fixed frontend
robert-oleynik ce831fb
read origin from configuration
robert-oleynik dbccd1f
apply format
robert-oleynik e652f78
fix future exception handling
robert-oleynik 3bdd978
rename PassKeys to Passkeys and add missing await
robert-oleynik f7734e4
merge manage passkeys and change password view
robert-oleynik c827ee5
fix frontend
robert-oleynik 01198f5
catch WebAuthn exception on log in
robert-oleynik 8a495fd
minor fixes
robert-oleynik d263bec
fix frontend
robert-oleynik 69885d7
rework webAuthnRegistrationStart
robert-oleynik 9987c58
add json format for WebAuthn types
robert-oleynik 699ac9d
setup webauthn registration
robert-oleynik 9157c18
Add webauthn4j
robert-oleynik e65a551
Merge branch 'master' into webauthn2
robert-oleynik 8e4771e
Merge branch 'master' into webauthn2
robert-oleynik 881df01
Merge branch 'webauthn2' of github.com:scalableminds/webknossos into …
robert-oleynik 7b95eb9
Add WebAuthnPublicKeyCredentialRequestOptions
robert-oleynik a6ac15f
rework webauthn assertion
robert-oleynik 5c53f6d
remove old webauthn dependencies
robert-oleynik 6cd7b7f
Merge branch 'master' into webauthn2
robert-oleynik 1c16b1a
Merge branch 'master' into webauthn2
robert-oleynik 35db834
fix merge artifacts
robert-oleynik 8814f2f
fix registration flow
robert-oleynik bf2fa71
move encoding to backend
robert-oleynik 9034377
fix frontend
robert-oleynik da5c092
apply review comments
robert-oleynik 4663fee
Merge branch 'master' into webauthn2
robert-oleynik 50b830a
fix frontend
robert-oleynik 1f1b0b3
apply some fixes
robert-oleynik 2c32d15
format and lint backend
robert-oleynik fa78c96
use CollectionConverters
robert-oleynik 48b5f83
apply format
robert-oleynik 5dd64dc
fix application conf
robert-oleynik c449e89
add coderabbit suggestions
robert-oleynik 0b8e657
apply suggestions
robert-oleynik 8cf40ca
fix lints
robert-oleynik 8225850
fix types
robert-oleynik 7e9fa24
apply format links
robert-oleynik 8caa46b
fix start-tls webpack
robert-oleynik 290b60f
improve login form
robert-oleynik 37139e2
proxy.js improve packed args array
robert-oleynik a97b217
improve change password view
robert-oleynik 60e3162
create WebAuthnCredentials view
robert-oleynik 48a0ccd
fix format
robert-oleynik 66e3db7
pass webauthn credential id in url for deletion
robert-oleynik 1292025
add option to disable passkeys
robert-oleynik 566d037
fix types
robert-oleynik 98d638a
useEffect run once
robert-oleynik 435beb2
disable passkeys if feature is missing
robert-oleynik 828e6ea
update snapshots
robert-oleynik c6b704b
update docs, changelog and migrations
robert-oleynik 22aeaed
fix feature toggle
robert-oleynik f4f8e19
add missing authentication failed
robert-oleynik a0280ec
validate sign count
robert-oleynik 7edc631
use webauthn timeout for assertion store
robert-oleynik 0507ef2
Merge branch 'master' into webauthn2
robert-oleynik 428ffcc
Set cookie SameSite policy to strict
robert-oleynik 92e1d69
Set cookie SameSite policy to strict
robert-oleynik e2fb57d
apply suggestions
robert-oleynik b71c7cd
fix remove webauthn key
robert-oleynik bc91c41
Merge branch 'master' into webauthn2
robert-oleynik 2fca534
cleanup merge artificats
robert-oleynik 560d107
fix build and format issues
robert-oleynik 3b12222
fix evolution name
robert-oleynik 70a702c
fix dependency overrides
robert-oleynik 1434c02
update snapshots
robert-oleynik 2eb5c53
apply suggestions and fix comments
robert-oleynik 065fd1e
fix passkeys view and authentication process
robert-oleynik 4b11e93
enforce HTTPS connection for passkeys
robert-oleynik b839a2b
Merge branch 'master' into webauthn2
robert-oleynik 7070012
fix frontend
robert-oleynik 9d6956c
fix tsx extension
robert-oleynik f321d1c
obfuscate authentication errors
robert-oleynik f59e16f
serialize attestation statement and some flags
robert-oleynik a983204
apply suggestions
robert-oleynik eba46f8
format Fox to scala
robert-oleynik dea2135
Merge branch 'master' into webauthn2
robert-oleynik 4fac665
apply suggestions
robert-oleynik 5f7b577
Merge branch 'webauthn2' of github.com:scalableminds/webknossos into …
robert-oleynik 60ef914
update webauthn4j
robert-oleynik 766ea06
format backend
robert-oleynik c64692f
fix application.conf
robert-oleynik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
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.{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._ | ||
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 | ||
|
||
case class WebAuthnCredential( | ||
_id: ObjectId, | ||
_multiUser: ObjectId, | ||
name: String, | ||
credentialRecord: WebAuthnCredentialRecord, | ||
isDeleted: Boolean, | ||
) extends FoxImplicits { | ||
def serializeAttestedCredential(objectConverter: ObjectConverter): Array[Byte] = { | ||
val converter = new AttestedCredentialDataConverter(objectConverter); | ||
converter.convert(credentialRecord.getAttestedCredentialData) | ||
} | ||
|
||
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) | ||
extends SQLDAO[WebAuthnCredential, WebauthncredentialsRow, Webauthncredentials](sqlClient) { | ||
protected val collection = Webauthncredentials | ||
|
||
override protected def idColumn(x: Webauthncredentials): Rep[String] = x._Id | ||
override protected def isDeletedColumn(x: Webauthncredentials): Rep[Boolean] = x.isdeleted | ||
|
||
protected def parse(r: WebauthncredentialsRow): Fox[WebAuthnCredential] = { | ||
val objectConverter = new ObjectConverter() | ||
val converter = objectConverter.getJsonConverter | ||
val attestedCredentialDataConverter = new AttestedCredentialDataConverter(objectConverter) | ||
for { | ||
attestedCredential <- tryo(attestedCredentialDataConverter.convert(r.serializedattestedcredential)).toFox | ||
authenticatorExtensions <- tryo( | ||
converter.readValue(r.serializedextensions, | ||
new TypeReference[AuthenticationExtensionsAuthenticatorOutputs[ | ||
RegistrationExtensionAuthenticatorOutput]] {})).toFox | ||
attestationStatement <- tryo( | ||
converter.readValue(r.serializedattestationstatement, new TypeReference[AttestationStatementEnvelope] {})).toFox | ||
record = new WebAuthnCredentialRecord( | ||
attestationStatement.getAttestationStatement, | ||
r.userverified, | ||
r.backupeligible, | ||
r.backupstate, | ||
r.signaturecount.toLong, | ||
attestedCredential, | ||
authenticatorExtensions, | ||
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) | ||
} | ||
|
||
def findAllForUser(multiUserId: ObjectId)(implicit ctx: DBAccessContext): Fox[List[WebAuthnCredential]] = | ||
for { | ||
accessQuery <- readAccessQuery | ||
r <- run( | ||
q"SELECT $columns FROM $existingCollectionName WHERE _multiUser = $multiUserId AND $accessQuery" | ||
.as[WebauthncredentialsRow]) | ||
parsed <- parseAll(r) | ||
} yield parsed | ||
|
||
def findByCredentialId(multiUserId: ObjectId, credentialId: Array[Byte])( | ||
implicit ctx: DBAccessContext): Fox[WebAuthnCredential] = | ||
for { | ||
accessQuery <- readAccessQuery | ||
r <- run( | ||
q"SELECT $columns FROM $existingCollectionName WHERE _multiUser = $multiUserId AND credentialId = $credentialId AND $accessQuery" | ||
.as[WebauthncredentialsRow]) | ||
parsed <- parseFirst(r, multiUserId) | ||
} yield parsed | ||
|
||
def insertOne(c: WebAuthnCredential): Fox[Unit] = { | ||
val converter = new ObjectConverter() | ||
val serializedAttestedCredential = c.serializeAttestedCredential(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) | ||
serializedAttestationStatement <- c.serializeAttestationStatement(converter) | ||
_ <- run( | ||
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}, ${serializedAttestationStatement}, | ||
${serializedAttestedCredential}, ${serializedAuthenticatorExtensions}, ${c.credentialRecord.getCounter.toInt})""".asUpdate) | ||
} yield () | ||
} | ||
|
||
def updateSignCount(c: WebAuthnCredential): Fox[Unit] = { | ||
val signatureCount = c.credentialRecord.getCounter | ||
for { | ||
_ <- run(q"""UPDATE $existingCollectionName SET signatureCount = $signatureCount WHERE _id = ${c._id}""".asUpdate) | ||
} yield () | ||
} | ||
|
||
def removeById(id: ObjectId, multiUser: ObjectId): Fox[Unit] = | ||
for { | ||
_ <- run( | ||
q"""UPDATE $existingCollectionName SET isDeleted = true WHERE _id = ${id} AND _multiUser=${multiUser}""".asUpdate) | ||
} yield () | ||
|
||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
START TRANSACTION; | ||
|
||
do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 135, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; | ||
|
||
CREATE TABLE webknossos.webauthnCredentials( | ||
_id TEXT PRIMARY KEY, | ||
_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 JSONB NOT NULL, | ||
signatureCount INTEGER NOT NULL, | ||
isDeleted BOOLEAN NOT NULL DEFAULT false, | ||
UNIQUE (_id, credentialId) | ||
); | ||
|
||
CREATE VIEW webknossos.webauthnCredentials_ as SELECT * FROM webknossos.webauthnCredentials WHERE NOT isDeleted; | ||
|
||
ALTER TABLE webknossos.webauthnCredentials | ||
ADD FOREIGN KEY (_multiUser) REFERENCES webknossos.multiUsers(_id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE; | ||
|
||
UPDATE webknossos.releaseInformation SET schemaVersion = 136; | ||
|
||
COMMIT TRANSACTION; |
10 changes: 10 additions & 0 deletions
10
conf/evolutions/reversions/136-add-webauthn-credentials.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
START TRANSACTION; | ||
|
||
do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 136, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; | ||
|
||
DROP TABLE webknossos.webauthnCredentials; | ||
DROP VIEW webknossos.webauthnCredentials_; | ||
|
||
UPDATE webknossos.releaseInformation SET schemaVersion = 135; | ||
|
||
COMMIT TRANSACTION; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,10 @@ | ||
# Managing Users & Access Permissions | ||
|
||
WEBKNOSSOS offers a built-in user management system to administer different user roles and permissions. In the following pages, you will learn more about: | ||
WEBKNOSSOS offers a built-in user management system to administer different user roles and permissions. In the following pages, you will learn more about: | ||
|
||
- [Organizations](organizations.md) | ||
- [Teams](teams.md) | ||
- [Access rights and roles](access_rights.md) | ||
- [Users](new_users.md) | ||
- [Password and account](password.md) | ||
- [Passkeys](passkeys.md) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# Passkeys | ||
|
||
Passkeys are a new, more secure way to log in to websites and apps without using a password. | ||
Instead of typing a password, you use a fingerprint, face scan, or a device PIN to authenticate. | ||
|
||
## Adding and Removing Passkeys | ||
|
||
Users can _add_ and _remove_ their passkeys as long as they are logged in. | ||
Passkeys can be found by clicking on the user's avatar in the navigation bar in the top-right corner of the screen and selecting `Change Password`. | ||
Users find their passkeys below the change password form. | ||
To add a new passkey user can press the `Register Passkey` button and then set a name for the passkey. | ||
These passkeys can be deleted by pressing the `Delete` button next to the passkey's name. | ||
|
||
In case the user lost access to its passkey and can not sign in. | ||
Please use the `Forgot Password` button in the log in screen (see [Password and Account](./password.md)). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
robert-oleynik marked this conversation as resolved.
Show resolved
Hide resolved
|
File renamed without changes.
File renamed without changes.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.