Skip to content

Commit ebd24af

Browse files
committed
Add support for auto retry with refresh token + cookies support
1 parent 916ca7c commit ebd24af

3 files changed

Lines changed: 399 additions & 74 deletions

File tree

Sources/ServiceStack/JsonServiceClient.swift

Lines changed: 136 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ public protocol ServiceClient {
5454
func send<T: Codable>(intoResponse: T, request: NSMutableURLRequest) throws -> T
5555
func sendAsync<T: Codable>(intoResponse: T, request: NSMutableURLRequest) -> Promise<T>
5656

57-
func getData(_ url: String) throws -> Data
58-
func getDataAsync(_ url: String) -> Promise<Data>
57+
func getData(url: String) throws -> (Data, HTTPURLResponse)
58+
func getDataAsync(url: String) -> Promise<(Data, HTTPURLResponse)>
59+
func getData(request: URLRequest, retryIf:((HTTPURLResponse) -> Bool)?) throws -> (Data, HTTPURLResponse)
60+
func getDataAsync(request: URLRequest, retryIf:((HTTPURLResponse) -> Promise<Bool>)?) -> Promise<(Data, HTTPURLResponse)>
61+
5962
func getCookies() -> [String:String]
6063
func getTokenCookie() -> String?
6164
func getRefreshTokenCookie() -> String?
@@ -274,58 +277,161 @@ open class JsonServiceClient: NSObject, ServiceClient, IHasBearerToken, IHasSess
274277

275278
return req
276279
}
280+
281+
func retryAfterReauth(response: HTTPURLResponse) -> Bool {
282+
if response.statusCode == 401 {
283+
let hasRefreshTokenCookie = self.getRefreshTokenCookie() != nil
284+
if self.refreshToken != nil || hasRefreshTokenCookie {
285+
return self.fetchNewAccessToken()
286+
}
287+
}
288+
return false
289+
}
290+
291+
func retryAfterReauthAsync(response: HTTPURLResponse) -> Promise<Bool> {
292+
if response.statusCode == 401 {
293+
let hasRefreshTokenCookie = self.getRefreshTokenCookie() != nil
294+
if self.refreshToken != nil || hasRefreshTokenCookie {
295+
return self.fetchNewAccessTokenAsync()
296+
}
297+
}
298+
return Promise<Bool> { seal in seal.fulfill(false) }
299+
}
277300

