Skip to content

Instantly share code, notes, and snippets.

@saroar
Last active February 17, 2024 02:19
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save saroar/ca78de9dc798cdbaaa47791380062596 to your computer and use it in GitHub Desktop.
Save saroar/ca78de9dc798cdbaaa47791380062596 to your computer and use it in GitHub Desktop.
RefreshToken URLSession + Combine swift
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, macCatalyst 13.0, *)
extension JSONDecoder {
public static let ISO8601JSONDecoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
}
var rToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVmNDUyY2RjZDUyMTE1ZDE0Yzk2NDUwYiIsImlhdCI6MTU5ODk4NTQyMCwiZXhwIjoxNjMwNTIxNDIwfQ.of5jT0LlqrlVoPZ7N6zXXWjvgsmZtkQKfhj0sWKB3rinhxFe1QeY-wueaWBrbxHYdI9cI7Kmj6dSPfK8b9Oc4yOGIOzQ4yONHHAShXKp6HszArjVe8wdRcZN02rxilHDCJoqXAMnjQXi7tMsMDtuX2e7iHsuNgSDNU9WMAtMJ6iMHj95IE-W5jOPyNXodQmqfSP5XJcyRI1LPq-nuMWa86L60BVhH2oySUNzFYLYGG3mrF2wAf6pC9XtWUMgQJ1zWptS08_O3DKzWwbDTzjbdnnx5aBH5SwOBxvgci-Ngl4Ix7EyTfHX1Akuxy8gwBu5qTP3erPeyH6uK0vrbkyJeQ"
var invalidToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVmNDUyY2RjZDUyMTE1ZDE0Yzk2NDUwYiIsImlhdCI6MTU5OTAyOTczNCwiZXhwIjoxNjMwNTY1NzM0fQ.T1Emc8J3QUh49nUO9GLwiWkGIKA9EoQBej0P6_-uNo0BenkcLpJWOq_DSKAexhT06S4_CqlGiJ1kn8q7gDJOZR4tX7xDXfObQeZisbbsgo_UIWlaSZTu3l3Ey_93vlt8c0W4-pOj99-voSwQ_Q4RvRi6r3r3P1aGb5JZ48vCZ_ulT13SGSV1xQL08VuV87KwsosoXLa56hJTBqpKyohkbvTr6Nb0rLwS48FEn-T2mKkyZmARvQlpEO3j2IGroskNYelMt2qU80h7k6GyzTOJh1mB1ZTBHXQSaE5z3VHNpFN5M9sRvegkkVucU9zrfQ85OM4Rs4vx9RJMmDfAIbSuyw"
var aToken: String = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOjAsImV4cCI6MTU5OTAzMzEyOCwiaWF0IjoxNTk5MDMzMDY4LCJ1c2VySWQiOiI1ZjQ1MmNkY2Q1MjExNWQxNGM5NjQ1MGIiLCJwaG9uZU51bWJlciI6Iis3OTIxODgyMTIxNyJ9.UUZ5TUPGPRreS3AKko7NZ_gcoJYvKuJCipUHJPxS1ormw1yQXolQzrCCf34EK58peZm5WngLwo3nAuPplL5rmInvjcYQotI0N0grpKkMf_ITPRMv80iqObpBr1r2zsvJVqwMysmRM4wP-mipvvwvlb0lkKXPoqn2M5Eckkk97hsrQ5pAFsaMJQytpm6YW-IC3NXinYAeTlWQbm_7_9naxSobLQyVJ2VXc4lArddzDLpEMoZEkC28fXLoQKL36W93MBcoNFxjdHCYyuxBFHQrtu7drCYJ1EyGOA-lCACf5twslGoZKWJjvqa8IjWcaJVfGveMVakaBfR90Do6f-f6ZA"
var headers = [
"authorization": "Bearer \(aToken)",
"Content-Type": "application/json"
]
public struct RefreshTokenResponse: Codable {
public var access_token: String
public var refresh_token: String
// enum CodingKeys: String, CodingKey {
// case accessToken = "access_token"
// case refreshToken = "refresh_token"
// }
}
public struct RefreshTokenInput: Codable {
public var refresh_token: String
// enum CodingKeys: String, CodingKey {
// case refreshToken = "refresh_token"
// }
}
class Authenticator {
private var currentToken = RefreshTokenResponse(
access_token: aToken,
refresh_token: rToken
)
private var cancellationToken: AnyCancellable?
func refreshToken<S: Subject>(using subject: S) where S.Output == RefreshTokenResponse {
//self.currentToken = Token(isValid: true)
let parameters: [String: Any] = [
"refresh_token": rToken,
]
let jsonDecoder: JSONDecoder = .ISO8601JSONDecoder
let url: URL = URL(string: "http://10.0.1.3:8080/v1/auth/refreshToken")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
}
URLSession.shared.dataTaskPublisher(for: request)
.retry(3)
.sink { com in
print(com)
subject.send(self.currentToken)
} receiveValue: { data in
print(#line, data)
do {
let jsonDecoder2 = JSONDecoder()
let rtResponse = try jsonDecoder2.decode(RefreshTokenResponse.self, from: data.data)
aToken = rtResponse.access_token
rToken = rtResponse.refresh_token
headers = [
"authorization": "Bearer \(rtResponse.access_token)",
"Content-Type": "application/json"
]
} catch {
print(#line, error)
}
}.store(in: &cancellables)
}
func tokenSubject() -> CurrentValueSubject<RefreshTokenResponse, Never> {
print(#line, currentToken)
return CurrentValueSubject(currentToken)
}
}
struct UserApi {
let authenticator: Authenticator
@available(iOS 14.0, *)
func getProfile() -> AnyPublisher<Data, URLError> {
let tokenSubject = authenticator.tokenSubject()
return tokenSubject
.flatMap({ token -> AnyPublisher<Data, URLError> in
let url: URL = URL(string: "http://10.0.1.3:8080/v1/events")!
var request = URLRequest(url: url)
request.allHTTPHeaderFields = headers
print(#line, headers)
return URLSession.shared.dataTaskPublisher(for: request)
.flatMap({ result -> AnyPublisher<Data, URLError> in
if let httpResponse = result.response as? HTTPURLResponse,
httpResponse.statusCode == 401 {
self.authenticator.refreshToken(using: tokenSubject)
print(#line, tokenSubject)
return Empty().eraseToAnyPublisher()
}
return Just(result.data)
.setFailureType(to: URLError.self)
.eraseToAnyPublisher()
})
.eraseToAnyPublisher()
})
.handleEvents(receiveOutput: { _ in
tokenSubject.send(completion: .finished)
})
.eraseToAnyPublisher()
}
}
let authenticator = Authenticator()
let get = UserApi(authenticator: authenticator).getProfile()
var cancellationToken: AnyCancellable?
cancellationToken = get
.retry(3)
.sink { com in
print(com)
} receiveValue: { data in
print(#line, data)
}
233 RefreshTokenResponse(access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVmNDUyY2RjZDUyMTE1ZDE0Yzk2NDUwYiIsImlhdCI6MTU5OTAyOTczNCwiZXhwIjoxNjMwNTY1NzM0fQ.T1Emc8J3QUh49nUO9GLwiWkGIKA9EoQBej0P6_-uNo0BenkcLpJWOq_DSKAexhT06S4_CqlGiJ1kn8q7gDJOZR4tX7xDXfObQeZisbbsgo_UIWlaSZTu3l3Ey_93vlt8c0W4-pOj99-voSwQ_Q4RvRi6r3r3P1aGb5JZ48vCZ_ulT13SGSV1xQL08VuV87KwsosoXLa56hJTBqpKyohkbvTr6Nb0rLwS48FEn-T2mKkyZmARvQlpEO3j2IGroskNYelMt2qU80h7k6GyzTOJh1mB1ZTBHXQSaE5z3VHNpFN5M9sRvegkkVucU9zrfQ85OM4Rs4vx9RJMmDfAIbSuyw", refresh_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVmNDUyY2RjZDUyMTE1ZDE0Yzk2NDUwYiIsImlhdCI6MTU5ODk4NTQyMCwiZXhwIjoxNjMwNTIxNDIwfQ.of5jT0LlqrlVoPZ7N6zXXWjvgsmZtkQKfhj0sWKB3rinhxFe1QeY-wueaWBrbxHYdI9cI7Kmj6dSPfK8b9Oc4yOGIOzQ4yONHHAShXKp6HszArjVe8wdRcZN02rxilHDCJoqXAMnjQXi7tMsMDtuX2e7iHsuNgSDNU9WMAtMJ6iMHj95IE-W5jOPyNXodQmqfSP5XJcyRI1LPq-nuMWa86L60BVhH2oySUNzFYLYGG3mrF2wAf6pC9XtWUMgQJ1zWptS08_O3DKzWwbDTzjbdnnx5aBH5SwOBxvgci-Ngl4Ix7EyTfHX1Akuxy8gwBu5qTP3erPeyH6uK0vrbkyJeQ")
251 ["authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVmNDUyY2RjZDUyMTE1ZDE0Yzk2NDUwYiIsImlhdCI6MTU5OTAyOTczNCwiZXhwIjoxNjMwNTY1NzM0fQ.T1Emc8J3QUh49nUO9GLwiWkGIKA9EoQBej0P6_-uNo0BenkcLpJWOq_DSKAexhT06S4_CqlGiJ1kn8q7gDJOZR4tX7xDXfObQeZisbbsgo_UIWlaSZTu3l3Ey_93vlt8c0W4-pOj99-voSwQ_Q4RvRi6r3r3P1aGb5JZ48vCZ_ulT13SGSV1xQL08VuV87KwsosoXLa56hJTBqpKyohkbvTr6Nb0rLwS48FEn-T2mKkyZmARvQlpEO3j2IGroskNYelMt2qU80h7k6GyzTOJh1mB1ZTBHXQSaE5z3VHNpFN5M9sRvegkkVucU9zrfQ85OM4Rs4vx9RJMmDfAIbSuyw", "Content-Type": "application/json"]
259 Combine.CurrentValueSubject<__lldb_expr_28.RefreshTokenResponse, Swift.Never>
212 (data: 1036 bytes, response: <NSHTTPURLResponse: 0x60000219ea60> { URL: http://10.0.1.3:8080/v1/auth/refreshToken } { Status Code: 200, Headers {
Connection = (
"keep-alive",
"keep-alive"
);
"Content-Length" = (
1036
);
"Content-Type" = (
"application/json; charset=utf-8"
);
Date = (
"Wed, 02 Sep 2020 07:51:08 GMT",
"Wed, 02 Sep 2020 07:51:08 GMT"
);
} })
finished
251 ["Content-Type": "application/json", "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOjAsImV4cCI6MTU5OTAzMzEyOCwiaWF0IjoxNTk5MDMzMDY4LCJ1c2VySWQiOiI1ZjQ1MmNkY2Q1MjExNWQxNGM5NjQ1MGIiLCJwaG9uZU51bWJlciI6Iis3OTIxODgyMTIxNyJ9.UUZ5TUPGPRreS3AKko7NZ_gcoJYvKuJCipUHJPxS1ormw1yQXolQzrCCf34EK58peZm5WngLwo3nAuPplL5rmInvjcYQotI0N0grpKkMf_ITPRMv80iqObpBr1r2zsvJVqwMysmRM4wP-mipvvwvlb0lkKXPoqn2M5Eckkk97hsrQ5pAFsaMJQytpm6YW-IC3NXinYAeTlWQbm_7_9naxSobLQyVJ2VXc4lArddzDLpEMoZEkC28fXLoQKL36W93MBcoNFxjdHCYyuxBFHQrtu7drCYJ1EyGOA-lCACf5twslGoZKWJjvqa8IjWcaJVfGveMVakaBfR90Do6f-f6ZA"]
286 1940 bytes
finished
@rahulGraphy
Copy link

Hi Saroar,

I am unable to get it working, I'll appreciate your help with this.
Once the 401 error is received, the refresh token request is made but after that request completes successfully, original request doesn't get retried.

Please find my implementation below: -

This is the Network Agent that makes all the api calls -

class NetworkAgent {
struct Response {
let value: T
let response: URLResponse
}
let authenticator = Authenticator()

func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<Response<T>, Error> {
    let tokenSubject = authenticator.tokenSubject()
    // a better way to handle refresh token logic would be to use tryCatch operator suceeded by retry operator
    // should be done in one of the upcoming releases
    
    return tokenSubject
        .flatMap({ token -> AnyPublisher<Response<T>, Error> in
            return URLSession.shared
                .dataTaskPublisher(for: request)
                .tryMap { [weak self] result -> Response<T> in
                    self?.logResponse(result.response, request: request, data: result.data)
                    
                    // check for the response status
                    if let httpURLResponse = result.response as? HTTPURLResponse {
                        let networkResponse = APIClient.status(httpURLResponse)
                        switch networkResponse {
                        case .success: break
                            
                        case .failure(let error):
                            if error == HTTPURLResponseError.authenticationError {
                                // refresh token logic
                                if request.url?.absoluteString.contains("/auth/token/refresh") == false {
                                    self?.authenticator.refreshToken(using: tokenSubject)
                                }
                            } else {
                                throw error
                            }
                        }
                    }
                    
                    // map response and return in case of a successful request
                    let value = try JSONDecoder().decode(T.self, from: result.data)
                    return Response(value: value, response: result.response)
                }
                .retry(3)
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        })
        .handleEvents(receiveOutput: { _ in
            tokenSubject.send(completion: .finished)
        })
        .eraseToAnyPublisher()
}

}

And this is the Authenticator class implementation -

class Authenticator {
//MARK:- Properties
private var disposables = Set()
private let queue = DispatchQueue(label: "Autenticator.(UUID().uuidString)")

//MARK:- Functions
func refreshToken<S: Subject>(using subject: S) where S.Output == Bool {
    queue.sync {
        URLSession.shared
            .dataTaskPublisher(for: APIRouter.refreshToken.request()!)
            .retry(3)
            .sink { com in
                print(com)
                subject.send(true)
            } receiveValue: { data in
                print(#line, data)
                
                do {
                    let jsonDecoder2 = JSONDecoder()
                    let user = try jsonDecoder2.decode(User.self, from: data.data)
                    
                    LocalData.loggedInUser = user
                    LocalData.token = user.token
                    
                } catch  {
                    print(#line, error)
                }
                
            }
            .store(in: &disposables)
    }
}

func tokenSubject() -> CurrentValueSubject<Bool, Never> {
    return CurrentValueSubject(true)
}

}

Few things to note - Tokens are saved locally so it's not required to send them via the publisher subject I believe.

@rahulGraphy
Copy link

Hi Saroar,

Did you get a chance to look at it?

@saroar
Copy link
Author

saroar commented Apr 16, 2021

sorry don't have time now but when will get time i will update with you real-world app where i will use it thanks

@rahulGraphy
Copy link

sure, hope to see it soon.
Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment