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

FR-17643 - Bugs fixes and improvements #61

Merged
merged 6 commits into from
Aug 30, 2024
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
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
frontegg-david marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading