Swiftpack.co - Package - mindbody/Conduit

Conduit

Release Build Status Carthage Compatible CocoaPods Compatible Platform

Conduit is a session-based Swift HTTP networking and auth library.

Within each session, requests are sent through a serial pipeline before being dispatched to the network queue. Within the pipeline, requests are processed through a collection of middleware that can decorate requests, pause the session pipeline, and empty the outgoing queue. From this pattern, Conduit bundles pre-defined middleware for OAuth2 authorization grants through all major flows defined within RFC 6749 and automatically applies tokens to requests as defined in RFC 6750.

Features

  • ☑ Session-based network clients
  • ☑ Configurable middleware for outbound requests
  • ☑ Powerful HTTP request construction and serialization
  • ☑ JSON, XML, SOAP, URL-encoded, and Multipart Form serialization and response deserialization
  • ☑ Complex query parameter serialization
  • ☑ Cancellable/pausable session tasks with upload/download progress closures
  • ☑ SSL pinning / server trust policies
  • ☑ Network Reachability
  • ☑ OAuth2 client management
  • ☑ Automatic token refreshes, client_credential grants, and token storage
  • ☑ Secure token storage with AES-256 CBC encryption
  • ☑ Full manual control over all token grants within RFC 6749
  • ☑ Automatic bearer/basic token application
  • ☑ Embedded authorization page / authorization code grant strategies
  • ☑ Support for multiple network sessions / OAuth2 clients
  • ☑ Interfaces for migrating from pre-existing networking layers

Requirements

  • iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
  • Xcode 8.1+
Conduit Version Swift Version
0.4.x 3.x
0.5 - 0.7.x 4.0
0.8 - 0.13.x 4.1
0.14.0 - 0.17.x 4.2
0.18.0+ 5.0

Installation

Carthage

Add Conduit to your Cartfile:

github "mindbody/Conduit"

Cocoapods

Add Conduit to your Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

target 'MyApplicationTarget' do
    pod 'Conduit'
end

Swift Package Manager

Add Conduit to your Package.swift:

// swift-tools-version:4.1
import PackageDescription

let package = Package(
    dependencies: [
        .package(url: "https://github.com/mindbody/Conduit.git", from: "0.22.0")
    ]
)

Core Networking

URLSessionClient

The heart and soul of Conduit is URLSessionClient. Each client is backed by a URLSession; therefore, URLSessionClient's are initialized with an optional URLSessionConfiguration and a delegate queue.

// Creates a new URLSessionClient with no persistent cache storage and that fires events on a background queue
let mySessionClient = URLSessionClient(sessionConfiguration: URLSessionConfiguration.ephemeral, delegateQueue: OperationQueue())

URLSessionClient is a struct, meaning that it uses value semantics. After initializing a URLSessionClient, any copies can be mutated directly without affecting other copies. However, multiple copies of a single client will use the same network pipeline; they are still part of a single session. In other words, a URLSessionClient should only ever be initialized once per network session.

class MySessionClientManager {

    /// Lazy-loaded URLSessionClient used for interacting with the Kittn API 🐱
    static let kittnAPISessionClient: URLSessionClient = {
        return URLSessionClient()
    }()

}

/// Example usage ///

var sessionClient = MySessionClientManager.kittnAPISessionClient

// As a copy, this won't mutate the original copy or any other copies
sessionClient.middleware = [MyCustomMiddleware()]

HTTP Requests / Responses

URLSessionClient would be nothing without URLRequest's to send to the network. In order to scale against many different possible transport formats within a single session, URLSessionClient has no sense of serialization or deserialization; instead, we fully construct and serialize a URLRequest with an HTTPRequestBuilder and a RequestSerializer and then manually deserialize the response with a ResponseDeserializer.

let requestBuilder = HTTPRequestBuilder(url: kittensRequestURL)
requestBuilder.method = .GET
// Can be serialzed via url-encoding, XML, or multipart/form-data
requestBuilder.serializer = JSONRequestSerializer()
// Powerful query string formatting options allow for complex query parameters
requestBuilder.queryStringParameters = [
    "options" : [
        "include" : [
            "fuzzy",
            "fluffy",
            "not mean"
        ],
        "2+2" : 4
    ]
]
requestBuilder.queryStringFormattingOptions.dictionaryFormat = .dotNotated
requestBuilder.queryStringFormattingOptions.arrayFormat = .commaSeparated
requestBuilder.queryStringFormattingOptions.spaceEncodingRule = .replacedWithPlus
requestBuilder.queryStringFormattingOptions.plusSymbolEncodingRule = .replacedWithDecodedPlus

let request = try requestBuilder.build()

let sessionClient = MySessionClientManager.kittnAPISessionClient
sessionClient.begin(request) { (data, response, error) in
    let deserializer = JSONResponseDeserializer()
    let responseDict = try? deserializer.deserialize(response: response, data: data) as? [String : Any]
    ...
}

The MultipartFormRequestSerializer uses predetermined MIME types to heavily simplify multipart/form-data request construction.

let serializer = MultipartFormRequestSerializer()
let kittenImage = UIImage(named: "jingles")
let kittenImageFormPart = FormPart(name: "kitten", filename: "mr-jingles.jpg", content: .image(kittenImage, .jpeg(compressionQuality: 0.8)))
let pdfFormPart = FormPart(name: "pedigree", filename: "pedigree.pdf", content: .pdf(pedigreePDFData))
let videoFormPart = FormPart(name: "cat-video", filename: "cats.mov", content: .video(catVideoData, .mov))

serializer.append(formPart: kittenImageFormPart)
serializer.append(formPart: pdfFormPart)
serializer.append(formPart: videoFormPart)

requestBuilder.serializer = serializer

XMLRequestSerializer and XMLResponseDeserializer utilize the project-defined XML and XMLNode. XML data is automatically parsed into an indexable and subscriptable tree.

let requestBodyXMLString = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request>give me cats</Request>"

requestBuilder.requestSerializer = XMLRequestSerializer()
requestBuilder.method = .POST
requestBuilder.bodyParameters = XML(xmlString: requestBodyXMLString)

Middleware

When a request is sent through a URLSessionClient, it is first processed serially through a pipeline that may potentially contain middleware. Each middleware component may modify the request, cancel the request, or freeze the pipeline altogether.

Network Pipeline Architecture

This could be used for logging, proxying, authorization, and implementing strict network behaviors.

/// Simple middelware example that logs each outbound request
struct LoggingRequestPipelineMiddleware: RequestPipelineMiddleware {

    public func prepareForTransport(request: URLRequest, completion: @escaping Result<Void>.Block) {
        print("Outbound request: \(request)")
    }

}

mySessionClient.middleware = [LoggingRequestPipelineMiddleware()]

SSL Pinning

Server trust evaluation is built right in to URLSessionClient. A ServerAuthenticationPolicy evaluates session authentication challenges. The most common server authentication request is the start of a TLS/SSL connection, which can be verified with an SSLPinningServerAuthenticationPolicy.

Since it's possible that a single session client may interact with disconnected third-party hosts, the initializer requires a predicate that determines whether or not the trust chain should be pinned against.

let sslPinningPolicy = SSLPinningServerAuthenticationPolicy(certificates: CertificateBundle.certificatesInBundle) { challenge in
    // All challenges from other hosts will be ignored and will proceed through normal system evaluation
    return challenge.protectionSpace.host == "api.example.com"
}

mySessionClient.serverAuthenticationPolicies = [sslPinningPolicy]

Auth

Conduit implements all major OAuth2 flows and intricacies within RFC 6749 and RFC 6750. This makes Conduit an ideal foundational solution for OAuth2-based API SDK's.

Configuration

Every Auth session requires a client configuration, which, in turn, requires an OAuth2 server environment.

guard let tokenGrantURL = URL(string: "https://api.example.com/oauth2/issue/token") else {
    return
}

let scope = "cats dogs giraffes"
let serverEnvironment = OAuth2ServerEnvironment(scope: scope, tokenGrantURL: tokenGrantURL)

let clientID = "my_oauth2_client"
let clientSecret = "shhhh"

let clientConfiguration = OAuth2ClientConfiguration(clientIdentifier: clientID, clientSecret: clientSecret, environment: serverEnvironment)

// Only for convenience for single-client applications; can be managed elsewhere
Auth.defaultClientConfiguration = clientConfiguration

Token Storage

OAuth2 token storage allows for automatic retrieval/updates within token grant flows.

// Stores user and client tokens to the keychain
let keychainStore = OAuth2KeychainStore(serviceName: "com.company.app-name.oauth-token", accessGroup: "com.company.shared-access-group")

// Stores user and client tokens to UserDefaults or a defined storage location
let diskStore = OAuth2TokenDiskStore(storageMethod: .userDefaults)

// Stores user and client tokens to memory; useful for tests/debugging
let memoryStore = OAuth2TokenMemoryStore()

// Only for convenience for single-client applications; can be managed elsewhere
Auth.defaultTokenStore = keychainStore

Token Grants

OAuth2 token grants are handled via strategies. Conduit supports all grants listed in RFC 6749: password, client_credentials, authorization_code, refresh_token, and custom extension grants.

In many places throughout Conduit Auth, an OAuth2Authorization is required. OAuth2Authorization is a simple struct that segregates client authorization from user authorization, and Bearer credentials from Basic credentials. While certain OAuth2 servers may not actually respect these as different roles or identities, it allows for clear-cut management over user-sensitive data vs. client-sensitive data.

When manually creating and using an OAuth2TokenGrantStrategy (common for Resource Owner flows), tokens must also be manually stored:

// This token grant is most-likely issued on behalf of a user, so the authorization level is "user", and the authorization type is "bearer"
let tokenGrantStrategy = OAuth2PasswordTokenGrantStrategy(username: "user@example.com", password: "hunter2", clientConfiguration: Auth.defaultClientConfiguration)
tokenGrantStrategy.issueToken { result in
    guard case .value(let token) = result else {
        // Handle failure
        return
    }
    let userBearerAuthorization = OAuth2Authorization(type: .bearer, level: .user)
    Auth.defaultTokenStore.store(token: token, for: Auth.defaultClientConfiguration, with: userBearerAuthorization)
    // Handle success
}
// This token grant is issued on behalf of a client, so the authorization level is "client"
let tokenGrantStrategy = OAuth2ClientCredentialsTokenGrantStrategy(clientConfiguration: Auth.defaultClientConfiguration)
tokenGrantStrategy.issueToken { result in
    guard case .value(let token) = result else {
        // Handle failure
        return
    }
    let clientBearerAuthorization = OAuth2Authorization(type: .bearer, level: .client)
    Auth.defaultTokenStore.store(token: token, for: Auth.defaultClientConfiguration, with: clientBearerAuthorization)
    // Handle success
}

For the Authorization Code flow, there exists OAuth2AuthorizationStrategy. Currently, implementation only exists for iOS Safari.

// AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    OAuth2AuthorizationRedirectHandler.default.authorizationURLScheme = "x-my-custom-scheme"
    // Other setup
    return true
}

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    if OAuth2AuthorizationRedirectHandler.default.handleOpen(url) {
        return true
    }
    ...
}
// SampleAuthManager.swift

guard let authorizationBaseURL = URL(string: "https://api.example.com/oauth2/authorize"),
    let redirectURI = URL(string: "x-my-custom-scheme://authorize") else {
    return
}
let authorizationStrategy = OAuth2SafariAuthorizationStrategy(presentingViewController: visibleViewController, authorizationRequestEndpoint: authorizationBaseURL)

var authorizationRequest = OAuth2AuthorizationRequest(clientIdentifier: "my_oauth2_client")
authorizationRequest.redirectURI = redirectURI
authorizationRequest.scope = "cats dogs giraffes"
authorizationRequest.state = "abc123"
authorizationRequest.additionalParameters = [
    "custom_param_1" : "value"
]

