'Refreshing auth token with Moya
I'm using Moya to communicate with my API. For many of my endpoints, I require that the user be authenticated (i.e. a bearer token is based in the Authorization header).
In the Moya documentation, here, I found how to include the Authorization header, along with the bearer token.
However, I now need to implement auth token refreshing, and I'm not sure how to do this.
I found this thread on Moya's Github with an answer that looks like it might help, but I have no idea where to put the code. Here is what the answer's code looks like:
// (Endpoint<Target>, NSURLRequest -> Void) -> Void
static func endpointResolver<T>() -> MoyaProvider<T>.RequestClosure where T: TargetType {
return { (endpoint, closure) in
let request = endpoint.urlRequest!
request.httpShouldHandleCookies = false
if (tokenIsOK) {
// Token is valid, so just resume the request and let AccessTokenPlugin set the Authentication header
closure(.success(request))
return
}
// authenticationProvider is a MoyaProvider<Authentication> for example
authenticationProvider.request(.refreshToken(params)) { result in
switch result {
case .success(let response):
self.token = response.mapJSON()["token"]
closure(.success(request)) // This line will "resume" the actual request, and then you can use AccessTokenPlugin to set the Authentication header
case .failure(let error):
closure(.failure(error)) //something went terrible wrong! Request will not be performed
}
}
}
}
And here is my class for my Moya provider:
import Foundation
import Moya
enum ApiService {
case signIn(email: String, password: String)
case like(id: Int, type: String)
}
extension ApiService: TargetType, AccessTokenAuthorizable {
var authorizationType: AuthorizationType {
switch self {
case .signIn(_, _):
return .basic
case .like(_, _):
return .bearer
}
}
var baseURL: URL {
return URL(string: Constants.apiUrl)!
}
var path: String {
switch self {
case .signIn(_, _):
return "user/signin"
case .like(_, _):
return "message/like"
}
}
var method: Moya.Method {
switch self {
case .signIn, .like:
return .post
}
}
var task: Task {
switch self {
case let .signIn(email, password):
return .requestParameters(parameters: ["email": email, "password": password], encoding: JSONEncoding.default)
case let .like(id, type):
return .requestParameters(parameters: ["messageId": id, "type": type], encoding: JSONEncoding.default)
}
}
var sampleData: Data {
return Data()
}
var headers: [String: String]? {
return ["Content-type": "application/json"]
}
}
private extension String {
var urlEscaped: String {
return addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
}
var utf8Encoded: Data {
return data(using: .utf8)!
}
}
Where would I put the answer's code in my code? Am I missing something?
Solution 1:[1]
Actually, that example is a bit old. So here is a new one:
extension MoyaProvider {
convenience init(handleRefreshToken: Bool) {
if handleRefreshToken {
self.init(requestClosure: MoyaProvider.endpointResolver())
} else {
self.init()
}
}
static func endpointResolver() -> MoyaProvider<Target>.RequestClosure {
return { (endpoint, closure) in
//Getting the original request
let request = try! endpoint.urlRequest()
//assume you have saved the existing token somewhere
if (#tokenIsNotExpired#) {
// Token is valid, so just resume the original request
closure(.success(request))
return
}
//Do a request to refresh the authtoken based on refreshToken
authenticationProvider.request(.refreshToken(params)) { result in
switch result {
case .success(let response):
let token = response.mapJSON()["token"]
let newRefreshToken = response.mapJSON()["refreshToken"]
//overwrite your old token with the new token
//overwrite your old refreshToken with the new refresh token
closure(.success(request)) // This line will "resume" the actual request, and then you can use AccessTokenPlugin to set the Authentication header
case .failure(let error):
closure(.failure(error)) //something went terrible wrong! Request will not be performed
}
}
}
}
Usage:
public var provider: MoyaProvider<SomeTargetType> = MoyaProvider(handleRefreshToken: true)
provider.request(...)
Solution 2:[2]
I use a plugin to refresh token.
class ApiManager {
static let shared = ApiManager()
private(set) var srAccountProvider: MoyaProvider<SRAccountApi>!
private init() {
let refreshTokenPlugin = RefreshTokenPlugin()
srAccountProvider = MoyaProvider<SRAccountApi>(plugins: [refreshTokenPlugin])
}
}
//
// RefreshTokenPlugin.swift
// Moya Token
//
// Created by maginawin on 2022/4/20.
//
import Foundation
import Moya
public class RefreshTokenPlugin: PluginType {
private var semaphore = DispatchSemaphore(value: 0)
public init() {
}
public func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
NSLog("prepare request", "")
guard let authorizable = target as? AccessTokenAuthorizable,
let authorizationType = authorizable.authorizationType else {
return request
}
let now = Date().timeIntervalSince1970
// if less than 1 hour, refresh token.
if (TokenManager.shared.expiredTimestamp - now) < 3600, let refreshToke = TokenManager.shared.refreshToken {
NSLog("start refresh token automatic", "")
let provider = MoyaProvider<SRAccountApi>()
// refresh token once
provider.request(.refreshToken(refreshToken: refreshToke), callbackQueue: DispatchQueue.global()) { result in
defer {
self.semaphore.signal()
}
do {
let response = try result.get()
let value = try response.mapJSON() as? [String: Any]
if let code = value?["code"] as? String,
let message = value?["message"] as? String,
code == "10001",
let data = value?["data"] as? [String: Any],
let authorization = data["authorization"] as? [String: Any] {
print("refresh token successful! \(code) \(message) \(data)")
// Update tokens and expired timestamp.
TokenManager.shared.accessToken = authorization["accessToken"] as? String ?? ""
TokenManager.shared.refreshToken = authorization["refreshToken"] as? String
TokenManager.shared.expiredTimestamp = (authorization["expiredTimestamp"] as? TimeInterval ?? 0) / 1000
} else {
print("refresh token failed!")
TokenManager.shared.refreshToken = nil
}
} catch {
NSLog("error %@", error.localizedDescription)
TokenManager.shared.refreshToken = nil
}
}
semaphore.wait()
}
var request = request
let authValue = authorizationType.value + " " + TokenManager.shared.accessToken
request.addValue(authValue, forHTTPHeaderField: "Authorization")
return request
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | maginawin |