Skip to content

Commit

Permalink
Clean up and cancellation support
Browse files Browse the repository at this point in the history
  • Loading branch information
dnys1 committed Mar 4, 2024
1 parent 072078c commit f7bebee
Show file tree
Hide file tree
Showing 14 changed files with 396 additions and 233 deletions.
Original file line number Diff line number Diff line change
@@ -1,79 +1,64 @@
package dev.celest.celest_auth

import android.content.BroadcastReceiver
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.CancellationSignal
import android.service.voice.VoiceInteractionSession.ActivityId
import androidx.annotation.Keep
import androidx.credentials.CreateCredentialResponse
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.CredentialManagerCallback
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.exceptions.CreateCredentialException
import androidx.credentials.exceptions.GetCredentialException
import com.google.android.gms.tasks.OnSuccessListener
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.google.android.gms.fido.Fido
import kotlinx.coroutines.coroutineScope
import java.util.concurrent.Executors
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

@Keep
class CelestAuthListener: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
TODO("Not yet implemented")
}
}

@Keep
class CelestAuth {
private lateinit var context: Context
private lateinit var credentialManager: CredentialManager

fun init(context: Context) {
this.context = context
credentialManager = CredentialManager.create(context)
}

fun signInWithCustomTabs(
uri: Uri,
onSuccess: () -> Unit,
onError: (Exception) -> Unit,
) {

}
class CelestAuth(private val mainActivity: Activity) {
private val credentialManager: CredentialManager = CredentialManager.create(mainActivity)
private val executor = Executors.newCachedThreadPool()

// Adapted from: https://developer.android.com/training/sign-in/credential-manager
suspend fun signInWithGoogle(
clientId: String,
nonce: String
) {
// TODO: GetSignInWithGoogleOption?
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setAutoSelectEnabled(true)
.setServerClientId(clientId)
.setNonce(nonce)
.build()
val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
coroutineScope {
try {
val result = credentialManager.getCredential(context, request)
} catch (e: GetCredentialException) {
TODO()
}
}
fun register(
requestJson: String,
callback: CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>,
): CancellationSignal {
val request = CreatePublicKeyCredentialRequest(
requestJson = requestJson,
preferImmediatelyAvailableCredentials = true
)
val cancellationSignal = CancellationSignal()
credentialManager.createCredentialAsync(
mainActivity,
request,
cancellationSignal,
executor,
callback,
)
return cancellationSignal
}

fun handleSignIn(result: GetCredentialResponse) {
when (val credential = result.credential) {
is CustomCredential -> {
if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
// Use googleIdTokenCredential and extract id to validate and
// authenticate on your server.
val googleIdTokenCredential = GoogleIdTokenCredential
.createFrom(credential.data)
}
}
}
fun authenticate(
requestJson: String,
callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
): CancellationSignal {
val request = GetCredentialRequest.Builder().addCredentialOption(
GetPublicKeyCredentialOption(requestJson = requestJson)
).build()
val cancellationSignal = CancellationSignal()
credentialManager.getCredentialAsync(
mainActivity,
request,
cancellationSignal,
executor,
callback,
)
return cancellationSignal
}
}
73 changes: 56 additions & 17 deletions packages/celest_auth/darwin/Classes/CelestAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import AppKit
#endif

import AuthenticationServices
import OSLog

public typealias OnSuccess = (UnsafePointer<UInt8>) -> Void
public typealias OnError = (CelestAuthErrorCode, UnsafePointer<UInt8>) -> Void
Expand All @@ -15,6 +16,13 @@ public typealias OnError = (CelestAuthErrorCode, UnsafePointer<UInt8>) -> Void
case unknown = 0
case unsupported = 1
case serde = 2

// From ASAuthorizationError.Code
case canceled = 1001
case invalidResponse = 1002
case notHandled = 1003
case failed = 1004
case notInteractive = 1005
}

@objc protocol CelestAuthProtocol: NSObjectProtocol {
Expand All @@ -36,6 +44,8 @@ public typealias OnError = (CelestAuthErrorCode, UnsafePointer<UInt8>) -> Void
onSuccess: @escaping OnSuccess,
onError: @escaping OnError
)

@objc func cancel()
}

@objc public class CelestAuth: NSObject, CelestAuthProtocol {
Expand All @@ -53,6 +63,10 @@ public typealias OnError = (CelestAuthErrorCode, UnsafePointer<UInt8>) -> Void
impl.isPasskeysSupported
}

@objc public func cancel() {
impl.cancel()
}

@objc public func register(request: String, onSuccess: @escaping OnSuccess, onError: @escaping OnError) {
impl.register(request: request, onSuccess: onSuccess, onError: onError)
}
Expand All @@ -68,12 +82,21 @@ public typealias OnError = (CelestAuthErrorCode, UnsafePointer<UInt8>) -> Void

