Skip to content
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

Add support for native passkey authentication #49

Merged
merged 3 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
12 changes: 0 additions & 12 deletions src/internal/others/Deprecated.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@

import Foundation

/// See the documentation for `DescopeUser`.
@available(*, unavailable, renamed: "DescopeUser")
public typealias MeResponse = DescopeUser

/// See the documentation for `SignUpDetails`.
@available(*, unavailable, renamed: "SignUpDetails")
public typealias User = SignUpDetails

/// See the documentation for `PasswordPolicyResponse`.
@available(*, unavailable, renamed: "PasswordPolicyResponse")
public typealias PasswordPolicy = PasswordPolicyResponse

public extension DescopeOTP {
@available(*, deprecated, message: "Pass a value (or an empty array) for the options parameter")
func signIn(with method: DeliveryMethod, loginId: String) async throws -> String {
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