AtomNetworking

4.7.0

The lightweight & delightful networking library.
AlaskaAirlines/atom

What's New

4.7.0

2025-12-29T23:04:00Z

Introduction:

This Pull Request enhances support for the Proof Key for Code Exchange (PKCE) protocol in Atom's token refresh mechanism, as defined in RFC 7636. PKCE enhances security for public clients (e.g., mobile apps) by allowing client authentication without a client secret. To achieve this, the ClientCredential struct now treats the secret property as optional, defaulting to nil. This enables clients to omit the client_secret parameter in token refresh requests when using PKCE, while still supporting legacy flows that require it.
Additionally, this PR improves error handling by distinguishing between general "Bad Request" (HTTP 400) errors from regular API calls and those specifically from authentication endpoints (e.g., during token refresh). This ensures that a failed token refresh due to an invalid or expired refresh token triggers the appropriate notification (Atom.didFailToRefreshAccessToken), allowing observers to handle error gracefully.
These changes are additive and do not break existing source compatibility. They build upon the optimizations introduced in #20 by refining authentication flows without altering queuing or serialization logic.

Key Changes:

  • PKCE Support: Improves security for public clients by eliminating the need for a shared secret, reducing the risk of secret exposure.
  • Flexible Client Credentials: Allows seamless integration with both PKCE-enabled and legacy authorization servers.
  • Error Notification: Ensures notifications are only posted for authentic token refresh failures, improving app reliability and user experience.

Proposed Solution:

  1. Update ClientCredential to Support Optional Secret (PKCE):
    • Made secret optional with a default of nil in the initializer.
    • Updated documentation to reference RFC 6749 and emphasize optional usage.
    • No changes to equality or sendability.
    /// The `ClientCredential` type declares an object used by Atom for automated refreshing of the access token.
    /// See https://tools.ietf.org/html/RFC6749
    public struct ClientCredential: Sendable, Equatable {
        // MARK: - Properties
    
        /// The authorization grant type as described in Sections 4.1.3, 4.3.2, 4.4.2, RFC 6749. Starting with
        /// version 4.0, Atom supports automated token refresh using `refresh_token` grant type as defined in RFC 6749.
        public let grantType: GrantType
    
        /// The client identifier issued to the client during the registration process described in Section 2.2, RFC 6749.
        public let id: String
    
        /// The client secret. The client MAY omit the parameter if the client secret is an empty string. See RFC 6749.
        public let secret: String?
    
        // MARK: - Lifecycle
    
        /// Creates a `ClientCredential` instance given the provided parameter(s).
        ///
        /// - Parameters:
        /// - grantType: The authorization grant type as described in Sections 4.1.3, 4.3.2, 4.4.2, RFC 6749.
        /// - id:        The client identifier issued to the client during the registration process described by Section 2.2, RFC 6749.
        /// - secret:    The client secret. The client MAY omit the parameter if the client secret is an empty string. See RFC 6749.
        public init(grantType: GrantType = .refreshToken, id: String, secret: String? = nil) {
            self.id = id
            self.grantType = grantType
            self.secret = secret
        }
    }
  2. Conditional Inclusion of Client Secret in Token Refresh Request:
    • In the RefreshTokenEndpoint updated the method property to append client_secret only if credential.secret is non-nil.
    • This supports PKCE by omitting the secret when nil, while including it for backward compatibility.
    var method: HTTPMethod {
        let grantType = Identifier.grantType.appending(credential.grantType.rawValue)
        let clientID = Identifier.clientID.appending(credential.id)
        let refreshToken = Identifier.refreshToken.appending(writable.tokenCredential.refreshToken)
        var bodyString = grantType + "&" + clientID + "&" + refreshToken
    
        if let secret = credential.secret {
            bodyString.append("&")
            bodyString.append(Identifier.clientSecret.appending(secret))
        }
    
        return .post(Data(bodyString.utf8))
    }
  3. Add isAuthenticationEndpoint to Requestable:
    • Extended Requestable with a computed property to check if the request targets an authentication endpoint (via type check against AuthenticationEndpoint).
    • This enables quick identification of auth-related requests for specialized handling.
    extension Requestable {
        /// Returns a `Bool` indicating whether the current requestable is targeted at an authentication endpoint.
        ///
        /// This property is `true` when the conforming type is `AuthenticationEndpoint`. It allows Atom to quickly identify
        /// whether the request involves authentication-related operations, such as token refresh.
        var isAuthenticationEndpoint: Bool {
            self is AuthenticationEndpoint
        }
    }
  4. Fix Error Handling with Notification Distinction:
    • In the error handling flow added a check using isAuthenticationEndpoint and isBadRequest to post Atom.didFailToRefreshAccessToken specifically for auth endpoint 400 errors.
    • Falls back to Atom.didFailToAuthorizeRequest for authorization failures (e.g., 401).
    if requestable.isAuthenticationEndpoint, error.isBadRequest {
        // Notify observers that a token refresh has failed.
        NotificationCenter.default.post(name: Atom.didFailToRefreshAccessToken, object: nil, userInfo: ["error": error])
    } else if error.isAuthorizationFailure {
        // Notify observers that request authorization has failed.
        NotificationCenter.default.post(name: Atom.didFailToAuthorizeRequest, object: nil, userInfo: ["error": error])
    }

