Skip to content

Commit

Permalink
Merge branch 'master' into FR-19079/save_credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
frontegg-david authored Dec 23, 2024
2 parents 3561985 + 87a1575 commit b788ef2
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 29 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/onPush.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ jobs:
- name: Show eligible build destinations for the "demo"
run: xcodebuild -project demo/demo.xcodeproj -showdestinations -scheme "demo"
- name: Build for Testing
run: xcodebuild CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ build-for-testing -scheme "demo" -project "demo/demo.xcodeproj" -destination "platform=iOS Simulator,name=iPhone 14 Pro" -configuration "Debug" -enableCodeCoverage "YES"
run: xcodebuild CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ build-for-testing -scheme "demo" -project "demo/demo.xcodeproj" -destination "platform=iOS Simulator,name=iPhone 15 Pro" -configuration "Debug" -enableCodeCoverage "YES"
- name: Validate lint for lib and spec Cocoapods
env:
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
run: |
pod lib lint --verbose
# - name: Test without Building
# run: xcodebuild CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ test-without-building -scheme "demo" -project "demo/demo.xcodeproj" -destination "platform=iOS Simulator,name=iPhone 14 Pro" -configuration "Debug" -resultBundlePath "TestResults" -enableCodeCoverage "YES"
# run: xcodebuild CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ test-without-building -scheme "demo" -project "demo/demo.xcodeproj" -destination "platform=iOS Simulator,name=iPhone 15 Pro" -configuration "Debug" -resultBundlePath "TestResults" -enableCodeCoverage "YES"
# - name: "Parse Test XCResults"
# uses: kishikawakatsumi/xcresulttool@v1
# if: success() || failure()
Expand Down
2 changes: 1 addition & 1 deletion FronteggSwift.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'FronteggSwift'
s.version = '1.2.23'
s.version = '1.2.26'
s.summary = 'A swift library for easy integrating iOS application with Frontegg Services'
s.description = 'Frontegg is an end-to-end user management platform for B2B SaaS, powering strategies from PLG to enterprise readiness. Easy migration, no credit card required'
s.homepage = 'https://github.com/frontegg/frontegg-ios-swift'
Expand Down
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Welcome to the @frontegg/ios Swift SDK! This SDK provides a seamless way to inte
- [Multi-apps Support](#multi-apps-support)
- [Multi-Region support](#multi-region-support)
- [Login with ASWebAuthenticationSession](#login-with-aswebauthenticationsession)
- [Passkeys Authentication](#passkeys-authentication)

## Project Requirements

Expand Down Expand Up @@ -525,3 +526,88 @@ struct ContentView: View {
}
}
```

## Passkeys Authentication

Passkeys provide a seamless, passwordless authentication experience, leveraging platform-level biometric authentication and WebAuthn. Follow the steps below to integrate passkeys functionality into your iOS app.

### Prerequisites

1. **iOS Version**: Ensure your project targets **iOS 15 or later** to support the necessary WebAuthn APIs.
2. **Associated Domain**: Configure your app's associated domains to enable passkeys functionality.
3. **Frontegg SDK Version**: Use Frontegg iOS SDK version **1.2.24 or later**.

### Configuring Associated Domains

Passkeys require the associated domains to be correctly configured in your app. Follow these steps:

1. **Set up the Associated Domains Capability**:
- Open your project in Xcode.
- Go to the **Signing & Capabilities** tab.
- Add **Associated Domains** under the **+ Capability** section.
- Enter the domain for your app in the format:
```
webcredentials:[YOUR_DOMAIN]
```
Example:
```
webcredentials:example.com
```
2. **Host the WebAuthn Configuration File**:
- Add a `.well-known/webauthn` JSON file to your domain server with the following structure:
```json
{
"origins": [
"https://example.com",
"https://subdomain.example.com"
]
}
```
- Ensure this file is publicly accessible at `https://example.com/.well-known/webauthn`.
3. **Test Associated Domains**:
- Verify that your associated domain configuration works using Apple's [Associated Domains Validator](https://developer.apple.com/contact/request/associated-domains).
---
### Registering Passkeys
The Frontegg SDK provides a simple method to register passkeys in your application.
#### Example Code for Passkeys Registration:
```swift
import FronteggSwift
func registerPasskeys() {
if #available(iOS 15.0, *) {
FronteggAuth.shared.registerPasskeys()
} else {
print("Passkeys are only supported on iOS 15 or later.")
}
}
```

### Logging in with Passkeys

To authenticate users with passkeys, use the following method provided by the SDK:

#### Example Code for Passkeys Login:
```swift
import FronteggSwift

func loginWithPasskeys() {
if #available(iOS 15.0, *) {
FronteggAuth.shared.loginWithPasskeys { result in
switch result {
case .success(let user):
print("User logged in: \(user)")
case .failure(let error):
print("Error logging in: \(error)")
}
}
} else {
print("Passkeys are only supported on iOS 15 or later.")
}
}
```
19 changes: 13 additions & 6 deletions Sources/FronteggSwift/FronteggAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class FronteggAuth: ObservableObject {
public var api: Api
private var subscribers = Set<AnyCancellable>()
private var refreshTokenDispatch: DispatchWorkItem?

var loginCompletion: CompletionHandler? = nil

init (baseUrl:String,
clientId: String,
Expand Down Expand Up @@ -498,6 +498,8 @@ public class FronteggAuth: ObservableObject {
}
public typealias CompletionHandler = (Result<User, FronteggError>) -> Void

public typealias ConditionCompletionHandler = (_ error: FronteggError?) -> Void

public func login(_ _completion: FronteggAuth.CompletionHandler? = nil, loginHint: String? = nil) {

if(self.embeddedMode){
Expand Down Expand Up @@ -612,7 +614,7 @@ public class FronteggAuth: ObservableObject {


public func loginWithSSO(email: String, _ _completion: FronteggAuth.CompletionHandler? = nil) {
let completion = _completion ?? { res in
let completion = _completion ?? self.loginCompletion ?? { res in

}

Expand Down Expand Up @@ -726,8 +728,9 @@ public class FronteggAuth: ObservableObject {
return urlComponent.url!
}

func loginWithSocialLogin(socialLoginUrl: String, _ _completion: FronteggAuth.CompletionHandler? = nil) {
let completion = _completion ?? { res in
func loginWithSocialLogin(socialLoginUrl: String, _ _completion: FronteggAuth.CompletionHandler? = nil) {
let completion = _completion ?? self.loginCompletion ?? { res in


}

Expand Down Expand Up @@ -756,6 +759,10 @@ func loginWithSocialLogin(socialLoginUrl: String, _ _completion: FronteggAuth.Co

if let rootVC = self.getRootVC() {
self.loginHint = loginHint
self.loginCompletion = { result in
_completion?(result)
self.loginCompletion = nil
}
let loginModal = EmbeddedLoginModal(parentVC: rootVC)
let hostingController = UIHostingController(rootView: loginModal)
hostingController.modalPresentationStyle = .fullScreen
Expand Down Expand Up @@ -848,10 +855,10 @@ func loginWithSocialLogin(socialLoginUrl: String, _ _completion: FronteggAuth.Co
// Fallback on earlier versions
}
}
public func registerPasskeys() {
public func registerPasskeys(_ completion: FronteggAuth.ConditionCompletionHandler? = nil) {

if #available(iOS 15.0, *) {
PasskeysAuthenticator.shared.startWebAuthn()
PasskeysAuthenticator.shared.startWebAuthn(completion)
} else {
// Fallback on earlier versions
}
Expand Down
40 changes: 33 additions & 7 deletions Sources/FronteggSwift/authenticators/PasskeysAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,32 @@ class PasskeysAuthenticator: NSObject, ASAuthorizationControllerDelegate, ASAuth

// MARK: - WebAuthn Registration

func startWebAuthn() {
func startWebAuthn(_ completion: FronteggAuth.ConditionCompletionHandler? = nil) {
let baseUrl = FronteggAuth.shared.baseUrl

if let completion = completion {
self.callbackAction = { (data, error) in
if let regsitration = data as? WebauthnRegistration {
self.verifyNewDeviceSession(publicKey: regsitration)
} else {
if error == nil {
completion(nil)
}else if let frotneggError = error as? FronteggError {
completion(frotneggError)
} else {
completion(FronteggError.authError(.unknown))
}
}
}
}
guard let url = URL(string: "\(baseUrl)/frontegg/identity/resources/users/webauthn/v1/devices"),
let accessToken = FronteggAuth.shared.accessToken else {
logger.error("Invalid base URL or missing access token")
self.callbackAction?(nil, FronteggError.authError(.notAuthenticated))
return
}


var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
Expand Down Expand Up @@ -249,12 +266,21 @@ class PasskeysAuthenticator: NSObject, ASAuthorizationControllerDelegate, ASAuth
}

do {
if let jsonResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
self.logger.debug("Response from verify endpoint: \(jsonResponse)")
self.callbackAction?(jsonResponse, nil)
} else {
self.logger.error("Invalid JSON structure in response")
self.callbackAction?(nil, FronteggError.authError(.invalidPasskeysRequest))

if let dataStr = String(data:data, encoding: .utf8),
let httpResponse = response as? HTTPURLResponse,
dataStr.isEmpty, httpResponse.statusCode < 300 {
self.logger.debug("Response from verify succeeded with empty body")
self.callbackAction?(nil, nil)
}else {

if let jsonResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
self.logger.debug("Response from verify endpoint: \(jsonResponse)")
self.callbackAction?(jsonResponse, nil)
} else {
self.logger.error("Invalid JSON structure in response")
self.callbackAction?(nil, FronteggError.authError(.invalidPasskeysRequest))
}
}
} catch {
self.logger.error("Error parsing JSON: \(error.localizedDescription)")
Expand Down
1 change: 1 addition & 0 deletions Sources/FronteggSwift/embedded/CustomWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ class CustomWebView: WKWebView, WKNavigationDelegate {
CredentialManager.saveCodeVerifier(codeVerifier)
_ = webView?.load(URLRequest(url: url))
}
FronteggAuth.shared.loginCompletion?(res)
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/FronteggSwift/models/errors/AuthenticationError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extension FronteggError {
case failedToAuthenticateWithPasskeys(_ message: String)
case operationCanceled
case mfaRequired(_ json: [String:Any], refreshToken: String? = nil)
case notAuthenticated
case unknown
case other(Error)
}
Expand All @@ -43,6 +44,7 @@ extension FronteggError.Authentication {
case let .failedToAuthenticateWithPasskeys(message): "Failed to authenticate with Passkeys, \(message)"
case .operationCanceled: "Operation canceled by user"
case .mfaRequired: "MFA is required for authentication"
case .notAuthenticated: "Not authenticated exception"
case .unknown: "Unknown error occurred"
case let .other(error): error.localizedDescription
}
Expand All @@ -61,6 +63,7 @@ extension FronteggError.Authentication {
case .failedToAuthenticateWithPasskeys: "failedToAuthenticateWithPasskeys"
case .operationCanceled: "operationCanceled"
case .mfaRequired: "mfaRequired"
case .notAuthenticated: "notAuthenticated"
case .unknown: "unknown"
case .other: "other"
}
Expand Down
4 changes: 3 additions & 1 deletion demo-embedded/demo-embedded/Frontegg.plist
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
<key>useAsWebAuthenticationForAppleLogin</key>
<true/>
<key>embeddedMode</key>
<true/>
<key>embeddedMode</key>
<false/>
<key>baseUrl</key>
<string>https://autheu.davidantoon.me</string>
<key>clientId</key>
<string>04ae2174-d8d9-4a90-8bab-2548e210a508</string>
<string>b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca</string>
<key>logLevel</key>
<string>trace</string>
</dict>
Expand Down
3 changes: 2 additions & 1 deletion demo-embedded/demo-embedded/MyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct MyApp: View {

var body: some View {
ZStack {

if fronteggAuth.isAuthenticated {
TabView {
ProfileTab()
Expand All @@ -27,9 +28,9 @@ struct MyApp: View {
}
}
} else {

if(!fronteggAuth.isAuthenticated){
VStack {

Button {
fronteggAuth.login()
} label: {
Expand Down
5 changes: 3 additions & 2 deletions demo-multi-region/demo-multi-region/Frontegg.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>

<key>regions</key>
<array>
<dict>
<key>key</key>
<string>eu</string>
<key>baseUrl</key>
<string>https://auth.davidantoon.me</string>
<string>https://autheu.davidantoon.me</string>
<key>clientId</key>
<string>b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca</string>
</dict>
<dict>
<key>key</key>
<string>us</string>
<key>baseUrl</key>
<string>https://davidprod.frontegg.com</string>
<string>https://authus.davidantoon.me</string>
<key>clientId</key>
<string>d7d07347-2c57-4450-8418-0ec7ee6e096b</string>
</dict>
Expand Down
13 changes: 8 additions & 5 deletions demo-multi-region/demo-multi-region/MyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ struct MyApp: View {
} else {

if(fronteggAuth.lateInit) {
Button("EU") {
FronteggApp.shared.manualInit(baseUrl: "https://auth.davidantoon.me", cliendId: "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca")
}.padding(.top, 40)

Button("US") {
FronteggApp.shared.manualInit(baseUrl: "https://davidprod.frontegg.com", cliendId: "d7d07347-2c57-4450-8418-0ec7ee6e096b")
VStack{
Button("EU") {
FronteggApp.shared.manualInit(baseUrl: "https://autheu.davidantoon.me", cliendId: "b6adfe4c-d695-4c04-b95f-3ec9fd0c6cca")
}.padding(.bottom, 40)

Button("US") {
FronteggApp.shared.manualInit(baseUrl: "https://authus.davidantoon.me", cliendId: "d7d07347-2c57-4450-8418-0ec7ee6e096b")
}
}
}else {
DefaultLoader().onAppear(){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:auth.davidantoon.me</string>
<string>applinks:auth.davidantoon.me</string>
<string>webcredentials:davidprod.frontegg.com</string>
<string>applinks:davidprod.frontegg.com</string>
<string>webcredentials:autheu.davidantoon.me</string>
<string>applinks:autheu.davidantoon.me</string>
<string>webcredentials:authus.davidantoon.me</string>
<string>applinks:authus.davidantoon.me</string>
<string>webcredentials:davidantoon.me</string>
</array>
</dict>
</plist>

0 comments on commit b788ef2

Please sign in to comment.