// // AuthSessionHandler.swift // Small wrapper for ASWebAuthenticationSession // // import Foundation import AuthenticationServices import UIKit final class AuthSessionHandler: NSObject { static let shared = AuthSessionHandler() private var session: ASWebAuthenticationSession? private var completion: ((URL?) -> Void)? var isSessionActive: Bool = false func appLaunchSequence(url:URL, isLoggedOut:Bool) { AuthSessionHandler.shared.isSessionActive = true; if isLoggedOut { logoutAndStartFreshLogin(url:url); return; } // Try to refresh token silently TokenManager.shared.ensureFreshAccessToken { [weak self] success in DispatchQueue.main.async { guard let self = self else { return } if success { // We have a valid access token, bootstrap WKWebView self.bootstrapAndLaunchWebApp(url: url) } else { // Need interactive login self.startLoginWithASWebAuthenticationSession(url: url) } } } } // MARK: - Bootstrap WKWebView with session cookie func bootstrapAndLaunchWebApp(url:URL) { guard let accessToken = TokenManager.shared.accessToken else { startLoginWithASWebAuthenticationSession(url:url) return } TokenManager.shared.loadValidate(with:accessToken) } func logoutAndStartFreshLogin(url:URL) { TokenManager.shared.clearTokens() let redirectUri = TokenManager.shared.domainUrl + "/GWCore.Web/connect/authorize" + "?client_id=" + TokenManager.shared.clientId + "%26redirect_uri=\(TokenManager.shared.redirectUri)" + "%26response_type=code" + "%26scope=openid%20profile%20offline_access" _ = redirectUri.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! if let logoutURL = URL(string: TokenManager.shared.identityProvider + "/logout.aspx?redirect_uri=" + redirectUri) { AuthSessionHandler.shared.start(loginUrl: logoutURL, callbackScheme: "k12net") { redirectURL in self.exchangeAuthorizationCode(redirectURL:redirectURL,url:url) } } } func startLoginWithASWebAuthenticationSession(url:URL) { let authURLString = TokenManager.shared.domainUrl + "/GWCore.Web/connect/authorize" + "?client_id=" + TokenManager.shared.clientId + "&redirect_uri=\(TokenManager.shared.redirectUri)" + "&response_type=code" + "&scope=openid%20profile%20offline_access" let callbackScheme = "k12net" guard let authURL = URL(string: authURLString) else { print("Invalid authorize URL") AuthSessionHandler.shared.isSessionActive = false; return } AuthSessionHandler.shared.start(loginUrl: authURL, callbackScheme: callbackScheme) { redirectURL in self.exchangeAuthorizationCode(redirectURL:redirectURL,url:url) } } func exchangeAuthorizationCode(redirectURL:URL?, url:URL){ DispatchQueue.main.async { guard let redirectURL = redirectURL else { print("Login failed or cancelled.") AuthSessionHandler.shared.isSessionActive = false; K12NETApp.webView.load(URLRequest(url: url)) return } guard let comps = URLComponents(url: redirectURL, resolvingAgainstBaseURL: false), let code = comps.queryItems?.first(where: { $0.name == "code" })?.value, var identityProvider = comps.queryItems?.first(where: { $0.name == "domain" })?.value else { print("Auth code not found in redirect URL") AuthSessionHandler.shared.isSessionActive = false; K12NETApp.webView.load(URLRequest(url: url)) return } var identityProviderUrl = url if(identityProvider.contains("azure.k12net")) { identityProviderUrl = URL(string: url.absoluteString.replacingOccurrences(of: "https://okul.k12net.com", with: identityProvider))! } else { identityProvider = TokenManager.shared.identityProvider } UserDefaults.standard.set(identityProvider, forKey: "identityProvider") print(" -> identityProvider:" + identityProvider) TokenManager.shared.refreshIdentityProvider() TokenManager.shared.exchangeAuthorizationCode(code: code) { success in DispatchQueue.main.async { if success { self.bootstrapAndLaunchWebApp(url:identityProviderUrl) } else { // handle error print("exchangeAuthorizationCode failed!") AuthSessionHandler.shared.isSessionActive = false K12NETApp.webView.load(URLRequest(url: url)) } } } } } /// - Parameters: /// - loginUrl: URL opened by auth provider (ex: login.aspx URL) /// - callbackScheme: k12net URL sheme that added from app side in Info.plist /// - completion: redirect URL called when event rised (nil = error/user cancel) func start(loginUrl: URL, callbackScheme: String = "k12net", completion: @escaping (URL?) -> Void) { self.isSessionActive = true self.completion = completion let session = ASWebAuthenticationSession(url: loginUrl, callbackURLScheme: callbackScheme) { [weak self] callbackURL, error in self?.isSessionActive = false self?.session = nil if let err = error { // error or user cancel print("ASWebAuthenticationSession error: \(err.localizedDescription)") DispatchQueue.main.async { self?.completion?(nil) } return } DispatchQueue.main.async { self?.completion?(callbackURL) } } if #available(iOS 13.0, *) { session.presentationContextProvider = self } // Password save (iCloud Keychain) => ephemeral = false session.prefersEphemeralWebBrowserSession = false self.session = session let started = session.start() if !started { print("ASWebAuthenticationSession failed to start") } } /// If SceneDelegate / AppDelegate get URL, you can continue with manuel func handleRedirect(url: URL) { completion?(url) } } @available(iOS 13.0, *) extension AuthSessionHandler: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { // Key window find and return if #available(iOS 13.0, *) { return UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap { $0.windows } .first { $0.isKeyWindow } ?? ASPresentationAnchor() } else { return ASPresentationAnchor() } //UIApplication.shared.windows.first { $0.isKeyWindow }! } }