// // TokenManager.swift // K12NET App // // Created by Ilhami Sisnelioglu on 27.10.2025. // import Foundation final class TokenManager { static let shared = TokenManager() // Configure for your environment public var deviceToken = "" public let clientId = "apply_to_k12net_to_get_your_clientId" // must match IdentityServer client public let secretId = "apply_to_k12net_to_get_your_secretId" // must match IdentityServer client public var domainUrl = "https://xyz.k12net.com" // change xyz to your domain public var identityProvider = "https://api.k12net.com" // select api | azure public var tokenEndpoint = URL(string: "https://api.k12net.com/GWCore.Web/connect/token")! // select api | azure public var redirectUri = "https://api.k12net.com/oidc.aspx".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! // select api | azure private let scope = "openid profile offline_access"//api private let refreshKey = "refresh_token" private let accessKey = "access_token" private let expiryKey = "access_expiry" // epoch seconds var accessToken: String? { Keychain.get(accessKey) } public init() { self.refreshIdentityProvider() self.refreshDomainUrl() } func clearTokens() { self.refreshIdentityProvider() self.refreshDomainUrl() Keychain.remove(accessKey) Keychain.remove(refreshKey) Keychain.remove(expiryKey) } func refreshDomainUrl() { if(UserDefaults.standard.string(forKey: "WebDomain") != nil) { self.domainUrl = UserDefaults.standard.string(forKey: "WebDomain") ?? "https://okul.k12net.com" } print(" - > domainUrl : " + domainUrl) } func refreshIdentityProvider() { if(UserDefaults.standard.string(forKey: "identityProvider") != nil) { self.identityProvider = UserDefaults.standard.string(forKey: "identityProvider") ?? "https://api.k12net.com" } else { self.identityProvider = "https://api.k12net.com" } if(self.identityProvider.contains("okul.k12net")) { self.identityProvider = "https://api.k12net.com" } if(self.identityProvider.contains("azure.k12net") && self.domainUrl.contains("okul.k12net")) { self.domainUrl = "https://azure.k12net.com" UserDefaults.standard.set(self.domainUrl, forKey: "WebDomain") } print(" - > identityProvider : " + identityProvider) self.tokenEndpoint = URL(string: self.identityProvider + "/GWCore.Web/connect/token")! self.redirectUri = (self.identityProvider + "/oidc.aspx").addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! } // MARK: - Store / Clear tokens func storeInitialTokens(_ t: OAuthTokenResponse) { Keychain.set(t.access_token, for: accessKey) Keychain.set(String(Int(Date().timeIntervalSince1970) + t.expires_in - 30), for: expiryKey) if let r = t.refresh_token { Keychain.set(r, for: refreshKey) } } // MARK: - Silent refresh func ensureFreshAccessToken(completion: @escaping (Bool) -> Void) { let now = Int(Date().timeIntervalSince1970) let exp = Int(Keychain.get(expiryKey) ?? "0") ?? 0 if now < exp, accessToken != nil { print(" - > ensureFreshAccessToken has access token") completion(true) return } guard let refresh = Keychain.get(refreshKey) else { print(" - > ensureFreshAccessToken : needs interactive login") completion(false) // needs interactive login return } refreshAccessToken(refreshToken: refresh, completion: completion) } private func refreshAccessToken(refreshToken: String, completion: @escaping (Bool) -> Void) { var req = URLRequest(url: tokenEndpoint) req.httpMethod = "POST" req.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") let body = [ "grant_type=refresh_token", "client_id=\(clientId)", "client_secret=\(secretId)", "refresh_token=\(refreshToken)" ].joined(separator: "&") req.httpBody = body.data(using: .utf8) print(" - > refreshAccessToken ") URLSession.shared.dataTask(with: req) { data, resp, _ in guard let data, let http = resp as? HTTPURLResponse, http.statusCode == 200, let tok = try? JSONDecoder().decode(OAuthTokenResponse.self, from: data) else { completion(false); return } Keychain.set(tok.access_token, for: self.accessKey) Keychain.set(String(Int(Date().timeIntervalSince1970) + tok.expires_in - 30), for: self.expiryKey) if let r = tok.refresh_token { Keychain.set(r, for: self.refreshKey) } completion(true) }.resume() } // MARK: - First-time login: exchange authorization code func exchangeAuthorizationCode(code: String, completion: @escaping (Bool) -> Void) { var req = URLRequest(url: tokenEndpoint) req.httpMethod = "POST" req.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") let body = [ "grant_type=authorization_code", "code=\(code)", "redirect_uri=\(redirectUri)", "client_id=\(clientId)", "client_secret=\(secretId)", "scope=\(scope)" ].joined(separator: "&") req.httpBody = body.data(using: .utf8) URLSession.shared.dataTask(with: req) { data, resp, error in if let error = error { print("❌ Network error:", error.localizedDescription) } if let http = resp as? HTTPURLResponse { print("🌐 HTTP status:", http.statusCode) } else { print("⚠️ Response is not HTTPURLResponse:", String(describing: resp)) } if let data = data { if let dataStr = String(data: data, encoding: .utf8) { if dataStr.contains(":\"invalid_grant\"") { self.identityProvider = (self.identityProvider == "https://azure.k12net.com" ? "https://api.k12net.com" : "https://azure.k12net.com") UserDefaults.standard.set(self.identityProvider , forKey: "identityProvider") } print("📦 Raw response body:", dataStr) } } else { print("⚠️ No data returned from server") } guard let data, let http = resp as? HTTPURLResponse, http.statusCode == 200, let tokenResponse = try? JSONDecoder().decode(OAuthTokenResponse.self, from: data) else { completion(false) return } // Store tokens in Keychain self.storeInitialTokens(tokenResponse) completion(true) }.resume() } func loadValidate(with token: String) { let url = URL(string: domainUrl + "/GWCore.Web/api/login/oidc/Validate")! var request = URLRequest(url: url) request.httpMethod = "GET" // Add Authorization header request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.setValue("*/*", forHTTPHeaderField: "Accept") if !TokenManager.shared.deviceToken.isEmpty { request.setValue(TokenManager.shared.deviceToken, forHTTPHeaderField: "Atlas-DeviceID") request.setValue("9c260947-ba8f-e511-bf62-3c15c2ddcd05", forHTTPHeaderField: "Atlas-DeviceTypeID") request.setValue("WebPush - PWA", forHTTPHeaderField: "Atlas-DeviceModel") } DispatchQueue.main.async { print(" - > loadValidate started in webview") K12NETApp.webView.load(request) AuthSessionHandler.shared.isSessionActive = false; } } } // MARK: - OAuthTokenResponse struct OAuthTokenResponse: Decodable { let access_token: String let expires_in: Int let refresh_token: String? let id_token: String? let token_type: String }