Testing

  • Verified PKCE flow: Token refresh succeeds without client_secret when secret is nil.
  • Tested legacy flow: Token refresh includes client_secret when provided.
  • Simulated 400 errors: Confirmed notification posts only for auth endpoints; regular API 400s do not trigger it.
  • Ensured no regressions in queuing from #20 by running high-throughput scenarios.

Source Compatibility:

Please check the box to indicate the impact of this proposal on source compatibility.

  • This change is additive and does not impact existing source code.
  • This change breaks existing source code.

Overview

The lightweight & delightful networking library.

Atom is a wrapper library built around a subset of features offered by URLSession with added ability to decode data into models, handle access token refresh and authorization headers on behalf of the client, and more. It takes advantage of Swift features such as default implementation for protocols, generics and Decodable to make it extremely easy to integrate and use in an existing project. Atom offers support for any endpoint, a much stricter URL host and path validation, comprehensive documentation and an example application to eliminate any guesswork.

Features

  • Simple to setup, easy to use & efficient
  • Supports any endpoint
  • Supports Combine publishers
  • Supports Multipath TCP configuration
  • Handles object decoding from data returned by the service
  • Handles token refresh
  • Handles and applies authorization headers on behalf of the client
  • Handles URL host validation
  • Handles URL path validation
  • Complete Documentation

Requirements

  • iOS 16.0+
  • Xcode 16.0+
  • Swift 6.0+

Installation

Swift Package Manager

The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler.

Once you have your Swift package set up, adding Atom as a dependency is as easy as adding it to the dependencies value of your Package.swift.

If you are using Xcode, adding Atom as a dependency is even easier. First, select your application, then your application project. Once you see Swift Packages tab at the top of the Project Editor, click on it. Click + button and add the following URL:

https://github.com/alaskaairlines/atom/

At this point you can setup your project to either use a branch or tagged version of the package.

Usage

Getting started is easy. First, create an instance of Atom.

let atom = Atom()

In the above example, the default configuration will be used. This configuration sets up URLSession to use an ephemeral session and ensures that the data returned by the service is available on the main thread when calling completion-based APIs.

When using async/await APIs, Atom will return results on the same thread where URLSession returns data. You are empowered to use custom actors or apply the @MainActor attribute to a function or an entire type (e.g., ViewModel) to ensure operations run on the main thread.

Any endpoint needs to conform and implement Requestable protocol. The Requestable protocol provides default implementation for all of its properties - except for the func baseURL() throws(AtomError) -> BaseURL. See documentation for more information.

extension Seatmap {
    enum Endpoint: Requestable {
        case refresh

        func baseURL() throws(AtomError) -> BaseURL {
            try BaseURL(host: "api.alaskaair.net")
        }
    }
}

Atom offers a handful of methods with support for fully decoded model objects, raw data, or status indicating success / failure of a request.

typealias Endpoint = Seatmap.Endpoint

let seatmap = try await atom.enqueue(Endpoint.refresh).resume(expecting: Seatmap.self)

