Skip to content

Commit

Permalink
Merge pull request #61 from frontegg/FR-17643-bugs-fixes
Browse files Browse the repository at this point in the history
FR-17643 - Bugs fixes and improvements
  • Loading branch information
frontegg-david authored Aug 30, 2024
2 parents 922b644 + 989f4d3 commit 1a0bbf2
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 23 deletions.
164 changes: 150 additions & 14 deletions Sources/FronteggSwift/FronteggAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class FronteggAuth: ObservableObject {
@Published public var appLink: Bool = false
@Published public var externalLink: Bool = false
@Published public var selectedRegion: RegionConfig? = nil
@Published public var refreshingToken: Bool = false

public var embeddedMode: Bool
public var isRegional: Bool
Expand All @@ -49,6 +50,8 @@ public class FronteggAuth: ObservableObject {
private let credentialManager: CredentialManager
public var api: Api
private var subscribers = Set<AnyCancellable>()
private var refreshTokenDispatch: DispatchWorkItem?

var webAuthentication: WebAuthentication = WebAuthentication()

init (baseUrl:String,
Expand All @@ -71,6 +74,20 @@ public class FronteggAuth: ObservableObject {
self.api = Api(baseUrl: self.baseUrl, clientId: self.clientId, applicationId: self.applicationId)
self.selectedRegion = self.getSelectedRegion()

NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)

if ( isRegional || isLateInit == true ) {
initializing = false
showLoader = false
Expand All @@ -81,6 +98,14 @@ public class FronteggAuth: ObservableObject {
self.initializeSubscriptions()
}


deinit {
// Remove the observer when the instance is deallocated
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
}


public func manualInit(baseUrl:String, clientId:String, applicationId: String?) {
self.lateInit = false
self.baseUrl = baseUrl
Expand Down Expand Up @@ -108,6 +133,19 @@ public class FronteggAuth: ObservableObject {
}


@objc private func applicationDidBecomeActive() {
logger.info("application become active")

if(initializing){
return
}
refreshTokenWhenNeeded()
}

@objc private func applicationDidEnterBackground(){
logger.info("application enter background")
}

public func reinitWithRegion(config:RegionConfig) {
self.baseUrl = config.baseUrl
self.clientId = config.clientId
Expand Down Expand Up @@ -143,7 +181,6 @@ public class FronteggAuth: ObservableObject {
self.showLoader = initializingValue || (!isAuthenticatedValue && isLoadingValue)
}.store(in: &subscribers)


if let refreshToken = try? credentialManager.get(key: KeychainKeys.refreshToken.rawValue),
let accessToken = try? credentialManager.get(key: KeychainKeys.accessToken.rawValue) {

Expand All @@ -162,6 +199,7 @@ public class FronteggAuth: ObservableObject {
}
}


public func setCredentials(accessToken: String, refreshToken: String) async {

do {
Expand All @@ -184,12 +222,11 @@ public class FronteggAuth: ObservableObject {
// isLoading must be at the bottom
self.isLoading = false

let offset = Double((decode["exp"] as! Int) - Int(Date().timeIntervalSince1970)) * 0.9
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + offset) {
Task{
await self.refreshTokenIfNeeded()
}
}

let offset = calculateOffset(expirationTime: decode["exp"] as! Int)

scheduleTokenRefresh(offset: offset)

}
} catch {
logger.error("Failed to load user data, \(error)")
Expand All @@ -207,6 +244,93 @@ public class FronteggAuth: ObservableObject {
}
}

/// Calculates the optimal delay for refreshing the token based on the expiration time.
/// - Parameter expirationTime: The expiration time of the token in seconds since the Unix epoch.
/// - Returns: The calculated offset in seconds before the token should be refreshed. If the remaining time is less than 20 seconds, it returns 0 for immediate refresh.
func calculateOffset(expirationTime: Int) -> TimeInterval {
let now = Date().timeIntervalSince1970 * 1000 // Current time in milliseconds
let remainingTime = (Double(expirationTime) * 1000) - now

let minRefreshWindow: Double = 20000 // Minimum 20 seconds before expiration, in milliseconds
let adaptiveRefreshTime = remainingTime * 0.8 // 80% of remaining time

return remainingTime > minRefreshWindow ? adaptiveRefreshTime / 1000 : max((remainingTime - minRefreshWindow) / 1000, 0)
}

func refreshTokenWhenNeeded() {
do {
logger.info("Checking if refresh token is available...")

// Check if the refresh token is available
guard let _ = self.refreshToken else {
logger.debug("No refresh token available. Exiting...")
return
}

logger.debug("Refresh token is available. Checking access token...")

// Check if the access token is available
guard let accessToken = self.accessToken else {
logger.debug("No access token found. Attempting to refresh token...")
Task {
await self.refreshTokenIfNeeded()
}
return
}

logger.debug("Access token found. Attempting to decode JWT...")

// Decode the access token to get the expiration time
let decode = try JWTHelper.decode(jwtToken: accessToken)
let expirationTime = decode["exp"] as! Int
logger.debug("JWT decoded successfully. Expiration time: \(expirationTime)")

let offset = calculateOffset(expirationTime: expirationTime)
logger.debug("Calculated offset for token refresh: \(offset) seconds")

// If offset is zero, refresh immediately
if offset == 0 {
logger.info("Offset is zero. Refreshing token immediately...")
Task {
await self.refreshTokenIfNeeded()
}
} else {
logger.info("Scheduling token refresh after \(offset) seconds")
self.scheduleTokenRefresh(offset: offset)
}
} catch {
logger.error("Failed to decode JWT: \(error.localizedDescription)")
Task {
await self.refreshTokenIfNeeded()
}
}
}



func scheduleTokenRefresh(offset: TimeInterval) {
cancelScheduledTokenRefresh()
logger.info("Schedule token refresh after, (\(offset) s)")

var workItem: DispatchWorkItem? = nil
workItem = DispatchWorkItem {
if !(workItem!.isCancelled) {
Task {
await self.refreshTokenIfNeeded()
}
}
}
refreshTokenDispatch = workItem
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + offset, execute: workItem!)

}

func cancelScheduledTokenRefresh() {
logger.info("Canceling previous refresh token task")
refreshTokenDispatch?.cancel()
refreshTokenDispatch = nil
}

public func logout(_ completion: @escaping (Result<Bool, FronteggError>) -> Void) {
self.isLoading = true

Expand Down Expand Up @@ -240,21 +364,33 @@ public class FronteggAuth: ObservableObject {
}

public func refreshTokenIfNeeded() async -> Bool {
guard let refreshToken = self.refreshToken, let accessToken = self.accessToken else {
guard let refreshToken = self.refreshToken else {
self.logger.info("no refresh token found")
return false
}

self.logger.info("refreshing token")
let accessToken = self.accessToken ?? ""

DispatchQueue.main.sync {
self.refreshingToken=true
}
if let data = await self.api.refreshToken(accessToken: accessToken, refreshToken: refreshToken) {
await self.setCredentials(accessToken: data.access_token, refreshToken: data.refresh_token)
self.logger.info("token refreshed successfully")
DispatchQueue.main.sync {
self.refreshingToken=false
}
return true
} else {
self.logger.info("refresh token failed, isAuthenticated = false")
DispatchQueue.main.sync {
self.initializing = false
self.isAuthenticated = false
self.accessToken = nil
self.refreshToken = nil
self.credentialManager.clear()

self.refreshingToken=false
// isLoading must be at the last bottom
self.isLoading = false
}
Expand Down Expand Up @@ -368,12 +504,12 @@ public class FronteggAuth: ObservableObject {
}


public func loginWithPopup(window: UIWindow?, ephemeralSesion: Bool? = true, loginHint: String? = nil, loginAction: String? = nil, _completion: FronteggAuth.CompletionHandler? = nil) {
public func loginWithPopup(window: UIWindow?, ephemeralSession: Bool? = true, loginHint: String? = nil, loginAction: String? = nil, _completion: FronteggAuth.CompletionHandler? = nil) {

self.webAuthentication.webAuthSession?.cancel()
self.webAuthentication = WebAuthentication()
self.webAuthentication.window = window;
self.webAuthentication.ephemeralSesion = ephemeralSesion ?? true
self.webAuthentication.ephemeralSession = ephemeralSession ?? true

let completion = _completion ?? { res in

Expand All @@ -385,12 +521,12 @@ public class FronteggAuth: ObservableObject {
self.webAuthentication.start(authorizeUrl, completionHandler: oauthCallback)
}

public func directLoginAction(window: UIWindow?, type: String, data: String, ephemeralSesion: Bool? = true, _completion: FronteggAuth.CompletionHandler? = nil) {
public func directLoginAction(window: UIWindow?, type: String, data: String, ephemeralSession: Bool? = true, _completion: FronteggAuth.CompletionHandler? = nil) {

self.webAuthentication.webAuthSession?.cancel()
self.webAuthentication = WebAuthentication()
self.webAuthentication.window = window ?? getRootVC()?.view.window;
self.webAuthentication.ephemeralSesion = ephemeralSesion ?? true
self.webAuthentication.ephemeralSession = ephemeralSession ?? true

let completion = _completion ?? { res in

Expand Down Expand Up @@ -460,7 +596,7 @@ public class FronteggAuth: ObservableObject {
let oauthCallback = createOauthCallbackHandler(completion)
self.webAuthentication.webAuthSession?.cancel()
self.webAuthentication = WebAuthentication()
self.webAuthentication.ephemeralSesion = true
self.webAuthentication.ephemeralSession = true
self.webAuthentication.window = getRootVC()?.view.window

let (authorizeUrl, codeVerifier) = AuthorizeUrlGenerator.shared.generate(loginHint: email, remainCodeVerifier: true)
Expand Down
21 changes: 18 additions & 3 deletions Sources/FronteggSwift/embedded/CustomWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ class CustomWebView: WKWebView, WKNavigationDelegate {
self.lastResponseStatusCode = nil;
self.fronteggAuth.webLoading = false

if(url.absoluteString.starts(with: "\(fronteggAuth.baseUrl)/oauth/authorize")){
self.fronteggAuth.webLoading = false

let encodedUrl = url.absoluteString.replacingOccurrences(of: "\"", with: "\\\"")
let reloadScript = "setTimeout(()=>window.location.href=\"\(encodedUrl)\", 4000)"
let jsCode = "(function(){\n" +
" var script = document.createElement('script');\n" +
" script.innerHTML=`\(reloadScript)`;" +
" document.body.appendChild(script)\n" +
" })()"
webView.evaluateJavaScript(jsCode)
logger.error("Failed to load page \(encodedUrl), status: \(statusCode)")
self.fronteggAuth.webLoading = false
return
}

webView.evaluateJavaScript("JSON.parse(document.body.innerText).errors.join('\\n')") { [self] res, err in
let errorMessage = res as? String ?? "Unknown error occured"
Expand All @@ -113,7 +128,7 @@ class CustomWebView: WKWebView, WKNavigationDelegate {
let urlType = getOverrideUrlType(url: url)
logger.info("urlType: \(urlType), for: \(url.absoluteString)")

if(urlType == .internalRoutes && response.mimeType == "application/json"){
if(urlType == .internalRoutes){
self.lastResponseStatusCode = response.statusCode
decisionHandler(.allow)
return
Expand Down Expand Up @@ -227,12 +242,12 @@ class CustomWebView: WKWebView, WKNavigationDelegate {
return .cancel
}

private func startExternalBrowser(_ _webView:WKWebView?, _ url:URL, _ ephemeralSesion:Bool = false) -> Void {
private func startExternalBrowser(_ _webView:WKWebView?, _ url:URL, _ ephemeralSession:Bool = false) -> Void {

weak var webView = _webView
fronteggAuth.webAuthentication.webAuthSession?.cancel()
fronteggAuth.webAuthentication = WebAuthentication()
fronteggAuth.webAuthentication.ephemeralSesion = ephemeralSesion
fronteggAuth.webAuthentication.ephemeralSession = ephemeralSession

fronteggAuth.webAuthentication.window = self.window
fronteggAuth.webAuthentication.start(url) { callbackUrl, error in
Expand Down
2 changes: 0 additions & 2 deletions Sources/FronteggSwift/services/Api.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@ public class Api {
"refresh_token": refreshToken,
])

let text = String(data: data, encoding: .utf8)!
print("result \(text)")
return try JSONDecoder().decode(AuthResponse.self, from: data)
} catch {
print(error)
Expand Down
4 changes: 2 additions & 2 deletions Sources/FronteggSwift/services/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import UIKit
class WebAuthentication: NSObject, ObservableObject, ASWebAuthenticationPresentationContextProviding {

weak var window: UIWindow? = nil
var ephemeralSesion: Bool = false
var ephemeralSession: Bool = false

func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {

Expand All @@ -33,7 +33,7 @@ class WebAuthentication: NSObject, ObservableObject, ASWebAuthenticationPresenta
completionHandler: completionHandler)
// Run the session
webAuthSession.presentationContextProvider = self
webAuthSession.prefersEphemeralWebBrowserSession = self.ephemeralSesion
webAuthSession.prefersEphemeralWebBrowserSession = self.ephemeralSession


self.webAuthSession = webAuthSession
Expand Down
2 changes: 1 addition & 1 deletion Sources/FronteggSwift/services/CredentialManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public class CredentialManager {
let status = SecItemCopyMatching(query, &result)

if status != errSecSuccess {
logger.error("Unknown error occured while trying to retrieve the key: \(key) from keyhcain")
//logger.error("Unknown error occured while trying to retrieve the key: \(key) from keyhcain")
throw KeychainError.unknown(status)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "icons8-account (1).pdf",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
2 changes: 1 addition & 1 deletion demo-embedded/demo-embedded/MyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ struct MyApp: View {
}.padding(.vertical, 20)

Button {
fronteggAuth.directLoginAction(window: nil, type: "social-login", data: "google")
fronteggAuth.directLoginAction(window: nil, type: "social-login", data: "google", ephemeralSession: false)
} label: {
Text("Login with popup")
}
Expand Down

0 comments on commit 1a0bbf2

Please sign in to comment.