{"id":13465919,"url":"https://github.com/mindbody/Conduit","last_synced_at":"2025-03-25T21:30:45.721Z","repository":{"id":26090229,"uuid":"96946735","full_name":"mindbody/Conduit","owner":"mindbody","description":"Robust Swift networking for web APIs","archived":false,"fork":false,"pushed_at":"2025-03-03T10:37:41.000Z","size":2765,"stargazers_count":52,"open_issues_count":1,"forks_count":8,"subscribers_count":23,"default_branch":"main","last_synced_at":"2025-03-03T11:35:37.138Z","etag":null,"topics":["ios","ios-framework","networking","oauth2-client","swift","swift-package","swift-package-manager"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mindbody.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-07-11T23:55:27.000Z","updated_at":"2024-11-27T05:10:55.000Z","dependencies_parsed_at":"2022-07-27T06:16:06.069Z","dependency_job_id":"2f3e6d97-214a-4735-8358-2e565452b035","html_url":"https://github.com/mindbody/Conduit","commit_stats":{"total_commits":161,"total_committers":12,"mean_commits":"13.416666666666666","dds":0.515527950310559,"last_synced_commit":"5fa6640a3cd5bfff567c92350fbda64dc4544702"},"previous_names":[],"tags_count":48,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindbody%2FConduit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindbody%2FConduit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindbody%2FConduit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindbody%2FConduit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mindbody","download_url":"https://codeload.github.com/mindbody/Conduit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245546887,"owners_count":20633250,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ios","ios-framework","networking","oauth2-client","swift","swift-package","swift-package-manager"],"created_at":"2024-07-31T15:00:36.931Z","updated_at":"2025-03-25T21:30:45.172Z","avatar_url":"https://github.com/mindbody.png","language":"Swift","funding_links":[],"categories":["Libs","Network [🔝](#readme)"],"sub_categories":["Network"],"readme":"# Conduit\n\n[![Release](https://img.shields.io/github/release/mindbody/conduit.svg)](https://github.com/mindbody/Conduit/releases)\n[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Conduit.svg)](https://cocoapods.org/pods/Conduit)\n[![Platform](https://img.shields.io/cocoapods/p/Conduit.svg?style=flat)](http://cocoadocs.org/docsets/Conduit)\n\nConduit is a session-based Swift HTTP networking and auth library.\n\nWithin each session, requests are sent through a serial [pipeline](https://en.wikipedia.org/wiki/Pipeline_(software)) before being dispatched to the network queue. Within the pipeline, requests are processed through a collection of [middleware](https://en.wikipedia.org/wiki/Interceptor_pattern) that can decorate requests, pause the session pipeline, and empty the outgoing queue. From this pattern, Conduit bundles pre-defined middleware for [OAuth2](https://oauth.net/2/) authorization grants through all major flows defined within [RFC 6749](https://tools.ietf.org/html/rfc6749) and automatically applies tokens to requests as defined in [RFC 6750](http://tools.ietf.org/html/rfc6750).\n\n- [Features](#features)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Core Networking](#core-networking)\n\t- [URLSessionClient](#urlsessionclient)\n\t- [HTTP Requests / Responses](#http-requests--responses)\n\t- [Middleware](#middleware)\n\t- [SSL Pinning](#ssl-pinning)\n- [Auth](#auth)\n\t- [Configuration](#configuration)\n\t- [Token Storage](#token-storage)\n\t- [Token Grants](#token-grants)\n\t- [Auth Middleware](#auth-middleware)\n- [Examples](#examples)\n- [Credits](#credits)\n- [License](#license)\n\n## Features\n\n- [x] Session-based network clients\n- [x] Configurable middleware for outbound requests\n- [x] Powerful HTTP request construction and serialization\n- [x] JSON, XML, SOAP, URL-encoded, and Multipart Form serialization and response deserialization\n- [x] Complex query parameter serialization\n- [x] Cancellable/pausable session tasks with upload/download progress closures\n- [x] SSL pinning / server trust policies\n- [x] Network Reachability\n- [x] OAuth2 client management\n- [x] Automatic token refreshes, client_credential grants, and token storage\n- [x] Secure token storage with AES-256 CBC encryption\n- [x] Full manual control over all token grants within RFC 6749\n- [x] Automatic bearer/basic token application\n- [x] Embedded authorization page / authorization code grant strategies\n- [x] Support for multiple network sessions / OAuth2 clients\n- [x] Interfaces for migrating from pre-existing networking layers\n\n## Requirements\n- iOS 9.0+ / macOS 10.11+ / tvOS 9.0+ / watchOS 2.0+\n- Xcode 8.1+\n\n| Conduit  Version | Swift Version |\n|------------------|---------------|\n| 0.4.x            | 3.x           |\n| 0.5 - 0.7.x      | 4.0           |\n| 0.8 - 0.13.x     | 4.1           |\n| 0.14.0 - 0.17.x  | 4.2           |\n| 0.18.0+          | 5.0           |\n\n## Installation\n\n### Swift Package Manager (recommended)\n\nAdd `Conduit` to your `Package.swift`:\n\n```swift\n// swift-tools-version:5.0\nimport PackageDescription\n\nlet package = Package(\n    dependencies: [\n        .package(url: \"https://github.com/mindbody/Conduit.git\", from: \"1.0.0\")\n    ]\n)\n```\n\n### Cocoapods\n\nAdd `Conduit` to your `Podfile`:\n\n```\nsource 'https://github.com/CocoaPods/Specs.git'\nplatform :ios, '9.0'\nuse_frameworks!\n\ntarget 'MyApplicationTarget' do\n    pod 'Conduit'\nend\n```\n\n## Core Networking\n\n### URLSessionClient\n\nThe 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.\n\n```swift\n// Creates a new URLSessionClient with no persistent cache storage and that fires events on a background queue\nlet mySessionClient = URLSessionClient(sessionConfiguration: URLSessionConfiguration.ephemeral, delegateQueue: OperationQueue())\n```\n\n`URLSessionClient` is a struct, meaning that it uses [value semantics](https://news.realm.io/news/swift-gallagher-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.\n\n```swift\nclass MySessionClientManager {\n\n    /// Lazy-loaded URLSessionClient used for interacting with the Kittn API 🐱\n    static let kittnAPISessionClient: URLSessionClient = {\n        return URLSessionClient()\n    }()\n\n}\n\n/// Example usage ///\n\nvar sessionClient = MySessionClientManager.kittnAPISessionClient\n\n// As a copy, this won't mutate the original copy or any other copies\nsessionClient.middleware = [MyCustomMiddleware()]\n```\n\n### HTTP Requests / Responses\n\n`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`.\n\n```swift\nlet requestBuilder = HTTPRequestBuilder(url: kittensRequestURL)\nrequestBuilder.method = .GET\n// Can be serialzed via url-encoding, XML, or multipart/form-data\nrequestBuilder.serializer = JSONRequestSerializer()\n// Powerful query string formatting options allow for complex query parameters\nrequestBuilder.queryStringParameters = [\n    \"options\" : [\n        \"include\" : [\n            \"fuzzy\",\n            \"fluffy\",\n            \"not mean\"\n        ],\n        \"2+2\" : 4\n    ]\n]\nrequestBuilder.queryStringFormattingOptions.dictionaryFormat = .dotNotated\nrequestBuilder.queryStringFormattingOptions.arrayFormat = .commaSeparated\nrequestBuilder.queryStringFormattingOptions.spaceEncodingRule = .replacedWithPlus\nrequestBuilder.queryStringFormattingOptions.plusSymbolEncodingRule = .replacedWithDecodedPlus\n\nlet request = try requestBuilder.build()\n\nlet sessionClient = MySessionClientManager.kittnAPISessionClient\nsessionClient.begin(request) { (data, response, error) in\n    let deserializer = JSONResponseDeserializer()\n    let responseDict = try? deserializer.deserialize(response: response, data: data) as? [String : Any]\n    ...\n}\n```\n\nThe `MultipartFormRequestSerializer` uses predetermined MIME types to heavily simplify multipart/form-data request construction.\n\n```swift\nlet serializer = MultipartFormRequestSerializer()\nlet kittenImage = UIImage(named: \"jingles\")\nlet kittenImageFormPart = FormPart(name: \"kitten\", filename: \"mr-jingles.jpg\", content: .image(kittenImage, .jpeg(compressionQuality: 0.8)))\nlet pdfFormPart = FormPart(name: \"pedigree\", filename: \"pedigree.pdf\", content: .pdf(pedigreePDFData))\nlet videoFormPart = FormPart(name: \"cat-video\", filename: \"cats.mov\", content: .video(catVideoData, .mov))\n\nserializer.append(formPart: kittenImageFormPart)\nserializer.append(formPart: pdfFormPart)\nserializer.append(formPart: videoFormPart)\n\nrequestBuilder.serializer = serializer\n```\n\n`XMLRequestSerializer` and `XMLResponseDeserializer` utilize the project-defined `XML` and `XMLNode`. XML data is automatically parsed into an indexable and subscriptable tree.\n\n```swift\nlet requestBodyXMLString = \"\u003c?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\"?\u003e\u003cRequest\u003egive me cats\u003c/Request\u003e\"\n\nrequestBuilder.requestSerializer = XMLRequestSerializer()\nrequestBuilder.method = .POST\nrequestBuilder.bodyParameters = XML(xmlString: requestBodyXMLString)\n```\n\n### Middleware\n\nWhen 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.\n\n![Network Pipeline Architecture](images/NetworkPipelineArchitecture.png)\n\nThis could be used for logging, proxying, authorization, and implementing strict network behaviors.\n\n```swift\n/// Simple middelware example that logs each outbound request\nstruct LoggingRequestPipelineMiddleware: RequestPipelineMiddleware {\n\n    public func prepareForTransport(request: URLRequest, completion: @escaping Result\u003cVoid\u003e.Block) {\n        print(\"Outbound request: \\(request)\")\n    }\n\n}\n\nmySessionClient.middleware = [LoggingRequestPipelineMiddleware()]\n```\n\n### SSL Pinning\n\nServer 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`.\n\nSince 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.\n\n```swift\nlet sslPinningPolicy = SSLPinningServerAuthenticationPolicy(certificates: CertificateBundle.certificatesInBundle) { challenge in\n    // All challenges from other hosts will be ignored and will proceed through normal system evaluation\n    return challenge.protectionSpace.host == \"api.example.com\"\n}\n\nmySessionClient.serverAuthenticationPolicies = [sslPinningPolicy]\n```\n\n## Auth\n\nConduit implements all major OAuth2 flows and intricacies within [RFC 6749](https://tools.ietf.org/html/rfc6749) and [RFC 6750](https://tools.ietf.org/html/rfc6750). This makes Conduit an ideal foundational solution for OAuth2-based API SDK's.\n\n### Configuration\n\nEvery Auth session requires a client configuration, which, in turn, requires an OAuth2 server environment.\n\n```swift\nguard let tokenGrantURL = URL(string: \"https://api.example.com/oauth2/issue/token\") else {\n    return\n}\n\nlet scope = \"cats dogs giraffes\"\nlet serverEnvironment = OAuth2ServerEnvironment(scope: scope, tokenGrantURL: tokenGrantURL)\n\nlet clientID = \"my_oauth2_client\"\nlet clientSecret = \"shhhh\"\n\nlet clientConfiguration = OAuth2ClientConfiguration(clientIdentifier: clientID, clientSecret: clientSecret, environment: serverEnvironment)\n\n// Only for convenience for single-client applications; can be managed elsewhere\nAuth.defaultClientConfiguration = clientConfiguration\n```\n\n### Token Storage\n\nOAuth2 token storage allows for automatic retrieval/updates within token grant flows.\n\n```swift\n// Stores user and client tokens to the keychain\nlet keychainStore = OAuth2KeychainStore(serviceName: \"com.company.app-name.oauth-token\", accessGroup: \"com.company.shared-access-group\")\n\n// Stores user and client tokens to UserDefaults or a defined storage location\nlet diskStore = OAuth2TokenDiskStore(storageMethod: .userDefaults)\n\n// Stores user and client tokens to memory; useful for tests/debugging\nlet memoryStore = OAuth2TokenMemoryStore()\n\n// Only for convenience for single-client applications; can be managed elsewhere\nAuth.defaultTokenStore = keychainStore\n```\n\n### Token Grants\n\nOAuth2 token grants are handled via [strategies](https://en.wikipedia.org/wiki/Strategy_pattern). Conduit supports all grants listed in RFC 6749: `password`, `client_credentials`, `authorization_code`, `refresh_token`, and custom extension grants.\n\nIn 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.\n\nWhen manually creating and using an `OAuth2TokenGrantStrategy` (common for Resource Owner flows), tokens must also be manually stored:\n\n```swift\n// This token grant is most-likely issued on behalf of a user, so the authorization level is \"user\", and the authorization type is \"bearer\"\nlet tokenGrantStrategy = OAuth2PasswordTokenGrantStrategy(username: \"user@example.com\", password: \"hunter2\", clientConfiguration: Auth.defaultClientConfiguration)\ntokenGrantStrategy.issueToken { result in\n    guard case .value(let token) = result else {\n        // Handle failure\n        return\n    }\n    let userBearerAuthorization = OAuth2Authorization(type: .bearer, level: .user)\n    Auth.defaultTokenStore.store(token: token, for: Auth.defaultClientConfiguration, with: userBearerAuthorization)\n    // Handle success\n}\n```\n\n```swift\n// This token grant is issued on behalf of a client, so the authorization level is \"client\"\nlet tokenGrantStrategy = OAuth2ClientCredentialsTokenGrantStrategy(clientConfiguration: Auth.defaultClientConfiguration)\ntokenGrantStrategy.issueToken { result in\n    guard case .value(let token) = result else {\n        // Handle failure\n        return\n    }\n    let clientBearerAuthorization = OAuth2Authorization(type: .bearer, level: .client)\n    Auth.defaultTokenStore.store(token: token, for: Auth.defaultClientConfiguration, with: clientBearerAuthorization)\n    // Handle success\n}\n```\n\nFor the Authorization Code flow, there exists `OAuth2AuthorizationStrategy`. Currently, implementation only exists for iOS Safari.\n\n```swift\n// AppDelegate.swift\nfunc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -\u003e Bool {\n    OAuth2AuthorizationRedirectHandler.default.authorizationURLScheme = \"x-my-custom-scheme\"\n    // Other setup\n    return true\n}\n\nfunc application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -\u003e Bool {\n    if OAuth2AuthorizationRedirectHandler.default.handleOpen(url) {\n        return true\n    }\n    ...\n}\n```\n\n```swift\n// SampleAuthManager.swift\n\nguard let authorizationBaseURL = URL(string: \"https://api.example.com/oauth2/authorize\"),\n    let redirectURI = URL(string: \"x-my-custom-scheme://authorize\") else {\n    return\n}\nlet authorizationStrategy = OAuth2SafariAuthorizationStrategy(presentingViewController: visibleViewController, authorizationRequestEndpoint: authorizationBaseURL)\n\nvar authorizationRequest = OAuth2AuthorizationRequest(clientIdentifier: \"my_oauth2_client\")\nauthorizationRequest.redirectURI = redirectURI\nauthorizationRequest.scope = \"cats dogs giraffes\"\nauthorizationRequest.state = \"abc123\"\nauthorizationRequest.additionalParameters = [\n    \"custom_param_1\" : \"value\"\n]\n\nauthorizationStrategy.authorize(request: authorizationRequest) { result in\n    guard case .value(let response) = result else {\n        // Handle failure\n        return\n    }\n    if response.state != authorizationRequest.state {\n        // We've been attacked! 👽\n        return\n    }\n    let tokenGrantStrategy = OAuth2AuthorizationCodeTokenGrantStrategy(code: response.code, redirectURI: redirectURI, clientConfiguration: Auth.defaultClientConfiguration)\n\n    tokenGrantStrategy.issueToken { result in\n        // Store token\n        // Handle success/failure\n    }\n}\n```\n\n### Auth Middleware\n\nTying it all together, Conduit provides middleware that handles most of dirty work involved with OAuth2 clients. This briefly sums up the power of `OAuth2RequestPipelineMiddleware`:\n\n- Automatically applies stored Bearer token for the given OAuth2 client if one exists and is valid\n- Pauses/empties the outbound network queue and attempts a `refresh_token` grant for expired tokens, if a refresh token exists\n- Attempts a `client_credentials` grant for client-bearer authorizations if the token is expired or doesn't exist\n- Automatically applies Basic authorization for client-basic authorizations\n\nWhen 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:\n\n```swift\nlet requestBuilder = HTTPRequestBuilder(url: protectedKittensRequestURL)\nrequestBuilder.method = .GET\nrequestBuilder.serializer = JSONRequestSerializer()\nrequestBuilder.queryStringParameters = [\n    \"options\" : [\n        \"include\" : [\n            \"fuzzy\",\n            \"fluffy\",\n            \"not mean\"\n        ],\n        \"2+2\" : 4\n    ]\n]\nrequestBuilder.queryStringFormattingOptions.dictionaryFormat = .dotNotated\nrequestBuilder.queryStringFormattingOptions.arrayFormat = .commaSeparated\nrequestBuilder.queryStringFormattingOptions.spaceEncodingRule = .replacedWithPlus\nrequestBuilder.queryStringFormattingOptions.plusSymbolEncodingRule = .replacedWithDecodedPlus\n\nlet request = try requestBuilder.build()\n\nlet bearerUserAuthorization = OAuth2Authorization(type: .bearer, level: .user)\nlet authMiddleware = OAuth2RequestPipelineMiddleware(clientConfiguration: Auth.defaultClientConfiguration, authorization: userBearerAuthorization, tokenStorage: Auth.defaultTokenStore)\n\nvar sessionClient = MySessionClientManager.kittnAPISessionClient\n// Again, this is a copy, so we're free to mutate the middleware within the copy\nsessionClient.middleware.append(authMiddleware)\n\nsessionClient.begin(request) { (data, response, error) in\n    let deserializer = JSONResponseDeserializer()\n    let responseDict = try? deserializer.deserialize(response: response, data: data) as? [String : Any]\n    ...\n}\n```\n\n## Examples\n\nThis repo includes an iOS example, which is attached to `Conduit.xcworkspace`\n\n## License\n\nReleased under the Apache 2.0 license. See [LICENSE](LICENSE) for more details.\n\n## Credits\n\n[![mindbody-logo](images/MindbodyLogo.png)](https://mindbodyonline.com/careers)\n\nConduit is owned by MINDBODY, Inc. and continuously maintained by our [contributors](https://github.com/mindbody/Conduit/graphs/contributors).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmindbody%2FConduit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmindbody%2FConduit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmindbody%2FConduit/lists"}