Lambda-School-Labs/community-calendar-ios

View on GitHub
Community Calendar/Pods/OktaOidc/Okta/OktaOidc/OktaOidcStateManager.swift

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved.
 * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
 *
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and limitations under the License.
 */

import Foundation

open class OktaOidcStateManager: NSObject, NSSecureCoding {

    public static var supportsSecureCoding = true

    @objc open var authState: OIDAuthState
    @objc open var accessibility: CFString

    @objc open var accessToken: String? {
        // Return the known accessToken if it hasn't expired
        get {
            guard let tokenResponse = self.authState.lastTokenResponse,
                  let token = tokenResponse.accessToken,
                  let tokenExp = tokenResponse.accessTokenExpirationDate,
                  tokenExp.timeIntervalSince1970 > Date().timeIntervalSince1970 else {
                    return nil
            }
            return token
        }
    }

    @objc open var idToken: String? {
        // Return the known idToken if it is valid
        get {
            guard let tokenResponse = self.authState.lastTokenResponse,
                let token = tokenResponse.idToken else {
                    return nil
            }

            if let _ = validateToken(idToken: token) {
                return nil
            }

            return token
        }
    }

    @objc open var refreshToken: String? {
        return self.authState.refreshToken
    }
    
    // Needed for UTs only. Entry point for mocking network calls.
    var restAPI: OktaOidcHttpApiProtocol = OktaOidcRestApi()

    @objc public init(authState: OIDAuthState, accessibility: CFString = kSecAttrAccessibleWhenUnlockedThisDeviceOnly) {
        self.authState = authState
        self.accessibility = accessibility
        OktaOidcConfig.setupURLSession()
        
        super.init()
    }

    @objc required public convenience init?(coder decoder: NSCoder) {
        guard let state = decoder.decodeObject(forKey: "authState") as? OIDAuthState else {
            return nil
        }
        
        self.init(
            authState: state,
            accessibility: decoder.decodeObject(forKey: "accessibility") as! CFString
        )
    }

    @objc public func encode(with coder: NSCoder) {
        coder.encode(self.authState, forKey: "authState")
        coder.encode(self.accessibility, forKey: "accessibility")
    }

    @objc public func validateToken(idToken: String?) -> Error? {
        guard let idToken = idToken,
            let tokenObject = OIDIDToken(idTokenString: idToken) else {
                return OktaOidcError.JWTDecodeError
        }
        
        if tokenObject.expiresAt.timeIntervalSinceNow < 0 {
            return OktaOidcError.JWTValidationError("ID Token expired")
        }
        
        return nil
    }
    
    // Decodes the payload of a JWT
    @objc public static func decodeJWT(_ token: String) throws -> [String: Any] {
        let payload = token.split(separator: ".")
        guard payload.count > 1 else {
            return [:]
        }
        
        var encodedPayload = "\(payload[1])"
        if encodedPayload.count % 4 != 0 {
            let padding = 4 - encodedPayload.count % 4
            encodedPayload += String(repeating: "=", count: padding)
        }

        guard let data = Data(base64Encoded: encodedPayload, options: []) else {
            throw OktaOidcError.JWTDecodeError
        }
        
        let jsonObject = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
        
        guard let result = jsonObject as? [String: Any] else {
            throw OktaOidcError.JWTDecodeError
        }
        
        return result
    }

    @objc public func renew(callback: @escaping ((OktaOidcStateManager?, Error?) -> Void)) {
        authState.setNeedsTokenRefresh()
        authState.performAction(freshTokens: { accessToken, idToken, error in
            if error != nil {
                callback(nil, OktaOidcError.errorFetchingFreshTokens(error!.localizedDescription))
                return
            }
            
            callback(self, nil)
        })
    }
    
    @objc public func introspect(token: String?, callback: @escaping ([String : Any]?, Error?) -> Void) {
        perfromRequest(to: .introspection, token: token, callback: callback)
    }

    @objc public func revoke(_ token: String?, callback: @escaping (Bool, Error?) -> Void) {
        perfromRequest(to: .revocation, token: token) { payload, error in
            if let error = error {
                callback(false, error)
                return
            }            

            // Token is considered to be revoked if there is no payload.
            callback(payload?.isEmpty ?? true , nil)
        }
    }