authorizationStrategy.authorize(request: authorizationRequest) { result in
    guard case .value(let response) = result else {
        // Handle failure
        return
    }
    if response.state != authorizationRequest.state {
        // We've been attacked! 👽
        return
    }
    let tokenGrantStrategy = OAuth2AuthorizationCodeTokenGrantStrategy(code: response.code, redirectURI: redirectURI, clientConfiguration: Auth.defaultClientConfiguration)

    tokenGrantStrategy.issueToken { result in
        // Store token
        // Handle success/failure
    }
}

Auth Middleware

Tying it all together, Conduit provides middleware that handles most of dirty work involved with OAuth2 clients. This briefly sums up the power of OAuth2RequestPipelineMiddleware:

  • Automatically applies stored Bearer token for the given OAuth2 client if one exists and is valid
  • Pauses/empties the outbound network queue and attempts a refresh_token grant for expired tokens, if a refresh token exists
  • Attempts a client_credentials grant for client-bearer authorizations if the token is expired or doesn't exist
  • Automatically applies Basic authorization for client-basic authorizations

When fully utilized, Conduit makes service operations extremely easy to read and understand, from the parameters/encoding required all the way to the exact type and level of authorization needed:

let requestBuilder = HTTPRequestBuilder(url: protectedKittensRequestURL)
requestBuilder.method = .GET
requestBuilder.serializer = JSONRequestSerializer()
requestBuilder.queryStringParameters = [
    "options" : [
        "include" : [
            "fuzzy",
            "fluffy",
            "not mean"
        ],
        "2+2" : 4
    ]
]
requestBuilder.queryStringFormattingOptions.dictionaryFormat = .dotNotated
requestBuilder.queryStringFormattingOptions.arrayFormat = .commaSeparated
requestBuilder.queryStringFormattingOptions.spaceEncodingRule = .replacedWithPlus
requestBuilder.queryStringFormattingOptions.plusSymbolEncodingRule = .replacedWithDecodedPlus

let request = try requestBuilder.build()

let bearerUserAuthorization = OAuth2Authorization(type: .bearer, level: .user)
let authMiddleware = OAuth2RequestPipelineMiddleware(clientConfiguration: Auth.defaultClientConfiguration, authorization: userBearerAuthorization, tokenStorage: Auth.defaultTokenStore)

var sessionClient = MySessionClientManager.kittnAPISessionClient
// Again, this is a copy, so we're free to mutate the middleware within the copy
sessionClient.middleware.append(authMiddleware)

sessionClient.begin(request) { (data, response, error) in
    let deserializer = JSONResponseDeserializer()
    let responseDict = try? deserializer.deserialize(response: response, data: data) as? [String : Any]
    ...
}

Examples

This repo includes an iOS example, which is attached to Conduit.xcworkspace

License

Released under the Apache 2.0 license. See LICENSE for more details.

Credits

mindbody-logo

Conduit is owned by MINDBODY, Inc. and continuously maintained by our contributors.

Github

link
Stars: 47

Dependencies

Used By

Total: 0

Releases

Release 0.22.0 -

Breaking

  • Remove deprecated types: BearerOAuth2Token, OAuth2TokenAES256CBCCipher, and OAuth2TokenDiskStore

Enhancements

  • None

Bug Fixes

  • None

Other

  • None

Release 0.21.0 -

Breaking

  • ResponsePipelineMiddleware protocol has been updated.

Enhancements

  • Surface request metrics to response middleware
    • SessionDelegate has been updated to capture request metrics in TaskResponse.
    • URLSessionClient has been updated to pass TaskResponse to any response middleware.
    • ResponsePipelineMiddleware has been refactored to pass a TaskResponse structure.

Bug Fixes

  • None

Other

  • None

Release 0.20.0 -

Breaking

  • None

Enhancements

  • Add TokenMigrator to perform migrations between token stores and client configurations.
  • Add ConduitDynamic dynamic library to Package description.

Bug Fixes

  • None

Other

  • None

Release 0.19.0 -

Breaking

  • AES256CBCCipher.Error has been completely removed in favor of CryptoError

