-
Notifications
You must be signed in to change notification settings - Fork 75
[PM-26177] Implement device-bound PRF passkey without SDK #2009
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
base: main
Are you sure you want to change the base?
Changes from all commits
1c460fb
4c8e2d1
cb49995
bb55977
3ffb9dd
b2fb1ba
41a1e64
1d5a7fe
fa708e2
c9740a1
058a1ca
a2da0ec
a8fab21
9371228
a5f0bc4
aeae5b3
fd8ae08
a9a283f
192e272
306ae16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import Foundation | ||
|
||
extension Data { | ||
public func base64UrlEncodedString(trimPadding: Bool? = true) -> String { | ||
let shouldTrim = if trimPadding != nil { trimPadding! } else { true } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ You can just say ๐ค But why is |
||
let encoded = base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_") | ||
if shouldTrim { | ||
return encoded.trimmingCharacters(in: CharacterSet(["="])) | ||
} else { | ||
return encoded | ||
} | ||
} | ||
|
||
public init?(base64UrlEncoded str: String) { | ||
self.init(base64Encoded: normalizeBase64Url(str)) | ||
} | ||
} | ||
|
||
private func normalizeBase64Url(_ str: String) -> String { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ค I don't think we like having global functions if at all possible. This really feels like something that should be an extension on String to me |
||
let hasPadding = str.last == "=" | ||
let padding = if !hasPadding { | ||
switch str.count % 4 { | ||
case 2: "==" | ||
case 3: "=" | ||
default: "" | ||
} | ||
} else { "" } | ||
return str | ||
.replacingOccurrences(of: "-", with: "+") | ||
.replacingOccurrences(of: "_", with: "/") | ||
+ padding | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import Foundation | ||
import Networking | ||
|
||
struct SecretVerificationRequestModel: JSONRequestBody, Equatable { | ||
static let encoder = JSONEncoder() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โ๏ธ Personally, I think this should also have a |
||
|
||
// MARK: Properties | ||
|
||
let authRequestAccessCode: String? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ I think these properties should all have documentation comments |
||
let masterPasswordHash: String? | ||
let otp: String? | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โ๏ธ Missing a |
||
init(passwordHash: String) { | ||
authRequestAccessCode = nil | ||
masterPasswordHash = passwordHash | ||
otp = nil | ||
} | ||
|
||
init(otp: String) { | ||
masterPasswordHash = nil | ||
self.otp = otp | ||
authRequestAccessCode = nil | ||
} | ||
|
||
init(accessCode: String) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ I think these inits should be alphabetized, and they all need documentation comments |
||
authRequestAccessCode = accessCode | ||
masterPasswordHash = nil | ||
otp = nil | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import Foundation | ||
import Networking | ||
|
||
// MARK: - SaveCredentialRequestModel | ||
|
||
/// The request body for an answer login request request. | ||
/// | ||
struct WebAuthnLoginSaveCredentialRequestModel: JSONRequestBody, Equatable { | ||
static let encoder = JSONEncoder() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โ๏ธ See comment earlier about not using our standard encoder. |
||
|
||
// MARK: Properties | ||
// The response received from the authenticator. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ There should be a space on either side of a |
||
// This contains all information needed for future authentication flows. | ||
let deviceResponse: WebAuthnLoginAttestationResponseRequest | ||
|
||
// Nickname chosen by the user to identify this credential | ||
let name: String | ||
|
||
// Token required by the server to complete the creation. | ||
// It contains encrypted information that the server needs to verify the credential. | ||
let token: String | ||
|
||
// True if the credential was created with PRF support. | ||
let supportsPrf: Bool | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ Properties should be in alphabetical order |
||
|
||
// Used for vault encryption. See {@link RotateableKeySet.encryptedUserKey } | ||
let encryptedUserKey: String? | ||
|
||
// Used for vault encryption. See {@link RotateableKeySet.encryptedPublicKey } | ||
let encryptedPublicKey: String? | ||
|
||
// Used for vault encryption. See {@link RotateableKeySet.encryptedPrivateKey } | ||
let encryptedPrivateKey: String? | ||
} | ||
|
||
struct WebAuthnLoginAttestationResponseRequest: Encodable, Equatable { | ||
let id: String | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ These should be alphabetized and should have documentation comments, as should the struct. As well, if there are multiple top-level entities in the file, they each should get a |
||
let rawId: String | ||
let type: String | ||
// let extensions: [String: Any] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ Personally, I think any code that's commented out should include a comment indicating why it's being kept around in comments. Otherwise, it should just be deleted. |
||
let response: WebAuthnLoginAttestationResponseRequestInner | ||
} | ||
|
||
struct WebAuthnLoginAttestationResponseRequestInner: Encodable, Equatable { | ||
let attestationObject: String | ||
let clientDataJson: String | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ Extra line at end of scope |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import Foundation | ||
import Networking | ||
|
||
struct WebAuthnLoginCredentialAssertionOptionsResponse: JSONResponse, Equatable, Sendable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ The struct should have documentation comments |
||
/// Options to be provided to the webauthn authenticator. | ||
let options: PublicKeyCredentialAssertionOptions; | ||
|
||
/// Contains an encrypted version of the {@link options}. | ||
/// Used by the server to validate the attestation response of newly created credentials. | ||
let token: String; | ||
} | ||
|
||
struct PublicKeyCredentialAssertionOptions: Codable, Equatable, Hashable { | ||
let allowCredentials: [BwPublicKeyCredentialDescriptor]? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โ๏ธ Documentation comments |
||
let challenge: String | ||
let extensions: AuthenticationExtensionsClientInputs? | ||
let rpId: String | ||
let timeout: Int? | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import Foundation | ||
import Networking | ||
|
||
struct WebAuthnLoginCredentialCreationOptionsResponse: JSONResponse, Equatable, Sendable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ As noted above, I think each of these should be |
||
/// Options to be provided to the webauthn authenticator. | ||
let options: PublicKeyCredentialCreationOptions; | ||
|
||
/// Contains an encrypted version of the {@link options}. | ||
/// Used by the server to validate the attestation response of newly created credentials. | ||
let token: String; | ||
} | ||
|
||
struct PublicKeyCredentialCreationOptions: Codable, Equatable, Hashable { | ||
// attestation?: AttestationConveyancePreference | ||
// let authenticatorSelection: AuthenticatorSelectionCriteria? | ||
let challenge: String | ||
let excludeCredentials: [BwPublicKeyCredentialDescriptor]? | ||
let extensions: AuthenticationExtensionsClientInputs? | ||
let pubKeyCredParams: [BwPublicKeyCredentialParameters] | ||
let rp: BwPublicKeyCredentialRpEntity | ||
let timeout: Int? | ||
let user: BwPublicKeyCredentialUserEntity | ||
} | ||
|
||
|
||
struct AuthenticationExtensionsClientInputs: Codable, Equatable, Hashable { | ||
let prf: AuthenticationExtensionsPRFInputs? | ||
} | ||
|
||
struct AuthenticationExtensionsPRFInputs: Codable, Equatable, Hashable { | ||
let eval: AuthenticationExtensionsPRFValues? | ||
let evalByCredential: [String: AuthenticationExtensionsPRFValues]? | ||
} | ||
|
||
struct AuthenticationExtensionsPRFValues: Codable, Equatable, Hashable { | ||
let first: String | ||
let second: String? | ||
} | ||
|
||
struct BwPublicKeyCredentialDescriptor: Codable, Equatable, Hashable { | ||
let type: String | ||
let id: String | ||
// let transports: [String]? | ||
} | ||
|
||
struct BwPublicKeyCredentialParameters: Codable, Equatable, Hashable { | ||
let type: String | ||
let alg: Int | ||
} | ||
|
||
struct BwPublicKeyCredentialRpEntity: Codable, Equatable, Hashable { | ||
let id: String | ||
let name: String | ||
} | ||
|
||
struct BwPublicKeyCredentialUserEntity: Codable, Equatable, Hashable { | ||
let id: String | ||
let name: String | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,16 @@ protocol AuthAPIService { | |
/// - Returns: The pending login request. | ||
/// | ||
func checkPendingLoginRequest(withId id: String, accessCode: String) async throws -> LoginRequest | ||
|
||
/// Retrieves the parameters for creating a new WebAuthn credential. | ||
/// - Parameters: | ||
/// - request: The data needed to send the request. | ||
func getCredentialCreationOptions(_ request: SecretVerificationRequestModel) async throws -> WebAuthnLoginCredentialCreationOptionsResponse | ||
|
||
/// Retrieves the parameters for authenticating with a WebAuthn credential. | ||
/// - Parameters: | ||
/// - request: The data needed to send the request. | ||
func getCredentialAssertionOptions(_ request: SecretVerificationRequestModel) async throws -> WebAuthnLoginCredentialAssertionOptionsResponse | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ These should be alphabetized |
||
|
||
/// Performs the identity token request and returns the response. | ||
/// | ||
|
@@ -83,6 +93,8 @@ protocol AuthAPIService { | |
/// - model: The data needed to send the request. | ||
/// | ||
func updateTrustedDeviceKeys(deviceIdentifier: String, model: TrustedDeviceKeysRequestModel) async throws | ||
|
||
func saveCredential(_ model: WebAuthnLoginSaveCredentialRequestModel) async throws | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ This both needs documentation comments, and to be slotted in alphabetically |
||
} | ||
|
||
extension APIService: AuthAPIService { | ||
|
@@ -93,6 +105,14 @@ extension APIService: AuthAPIService { | |
func checkPendingLoginRequest(withId id: String, accessCode: String) async throws -> LoginRequest { | ||
try await apiUnauthenticatedService.send(CheckLoginRequestRequest(accessCode: accessCode, id: id)) | ||
} | ||
|
||
func getCredentialCreationOptions(_ request: SecretVerificationRequestModel) async throws -> WebAuthnLoginCredentialCreationOptionsResponse { | ||
try await apiService.send(WebAuthnLoginGetCredentialCreationOptionsRequest(requestModel: request)) | ||
} | ||
|
||
func getCredentialAssertionOptions(_ request: SecretVerificationRequestModel) async throws -> WebAuthnLoginCredentialAssertionOptionsResponse { | ||
try await apiService.send(WebAuthnLoginGetCredentialAssertionOptionsRequest(requestModel: request)) | ||
} | ||
|
||
func getIdentityToken(_ request: IdentityTokenRequestModel) async throws -> IdentityTokenResponseModel { | ||
try await identityService.send(IdentityTokenRequest(requestModel: request)) | ||
|
@@ -133,6 +153,10 @@ extension APIService: AuthAPIService { | |
_ = try await apiUnauthenticatedService.send(ResendNewDeviceOtpRequest(model: model)) | ||
} | ||
|
||
func saveCredential(_ model: WebAuthnLoginSaveCredentialRequestModel) async throws { | ||
_ = try await apiService.send(WebAuthnLoginSaveCredentialRequest(requestModel: model)) | ||
} | ||
|
||
func updateTrustedDeviceKeys(deviceIdentifier: String, model: TrustedDeviceKeysRequestModel) async throws { | ||
_ = try await apiService.send(TrustedDeviceKeysRequest(deviceIdentifier: deviceIdentifier, requestModel: model)) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import Networking | ||
|
||
struct WebAuthnLoginGetCredentialAssertionOptionsRequest : Request { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ Documentation comments As well, we don't put spaces around colons: |
||
typealias Response = WebAuthnLoginCredentialAssertionOptionsResponse | ||
|
||
var body: SecretVerificationRequestModel? { requestModel } | ||
|
||
var path: String { "/webauthn/assertion-options" } | ||
|
||
var method: HTTPMethod { .post } | ||
|
||
let requestModel: SecretVerificationRequestModel | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import Networking | ||
|
||
struct WebAuthnLoginGetCredentialCreationOptionsRequest : Request { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ Documentation comments and extraneous space around colon |
||
typealias Response = WebAuthnLoginCredentialCreationOptionsResponse | ||
|
||
var body: SecretVerificationRequestModel? { requestModel } | ||
|
||
var path: String { "/webauthn/attestation-options" } | ||
|
||
var method: HTTPMethod { .post } | ||
|
||
let requestModel: SecretVerificationRequestModel | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import Networking | ||
|
||
struct WebAuthnLoginSaveCredentialRequest : Request { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐จ Documentation comments and extraneous space before colon |
||
typealias Response = EmptyResponse | ||
|
||
var body: WebAuthnLoginSaveCredentialRequestModel? { requestModel } | ||
|
||
var path: String { "/webauthn" } | ||
|
||
var method: HTTPMethod { .post } | ||
|
||
let requestModel: WebAuthnLoginSaveCredentialRequestModel | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ค Is this function really necessary when you can just
.base64EncodedString().urlEncoded()
? If we really care about whether or not we trim the padding, the current function can just be modified, I think?