Refresh token with SwiftUI Combine - swift

I'm trying to implement a refresh token strategy in Swift 5 and the Combine Framework for iOS.
I don't plan on using any third party package, just using what is provided by the framework, `URLSession.dataTaskPublisher`, so mu goal is to :
Make a request
If the request fails with 401, refresh the auth token (which is another request)
After the refresh token is done, retry the first request
If it fails throw the error to be handled by the caller
​
This is a very trivial use case, but seems to be very hard to implement in Combine, that makes it really hard to use in any real life scenario.
Any help would be welcome !
​
This is my try, which unfortonately doesn't work
private func dataTaskPublisherWithAuth(for request: URLRequest) -> URLSession.DataTaskPublisher {
return session.dataTaskPublisher(for: request)
.tryCatch { error -> URLSession.DataTaskPublisher in
guard error.errorCode == 401 else {
throw error
}
var components = URLComponents(url: self.baseUrl, resolvingAgainstBaseURL: true)
components?.path += "/refresh"
components?.queryItems = [
URLQueryItem(name: "refresh_token", value: KeychainHelper.RefreshToken),
]
let url = components?.url
var loginRequest = URLRequest(url: url!)
loginRequest.httpMethod = "GET"
loginRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
return session.dataTaskPublisher(for: loginRequest)
.decode(type: LoginResponse.self, decoder: JSONDecoder())
.map { result in
if result.accessToken != nil {
// Save access token
KeychainHelper.AccessToken = result.accessToken!
KeychainHelper.RefreshToken = result.refreshToken!
KeychainHelper.ExpDate = Date(timeIntervalSinceNow: TimeInterval(result.expiresIn!))
}
return result
}
.flatMap { data -> URLSession.DataTaskPublisher in
session.dataTaskPublisher(for: request)
}
}.eraseToAnyPublsiher()
}

You should use the .tryCatch method on Publisher here. This lets you replace an error with another publisher (such as replacing error 401 with a refresh request followed by a map switchToLastest auth request) or with another error (in this case if its not a 401 then just throw the original error).
Note that you probably shouldn't be using flatMap here because its not the same as .flatMapLatest in Rx or .flatmap(.latest) in Reactive Swift. You want to get into the habit of using .map and switchToLatest in Combine (ie apple decided the flattening and the mapping are two separate operators). If you don't do this you will get into trouble in some places that generate more than one inner publisher, such as search while you type, because instead of getting the latests inner value you will get ALL of them, in arbitrary order since the network requests complete in indeterminate time.

Related

Swift URLSession not working for localhost calls

I'm writing a basic API call in Swift using URLRequests, and for whatever reason my call is never executed. I have multiple calls to an external server API using the same method and the functionality is just as expected, however, for my server running locally I get no response or even behavior within the dataTask closure.
I have tried any relevant solutions I could find online such as: Swift URL Session and URL Request not working and Swift 3, URLSession dataTask completionHandler not called. But none of these solutions seem to fix my issue. I know that the local API is working as any calls through Postman go through without fail, yet even after using the Swift snippet provided by Postman, I get no functionality.
func doFoo(id: String, completion: #escaping ([[Float]]) -> ()) {
let semaphore = DispatchSemaphore(value: 0)
var request = URLRequest(url: URL(string: "127.0.0.1:8080/doFoo/\(id)")!, timeoutInterval: Double.infinity)
request.httpMethod = "GET"
print("THIS IS REACHED")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
print("THIS IS NEVER REACHED")
guard let data = data else {
self.semaphore.signal()
return
}
do {
// Decode json using JSONDecoder
// Call completion with JSON data
} catch {
print(error)
}
self.semaphore.signal()
}
task.resume()
self.semaphore.wait()
}
Other posts suggest that this could be an issue with the thread or execution completing before the closure is executed; while I am not super familiar with how the request executes and the behavior of semaphores, my understanding is that they are a way to request threads and prevent the above from happening.
If anyone more familiar with these topics could help me identify and understand why this issue is occurring, it would be greatly appreciated!

Authenticating to a GCP HTTP Cloud Function with a Firebase ID Token doesn't work?

