Skip to content

Commit

Permalink
Add support for native passkey authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
shilgapira committed Nov 14, 2023
1 parent 2ee8fe9 commit 13e2e54
Show file tree
Hide file tree
Showing 13 changed files with 728 additions and 55 deletions.
9 changes: 6 additions & 3 deletions src/DescopeKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public extension Descope {
/// Provides functions for authentication with TOTP codes.
static var totp: DescopeTOTP { sdk.totp }

/// Provides functions for authentication with passkeys.
static var passkey: DescopePasskey { sdk.passkey }

/// Provides functions for authentication with passwords.
static var password: DescopePassword { sdk.password }

/// Provides functions for authentication with magic links.
static var magicLink: DescopeMagicLink { sdk.magicLink }

Expand All @@ -70,9 +76,6 @@ public extension Descope {
/// Provides functions for authentication with SSO.
static var sso: DescopeSSO { sdk.sso }

/// Provides functions for authentication with passwords.
static var password: DescopePassword { sdk.password }

/// Provides functions for authentication using flows.
static var flow: DescopeFlow { sdk.flow }

Expand Down
60 changes: 60 additions & 0 deletions src/internal/http/DescopeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,66 @@ class DescopeClient: HTTPClient {
])
}

// MARK: - Passkey

struct PasskeyStartResponse: JSONResponse {
var transactionId: String
var options: String
var create: Bool
}

func passkeySignUpStart(loginId: String, details: SignUpDetails?, origin: String) async throws -> PasskeyStartResponse {
return try await post("auth/webauthn/signup/start", body: [
"loginId": loginId,
"user": details?.dictValue,
"origin": origin,
])
}

func passkeySignUpFinish(transactionId: String, response: String) async throws -> JWTResponse {
return try await post("auth/webauthn/signup/finish", body: [
"transactionId": transactionId,
"response": response,
])
}

func passkeySignInStart(loginId: String, origin: String, refreshJwt: String?, options: LoginOptions?) async throws -> PasskeyStartResponse {
return try await post("auth/webauthn/signin/start", headers: authorization(with: refreshJwt), body: [
"loginId": loginId,
"origin": origin,
"loginOptions": options?.dictValue,
])
}

func passkeySignInFinish(transactionId: String, response: String) async throws -> JWTResponse {
return try await post("auth/webauthn/signin/finish", body: [
"transactionId": transactionId,
"response": response,
])
}

func passkeySignUpInStart(loginId: String, origin: String, refreshJwt: String?, options: LoginOptions?) async throws -> PasskeyStartResponse {
return try await post("auth/webauthn/signup-in/start", headers: authorization(with: refreshJwt), body: [
"loginId": loginId,
"origin": origin,
"loginOptions": options?.dictValue,
])
}

func passkeyAddStart(loginId: String, origin: String, refreshJwt: String) async throws -> PasskeyStartResponse {
return try await post("auth/webauthn/update/start", headers: authorization(with: refreshJwt), body: [
"loginId": loginId,
"origin": origin,
])
}

func passkeyAddFinish(transactionId: String, response: String) async throws {
try await post("auth/webauthn/update/finish", body: [
"transactionId": transactionId,
"response": response,
])
}

// MARK: - Password

func passwordSignUp(loginId: String, password: String, details: SignUpDetails?) async throws -> JWTResponse {
Expand Down
47 changes: 47 additions & 0 deletions src/internal/others/Internal.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@

import AuthenticationServices

extension DescopeConfig {
static let initial: DescopeConfig = DescopeConfig(projectId: "")
}
Expand All @@ -16,3 +18,48 @@ extension DescopeError {
return DescopeError(code: code, desc: desc, message: message, cause: cause)
}
}

extension Data {
init?(base64URLEncoded base64URLString: String, options: Base64DecodingOptions = []) {
var str = base64URLString
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
if str.count % 4 > 0 {
str.append(String(repeating: "=", count: 4 - str.count % 4))
}
self.init(base64Encoded: str, options: options)
}

func base64URLEncodedString(options: Base64EncodingOptions = []) -> String {
return base64EncodedString(options: options)
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}

class DefaultPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding, ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return presentationAnchor
}

func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return presentationAnchor
}

private var presentationAnchor: ASPresentationAnchor {
#if os(macOS)
return ASPresentationAnchor()
#else
let scene = UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.compactMap { $0 as? UIWindowScene }
.first

let keyWindow = scene?.windows
.first { $0.isKeyWindow }

return keyWindow ?? ASPresentationAnchor()
#endif
}
}
21 changes: 8 additions & 13 deletions src/internal/routes/Flow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,7 @@ class Flow: Route, DescopeFlow {

// ensure that whatever the result of this method is we remove the reference
// to the runner from the current property
defer {
if current === runner {
log(.debug, "Resetting current flow property")
current = nil
}
}
defer { resetRunner(runner) }

// we wrap the callback based work with ASWebAuthenticationSession so it fits
// an async/await code style as any other action the SDK performs. The onCancel
Expand Down Expand Up @@ -174,6 +169,13 @@ class Flow: Route, DescopeFlow {

return code
}


private func resetRunner(_ runner: DescopeFlowRunner) {
guard current === runner else { return }
log(.debug, "Resetting current flow runner property")
current = nil
}
}

// Internal
Expand All @@ -184,13 +186,6 @@ private extension Data {
guard SecRandomCopyBytes(kSecRandomDefault, count, &bytes) == errSecSuccess else { return nil }
self = Data(bytes: bytes, count: count)
}

func base64URLEncodedString(options: Data.Base64EncodingOptions = []) -> String {
return base64EncodedString(options: options)
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}

private func prepareInitialRequest(for runner: DescopeFlowRunner) throws -> (url: URL, codeVerifier: String) {
Expand Down
Loading

0 comments on commit 13e2e54

Please sign in to comment.