Previous navigation:

Alamofire source learning directory collection

Server validation processing

Related documents: ServerTrustEvaluation. Swift

Introduction to the

The URLSessionDelegate calls back the method when the request needs to be authenticated

open func urlSession(_ session: URLSession.task: URLSessionTask.didReceive challenge: URLAuthenticationChallenge.completionHandler: @escaping (URLSession.AuthChallengeDisposition.URLCredential?). ->Void)
Copy the code

In Alamofire, the object that responds to the URLSessionDelegate is a SessionDelegate. In the corresponding callback method, the authentication type will be determined first. If the authentication type is server (HTTPS, self-signed certificate, etc.), The related object method in ServerTrustEvaluation is used for the validation, and the other types of validation are handled with credential in the Request.

Tool extension – auxiliary validation use

When verifying the server, the SecTrust, SecPolicy and other objects in the system Security framework are used to call the C type function for operation. To facilitate the call, Alamofire has extended the related types in ServerTrustEvaluation. Understanding how these extensions work will help you learn the validation process.

There are two ways to extend:

  1. Direct extensions, such as Array, directly extend Array, using generic constraints to constrain the scope of the extension
  2. Use AlamofireExtended protocol package extension, the object to be extended to implement the extension to implement AlamofireExtended protocol, and then extend the AlamofireExtended protocol + generic constraints, add methods for the object that needs to be extended, and need to use the object first.afThe call returns the AlamofireExtension wrapped object and then calls the related method, with the benefit that extension intrusion is not avoided. The details will be explained in detail in the following tool extension method study notes.

Foundation Framework Extension

  1. Validator object array extension, easy to traverse the extension one by one
extension Array where Element= =ServerTrustEvaluating {
    #if os(Linux)
    // Add this same convenience method for Linux.
    #else
    // An error will be thrown if any handler fails to pass through the array of hosts that need authentication
    public func evaluate(_ trust: SecTrust.forHost host: String) throws {
        for evaluator in self {
            try evaluator.evaluate(trust, forHost: host)
        }
    }
    #endif
}
Copy the code
  1. The Bundle extension is used to extract all the certificates and public keys in the app
extension Bundle: AlamofireExtended {}
extension AlamofireExtension where ExtendedType: Bundle {
    // Read all valid certificates from the bundle and return them
    public var certificates: [SecCertificate] {
        paths(forResourcesOfTypes: [".cer".".CER".".crt".".CRT".".der".".DER"]).compactMap { path in
            // We use compactMap to filter out the failed certificates
            guard
                let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData.let certificate = SecCertificateCreateWithData(nil, certificateData) else { return nil }

            return certificate
        }
    }

    // Return the public keys of all available certificates in the bundle
    public var publicKeys: [SecKey] {
        certificates.af.publicKeys
    }

    // Return the file paths of all the extensions in the bundle as an array based on the array of extension types
    public func paths(forResourcesOfTypes types: [String])- > [String] {
        Array(Set(types.flatMap { type.paths(forResourcesOfType: $0, inDirectory: nil)}}}))Copy the code

Authentication related class extensions

  1. Certificate object extension to extract the public key of the certificate
extension SecCertificate: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =SecCertificate {
    // Retrieve the public key from the certificate. If the extract fails, return nil
    public var publicKey: SecKey? {
        let policy = SecPolicyCreateBasicX509(a)var trust: SecTrust?
        let trustCreationStatus = SecTrustCreateWithCertificates(type, policy, &trust)

        guard let createdTrust = trust, trustCreationStatus = = errSecSuccess else { return nil }

        return SecTrustCopyPublicKey(createdTrust)
    }
}
Copy the code
  1. Certificate array extension to extract all public keys
// MARK: Certificate array extension
extension Array: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= = [SecCertificate] {
    // Return all the certificate objects in the array in Data format
    public var data: [Data] {
        type.map { SecCertificateCopyData($0) as Data}}// Extract the public keys of all certificate objects and filter the failed objects using compactMap
    public var publicKeys: [SecKey] {
        type.compactMap { $0.af.publicKey }
    }
}
Copy the code
  1. IOS12 Following evaluation results extension

The following method for evaluating and verifying SecTrust is SecTrustEvaluate(SecTrust, SecTrustResultType *). This method does not throw an error and returns the result using the SecTrustResultType pointer of the second parameter. Method returns the OSStatus status code to mark the detection status, so Alamofire extends OSStatus and SecTrustResultType by adding a computing attribute that quickly determines success

// MARK: OSStatus extension
extension OSStatus: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =OSStatus {
    // Return success
    public var isSuccess: Bool { type = = errSecSuccess }
}

// MARK: SecTrustResultType extension
extension SecTrustResultType: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =SecTrustResultType {
    // Return success
    public var isSuccess: Bool {
        (type = = .unspecified || type = = .proceed)
    }
}
Copy the code
  1. SecPolicy Extends security policies to quickly create three security policies for verifying server certificates
extension SecPolicy: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =SecPolicy {
    
    // Verify the server certificate, but do not need to match the host name
    public static let `default` = SecPolicyCreateSSL(true.nil)

    // Verify the server certificate, which must match the host name
    public static func hostname(_ hostname: String) -> SecPolicy {
        SecPolicyCreateSSL(true, hostname as CFString)}// If the certificate is revoked, an exception is thrown when the policy fails to be created
    public static func revocation(options: RevocationTrustEvaluator.Options) throws -> SecPolicy {
        guard let policy = SecPolicyCreateRevocation(options.rawValue) else {
            throw AFError.serverTrustEvaluationFailed(reason: .revocationPolicyCreationFailed)
        }

        return policy
    }
}
Copy the code
  1. The SecTrust extension is used to evaluate server reliability

The SecTrust object itself is only a pointer. It is used for certificate verification. A series of CAPI-style methods are called and the SecPolicy verification policy is applied to verify the information pointed to by the pointer. IOS12 started to provide a new Api for verification. The new Api could throw an error when the verification failed, while the old Api needed to assemble the error according to the status code. Therefore, Alamofire provides two sets of verification methods, both above iOS12 and below. And mark the old method as iOS12 Deprecated

extension SecTrust: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =SecTrust {
    
    //MARK: iOS12 above authentication method
    
    @available(iOS 12.macOS 10.14.tvOS 12.watchOS 5.*)
    public func evaluate(afterApplying policy: SecPolicy) throws {
        // Apply the security policy first, and then call evaluate to verify
        try apply(policy: policy).af.evaluate()
    }
    
    // Use the iOS12 API to evaluate trust for specified certificates and policies
    // The error type is returned with the CFError pointer
    @available(iOS 12.macOS 10.14.tvOS 12.watchOS 5.*)
    public func evaluate(a) throws {
        var error: CFError?
        // The error is returned with the CFError pointer
        let evaluationSucceeded = SecTrustEvaluateWithError(type, &error)

        if !evaluationSucceeded {
            // The verification failed and an error was thrown
            throw AFError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: error))
        }
    }

    //MARK: iOS12 the following authentication methods
    @available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)")
    @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate(afterApplying:)")
    @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)")
    @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate(afterApplying:)")
    public func validate(policy: SecPolicy.errorProducer: (_ status: OSStatus._ result: SecTrustResultType) - >Error) throws {
        // Again, apply the security policy first, and then call method validation
        try apply(policy: policy).af.validate(errorProducer: errorProducer)
    }
    
    
    // iOS12 The following methods are used to evaluate whether certificates and policies are trusted. The evaluation result is returned as a SecTrustResultType pointer, and the evaluation method returns the OSStatus value to judge the result. When the evaluation fails, Use the errorProducer of the function input to throw the two status codes as Error types
    @available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate()")
    @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate()")
    @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate()")
    @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate()")
    public func validate(errorProducer: (_ status: OSStatus._ result: SecTrustResultType) - >Error) throws {
        // Call the validation method below iOS12 to get the result and status
        var result = SecTrustResultType.invalid
        let status = SecTrustEvaluate(type, &result)

        // If an Error occurs, use the passed Error to generate a closure to generate an Error and throw it
        guard status.af.isSuccess && result.af.isSuccess else {
            throw errorProducer(status, result)
        }
    }
    

    // Apply the security policy to the SecTrust and prepare for the next evaluation. Failure will throw an error
    public func apply(policy: SecPolicy) throws -> SecTrust {
        let status = SecTrustSetPolicies(type, policy)

        guard status.af.isSuccess else {
            throw AFError.serverTrustEvaluationFailed(reason: .policyApplicationFailed(trust: type,
                                                                                       policy: policy,
                                                                                       status: status))
        }

        return type
    }


    //MARK: tool extension
    
    // Set the custom certificate to self to allow full verification of self-signed certificates
    public func setAnchorCertificates(_ certificates: [SecCertificate]) throws {
        // Add a certificate
        let status = SecTrustSetAnchorCertificates(type, certificates as CFArray)
        guard status.af.isSuccess else {
            throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: status,
                                                                                               certificates: certificates))
        }

        // Only the set certificate is trusted
        let onlyStatus = SecTrustSetAnchorCertificatesOnly(type, true)
        guard onlyStatus.af.isSuccess else {
            throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: onlyStatus,
                                                                                               certificates: certificates))
        }
    }

    // Get the list of public keys
    public var publicKeys: [SecKey] {
        certificates.af.publicKeys
    }

    // Obtain the certificate
    public var certificates: [SecCertificate] {
        // compactMap is used, because the index traversal may not get the certificate, so copactMap filters and returns an array of valid certificates
        (0..<SecTrustGetCertificateCount(type)).compactMap { index in
            SecTrustGetCertificateAtIndex(type, index)
        }
    }

    // The data type of the certificate
    public var certificateData: [Data] {
        certificates.af.data
    }

    // Use the default security policy for the evaluation and do not authenticate the host name
    public func performDefaultValidation(forHost host: String) throws {
        if #available(iOS 12.macOS 10.14.tvOS 12.watchOS 5.*) {
            try evaluate(afterApplying: SecPolicy.af.default)
        } else {
            try validate(policy: SecPolicy.af.default) { status, result in
                AFError.serverTrustEvaluationFailed(reason: .defaultEvaluationFailed(output: .init(host, type, status, result)))
            }
        }
    }

    // The default security policy is used for evaluation, and host name authentication is performed
    public func performValidation(forHost host: String) throws {
        if #available(iOS 12.macOS 10.14.tvOS 12.watchOS 5.*) {
            try evaluate(afterApplying: SecPolicy.af.hostname(host))
        } else {
            try validate(policy: SecPolicy.af.hostname(host)) { status, result in
                AFError.serverTrustEvaluationFailed(reason: .hostValidationFailed(output: .init(host, type, status, result)))
            }
        }
    }
}
Copy the code

ServerTrustManager — Certificate verification manager

The purpose of the ServerTrustManager is to hold different validators for different hosts during initialization. The ServerTrustManager is then held by the Session. In case the SessionDelegate needs to handle server validation, Obtain the ServerTrustManager from the Session using the SessionStateProvider agent in the SessionDelegate. The validator is then retrieved from the map according to the host and returned to the SessionDelegate for verification processing on the server.

open class ServerTrustManager {
    /// Whether all domain names require authentication. Default: true
    /// If true, each host must have a corresponding authenticator, otherwise an exception will be thrown
    /// If false, return nil and no error is thrown if a host does not have a corresponding authenticator
    public let allHostsMustBeEvaluated: Bool

    /// Save the mapping between host and authenticator
    public let evaluators: [String: ServerTrustEvaluating]

    /// initialization. Because different service areas may have different authentication modes, the managed authentication mode is based on domain name
    public init(allHostsMustBeEvaluated: Bool = true.evaluators: [String: ServerTrustEvaluating]) {
        self.allHostsMustBeEvaluated = allHostsMustBeEvaluated
        self.evaluators = evaluators
    }

    /// Return the corresponding authenticator based on the domain name, which can throw an error
    open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
        guard let evaluator = evaluators[host] else {
            // If all domain names are set to be authenticated, an error will be thrown if there is no corresponding authenticator
            if allHostsMustBeEvaluated {
                throw AFError.serverTrustEvaluationFailed(reason: .noRequiredEvaluator(host: host))
            }

            return nil
        }

        return evaluator
    }
}
Copy the code

ServerTrustEvaluating Protocol — Server authentication protocol

This protocol is used to verify the SecTrust object that needs to be verified. It also supports the check of host. The protocol method is very simple, only one method:

public protocol ServerTrustEvaluating {
    #if os(Linux)
    // There is a corresponding method of the same name in Linux
    #else
    /// Verify the SecTrust parameter and domain name. The verification result is saved in SecTrust. If the verification fails, an error is thrown
    func evaluate(_ trust: SecTrust.forHost host: String) throws
    #endif
}
Copy the code

Alamofire internally implements six validator classes that can be used directly. These six classes are all final and do not allow inheritance. If you need to implement your own validator logic, you need to implement your own protocol to validate incoming SecTrust objects

Alamofire implements six validators by default

DefaultTrustEvaluator — Default validator

Using the default security policy to verify the server simply controls whether the host name needs to be verified

public final class DefaultTrustEvaluator: ServerTrustEvaluating {
    private let validateHost: Bool

    // Initialize, which validates the host name by default
    public init(validateHost: Bool = true) {
        self.validateHost = validateHost
    }

    public func evaluate(_ trust: SecTrust.forHost host: String) throws {
        // Call two different validation extensions based on whether the host name is validated or not
        if validateHost {
            try trust.af.performValidation(forHost: host)
        }
        try trust.af.performDefaultValidation(forHost: host)
    }
}
Copy the code

Revocation the certificate verifier

You can use the default security policy to check whether the certificate is revoked. Alamofire’s previous tests found that apple only supports certificate revocation checks in iOS10.1

public final class RevocationTrustEvaluator: ServerTrustEvaluating {
    
    // Encapsulate CFOptionFlags to create a security policy for revocation certificate verification
    public struct Options: OptionSet {
        /// Perform revocation checking using the CRL (Certification Revocation List) method.
        public static let crl = Options(rawValue: kSecRevocationCRLMethod)
        /// Consult only locally cached replies; do not use network access.
        public static let networkAccessDisabled = Options(rawValue: kSecRevocationNetworkAccessDisabled)
        /// Perform revocation checking using OCSP (Online Certificate Status Protocol).
        public static let ocsp = Options(rawValue: kSecRevocationOCSPMethod)
        /// Prefer CRL revocation checking over OCSP; by default, OCSP is preferred.
        public static let preferCRL = Options(rawValue: kSecRevocationPreferCRL)
        /// Require a positive response to pass the policy. If the flag is not set, revocation checking is done on a
        /// "best attempt" basis, where failure to reach the server is not considered fatal.
        public static let requirePositiveResponse = Options(rawValue: kSecRevocationRequirePositiveResponse)
        /// Perform either OCSP or CRL checking. The checking is performed according to the method(s) specified in the
        /// certificate and the value of `preferCRL`.
        public static let any = Options(rawValue: kSecRevocationUseAnyAvailableMethod)

        /// The raw value of the option.
        public let rawValue: CFOptionFlags

        /// Creates an `Options` value with the given `CFOptionFlags`.
        ///
        /// - Parameter rawValue: The `CFOptionFlags` value to initialize with.
        public init(rawValue: CFOptionFlags) {
            self.rawValue = rawValue
        }
    }

    // Whether the default security check is required. The default is true
    private let performDefaultValidation: Bool
    // Whether host name authentication is required. The default value is true
    private let validateHost: Bool
    Options used to create a security policy for revocation certificate verification. The default is
    private let options: Options

    public init(performDefaultValidation: Bool = true.validateHost: Bool = true.options: Options = .any) {
        self.performDefaultValidation = performDefaultValidation
        self.validateHost = validateHost
        self.options = options
    }

    // Implement the protocol verification method
    public func evaluate(_ trust: SecTrust.forHost host: String) throws {
        if performDefaultValidation {
            // The default validation is required. The default validation is performed first when calling the method
            try trust.af.performDefaultValidation(forHost: host)
        }

        if validateHost {
            // The host name needs to be verified
            try trust.af.performValidation(forHost: host)
        }
        
        // You need to use the revocation certificate validation security policy to evaluate. IOS12 uses different methods to verify
        if #available(iOS 12.macOS 10.14.tvOS 12.watchOS 5.*) {
            try trust.af.evaluate(afterApplying: SecPolicy.af.revocation(options: options))
        } else {
            try trust.af.validate(policy: SecPolicy.af.revocation(options: options)) { status, result in
                AFError.serverTrustEvaluationFailed(reason: .revocationCheckFailed(output: .init(host, trust, status, result), options: options))
            }
        }
    }
}
Copy the code

Custom certificate verifier

You can use the custom certificate built in the APP to verify the server certificate, which can be used to verify the self-signed certificate.

public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating {
    // Save the custom certificates, which are all valid certificates in the app by default
    private let certificates: [SecCertificate]
    // Whether to add a custom certificate to the verification anchor certificate, which is used to verify self-signed certificates. The default value is false
    private let acceptSelfSignedCertificates: Bool
    // Whether the default validation is required. The default is true
    private let performDefaultValidation: Bool
    // Whether to verify the host name. Default: true
    private let validateHost: Bool

    public init(certificates: [SecCertificate] = Bundle.main.af.certificates,
                acceptSelfSignedCertificates: Bool = false.performDefaultValidation: Bool = true.validateHost: Bool = true) {
        self.certificates = certificates
        self.acceptSelfSignedCertificates = acceptSelfSignedCertificates
        self.performDefaultValidation = performDefaultValidation
        self.validateHost = validateHost
    }

    public func evaluate(_ trust: SecTrust.forHost host: String) throws {
        guard !certificates.isEmpty else {
            // If the custom certificate is empty, throw an error
            throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
        }

        if acceptSelfSignedCertificates {
            // Add the array of custom certificates to SecTrust if you need to verify self-signed certificates
            try trust.af.setAnchorCertificates(certificates)
        }

        if performDefaultValidation {
            // Perform default validation
            try trust.af.performDefaultValidation(forHost: host)
        }

        if validateHost {
            // Perform host name authentication
            try trust.af.performValidation(forHost: host)
        }

        // Obtain the server certificate from the verification result
        let serverCertificatesData = Set(trust.af.certificateData)
        // Obtain a custom certificate
        let pinnedCertificatesData = Set(certificates.af.data)
        // Whether the server certificate matches the custom certificate (by determining the intersection of the two sets)
        let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData)
        if !pinnedCertificatesInServerData {
            // If there is no match, the verification fails and an error is thrown
            throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host,
                                                                                        trust: trust,
                                                                                        pinnedCertificates: certificates,
                                                                                        serverCertificates: trust.af.certificates))
        }
    }
}
Copy the code

Public key validator

Using the custom public key to verify the server certificate, note that the custom certificate is not added to the SecTrust anchor certificate, so using this validator to verify the self-signed certificate will fail. Therefore, if you want to verify a self-signed certificate, use the custom certificate validator above

public final class PublicKeysTrustEvaluator: ServerTrustEvaluating {
    // Customize the public key array. The default is the public key available in all app built-in certificates
    private let keys: [SecKey]
    // Whether to perform the default validation, which defaults to true
    private let performDefaultValidation: Bool
    // Whether to verify the host name. Default: true
    private let validateHost: Bool

