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:
- Update
ClientCredentialto Support Optional Secret (PKCE):- Made
secretoptional with a default ofnilin 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 } }
- Made
- Conditional Inclusion of Client Secret in Token Refresh Request:
- In the
RefreshTokenEndpointupdated themethodproperty to appendclient_secretonly ifcredential.secretis 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)) }
- In the
- Add
isAuthenticationEndpointtoRequestable:- Extended
Requestablewith a computed property to check if the request targets an authentication endpoint (via type check againstAuthenticationEndpoint). - 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 } }
- Extended
- Fix Error Handling with Notification Distinction:
- In the error handling flow added a check using
isAuthenticationEndpointandisBadRequestto postAtom.didFailToRefreshAccessTokenspecifically for auth endpoint 400 errors. - Falls back to
Atom.didFailToAuthorizeRequestfor 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]) }
- In the error handling flow added a check using
Testing
- Verified PKCE flow: Token refresh succeeds without
client_secretwhensecretis nil. - Tested legacy flow: Token refresh includes
client_secretwhen 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.