The above example demonstrates how to use resume(expecting:) function to get a fully decoded Seatmap model object.

For more information, please see documentation.

Authentication

Atom can be configured to apply authorization headers on behalf of the client. It supports both Basic and Bearer authentication methods. When properly configured, Atom will automatically refresh tokens for the client if it determines that the access token has expired.

However, if the token refresh attempt fails, all subsequent network calls will fail.

Basic

You can configure Atom to apply Basic authorization header like this:

let atom: Atom = {
    let credential = BasicCredential(password: "password", username: "username")
    let basic = AuthenticationMethod.basic(credential)
    let configuration = ServiceConfiguration(authenticationMethod: basic)

    return Atom(serviceConfiguration: configuration)
}()

An existing implementation can be extended by conforming and implementing BasicCredentialConvertible protocol. A hypothetical configuration can look something like this:

actor CredentialManager {
    private(set) var username = String()
    private(set) var password = String()

    static let shared = CredentialManager()
    private init() {}

    func update(username aUsername: String) {
        username = aUsername
    }

    func update(password aPassword: String) {
        password = aPassword
    }
}

extension CredentialManager: BasicCredentialConvertible {
    var basicCredential: BasicCredential {
        .init(password: password, username: username)
    }
}

let atom: Atom = {
    let basic = AuthenticationMethod.basic(CredentialManager.shared.basicCredential)
    let configuration = ServiceConfiguration(authenticationMethod: basic)

    return Atom(serviceConfiguration: configuration)
}()

Once configured, Atom will combine username and password into a single string username:password, encode the result using base 64 encoding algorithm and apply it to a request as a Authorization: Basic TGlmZSBoYXMgYSBtZWFuaW5nLg== header key-value.

Bearer

You can configure Atom to apply Bearer authorization header. Here is an example:

actor TokenManager: TokenCredentialWritable {
    var tokenCredential: TokenCredential {
    	// Read values from the keychain.
        get { keychain.tokenCredential() }
        
        // Save new value to the keychain.  
        set { keychain.save(tokenCredential: newValue)  }
    }
}

let atom: Atom = {
    let endpoint = AuthorizationEndpoint(host: "api.alaskaair.net", path: "/oauth2")
    let clientCredential = ClientCredential(id: "client-id", secret: "client-secret")
    let tokenManager = TokenManager()

    let bearer = AuthenticationMethod.bearer(endpoint, clientCredential, tokenManager)
    let configuration = ServiceConfiguration(authenticationMethod: bearer)

    return Atom(serviceConfiguration: configuration)
}()

The setup is hopefully easy to understand. Atom requires a few pieces of information from the client:

  1. Authorization endpoint - Atom needs to know where to call to get a new token.
  2. Client credentials - Atom needs access to client id and client secret to get a new token.
  3. Token credential writable - Atom will pass newly obtained credentials to a client for safe storage.

Once configured, Atom will apply authorization header to a request as Authorization: Bearer ... header key-value.

NOTE: Please ensure that any type conforming to TokenCredentialWritable writes and reads keychain values in a thread-safe manner. The successful token refresh depends on being able to read the new token credential value after it has been saved to the keychain following a refresh.

Also, Atom will only decode token credential from a JSON objecting returned in this form:

{
    "access_token": "2YotnFZFEjr1zCsicMWpAA",
    "expires_in": 3600,
    "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA"
}

The above response is in accordance with RFC 6749, section 1.5.

NOTE: In a high-throughput scenario where the client enqueues a large number of network calls, it’s best practice to adjust the token’s expiration time to account for the service timeout. If your ServiceTimeout is set to the default of 30 seconds, configure TokenWritable to subtract those 30 seconds from the token’s expiration time. This ensures every enqueued call has at least 30 seconds to complete before the token expires.

For more information and Atom usage example, please see documentation and the provided Example application.

Communication

  • If you found a bug, open an issue.
  • If you have a feature request, open an issue.
  • If you want to contribute, submit a pull request.

Authors

Description

  • Swift Tools 6.0.0
View More Packages from this Author

Dependencies

  • None
Last updated: Wed Jan 21 2026 15:07:35 GMT-1000 (Hawaii-Aleutian Standard Time)