    public init(keys: [SecKey] = Bundle.main.af.publicKeys,
                performDefaultValidation: Bool = true.validateHost: Bool = true) {
        self.keys = keys
        self.performDefaultValidation = performDefaultValidation
        self.validateHost = validateHost
    }

    public func evaluate(_ trust: SecTrust.forHost host: String) throws {
        guard !keys.isEmpty else {
            // Throw an exception if the certificate is empty
            throw AFError.serverTrustEvaluationFailed(reason: .noPublicKeysFound)
        }

        if performDefaultValidation {
            // Perform default validation
            try trust.af.performDefaultValidation(forHost: host)
        }

        if validateHost {
            // Verify the host name
            try trust.af.performValidation(forHost: host)
        }

        // After the default verification is successful, check whether the user-defined public key exists in the public key of the server certificate
        let pinnedKeysInServerKeys: Bool = {
            // Iterate over the user-defined public key array and the public key array of the server certificate one by one. If the same pair exists, the verification succeeds
            for serverPublicKey in trust.af.publicKeys {
                for pinnedPublicKey in keys {
                    if serverPublicKey = = pinnedPublicKey {
                        return true}}}return false} ()if !pinnedKeysInServerKeys {
            // The public key matches the event, throwing an error
            throw AFError.serverTrustEvaluationFailed(reason: .publicKeyPinningFailed(host: host,
                                                                                      trust: trust,
                                                                                      pinnedKeys: keys,
                                                                                      serverKeys: trust.af.publicKeys))
        }
    }
}
Copy the code

Combination validator

Initialization holds an array of validators. During the verification, validators in the array are verified one by one. The verification succeeds only when all validators succeed

public final class CompositeTrustEvaluator: ServerTrustEvaluating {
    private let evaluators: [ServerTrustEvaluating]

    public init(evaluators: [ServerTrustEvaluating]) {
        self.evaluators = evaluators
    }

    public func evaluate(_ trust: SecTrust.forHost host: String) throws {
        // Simply call the extended validator array method to check each of the held arrays. Any failure will throw an error
        try evaluators.evaluate(trust, forHost: host)
    }
}
Copy the code

Verifier for testing

The validation method is null. It does not perform any validation on the server. It is only used for development purposes. The old version is called DisabledEvaluator, and the new version is called DisabledTrustEvaluator

@available(*, deprecated, renamed: "DisabledTrustEvaluator", message: "DisabledEvaluator has been renamed DisabledTrustEvaluator.")
public typealias DisabledEvaluator = DisabledTrustEvaluator

public final class DisabledTrustEvaluator: ServerTrustEvaluating {
    
    public init() {}

    public func evaluate(_ trust: SecTrust, forHost host: String) throws {}
}
Copy the code

conclusion

The above is the encapsulation of Alamofire for server verification. When the request needs to verify the server, it will obtain the corresponding verifier protocol object through ServerTrustManager to verify the SecTrust. ServerTrustManager is not aware of the internal implementation principle of the validator. After using the interface decoupling, the ServerTrustManager can choose to use the validator freely or implement more complex validators to process the business logic.

Authentication processing

Introduction to the

HTTP requests are stateless, so you need to use a token to indicate which user a request belongs to. There are many ways to mark the user’s state. The default way is to create a session in the background after login to mark the user, and send a cookie in the response header back to the request end. Each subsequent request from the requester carries the cookie with it, allowing the server to identify where the request came from. However, session sessions will expire, and the requestor needs to refresh the session in time to obtain new cookies or refresh the cookie validity period. OAuth2 also needs to obtain the token from the single sign-on party and write the token into the request to be sent to the server. Alamofire use RequestInterceptor request interceptor to encapsulate the AuthenticationInterceptor authentication interceptors, and use the interface to abstract the verifier and authentication credentials, interceptor only responsible for using interceptor request, And relevant methods to use validation agreement authentication credentials into the request, after receiving the server returned 401 authentication fails, notify the verifier to refresh operations such as authentication credentials, use the party you just need to focus on verification and validation documents, which need not to care about the complex logic validation logic and refresh.

AuthenticationCredential

A validation certificate is something that needs to be injected into the request. The location of the injection is determined by the verifier. The validation certificate itself only needs to tell the validation interceptor whether it needs to refresh.

public protocol AuthenticationCredential {
    // Whether the credentials need to be refreshed. If false is returned, the Authenticator interface object below calls the refresh method to refresh the credentials
    For example, if the credential is valid for 60 minutes, it is better to return true 5 minutes before the expiration date to refresh the credential, so that the credential will not expire on subsequent requests
    var requiresRefresh: Bool { get}}Copy the code

Verify the Authenticator

It has the following functions:

  1. Inject the credentials into the request
  2. Refreshes credentials and returns a new credential or error
  3. When the server returns a 401, if it’s OAuth2’s 401, do you need a verifier to verify that the 401 error was returned by the content side? It is returned by the single sign-on party (if it is returned by the single sign-on party, it indicates a verification failure and the interceptor will not retry the request. If it is returned by the content party, it indicates that the credentials need to be refreshed. The interceptor will ask the verifier to refresh the credentials and then request again.)
  4. If the request fails, it tells the interceptor whether the server authenticated (if yes, the interceptor retries the request directly; if not, the interceptor tells the verifier to refresh the credentials and re-request).