I know this is a duplicate question, but I haven't seen anyone present a swift and python implementation. In addition, i've tried everything listed in the other questions, and nothing seems to work
Environments and Conditions:
Calling the http Cloud Function from an iOS app after I get the ID
token (with the ID token)
Written in Swift
The Cloud Function is written in Python
I cannot use HTTP callables as they are not deployable via the current terraform infrastructure in place (at least not that I know of, but would be open to any ideas)
Problem:
So, I was under the assumption that including the Firebase ID token of a Firebase user inside of the Authorization header works ,but it hasn't been for me even with a force refresh. I get a 403 status response with message: The access token could not be verified. That being said, if I go into CLI and get the id token of my actual gcp user account via: gcloud auth print-identity-token then replace the header with said token, I am verified.
Swift Request Code (excuse the lack of convention, this is just POCing before I make a real implementation):
guard let user = Auth.auth(app: authAppToUse).currentUser else { fatalError("SearchMySQL -> user: \(String(describing: Auth.auth(app: authAppToUse).currentUser))") }
user.getIDTokenForcingRefresh(true) { (idToken, err) in
if err != nil{
fatalError("SearchMySQL -> user.getIDToken -> err: \(String(describing: err))")
}
guard let guardedIdToken = idToken else { fatalError("SearchMySQL -> guardedIdToken: \(String(describing: idToken))") }
let rawJSON = [
"data" : [
"table": table,
"search_by": searchBy,
"search_by_value": searchByValue
]
]
guard let guardedJSON = try? JSONSerialization.data(withJSONObject: rawJSON, options: .prettyPrinted) else {
return
}
guard let url = URL(string: "https://us-east1-fresh-customer-dev-ocean.cloudfunctions.net/mysql_search") else { return }
var request = URLRequest(url: url)
request.addValue("Bearer \(idToken)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = guardedJSON
let task = URLSession.shared.dataTask(with: request) { (data, response, err) in
if err != nil {
print(err?.localizedDescription)
fatalError("SearchMySQL -> URLSession.shared.dataTask -> err: \(String(describing: err))")
}
guard let guardedData = data else {
return
}
print(idToken)
print(response.debugDescription)
let json = try? JSONSerialization.jsonObject(with: guardedData, options: .allowFragments)
print(json)
completion(data)
}
task.resume()
}
Python Cloud Function:
def main(request):
"""Background Http Triggered Cloud Function for ********.
Validates authentication with Firebase ID token
Returns:
********.main(payload): http response code and data payload (or message)
"""
# validate request authorization
if not request.headers.get('Authorization'):
log.fatal('No Authorization Token provided')
return {'message': 'No Authorization Token provided'}, 400
try:
id_token = request.headers.get('Authorization')
auth.verify_id_token(id_token)
except (ValueError, InvalidIdTokenError, ExpiredIdTokenError, RevokedIdTokenError, CertificateFetchError) as e:
log.fatal(f'Authorization `id_token` error: {e}')
return {'message':f'Authorization `id_token` error: {e}'}, 400
data_payload = request.get_json(force=True)
if not data_payload.table:
log.fatal('Payload missing table field.')
return {'message': 'Payload missing table field'}, 422
if not data_payload.search_by:
log.fatal('Payload missing search_by field.')
return {'message': 'Payload missing search_by field'}, 422
return ********.main(data_payload)
Ideas/Questions:
Aren't Firebase ID tokens equivalent to Google ID Tokens?
Could there be an iAM permission issue for the (auto generated)
firebase service account?
Do I need to also add any of the plist values to the idtoken when
sending it over in the header?
Could it be something with the rules of my cloud function?
Am I missing something, or is this intended/expected behavior? With
http callables being regular http functions but with protocols
packaged to facilitate, I would think that this is a relatively easy
implementation....
I've thought of the route of using an admin function to send a
message to the mobile instance that needs a google id token during
login, but the overhead and latency would result in issues.

Removing Swift RxAlamofire dependency

I'm trying to remove my dependency on RxAlamofire.
I currently have this function:
func requestData(_ urlRequest: URLRequestConvertible) -> Observable<(HTTPURLResponse, Data)> {
RxAlamofire.request(urlRequest).responseData()
}
How can I refactor this and use Alamofire directly to build and return an RxSwift Observable?
I suggest you look at the way the library wraps URLRequest to get an idea on how to do it...
Below is an abbreviated example from the library. In essence, you need to use Observable.create, make the network call passing in a closure that knows how to use the observer that create gives you.
Make sure you send a completed when done and make sure the disposable knows how to cancel the request.
Your Base will be something in Alamofire (I don't use Alamofire so I'm not sure what that might be.)
extension Reactive where Base: URLSession {
/**
Observable sequence of responses for URL request.
Performing of request starts after observer is subscribed and not after invoking this method.
**URL requests will be performed per subscribed observer.**
Any error during fetching of the response will cause observed sequence to terminate with error.
- parameter request: URL request.
- returns: Observable sequence of URL responses.
*/
public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable.create { observer in
let task = self.base.dataTask(with: request) { data, response, error in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
observer.on(.next((httpResponse, data)))
observer.on(.completed)
}
task.resume()
return Disposables.create(with: task.cancel)
}
}
}

Using refresh token to get new authorization token and repeat failed api call