Enhancements

  • Encryptor and Decryptor protocols have been added to genericize crypto operations
  • Cipher is a typealias for a type that is both an Encryptor and Decryptor
  • AES256CBCCipher now implements Cipher
  • HybridCipher has been added to support hybrid encryption, which delegates asymmetric key generation to a HybridKeyProvider
  • KeychainHybridKeyProvider uses keychain queries to provide either RSA or ECC key pairs. ECC keys are stored on the Secure Enclave if possible. More details here.
  • OAuth2TokenCryptoCipher delegates token data encryption / decryption to an underlying Encryptor and Decryptor
  • OAuth2TokenAES256CBCCipher has been deprecated in favor of OAuth2TokenCryptoCipher provided with an AES256CBCCipher

Bug Fixes

  • None

Other

  • None

Release 0.18.3 -

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • Fix concurrency issues in URLSessionClient (#141)

Other

  • None

Release 0.18.2 -

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • (Bugfix) | Crash in URLSessionClient #138
  • (Bugfix) | Add SwiftLint exception to clear false-positive (duplicate_enum_case) #139

Other

  • None

Release 0.18.1 -

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • None

Other

  • Restrict plaforms in Package.swift

Release 0.18.0 -

Breaking

  • Update to Swift 5.0

Enhancements

  • None

Bug Fixes

  • None

Other

  • None

Release 0.17.0 -

Breaking

  • OAuth2TokenUserDefaultsStore conformance to OAuth2TokenEncryptedStore.
  • OAuth2TokenFileStore conformance to OAuth2TokenEncryptedStore.
  • OAuth2Authorization conformance to Equatable.
  • OAuth2Authorization now exposes read-only type and level properties.
  • OAuth2ClientConfiguration conformance to Equatable.
  • OAuth2ServerEnvironment conformance to Equatable.
  • OAuth2TokenStore protocol now exposes isRefreshTokenLockedFor, tokenIdentifierFor and tokenLockIdentifierFor.
  • BearerToken conformance to Equatable.
  • Add dependency to Security.framework

Enhancements

  • Introduce OAuth2TokenCipher and OAuth2TokenEncryptedStore protocols to allow for token encryption/decryption.
  • User Defaults token store now supports token encryption.
  • File token store now supports token encryption.
  • Fully support application-side custom token stores.
  • Introduce OAuth2TokenAES256CBCCipher cipher for AES 256bit CBC token encryption.

Bug Fixes

  • None

Other

  • None

Release 0.16.0 -

0.16.0

Breaking

  • None

Enhancements

  • Include expiring token in pre-fetch hook of Migrator

Bug Fixes

  • None

Other

  • None

Release 0.15.2 -

0.15.2

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • FormEncodedRequestSerializer can once again be created publicly

Other

  • None

Release 0.15.1 -

Breaking

  • None

Enhancements

  • Correct XML Serialization for Predefined Escape characters

Bug Fixes

  • Update XML Serialziation to correctly escape Predefined Escape characters disallowed in XML Requests

Other

  • None

Release 0.15.0 -

Breaking

  • None

Enhancements

  • Add context support to OAuth2TokenUserDefaultsStore to enable sandboxing at key level.

Bug Fixes

  • Update SOAP envelope encodingStyle property to non-optional.
  • Fix file-based token store when path does not exist.

Other

  • None

Release 0.14.0 -

Breaking

  • serialize(request:bodyParameters:) is now public since FormEncodedRequestSerializer is a final class.
  • defaultHTTPHeaders is now public since static properties cannot be open.
  • Add XMLNodeAttributes to preserve order of attributes on serialized XML nodes

Enhancements

  • Add new xmlString(format:) method to XML and XMLNode. XMLSerialization format options are:
    • .condensed -> same single-line condensed output as before.
    • .prettyPrinted(spaces: Int) -> human-readable format with flexible indentation level (number of spaces).

Bug Fixes

  • None

Other

  • None

Release 0.13.0 -

Breaking

  • None

Enhancements

  • Find XML nodes matching a given function.
  • Traverse XML tree upwards with parent property.

Bug Fixes

  • None

Other

  • None

Release 0.12.0 -

Breaking

  • None

Enhancements

  • Add customParameters to OAuth2AuthorizationResponse.
  • Improved verbose logging for middleware pipeline.
  • Allow direct manipulation of XML trees by converting XML and XMLNode from struct to class.

Bug Fixes

  • None

Other

  • None

Release 0.11.0 -

0.11.0

Breaking

  • middleware has been replaced by requestMiddleware

Enhancements

  • ResponsePipelineMiddleware added
  • URLSessionClient now accepts both request and response middleware

Bug Fixes

  • None

Other

  • None

Release 0.10.3 -

0.10.3

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • expires_in is no longer a required field for access token responses

Other

  • None

Release 0.10.2 -

0.10.2

Breaking

  • None

Enhancements

  • None

Bug Fixes

Other

  • None

Release 0.10.1 -

0.10.1

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • OAuth2TokenUserDefaultsStore doesn't default to .standard for certain operations

Other

  • None

Release 0.10.0 -

0.10.0

Breaking

  • OAuth2TokenStore now includes required interface for handling refresh token locks

Enhancements

  • Loose-IPC is now used to handle a single active session across multiple processes (i.e. app extensions). Token refreshes were previously only safeguarded via serial pipeline; now, they are also protected against concurrent refreshes from other processes using the same storage
  • Precise token lock expiration control is available via OAuth2RequestPipelineMiddleware.tokenRefreshLockRelinquishInterval
  • OAuth2TokenUserDefaultsStore adds the ability to store to user-defined UserDefaults, most commonly for app group containers
  • OAuth2TokenFileStore adds additional I/O control, such as multiprocess file coordination via NSFileCoordinator and file protection

Bug Fixes

  • OAuth2TokenFileStore solves a design flaw in OAuth2TokenDiskStore that prevented multiple tokens to be written for a single OAuth 2.0 client

Other

  • Code coverage is now enforced via codecov.io
  • Added XMLRequestSerializerTests
  • Added AuthTokenMigratorTests
  • OAuth2TokenDiskStore is now deprecated in favor of OAuth2TokenFileStore and OAuth2TokenUserDefaultsStore

Release 0.9.2 -

0.9.2

Breaking

  • None

Enhancements

  • Custom refresh grant strategies can be provided on OAuth2RequestPipelineMiddleware
  • Default token refresh logic has been moved to OAuth2RefreshTokenGrantStrategy

Bug Fixes

  • None

Other

  • None

Release 0.9.1 -

0.9.1

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • refresh_token grants no longer require a scope to be set

Other

  • None

Release 0.9.0 -

Breaking

  • Update XMLNode interface to better define usage expectations.
    • Default values for nodes(named:traversal:) and node() methods have been removed and traversal algorithm must be now set explicitly.
    • getValue(name:) has been updated to always use .firstLevel only.
    • New method findValue(name:traversal:) has been added, and requires the traversal algorithm to be set explicitly.

Enhancements

  • None

Bug Fixes

  • None

Other

  • None

Release 0.8.0 -

Breaking

  • Update to Xcode 9.3 / Swift 4.1

Enhancements

  • None

Bug Fixes

  • None

Other

  • None

Release 0.7.2 -

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • Fix issue where Logger levels where being ignored.

Other

  • None

Release 0.7.1 -

Breaking

  • None

Enhancements

  • Use Xcode new build system.
  • Run CI on Xcode 9.2 image.

Bug Fixes

  • None

Other

  • None

Release 0.7.0 -

Breaking

  • Remove implicit force unwrapped property Conduit.Auth.defaultClientConfiguration (now it is an optional).

Enhancements

  • Refactor unit tests to allow for parallel testing.

Bug Fixes

  • None

Other

  • None

Release 0.6.2 -

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • None

Other

  • Xcode 9.0.1 binaries

Release 0.6.1 -

Breaking

  • None

Enhancements

  • None

Bug Fixes

  • Fixed a critical issue with public-key pinning in SSLPinningServerAuthenticationPolicy