    @objc public func removeFromSecureStorage() throws {
        try OktaOidcKeychain.remove(key: self.clientId)
    }
    
    @objc public func clear() {
        OktaOidcKeychain.clearAll()
    }
    
    @objc public func getUser(_ callback: @escaping ([String:Any]?, Error?) -> Void) {
        guard let token = accessToken else {
            DispatchQueue.main.async {
                callback(nil, OktaOidcError.noBearerToken)
            }
            return
        }

        let headers = ["Authorization": "Bearer \(token)"]
        
        perfromRequest(to: .userInfo, headers: headers, callback: callback)
    }
}

@objc public extension OktaOidcStateManager {

    @available(*, deprecated, message: "Please use readFromSecureStorage(for config: OktaOidcConfig) function")
    class func readFromSecureStorage() -> OktaOidcStateManager? {
        return readFromSecureStorage(forKey: "OktaAuthStateManager")
    }

    @objc class func readFromSecureStorage(for config: OktaOidcConfig) -> OktaOidcStateManager? {
        return readFromSecureStorage(forKey: config.clientId)
    }
    
    @objc func writeToSecureStorage() {
        let authStateData: Data
        do {
            if #available(iOS 11, OSX 10.14, *) {
                authStateData = try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
            } else {
                authStateData = NSKeyedArchiver.archivedData(withRootObject: self)
            }

            try OktaOidcKeychain.set(
                key: self.clientId,
                data: authStateData,
                accessibility: self.accessibility
            )
        } catch let error {
            print("Error: \(error)")
        }
    }
    
    private class func readFromSecureStorage(forKey secureStorageKey: String) -> OktaOidcStateManager? {
        guard let encodedAuthState: Data = try? OktaOidcKeychain.get(key: secureStorageKey) else {
            return nil
        }

        let state: OktaOidcStateManager?
        if #available(iOS 11, OSX 10.14, *) {
            state = (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(encodedAuthState)) as? OktaOidcStateManager
        } else {
            state = NSKeyedUnarchiver.unarchiveObject(with: encodedAuthState) as? OktaOidcStateManager
        }

        return state
    }
}

internal extension OktaOidcStateManager {
    var discoveryDictionary: [String: Any]? {
        return authState.lastAuthorizationResponse.request.configuration.discoveryDocument?.discoveryDictionary
    }
}

private extension OktaOidcStateManager {
    var issuer: String? {
        return authState.lastAuthorizationResponse.request.configuration.issuer?.absoluteString
    }
    
    var clientId: String {
        return authState.lastAuthorizationResponse.request.clientID
    }

    
    func perfromRequest(to endpoint: OktaOidcEndpoint,
                        token: String?,
                        callback: @escaping ([String : Any]?, OktaOidcError?) -> Void) {
        guard let token = token else {
            DispatchQueue.main.async {
                callback(nil, OktaOidcError.noBearerToken)
            }
            return
        }
        
        let postString = "token=\(token)&client_id=\(clientId)"
        
        perfromRequest(to: endpoint, postString: postString, callback: callback)
    }
    
    func perfromRequest(to endpoint: OktaOidcEndpoint,
                        headers: [String: String]? = nil,
                        postString: String? = nil,
                        callback: @escaping ([String : Any]?, OktaOidcError?) -> Void) {
        guard let endpointURL = endpoint.getURL(discoveredMetadata: discoveryDictionary, issuer: issuer) else {
            DispatchQueue.main.async {
                callback(nil, endpoint.noEndpointError)
            }
            return
        }
        
        var requestHeaders = [
            "Accept": "application/json",
            "Content-Type": "application/x-www-form-urlencoded"
        ]
        
        if let headers = headers {
            requestHeaders.merge(headers) { (_, new) in new }
        }

        restAPI.post(endpointURL, headers: requestHeaders, postString: postString, onSuccess: { response in
            callback(response, nil)
        }, onError: { error in
            callback(nil, error)
        })
    }
}