Last active
February 17, 2024 02:19
-
-
Save saroar/ca78de9dc798cdbaaa47791380062596 to your computer and use it in GitHub Desktop.
RefreshToken URLSession + Combine swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 |
Hi Saroar,
Did you get a chance to look at it?
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
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
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()
}
And this is the Authenticator class implementation -
class Authenticator {
//MARK:- Properties
private var disposables = Set()
private let queue = DispatchQueue(label: "Autenticator.(UUID().uuidString)")
}
Few things to note - Tokens are saved locally so it's not required to send them via the publisher subject I believe.