278301
@discardableResult
279302
open func send<T: Codable>(intoResponse: T, request: NSMutableURLRequest) throws -> T {
303+
let (data, response) = try getData(request: request as URLRequest, retryIf: retryAfterReauth)
304+
if data.isEmpty {
305+
return Factory<T>.create()
306+
}
307+
let dto = try handleResponse(intoResponse: intoResponse, data: data, response: response)
308+
return dto
309+
}
310+
311+
open func getData(url: String) throws -> (Data, HTTPURLResponse) {
312+
let urlRequest = createRequest(url: resolveUrl(url), httpMethod: HttpMethods.Get)
313+
return try getData(request: urlRequest as URLRequest)
314+
}
315+
316+
open func getData(request: URLRequest, retryIf:((HTTPURLResponse) -> Bool)? = nil) throws -> (Data, HTTPURLResponse) {
280317
let dataTaskSync = createSession().dataTaskSync(request: request as URLRequest)
281318
lastTask = dataTaskSync.task
319+
let cb = dataTaskSync.callback
282320

283-
if dataTaskSync.callback?.response == nil {
284-
if let error = dataTaskSync.callback?.error {
321+
if cb?.response == nil {
322+
if let error = cb?.error {
285323
throw error
286324
}
287-
return Factory<T>.create()
325+
return (Data(), HTTPURLResponse())
288326
}
289327

290328
var error: NSError? = NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil)
291-
if let data = dataTaskSync.callback?.data,
292-
let response = dataTaskSync.callback?.response {
293-
let dto = try self.handleResponse(intoResponse: intoResponse, data: data, response: response, error: &error)
294-
return dto
329+
if let data = cb?.data, let response = cb?.response as? HTTPURLResponse {
330+
if let ex = self.createIfError(response, data, error: &error) {
331+
if let fn = retryIf {
332+
let success = fn(response)
333+
if success {
334+
return try getData(request: request)
335+
}
336+
}
337+
throw ex
338+
}
339+
return (data, response)
295340
}
296341

297-
return Factory<T>.create()
342+
return (Data(), HTTPURLResponse())
343+
}
344+
345+
open func fetchNewAccessToken() -> Bool {
346+
let jwtRequest = GetAccessToken()
347+
jwtRequest.refreshToken = self.refreshToken
348+
let request = self.createRequestDto(
349+
url: self.replyUrl.combinePath(Reflect<GetAccessToken>.typeName),
350+
httpMethod: HttpMethods.Post,
351+
request: jwtRequest)
352+
do {
353+
let (data, response) = try getData(request: request as URLRequest)
354+
let dto = try handleResponse(intoResponse: GetAccessTokenResponse(), data: data, response: response)
355+
self.bearerToken = dto.accessToken
356+
return true
357+
} catch let e {
358+
Log.debug("\(e)")
359+
return false
360+
}
298361
}
299362

300363
@discardableResult
301364
open func sendAsync<T: Codable>(intoResponse: T, request: NSMutableURLRequest) -> Promise<T> {
302-
let pendingPromise = Promise<T>.pending()
303-
let task = createSession().dataTask(with: request as URLRequest) { data, response, error in
304-
if error != nil {
305-
pendingPromise.resolver.reject(self.handleError(nsError: error! as NSError))
306-
} else {
307-
var error: NSError? = NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil)
308-
do {
309-
if let data = data,
310-
let response = response {
311-
312-
let nsResponse = response as! HTTPURLResponse
313-
if let ex = self.createIfError(nsResponse, data, error: &error) {
314-
pendingPromise.resolver.reject(ex)
365+
return getDataAsync(request: request as URLRequest, retryIf: retryAfterReauthAsync)
366+
.map { (data,response) in
367+
let dto = try self.handleResponse(intoResponse: intoResponse, data: data, response: response)
368+
return dto
369+
}
370+
}
371+
372+
open func getDataAsync(request: URLRequest, retryIf:((HTTPURLResponse) -> Promise<Bool>)? = nil) -> Promise<(Data, HTTPURLResponse)> {
373+
return Promise { seal in
374+
let task = createSession().dataTask(with: request as URLRequest) { data, response, error in
375+
if let error = error {
376+
seal.reject(self.handleError(nsError: error as NSError))
377+
} else if let response = response as? HTTPURLResponse, let data = data {
378+
var error: NSError? = NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil)
379+
if let ex = self.createIfError(response, data, error: &error) {
380+
if let fn = retryIf {
381+
_ = fn(response).done { success in
382+
if success {
383+
self.getDataAsync(request: request)
384+
.done { (response,data) in
385+
seal.fulfill((response,data))
386+
}.catch { retryEx in
387+
seal.reject(retryEx)
388+
}
389+
} else {
390+
seal.reject(ex)
391+
}
392+
}
315393
} else {
316-
let dto = try self.handleResponse(intoResponse: intoResponse, data: data, response: response)
317-
pendingPromise.resolver.fulfill(dto)
394+
seal.reject(ex)
318395
}
396+
} else {
397+
seal.fulfill((data, response))
319398
}
320-
} catch let e {
321-
pendingPromise.resolver.reject(e)
399+
} else {
400+
seal.fulfill((Data(), response as? HTTPURLResponse ?? HTTPURLResponse()))
322401
}
323402
}
403+
lastTask = task
404+
task.resume()
324405
}
406+
}
407+
408+
open func getDataAsync(url: String) -> Promise<(Data, HTTPURLResponse)> {
409+
let urlRequest = createRequest(url: resolveUrl(url), httpMethod: HttpMethods.Get)
410+
return getDataAsync(request: urlRequest as URLRequest)
411+
}
412+
413+
open func fetchNewAccessTokenAsync() -> Promise<Bool> {
414+
return Promise<Bool> { seal in
415+
let jwtRequest = GetAccessToken()
416+
jwtRequest.refreshToken = self.refreshToken
417+
let request = self.createRequestDto(
418+
url: self.replyUrl.combinePath(Reflect<GetAccessToken>.typeName),
419+
httpMethod: HttpMethods.Post,
420+
request: jwtRequest)
421+
422+
getDataAsync(request: request as URLRequest)
423+
.done { (data, response) in
424+
let dto = try self.handleResponse(intoResponse: GetAccessTokenResponse(), data: data, response: response)
425+
self.bearerToken = dto.accessToken
426+
seal.fulfill(true)
427+
}
428+
.catch { e in
429+
Log.debug("\(e)")
430+
seal.fulfill(false)
431+
}
432+
}
433+
}
325434

326-
task.resume()
327-
lastTask = task
328-
return pendingPromise.promise
329435
open func getCookies() -> [String:String] {
330436
let ret = urlCookies(URL(string: baseUrl)!)
331437
return ret
@@ -570,48 +676,6 @@ open class JsonServiceClient: NSObject, ServiceClient, IHasBearerToken, IHasSess
570676
}
571677
}
572678

573-
extension JsonServiceClient {
574-
open func getData(_ url: String) throws -> Data {
575-
let urlRequest = createRequest(url: resolveUrl(url), httpMethod: HttpMethods.Get)
576-
let dataTaskSync = createSession().dataTaskSync(request: urlRequest as URLRequest)
577-
lastTask = dataTaskSync.task
578-
579-
if let data = dataTaskSync.callback?.data {
580-
return data
581-
}
582-
583-
if let error = dataTaskSync.callback?.error {
584-
throw error
585-
}
586-
587-
return Data()
588-
}
589-
590-
open func getDataAsync(_ url: String) -> Promise<Data> {
591-
let urlRequest = createRequest(url: resolveUrl(url), httpMethod: HttpMethods.Get)
592-
let pendingPromise = Promise<Data>.pending()
593-
594-
let task = createSession().dataTask(with: urlRequest as URLRequest) { data, _, error in
595-
if let error = error {
596-
pendingPromise.resolver.reject(self.handleError(nsError: error as NSError))
597-
return
598-
}
599-
600-
if let data = data {
601-
pendingPromise.resolver.fulfill(data)
602-
return
603-
}
604-
605-
pendingPromise.resolver.fulfill(Data())
606-
}
607-
608-
task.resume()
609-
lastTask = task
610-
611-
return pendingPromise.promise
612-
}
613-
}
614-
615679
extension JsonServiceClient: URLSessionDelegate {
616680
public static func toHostsMap(_ urls: [String]) -> [String: Int] {
617681
var to: [String: Int] = [:]
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Demis Bellot on 3/2/21.
6+
//
7+
8+
@testable import ServiceStack
9+
import XCTest
10+
import Foundation
11+
import PromiseKit
12+
13+
class AuthTests: XCTestCase {
14+
var client: JsonServiceClient!
15+
16+
override func setUp() {
17+
super.setUp()
18+
client = JsonServiceClient(baseUrl: "http://test.servicestack.net")
19+
}
20+
21+
func createAuthRequest() -> Authenticate {
22+
let request = Authenticate()
23+
request.provider = "credentials"
24+
request.userName = "test"
25+
request.password = "test"
26+
return request
27+
}
28+
29+
func test_Does_fetch_AccessToken_using_RefreshTokenCookies() {
30+
let asyncTest = expectation(description: "asyncTest")
31+
32+
let request = Secured()
33+
request.name = "test"
34+
35+
_ = client.postAsync(createAuthRequest())
36+
.done { r in
37+
let initialAccessToken = self.client.getTokenCookie()
38+
let initialRefreshToken = self.client.getRefreshTokenCookie()
39+
XCTAssertNotNil(initialAccessToken)
40+
XCTAssertNotNil(initialRefreshToken)
41+
42+
_ = self.client.sendAsync(request)
43+
.done { r in
44+
XCTAssertEqual(r.result, request.name)
45+
_ = self.client.postAsync(InvalidateLastAccessToken())
46+
.done { r in
47+
_ = self.client.sendAsync(request)
48+
.done { r in
49+
XCTAssertEqual(r.result, request.name)
50+
51+
let lastAccessToken = self.client.getTokenCookie()
52+
XCTAssertNotEqual(lastAccessToken, initialAccessToken)
53+
54+
asyncTest.fulfill()
55+
}
56+
}
57+
}
58+
}
59+
60+
waitForExpectations(timeout: 10, handler: { error in
61+
XCTAssertNil(error, "Error")
62+
})
63+
}
64+
65+
func test_Does_fetch_AccessToken_using_RefreshTokenCookies_sync() throws {
66+
let request = Secured()
67+
request.name = "test"
68+
69+
try client.post(createAuthRequest())
70+
let initialAccessToken = client.getTokenCookie()
71+
let initialRefreshToken = client.getRefreshTokenCookie()
72+
XCTAssertNotNil(initialAccessToken)
73+
XCTAssertNotNil(initialRefreshToken)
74+
75+
var r = try client.send(request)
76+
XCTAssertEqual(r.result, request.name)
77+
78+
_ = try client.post(InvalidateLastAccessToken())
79+
80+
r = try client.send(request)
81+
XCTAssertEqual(r.result, request.name)
82+
83+
let lastAccessToken = self.client.getTokenCookie()
84+
XCTAssertNotEqual(lastAccessToken, initialAccessToken)
85+
}
86+
}
87+

0 commit comments

Comments
 (0)