@available(iOS 15.0, macOS 12.0, *)
class CelestAuthSupported: NSObject, CelestAuthProtocol, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {

private let logger = Logger(subsystem: "dev.celest.celest_auth", category: "debug")

private weak var controller: ASAuthorizationController?
private var onSuccess: OnSuccess?
private var onError: OnError?

var isPasskeysSupported: Bool { true }

func cancel() {
if #available(iOS 16.0, macOS 13.0, *) {
controller?.cancel()
}
}

func register(
request: String,
onSuccess: @escaping OnSuccess,
Expand All @@ -84,8 +107,7 @@ class CelestAuthSupported: NSObject, CelestAuthProtocol, ASAuthorizationControll
let challenge = Data(base64URLEncoded: options.challenge),
let userID = options.user.id.data(using: .utf8)
else {
onError(.serde, "Failed to deserialize registration request")
return
return onError(.serde, "Failed to deserialize registration request".unsafePointer)
}
self.onSuccess = onSuccess
self.onError = onError
Expand All @@ -98,7 +120,12 @@ class CelestAuthSupported: NSObject, CelestAuthProtocol, ASAuthorizationControll
let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest])
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()
if #available(iOS 16.0, macOS 13.0, *) {
authController.performRequests(options: .preferImmediatelyAvailableCredentials)
} else {
authController.performRequests()
}
self.controller = authController
}

func authenticate(
Expand All @@ -110,8 +137,7 @@ class CelestAuthSupported: NSObject, CelestAuthProtocol, ASAuthorizationControll
let options = try? JSONDecoder().decode(PasskeyAuthenticationOptions.self, from: data),
let challenge = Data(base64URLEncoded: options.challenge)
else {
onError(.serde, "Failed to deserialize authentication request")
return
return onError(.serde, "Failed to deserialize authentication request".unsafePointer)
}
self.onSuccess = onSuccess
self.onError = onError
Expand All @@ -120,7 +146,12 @@ class CelestAuthSupported: NSObject, CelestAuthProtocol, ASAuthorizationControll
let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest])
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()
if #available(iOS 16.0, macOS 13.0, *) {
authController.performRequests(options: .preferImmediatelyAvailableCredentials)
} else {
authController.performRequests()
}
self.controller = authController
}

private func reset() {
Expand All @@ -133,12 +164,14 @@ class CelestAuthSupported: NSObject, CelestAuthProtocol, ASAuthorizationControll
reset()
}

private func complete(error: CelestAuthErrorCode, _ message: String) {
private func complete(error: CelestAuthErrorCode, message: String) {
logger.error("Authorization completed with error: \(message)")
onError?(error, message.unsafePointer)
reset()
}

private func complete(error: Error) {
logger.error("Authorization completed with error: \(error)")
onError?(.unknown, error.localizedDescription.unsafePointer)
reset()
}
Expand All @@ -159,33 +192,39 @@ class CelestAuthSupported: NSObject, CelestAuthProtocol, ASAuthorizationControll
}

public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
logger.debug("Authorization completed successfully with result: \(authorization)")
switch authorization.credential {
case let credential as ASAuthorizationPlatformPublicKeyCredentialRegistration:
let response = PasskeyRegistration(credential: credential)
guard let responseJson = try? JSONEncoder().encode(response) else {
complete(error: .serde, "Failed to serialize registration response")
return
return complete(error: .serde, message: "Failed to serialize registration response")
}
return complete(value: responseJson)
} else if let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
complete(value: responseJson)
case let credential as ASAuthorizationPlatformPublicKeyCredentialAssertion:
let response = PasskeyAuthentication(credential: credential)
guard let responseJson = try? JSONEncoder().encode(response) else {
complete(error: .serde, "Failed to serialize authentication response")
return
return complete(error: .serde, message: "Failed to serialize authentication response")
}
return complete(value: responseJson)
} else {
complete(error: .unknown, "Unknown credential type: \(authorization.self)")
complete(value: responseJson)
default:
complete(error: .unknown, message: "Unknown credential type: \(authorization.self)")
}
}

public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
logger.error("Authorization completed with error: \(error)")
if let error = error as? ASAuthorizationError {
return complete(error: .init(rawValue: error.errorCode) ?? .unknown, message: error.localizedDescription)
}
complete(error: error)
}
}

class CelestAuthUnsupported: NSObject, CelestAuthProtocol {
var isPasskeysSupported: Bool { false }

func cancel() {}

func register(request: String, onSuccess: @escaping OnSuccess, onError: @escaping OnError) {
onError(.unsupported, "Unsupported platform".unsafePointer)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:65b7-136-24-157-119.ngrok-free.app?mode=developer</string>
<string>webcredentials:a102-136-24-157-119.ngrok-free.app?developer=true</string>
</array>
</dict>
</plist>
Loading

0 comments on commit f7bebee

Please sign in to comment.