public protocol Authenticator: AnyObject {
    // Authentication credential generics
    associatedtype Credential: AuthenticationCredential

    // Apply the credentials to the request
    // For example, OAuth2 authentication adds the Access token from the credential to the request header
    func apply(_ credential: Credential.to urlRequest: inout URLRequest)

    // Refresh the credentials. The completion closure is an escape closure
    // The refresh method can be called in two ways:
    // 1. When the request is ready to be sent, if the credentials need to be flushed, the interceptor will call the validator's refresh method to refresh the credentials before issuing the request
    // 2. When the response fails, the interceptor will ask the verifier if the authentication has failed. If so, the refresh method will be called and the request will be retried
    // If OAuth2 is used, there will be a disagreement as to whether the authentication request came from the content server. Is it from the authentication server? If it comes from the content server, all it needs is for the verifier to refresh the credentials and the interceptor to retry the request. If it comes from the authentication server, it needs to throw an error and let the user log in again.
    // If OAuth2 is used, you need to negotiate with the background partner to distinguish the two authentication cases. The interceptor will determine whether the authentication has failed based on the next method.
    func refresh(_ credential: Credential.for session: Session.completion: @escaping (Result<Credential.Error- > >)Void)

    // Check whether the request failed because of the authentication server. This method simply returns false if the credentials issued by the authentication server are not invalidated.
    // If the credentials issued by the authentication server are invalid, determine where the authentication request came from when the request encountered an error such as 401. If it comes from a content server, you need the verifier to refresh the credentials and retry the request. If it comes from an authentication server, you need to throw an error to get the user to log in again. Specific how to judge, need to consult with background development small partner
    // So if the protocol method returns true, the interceptor will not retry the request, but simply throw an error
    // If this method returns false, the interceptor will determine whether the credential is valid by using the following method. If it is valid, the interceptor will retry directly. If it is not valid, the validator will first refresh the credential and retry again
    func didRequest(_ urlRequest: URLRequest.with response: HTTPURLResponse.failDueToAuthenticationError error: Error) -> Bool

    // If the request fails, determine whether the request was authenticated by the current credential
    // This method simply returns true if the certificate issued by the server is not invalidated
    /* This is the case if the credentials issued by the authentication server are invalid: Credential A is still in the validity period, but has been marked as invalid by the authentication server, then the first request response after the invalidity will trigger the refresh logic. During the refresh process, there will be A series of requests authenticated by credential A that have not fallen to the ground, so when the response is triggered, According to this method, we need to check whether the request is authenticated by the current certificate. If so, the retry callback needs to be provisioned and executed after the credentials have been refreshed. If not, it means that the current certificate may already be the new certificate B, so directly retry the request */
    func isRequest(_ urlRequest: URLRequest.authenticatedWith credential: Credential) -> Bool
}
Copy the code

Authentication failure error definition

Two errors are defined:

  1. Voucher lost
  2. Too many flushes
public enum AuthenticationError: Error {
    // The certificate is missing
    case missingCredential
    // Refreshed credentials too many times during the refresh window
    case excessiveRefresh
}
Copy the code

Authentication AuthenticationInterceptor interceptor

  1. The RequestInterceptor protocol is implemented to intercept and retry requests
  2. It holds a verifier object, which is used to intercept the request and let the verifier inject the credentials into the request. At the same time, if the credentials expire during the injection, the verifier will first refresh the credentials and then send them in the injection request. When a request fails, a verifier is used to determine whether the credentials need to be refreshed to retry the request.
  3. It also has a time window object that defines the maximum number of times a credentialcan be refreshed in seconds before the request fails.

define

The type of the verifier held is declared using the generic constraint

public class AuthenticationInterceptor<AuthenticatorType> :RequestInterceptor where AuthenticatorType: Authenticator// Or write:public class AuthenticationInterceptor<AuthenticatorType: Authenticator> :RequestInterceptor
Copy the code

Internal type definition

Internal types are defined to handle internal processing logic

    /// Certificate alias
    public typealias Credential = AuthenticatorType.Credential

    // MARK: Helper Types

    // Refresh the window to limit the maximum number of refreshes within a specified period
    // The interceptor holds the timestamp for each flush. On each flush, it checks whether the number of flush-times in the most recent period has exceeded the maximum number of flush-times. If so, it cancles the flush and throws an error
    public struct RefreshWindow {
        // Limit the period, default 30s
        public let interval: TimeInterval

        // The maximum number of refreshes in a period. The default is 5
        public let maximumAttempts: Int

        public init(interval: TimeInterval = 30.0.maximumAttempts: Int = 5) {
            self.interval = interval
            self.maximumAttempts = maximumAttempts
        }
    }

    // When the interception request is ready to adapt the request, if the credentials need to be refreshed, the parameters of the adaptation method will be encapsulated into the structure and stored in the interceptor temporarily. After the refreshing is complete, completion will be called one by one for all the saved structures to send the request
    private struct AdaptOperation {
        let urlRequest: URLRequest
        let session: Session
        let completion: (Result<URLRequest.Error- > >)Void
    }
    // Intercepts the result of the adaptation request. The interceptor executes different logic according to different adaptation results
    private enum AdaptResult {
        // After the adaptation is complete and the credentials are obtained, it is time for the verifier to inject the credentials into the request
        case adapt(Credential)
        // The request will be cancelled if the authentication fails, the credentials are lost, or the refresh times are too many
        case doNotAdapt(AuthenticationError)
        // Credential refresh is going on, and the parameters of the adaptation method are wrapped into the above structure for temporary storage, and the execution will continue after credential refresh
        case adaptDeferred
    }

    // The mutable state will use the @protected modifier to ensure thread safety
    private struct MutableState {
        // Credentials, which may be empty
        var credential: Credential?
        // Whether credentials are being refreshed
        var isRefreshing = false
        // Refresh the timestamp of the credential
        var refreshTimestamps: [TimeInterval] = []
        // Hold the refresh limit window
        var refreshWindow: RefreshWindow?
        // The parameters related to the temporary adaptation request
        var adaptOperations: [AdaptOperation] = []
        // The completion closure of the staged retry request. When the interceptor retries the failed request, if it finds that the credentials need to be flushed, it will temporarily store the completed closure, then let the verisher refresh the credentials, and then iterate through the array one by one, executing the retry logic
        var requestsToRetry: [(RetryResult) - >Void] =[]}Copy the code

attribute

There are only four properties, one of which is a computed property because it puts several states that need to be thread-safe in MutableState

    // Credential, which reads and writes directly from the mutableState thread safe,
    public var credential: Credential? {
        get { mutableState.credential }
        set { mutableState.credential = newValue }
    }

    / / verification
    let authenticator: AuthenticatorType
    // Refresh the queue of credentials
    let queue = DispatchQueue(label: "org.alamofire.authentication.inspector")

    // Thread safe state objects
    @Protected
    private var mutableState = MutableState(a)Copy the code

The method to implement the interceptor protocol

The interceptor protocol has two methods:

  1. Adapt method, which intercepts the request before it is sent, ADAPTS the request, sends a new request when it succeeds, cancles the request if it fails, and throws an error. The interceptor uses the authenticator in this method to refresh and inject the credentials.
  2. The retry method is used to return retry logic to the Session to resend the request if the request fails. In this method, the interceptor uses the authenticator to validate the request and the corresponding credentials, refreshing the request if necessary.
    // Adaptation request
    public func adapt(_ urlRequest: URLRequest.for session: Session.completion: @escaping (Result<URLRequest.Error- > >)Void) {
        // To obtain the adaptation result, ensure thread safety
        let adaptResult: AdaptResult = $mutableState.write { mutableState in
            // Check whether the credentials are already being refreshed
            guard !mutableState.isRefreshing else {
                // While the credentials are being flushed, store all the parameters in adaptOperations and wait for the flush to complete before processing the parameters
                let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
                mutableState.adaptOperations.append(operation)
                // Return adaptation delay
                return .adaptDeferred
            }
            // Do not refresh credentials again, continue to adapt
            // Obtain the credentials
            guard let credential = mutableState.credential else {
                // The credentials are missing, return error
                let error = AuthenticationError.missingCredential
                return .doNotAdapt(error)
            }

            // Check whether the certificate is valid
            guard !credential.requiresRefresh else {
                // The credentials have expired and need to be refreshed
                let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
                mutableState.adaptOperations.append(operation)
                // Call the refresh credential method
                refresh(credential, for: session, insideLock: &mutableState)
                // Return adaptation delay
                return .adaptDeferred
            }

            // If the credential is valid, a message is displayed indicating successful adaptation
            return .adapt(credential)
        }
        // Process the adaptation result
        switch adaptResult {
        case let .adapt(credential):
            // If the adaptation succeeds, let the verifier inject the credentials into the request
            var authenticatedRequest = urlRequest
            authenticator.apply(credential, to: &authenticatedRequest)
            // Call the completion callback and return the adapted request
            completion(.success(authenticatedRequest))

        case let .doNotAdapt(adaptError):
            // Adaptation failed and the call completion callback threw an error
            completion(.failure(adaptError))

        case .adaptDeferred:
            // The adaptation will be delayed and no processing will be done. After the credential is refreshed, the completion in the temporary parameters will be used to continue processing
            break}}// MARK: Retry
    // Request retry
    public func retry(_ request: Request.for session: Session.dueTo error: Error.completion: @escaping (RetryResult) - >Void) {
        // If there is no URL request or response, do not retry
        guard let urlRequest = request.request, let response = request.response else {
            completion(.doNotRetry)
            return
        }

        // Ask the verifier if the authentication server failed (OAuth2)
        guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else {
            // If the authentication fails, the server does not retry and returns an error, requiring the user to log in again
            completion(.doNotRetry)
            return
        }

        // Whether the credential exists
        guard let credential = credential else {
            // The credentials are lost
            let error = AuthenticationError.missingCredential
            completion(.doNotRetryWithError(error))
            return
        }

        // Ask the verifier if the request is authenticated by the current credentials
        guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else {
            // If the request is not authenticated by the current credential, it indicates that the credential is new and retry directly
            completion(.retry)
            return
        }

        Otherwise, the current credential is invalid. Refresh the credential and try again
        $mutableState.write { mutableState in
            // Hold the completion callback
            mutableState.requestsToRetry.append(completion)
            // If you are refreshing credentials, return
            guard !mutableState.isRefreshing else { return }
            // No credentials are currently refreshed, so call refresh to start refreshing
            refresh(credential, for: session, insideLock: &mutableState)
        }
    }
Copy the code

Private refresh credential method

The interceptor’s core method, used to refresh credentials using the authenticator protocol object, can:

  1. Check the maximum number of refreshes before refreshing
  2. The refresh timestamp is saved after the request for the next refresh to check the number of times
  3. The staged request adaptation result callback is processed after refresh
  4. The staged request retry result callback is processed after refresh
    private func refresh(_ credential: Credential.for session: Session.insideLock mutableState: inout MutableState) {
        // Check whether the maximum number of refresh times is exceeded
        guard !isRefreshExcessive(insideLock: &mutableState) else {
            // The maximum number of refresh times is exceeded
            let error = AuthenticationError.excessiveRefresh
            handleRefreshFailure(error, insideLock: &mutableState)
            return
        }
        // Save the refresh timestamp
        mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime)
        // The tag is refreshing
        mutableState.isRefreshing = true

        // Call the validator's refresh method asynchronously in the queue. Because the interceptor is locked before calling the refresh method, the following asynchronously executes to break the lock and ensure that the refresh behavior will be synchronized.
        queue.async {
            self.authenticator.refresh(credential, for: session) { result in
                // Refresh to complete the callback
                self.$mutableState.write { mutableState in
                    switch result {
                    case let .success(credential):
                        // Processing succeeded
                        self.handleRefreshSuccess(credential, insideLock: &mutableState)
                    case let .failure(error):
                        // Failure handling
                        self.handleRefreshFailure(error, insideLock: &mutableState)
                    }
                }
            }
        }
    }

    // Check whether the maximum number of refresh times is exceeded
    private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool {
        // Get the time window object first. If not, there is no limit on the maximum number of times
        guard let refreshWindow = mutableState.refreshWindow else { return false }
        
        // The minimum timestamp of the time window
        let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval

        // Iterate over the saved refresh timestamp and use Reduce to calculate the number of refreshes in the next window
        let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in
            guard refreshWindowMin < = refreshTimestamp else { return }
            attempts + = 1
        }
        
        // Whether the maximum number of refreshes has been exceeded
        let isRefreshExcessive = refreshAttemptsWithinWindow > = refreshWindow.maximumAttempts

        return isRefreshExcessive
    }

    // Processing the refresh succeeded
    private func handleRefreshSuccess(_ credential: Credential.insideLock mutableState: inout MutableState) {
        // Save the certificate
        mutableState.credential = credential
        // Fetch an array of parameters for the adaptation request
        let adaptOperations = mutableState.adaptOperations
        // Fetch the staged retry callback array
        let requestsToRetry = mutableState.requestsToRetry
        // Empty the temporary data held by self
        mutableState.adaptOperations.removeAll()
        mutableState.requestsToRetry.removeAll()
        // Turn off the status in refresh
        mutableState.isRefreshing = false
        // Execute asynchronously in the queue to break the lock
        queue.async {
            // Continue the request adaptation logic one by one
            adaptOperations.forEach { self.adapt($0.urlRequest, for: $0.session, completion: $0.completion) }
            Retry blocks are executed one by one to start retry
            requestsToRetry.forEach { $0(.retry) }
        }
    }
    // Failed to process the refresh
    private func handleRefreshFailure(_ error: Error.insideLock mutableState: inout MutableState) {
        // Fetch the two temporary arrays
        let adaptOperations = mutableState.adaptOperations
        let requestsToRetry = mutableState.requestsToRetry
        // Empty the temporary array held by self
        mutableState.adaptOperations.removeAll()
        mutableState.requestsToRetry.removeAll()
        // Turn off the refresh status
        mutableState.isRefreshing = false
        // Execute asynchronously in the queue to break the lock
        queue.async {
            // The adapter fails to call one by one
            adaptOperations.forEach { $0.completion(.failure(error)) }
            // The rereiter also failed to call one after another
            requestsToRetry.forEach { $0(.doNotRetryWithError(error)) }
        }
    }
Copy the code