I have a general question about using tokens in swift to make api calls. The authorization token that is needed to make api calls expires every hour so I need a way to handle this in a generalized way for multiple api calls.
I'm facing an issue where if I get a 401 error I call a function to use the refresh token to get a new authorization token and I would like to re call the original function that gotten 401 error.
For example:
If I get a 401 error when I call getDetails() I want to call the getNewAuthToken() and after I get a new refresh token I want to call getDetails() again.
I want to do this in a way so that if I call any function getX() and I get a 401 error it calls getNewAuthToken() and then it calls the original function again getX()
What would be the best way to approach this without using any external libraries etc. Would the best way be using a sort of callback function ?
I have provided general code I've been implementing but as you can see when I get a 401 error it calls the getNewAuthToken() function but the original function is not called again. How can this code be modified to behave as needed?
import UIKit
import Combine
#Published var details: Details = nil
func getNewAuthToken(){
// here I request a token using the refresh token
}
func getDetails(){
self.getCurrentDetails{ details in
DispatchQueue.main.async {
self.details = details
}
}
}
func getCurrentDetails(_ completionHandler: #escaping (Details) -> ()) {
let url = "https://api.xxx.com/details"
guard let detailsURL = URL(string: url) else{
fatalError("URL not valid")
}
let authtoken = keychain.get("authtoken") ?? ""
var request = URLRequest(url: detailsURL)
request.httpMethod = "GET"
request.addValue("Bearer \(authtoken)", forHTTPHeaderField: "Authorization")
let session = URLSession.shared
let task = session.dataTask(with: request){
data, response, error in
let httpResponse = response as? HTTPURLResponse
// I request a new token
if(httpResponse?.statusCode == 401){
print("401")
self.getNewAuthToken()
return
}
do {
if(httpResponse?.statusCode != 200){
return
}
let decoder = JSONDecoder()
let details = try decoder.decode(Details.self, from:
data!)
completionHandler(details)
} catch let error2{
print(error2)
}
}
task.resume()
}
I don't think you need to separate function like "getX()" to handle this. All you need is a completion handler.
func getNewAuthToken(completionHandler: () -> Void) {
// here I request a token using the refresh token
completionHandler()
}
func someNetworkCall() {
getNewAuthToken {
someNetworkCall()
}
}

Swift program never enters CompletionHandler for a dataTask

I am in the process of implementing a REST API with Swift. Of course, part of this API is using HTTP requests to retrieve and send data.
Full disclosure, I am inexperienced with Swift and am using this as a learning project to get my feet wet, so to speak. But it's turned into much more of a difficult project than I anticipated.
In implementing the first get method, I have (finally) gotten rid of all the compilation errors. However, when I call the function which utilizes the URLRequest, URLSession, dataTask, etc, it is never entered.
Upon debugging the program, I can watch the program execution reach the CompletionHandler, and skip over it right to "task.resume()."
A similar construction works in a Swift Playground, but does not work in the actual project proper.
So far I have tried a few things, namely making the function access a class instance variable, in hopes that that would force it to execute. But it does not.
I think the issue may be dealing with synchronicity, and perhaps I need to use a Semaphore, but I want to make sure I'm not missing anything obvious first.
import Foundation
/**
A class to wrap all GET and POST requests, to avoid the necessity of repeatedly writing request code in each API method.
*/
class BasicRequest {
private var url: URL
private var header: [String: String]
private var responseType: String
private var jsonResponse: Any?
init(url: URL, header: [String: String], responseType: String) {
self.url = url
self.header = header
self.responseType = responseType
} //END INIT
public func requestJSON() -> Any {
// Create the URLRequest object, and fill the header with the header fields as provided.
var urlRequest = URLRequest(url: self.url)
for (value, key) in self.header {
urlRequest.addValue(value, forHTTPHeaderField: key)
}
let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
print("Entered the completion handler")
if error != nil {
return
}
guard let httpResponse = response as? HTTPURLResponse, 200 == httpResponse.statusCode else {
print("HTTP Request unsuccessful")
return
}
guard let mime = response?.mimeType, mime == "application/json" else {
print("Not a JSON response")
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: [])
print(json)
self.jsonResponse = json
} catch {
print("Could not transform to JSON")
return
}
}
task.resume()
return "Function has returned"
} //END REQUESTJSON
}
The expected result would be returning a JSON object, however that does not seem to be the case.
With respect to error messages, I get none. The only log I get in the debugger is the boilerplate "process exited with code 0."
To be truthful, I'm at a loss with what is causing this not to work.
It appears you're writing this in a command-line app. In that case the program is terminating before the URLRequest completes.
I think the issue may be dealing with synchronicity, and perhaps I need to use a Semaphore, but I want to make sure I'm not missing anything obvious first.
Exactly.
The typical tool in Swift is DispatchGroup, which is just a higher-level kind of semaphore. Call dispatchGroup.enter() before starting the request, and all dispatchGroup.leave() at the end of the completion handler. In your calling code, include dispatchGroup.wait() to wait for it. (If that's not clear, I can add code for it, but there are also a lot of SO answers you can find that will demonstrate it.)