Skip to content

4.7.0

Latest

Choose a tag to compare

@krishnavarma1 krishnavarma1 released this 29 Dec 23:04
9a21f6f

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.