Streamlining Authentication: Implementing Client-Side Automatic Token Refresh with CLEAN Architecture and Dependency Injection

Tharindu Ramesh Ketipearachchi
9 min readMar 5, 2024

--

Automatic token refresh from the client side presents a complex implementation, especially when adhering to CLEAN architecture principles and best practices. Below, we’ll discuss how to implement this approach seamlessly within a CLEAN architecture network layer with dependency injection (DI), ensuring robustness, maintainability, and adherence to architectural principles.

  1. Network Layer Implementation

Following is our CLEAN architecture based network layer implementation. We tried to keep it as simple as possible. We have posted all the supporting files needed as well.

import Foundation
/**
Enum for APIError types
*/
enum APIError: Error {
case networkError(Error)
case serverError(String)
case unknownError(Int)
case unAuthorized
case invalidURL
case invalidRequest
case invalidResponse
}
/**
Enum for HTTP Methods
*/
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
/**
Enum for Content Types
*/
enum ContentType: String {
case json = "application/json"
}
/**
Enum for HTTP Heeader Fields
*/
enum HTTPHeaderField: String {
case contentType = "Content-Type"
case authorization = "Authorization"
case accept = "Accept"
}
/**
Enum for Access Grant Type
*/
enum GrantType: String {
case password = "password"
case refresToken = "refresh_token"
}
/**
Enum for HTTP Status Codes
*/
enum HTTPStatusCode: Int {
case success = 200
case created = 201
case badRequest = 400
case unauthorized = 401
case forbidden = 403
case notFound = 404
case internalServerError = 500
case unknown
}
/**
APIClient
*/
struct APIClient {

private let session: URLSession

init(session: URLSession = .shared) {
self.session = session
}

func postResource<T: Decodable, E: Encodable>( to url: URL, tokenInteractor: TokenInteractor, body: E?, decodeTo type: T.Type) async throws -> T {
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.post.rawValue

setJsonHeaders(request: &request)
setAccessToken(request: &request, token: tokenInteractor.getAccessToken())

do {
let encoder = JSONEncoder()
let requestData = try encoder.encode(body)
request.httpBody = requestData

let (data, response) = try await session.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
return try await handleResponse(response: httpResponse, data: data, body: body, decodeTo: T.self)
} catch let error as NSError {
if error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet {
throw APIError.networkError(error )
}
throw error as Error
}
}
}
/**
Request Handling Extensions
*/
extension APIClient {
func setQueryParams(parameters: [String: Any], url: URL) -> URL {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.queryItems = parameters.map { element in URLQueryItem(name: element.key, value: String(describing: element.value) ) }
return components?.url ?? url
}

func setJsonHeaders(request: inout URLRequest) {
request.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
request.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.accept.rawValue)
}

func setAccessToken(request: inout URLRequest, token: String) {
request.addValue("Bearer \(token)", forHTTPHeaderField: HTTPHeaderField.authorization.rawValue)
}

func handleResponse <T: Decodable, E: Encodable>(response: HTTPURLResponse, data: Data, body: E?, decodeTo type: T.Type) async throws -> T {
switch response.statusCode {
case 200...299:
do {
let decoder = JSONDecoder()
let decoded = try decoder.decode(T.self, from: data)
return decoded
} catch {
throw APIError.invalidResponse
}
case 400:
if let error = try? JSONDecoder().decode(ServerError.self, from: data) {
throw APIError.serverError(error.error)
} else {
throw APIError.invalidResponse
}
case 401:
print("Handle automatic refresh token")
// Handle Automatic refresh token here
throw APIError.invalidResponse
case 402...499:
if let error = try? JSONDecoder().decode(ServerError.self, from: data) {
throw APIError.serverError(error.error)
} else {
throw APIError.invalidResponse
}
case 500...599:
throw APIError.unknownError(response.statusCode)
default:
throw APIError.invalidResponse
}
}
}
/**
Token Interactor
*/
final class TokenInteractor {

func saveAuthResponse(accessToken: String, refreshToken: String) -> Bool {
// save access token in keychain
return true
}

func getAccessToken() -> String {
//retrieve access token from keychain
return "accessToken"
}

func getRefreshToken() -> String {
//retrieve refresh token from keychain
return "refreshToken"
}
}
/**
Model for Server Error
*/
struct ServerError: Codable {
let error, timestamp, path: String
let status: Int
enum CodingKeys: String, CodingKey {
case error
case timestamp
case status
case path
}
}

Now we need to handle 401 case of the handleResponse() method and call a refresh token. For this we need to implement separate class to call token refresh function.

2. Auth Refresher Implementation

Following is our Auth Refresher

/**
Auth Refresher handling the automatic token refresh related functions
*/
final class AuthRefresher {
static let shared = AuthRefresher()
private var apiClient: APIClient?

private init() {}

func setAPIClient(_ apiClient: APIClient) {
self.apiClient = apiClient
}

func refreshToken(refreshToken: String, grantType: String) async throws -> AuthResDTO {
guard let url = APIEndpoint.refreshToken.url else {
throw APIError.invalidURL
}
var refreshRequest = RefreshRequest(refreshToken: refreshToken)
guard let apiClient = apiClient else {
throw APIError.unAuthorized
}
return try await apiClient.postResource(to: url, tokenInteractor: TokenInteractor(), body: refreshToken, decodeTo: AuthResDTO.self)
}
}

This class has a function to call refresh token API. It calls the postResource method of the APIClient . We need following files to support this implementation as well.

/**
Model for Refresh Request
*/
struct RefreshRequest: Codable {
let refreshToken: String
}
/**
DTO for Authentication Response
*/
struct AuthResDTO: Codable {
let accessToken, tokenType, refreshToken: String
let expiresIn: Int
let scope, jti: String

enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case tokenType = "token_type"
case refreshToken = "refresh_token"
case expiresIn = "expires_in"
case scope, jti
}
}
/**
API EndPoints
*/
enum APIEndpoint {
static let baseURL: String = {
Bundle.main.infoForKey("BaseUrl")
}()

case login
case logout
case refreshToken
case profile

var url: URL? {
switch self {
case .login:
return URL(string: "\(APIEndpoint.baseURL)/auth/mobile/token")
case .logout:
return URL(string: "\(APIEndpoint.baseURL)/auth/revoke_token")
case .refreshToken:
return URL(string: "\(APIEndpoint.baseURL)/auth/refresh_token")
case .profile:
return URL(string: "\(APIEndpoint.baseURL)/get_profile")
}
}
}

3. Refresh the token & Recall the API

For the more clarity, we are going to define a new function called refreshToken() in our APIClient and handle the token refresh and API recalling flow.

func refreshToken <T: Decodable, E: Encodable>(url: URL, method: HTTPMethod, tokenInteractor: TokenInteractor?, body: E?, decodeTo type: T.Type) async throws -> T {
guard let tokenInteractor = tokenInteractor else {
throw APIError.unAuthorized
}

do {
let auth: AuthResDTO = try await AuthRefresher.shared.refreshToken(refreshToken: tokenInteractor.getRefreshToken(), grantType: GrantType.refresToken.rawValue)
let isUpdated = tokenInteractor.saveAuthResponse(accessToken: auth.accessToken, refreshToken: auth.refreshToken)

if isUpdated {
return try await postResource(to: url, tokenInteractor: tokenInteractor, body: body, decodeTo: T.self)
} else {
throw APIError.unAuthorized
}

} catch {
throw APIError.unAuthorized
}
}

Then we need to do the following update of the 401 case of our postResource method.

case 401:
do {
let res = try await refreshToken(url: request.url, method: request.method, tokenInteractor: tokenInteractor, body: body, decodeTo: T.self)
return res
} catch {
throw APIError.unAuthorized
}

But what if our refresh token is also expired and refresh token API itself returns 401?

In here, we need to handle this as well. We need to check wether the API call is refresh token or not, if API is refresh token we need to redirect user to the login screen. Otherwise we can call the refreshToken() function.

To check the API is refresh token or not, you can use various methods, One of the easiest way is to pass the url request to the postResource method and check the end point url is equal to refreshToken end point url or not.

if request.url.absoluteString.contains(APIEndpoint.refreshToken.url?.absoluteString ?? "auth/refresh_token") {
throw APIError.unAuthorized
} else {
do {
let res = try await refreshToken(url: request.url, method: request.method, tokenInteractor: tokenInteractor, body: body, decodeTo: T.self)
return res
} catch {
throw APIError.unAuthorized
}
}

It’s up to you to decide, Here is the updated implementation of our APIClient class.

struct APIClient {

private let session: URLSession

init(session: URLSession = .shared) {
self.session = session
}

func postResource<T: Decodable, E: Encodable>( to url: URL, tokenInteractor: TokenInteractor, body: E?, decodeTo type: T.Type) async throws -> T {
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.post.rawValue

setJsonHeaders(request: &request)
setAccessToken(request: &request, token: tokenInteractor.getAccessToken())

do {
let encoder = JSONEncoder()
let requestData = try encoder.encode(body)
request.httpBody = requestData

let (data, response) = try await session.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
return try await handleResponse(response: httpResponse, data: data, tokenInteractor: tokenInteractor, request: APIRequest(method: HTTPMethod.post, url: url), body: body, decodeTo: T.self)
} catch let error as NSError {
if error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet {
throw APIError.networkError(error )
}
throw error as Error
}
}
}
/**
Request Handling Extensions
*/
extension APIClient {
func setQueryParams(parameters: [String: Any], url: URL) -> URL {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.queryItems = parameters.map { element in URLQueryItem(name: element.key, value: String(describing: element.value) ) }
return components?.url ?? url
}

func setJsonHeaders(request: inout URLRequest) {
request.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
request.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.accept.rawValue)
}

func setAccessToken(request: inout URLRequest, token: String) {
request.addValue("Bearer \(token)", forHTTPHeaderField: HTTPHeaderField.authorization.rawValue)
}

func handleResponse <T: Decodable, E: Encodable>(response: HTTPURLResponse, data: Data, tokenInteractor: TokenInteractor?, request: APIRequest, body: E?, decodeTo type: T.Type) async throws -> T {
switch response.statusCode {
case 200...299:
do {
let decoder = JSONDecoder()
let decoded = try decoder.decode(T.self, from: data)
return decoded
} catch {
throw APIError.invalidResponse
}
case 400:
if let error = try? JSONDecoder().decode(ServerError.self, from: data) {
throw APIError.serverError(error.error)
} else {
throw APIError.invalidResponse
}
case 401:
if request.url.absoluteString.contains(APIEndpoint.refreshToken.url?.absoluteString ?? "auth/refresh_token") {
throw APIError.unAuthorized
} else {
do {
let res = try await refreshToken(url: request.url, method: request.method, tokenInteractor: tokenInteractor, body: body, decodeTo: T.self)
return res
} catch {
throw APIError.unAuthorized
}
}
case 402...499:
if let error = try? JSONDecoder().decode(ServerError.self, from: data) {
throw APIError.serverError(error.error)
} else {
throw APIError.invalidResponse
}
case 500...599:
throw APIError.unknownError(response.statusCode)
default:
throw APIError.invalidResponse
}
}

func refreshToken <T: Decodable, E: Encodable>(url: URL, method: HTTPMethod, tokenInteractor: TokenInteractor?, body: E?, decodeTo type: T.Type) async throws -> T {
guard let tokenInteractor = tokenInteractor else {
throw APIError.unAuthorized
}

do {
let auth: AuthResDTO = try await AuthRefresher.shared.refreshToken(refreshToken: tokenInteractor.getRefreshToken(), grantType: GrantType.refresToken.rawValue)
let isUpdated = tokenInteractor.saveAuthResponse(accessToken: auth.accessToken, refreshToken: auth.refreshToken)

if isUpdated {
return try await postResource(to: url, tokenInteractor: tokenInteractor, body: body, decodeTo: T.self)
} else {
throw APIError.unAuthorized
}

} catch {
throw APIError.unAuthorized
}
}
}

We have implemented following model to pass the API request related data as well.

/**
Model to store API Request Data
*/
struct APIRequest {
let method: HTTPMethod
let url: URL
}

Now we are done with the Automatic refresh token implementation in a most complex way. But have you noticed any issues here? Yeah, there is a dependency issue with the AuthRefresher and the APIClient. We are calling AuthRefresher’s function inside the APIClient class, But AuthRefresher has a APIClient dependency as well.

4. How to fix the APIClient Circular Dependency issue?

Firstly we have implemented our AuthRefresher as a Singleton. So there will be only one instance for the whole application life cycle and that implementation will required an APIClient instance.

import Foundation
/**
Auth Refresher handling the automatic token refresh related functions
*/
final class AuthRefresher {
static let shared = AuthRefresher()
private var apiClient: APIClientProtocol?

private init() {}

func setAPIClient(_ apiClient: APIClientProtocol) {
self.apiClient = apiClient
}

func refreshToken(refreshToken: String, grantType: String) async throws -> AuthResDTO {
guard let url = APIEndpoint.refreshToken.url else {
throw APIError.invalidURL
}
var refreshData = [String: Any]()
refreshData[AuthRequestKey.refreshToken] = refreshToken
refreshData[AuthRequestKey.grantType] = grantType
guard let apiClient = apiClient else {
throw APIError.unAuthorized
}
return try await apiClient.postResource(to: url, params: refreshData, decodeTo: AuthResDTO.self)
}
}

My AuthRefresh API isn’t a Json one. It’s accepting X-Form Data. So I had to implement following custom method on my APIClient

func postResource<T: Decodable>( to url: URL, params: [String: Any], decodeTo type: T.Type) async throws -> T {
var request = URLRequest(url: url)

if !params.isEmpty {
let url = setQueryParams(parameters: params, url: url)
request = URLRequest(url: url)
}
setXauthHeaders(request: &request)
setBasicAuthHeaders(request: &request)
request.httpMethod = HTTPMethod.post.rawValue

do {
let (data, response) = try await session.data(for: request)

guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
return try handleAuthResponse(response: httpResponse, data: data, decodeTo: T.self)
} catch let error as NSError {
if error.domain == NSURLErrorDomain && error.code == NSURLErrorNotConnectedToInternet {
throw APIError.networkError(error )
}
ReportsProvider.shared.reportError(error: error as Error)
throw error as Error
}
}

And added following extensions as well.

/**
Request Handling Extensions
*/
extension APIClient {
func setXauthHeaders(request: inout URLRequest) {
request.addValue(ContentType.xwww.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
}

func setBasicAuthHeaders(request: inout URLRequest) {
request.addValue(xAuth, forHTTPHeaderField: HTTPHeaderField.authorization.rawValue)
}
}

Now to fix the Dependency Injection issue, Everytime you injected APIClient() to your viewModel, you have to set APIClient of the AuthRefresher as well. That’s why we have implemented following method of our AuthRefresher

func setAPIClient(_ apiClient: APIClientProtocol) {
self.apiClient = apiClient
}

This is how we do it on our ProfileViewModel

final class ProfileViewModel: ObservableObject {

private let tokenInteractor: TokenInteractor
private let apiClient: APIClient
private var cancellables: Set<AnyCancellable> = []


init( apiClient: APIClient,
tokenInteractor: TokenInteractor
) {
self.apiClient = apiClient
AuthRefresher.shared.setAPIClient(apiClient)
self.tokenInteractor = tokenInteractor
}
}

5. Conclusion

Now, instead of creating multiple instances of the APIClient object within your app, you’ve shared the same instance of APIClient with the AuthRefresher as well. With AuthRefresher being a Singleton, the APIClient no longer maintains a strong reference or dependency with the AuthRefresher. By passing a new APIClient instance to AuthRefresher during each viewModel initialization, AuthRefresher also avoids holding a reference to a single APIClient object at all times. This approach effectively resolves our circular dependency issue.

--

--

Tharindu Ramesh Ketipearachchi

Technical Lead (Swift, Objective C, Flutter, react-native) | iOS Developer | Mobile Development Lecturer |MSc in CS, BSc in CS (Col)