JWT Authentication with Alamofire download function - ios

I'm trying to download a zip file and save it from server using JWT token authentication thanks to Alamofire. The download works well without token authentication, the file is saved with success. When I activate the server-side authentication (using Passport.js with NodeJS), I always received 401. I attach the token to the header with the sessionManager adapter function. Others request (post, get using sessionManager.request(..) ) works well with this authentication mechanism.
Question is : Can we modify the header of Alamofire download function ? If yes how ?
Any advices appreciated
func getZip(){
let sessionManager = Alamofire.SessionManager.default
let authHandler = JWTAccessTokenAdapter(accessToken: Auth.getAccessToken())
sessionManager.retrier = authHandler
sessionManager.adapter = authHandler
let downloadUrl: String = Auth.getApiEndpoint() + "get_zip"
let destinationPath: DownloadRequest.DownloadFileDestination = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0];
let fileURL = documentsURL.appendingPathComponent("myZip.zip")
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
sessionManager.download(downloadUrl, method: .get, encoding: URLEncoding.httpBody, to: destinationPath)
.downloadProgress { progress in
print(">> Zip Download Progress: \(progress.fractionCompleted)")
}
.responseData { response in
switch response.result{
case .success:
if response.destinationURL != nil, let filePath = response.destinationURL?.absoluteString {
print("success & filepath : \(filePath)")
completionHandler(filePath, true)
}
break
case .failure:
print("faillure")
completionHandler("", false)
break
}
}
}
}
JWT Adapter :
class JWTAccessTokenAdapter: RequestAdapter {
typealias JWT = String
private var accessToken: JWT
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(Auth.getApiEndpoint()) {
/// Set the Authorization header value using the access token.
urlRequest.setValue(accessToken, forHTTPHeaderField: "Authorization")
}
return urlRequest
}
}
Output :
response: SUCCESS: 12 bytes // (Unauthorized) -> Corrupted zip file

Without a validation step in your request chain, all responses will be considered successful. So check your response code (or just add .validate() before responseData) and see if your request is actually successful. Also, you may want to double check your parameter encoding, though you don't seem to be sending any parameters.

Related

Authorization Header not setting in Alamofire 5?

I am setting the basic auth token to my URLRequest and using Alamofire to execute.
I set 3 headers, content, accept and auth...content and accept are visible in the network traffic but auth is not. It is available if i print the headers of the URLRequest before its sent...
Any ideas here? as i dont see how its removing the auth header before posting
Code as follows:
// Construct url
let url = try APIConstants.baseUrl.asURL()
// Append path
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
// Determine HTTP method
urlRequest.httpMethod = method.rawValue
let headers: HTTPHeaders = [
.contentType(APIConstants.ContentType.json.rawValue),
.accept(APIConstants.ContentType.json.rawValue),
]
if let token = token {
urlRequest.addValue("\(APIConstants.API.token.rawValue) \(token.key)",
forHTTPHeaderField: "Authorization")
}
urlRequest.headers = headers
// Add http body to request
if let parameters = parameters {
do {
let data = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
urlRequest.httpBody = data
} catch (_) {
print("APIRouter: Failed to parse body into request.")
}
}
//Encoding
let encoding: ParameterEncoding = {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}()
return try encoding.encode(urlRequest, with: parameters)
}
In my rest client i execute like this:
return Observable<T>.create { observer in
let request = AF.request(urlConvertible).responseDecodable { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
switch response.response?.statusCode {
case 403:
observer.onError(APIError.forbidden)
case 404:
observer.onError(APIError.notFound)
case 409:
observer.onError(APIError.conflict)
case 500:
observer.onError(APIError.internalServerError)
default:
observer.onError(error)
}
}
}
return Disposables.create {
request.cancel()
}
}
}
Edit:
Updated func to show further issue:
func asURLRequest() throws -> URLRequest {
// Construct url
let url = try APIConstants.baseUrl.asURL()
// Append path
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
// Determine HTTP method
urlRequest.httpMethod = method.rawValue
let headers: HTTPHeaders = [
.contentType(APIConstants.ContentType.json.rawValue),
.accept(APIConstants.ContentType.json.rawValue),
.authorization("Token a5555485aa251b28fdsfasdfdsb379c131fddad")
]
urlRequest.headers = headers
// Add http body to request
if let parameters = parameters {
do {
let data = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
urlRequest.httpBody = data
} catch (_) {
print("APIRouter: Failed to parse body into request.")
}
}
//Encoding
let encoding: ParameterEncoding = {
switch method {
case .get:
return URLEncoding.default
default:
return JSONEncoding.default
}
}()
return try encoding.encode(urlRequest, with: parameters)
}
After setting the Authorization header in your URLRequest:
urlRequest.addValue("\(APIConstants.API.token.rawValue) \(token.key)", forHTTPHeaderField: "Authorization")
then you override all the request's headers by setting headers property:
urlRequest.headers = headers
A nice solution would be to update the headers that you already created above, like this:
var headers: HTTPHeaders = [
.contentType(APIConstants.ContentType.json.rawValue),
.accept(APIConstants.ContentType.json.rawValue),
]
if let token = token {
headers.add(.authorization("\(APIConstants.API.token.rawValue) \(token.key)"))
}
urlRequest.headers = headers
Another solution is to use KeyValue pair as follows:
var header: HTTPHeaders = [:]
if let token = getValueFromUserDefaults(keyName: "authToken") as? String {
header["Authorization"] = token
}
I always use this method. It's more handy for me.

Refresh Bearer Token and resume current API

I need to resume my WebService if my bearer token expire by http code 401 ,
below is my code.
When a 401 error occurs that means the Access Token has expired and I need to send a Refresh Token over to my server to obtain a new Access Token.
After getting a new Access Token I need to redo the previous request that got the 401 error.
Suppose I hit webservice1 and its give 401 http code , then new bearer token request will generate and the same API resume to work. How can I do it ?
import UIKit
import CryptoKit
class SharedWebService: NSObject {
static let sharedApiInstance = SharedWebService()
func generalApiMethod (parameter : NSDictionary ,completion: #escaping ((NSMutableArray?) -> Void))
{
var dashboarddata : NSMutableArray = NSMutableArray()
let urlString = String(format:"URL OF API HERE")
let url = URL(string: urlString)
guard let requestUrl = url else { fatalError() }
var request = URLRequest(url: requestUrl)
request.timeoutInterval = 60
let bearerToken = "current bearer token"
request.setValue(bearerToken, forHTTPHeaderField: "Authorization")
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: request) {
(data, response, error) in
if let httpResponse = response as? HTTPURLResponse {
print(httpResponse.statusCode)
if httpResponse.statusCode == 401
{
// Refresh bearerToken get here
let bearerToken = self.getBearerTokenDevice() //fetch api to get new bearer token
return
}
}
guard error == nil else {
print(error!.localizedDescription);
DispatchQueue.main.async{
completion(dashboarddata)
}
return
}
guard let jsonData = data else { print("Empty data"); return }
if String(data: jsonData, encoding: String.Encoding.utf8) != nil
{
do {
let responseJSON = try? JSONSerialization.jsonObject(with: jsonData, options: [])
if let responseJSON = responseJSON as? [String: Any] {
//Success case do here reponse return
completion(dashboarddata)
}
}
}
}.resume()
}
func getBearerTokenDevice()-> String
{
//how to handle it
return "New bearer token from api fetch"
}
}
work arround is,
Always call Api at splah which fetches Bearer token from server, it will refresh the token every time user opens the app,
2.1 make Api call Queue to process Api calls (Use generics here)
2.2 if api is successfull, Ok. if not than call a special Api call yo get the token,
2.3 if after fetching the token, get last api from Api Queue and call it..
its just an idea, how i think, i think it will be done, might be different in your case
https://stevenpcurtis.medium.com/use-operationqueue-to-chain-api-calls-in-swift-71eefd6891ef
here is guide to make Api call chain

401 Authorization Error when using URLSession

I am trying to retrieve JSON from my app's server which needs a user/password authentication. Does anyone know why I am not being allowed entry into the server? I tried including an authorization header with the credentials needed but still get this error.
func retrieveJSON(){
let login = "login#mail.com"
let password = "password"
let url = NSURL(string: "http://server/admin/data.json")
let request = NSMutableURLRequest(url: url! as URL)
let config = URLSessionConfiguration.default
let userPasswordString = "\(login):\(password)"
let userPasswordData = userPasswordString.data(using: String.Encoding.utf8)
let base64EncodedCredential = userPasswordData!.base64EncodedString()
let authString = "Basic \(base64EncodedCredential)"
config.httpAdditionalHeaders = ["Authorization" : authString]
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest) { (data, response, error) -> Void in
debugPrint(response)
}
task.resume()
}
Response message with 401 error
Server authorization needed
Your web site does not appear to be using “basic” auth. It would appear to be using some different authentication scheme, which we cannot determine from an image of the HTML login page.
If it were using “Basic” auth (which it likely is not in your particular case), you could simplify the code a bit, removing NSURL, NSMutableURLRequest, the casts, etc. Also, if you’re going to create a URLSession, you will want to finishTasksAndInvalidate:
func retrieveJSON() {
let login = "login#mail.com"
let password = "password"
let url = URL(string: "http://server/admin/data.json")!
let request = URLRequest(url: url) // use `var` if you really need it to be mutable
let config = URLSessionConfiguration.default
let base64EncodedCredential = (login + ":" + password)
.data(using: .utf8)!
.base64EncodedString()
config.httpAdditionalHeaders = ["Authorization": "Basic " + base64EncodedCredential]
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { data, response, error in
debugPrint(response ?? "No response")
}
task.resume()
session.finishTasksAndInvalidate()
}
Alternatively, rather than building the Authorization header yourself, you can call CFHTTPMessageAddAuthentication to add authentication headers to a request. And, as an aside, adding the authentication to the request itself, you don’t have to create your own URLSession, but can use the shared instance.
func retrieveJSON() {
let login = "login#mail.com"
let password = "password"
let url = URL(string: "http://server/admin/data.json")!
var request = URLRequest(url: url)
request.updateBasicAuth(for: login, password: password)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
debugPrint(response ?? "No response")
}
task.resume()
}
Where
extension URLRequest {
/// Update request for HTTP authentication
///
/// - parameter username: The username
/// - parameter password: The password
/// - parameter authentication: The type of authentication to be applied
mutating func updateBasicAuth(for username: String, password: String, authentication: String = kCFHTTPAuthenticationSchemeBasic as String) {
let message = CFHTTPMessageCreateRequest(kCFAllocatorDefault, httpMethod! as CFString, url! as CFURL, kCFHTTPVersion1_1).takeRetainedValue()
if !CFHTTPMessageAddAuthentication(message, nil, username as CFString, password as CFString, authentication as CFString?, false) {
print("authentication not added")
}
if let authorizationString = CFHTTPMessageCopyHeaderFieldValue(message, "Authorization" as CFString)?.takeRetainedValue() {
setValue(authorizationString as String, forHTTPHeaderField: "Authorization")
} else {
print("didn't find authentication header")
}
}
}
But refinements to the “Basic” authentication are somewhat academic if your server is using a different authentication scheme.

How to make a NTML request with Alamofire 4.0?

These are request headers:
let headers: HTTPHeaders = [
"Accept": "application/json",
"username": "someUserName",
"password": "aPasswordForSomeUserName"
]
When making a request with below code it's giving me "Garbage at the end". However, when I checked the response with JSON parser online. It's a valid JSON.
Alamofire.request("http://myserver/list.svc/random", headers: headers).responseJSON { response in
print(response)
}
I also tried making a request like this:
Alamofire.request("http://myserver/list.svc/random", headers: headers).responseString { response in
print(response)
}
I am getting this message in console: "401 UNAUTHORIZED".
What am I doing wrong? I believe, when using responseJSON completion block it's not complaining about Unauthorization, but it's complaining about bad JSON (or some garbage).
P.S. The same request works fine with Advance Rest Client (a chrome extension) and also in chrome browser.
I don't know how relevant this is to you anymore but I've got a working solution I'll post for any future reference.
So, I had two issues. The first one being that the Authorization header fell of the request when it was redirected. The second one being the NTLM-challenge from the server not being handled. The following code should be quite self explanatory I hope :) It asumes you store the username and password in variables name just that.
let credentialData = "\(username):\(password)".data(using: String.Encoding.utf8)!
let base64Credentials = credentialData.base64EncodedString(options: [])
request.addValue("Basic \(base64Credentials)", forHTTPHeaderField: "Authorization")
let manager = Alamofire.SessionManager.default
let delegate: Alamofire.SessionDelegate = manager.delegate
// This bit will re-add the auth headers for the redirected request
delegate.taskWillPerformHTTPRedirection = { session, task, response, request in
var redirectedRequest = request
if let originalRequest = task.originalRequest, let redirectheaders = originalRequest.allHTTPHeaderFields {
if let authorizationHeaderValue = redirectheaders["Authorization"] {
redirectedRequest.setValue(authorizationHeaderValue, forHTTPHeaderField: "Authorization")
}
if let contentTypeHeaderValue = redirectheaders["Content-Type"] {
redirectedRequest.setValue(contentTypeHeaderValue, forHTTPHeaderField: "Content-Type")
}
}
return redirectedRequest
}
// This bit looks at challenges received and applies the correct credentials
delegate.taskDidReceiveChallenge = { session, task, challenge in
var disposition: URLSession.AuthChallengeDisposition = .useCredential
var credential: URLCredential = URLCredential()
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM) {
disposition = URLSession.AuthChallengeDisposition.useCredential
credential = URLCredential(user: username, password: password, persistence: URLCredential.Persistence.forSession)
}
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
disposition = URLSession.AuthChallengeDisposition.useCredential
credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
}
return(disposition, credential)
}
manager.request(request).responseData { (response) in
// Handle response accordingly
}
Hope this helps someone.
In Swift4.2
Alamofire has built in NTLM auth support.
You can make a request like that
let user = "YOUR_USER_NAME OR EMAIL"
let password = "YOUR_PASSWORD"
let url = "YOUR_API_URL"
let credential = URLCredential(user: user, password: password, persistence: .forSession)
//These headers are optional based on your api and your server.
//There were required for me
let headers = ["Accept": "application/json;odata=verbose",
"Content-type": "application/json;odata=verbose"]
Alamofire.request(url, method: .get, headers: headers).authenticate(usingCredential: credential).responseJSON {
(response) in
switch response.result {
case .success:
if let value = response.result.value {
print("The server response is: ", value)
}else{
print("There is error in the server response")
}
case .failure (let error):
print("The NTLM request error is: ", error.localizedDescription)
}
}

How to use Alamofire to send bytes?

I'm creating an application to download my university timetable. I've done the REST calls in Java first to demonstrate a prototype which works nicely. And now I'd like to do it in Swift using Alamofire (or anything else which works).
Below is the REST call in Java that I'm trying to replicate.
Client client = Client.create();
String authString = "UID=XXXXX&PASS=XXXXX";
byte[] authBytes = authString.getBytes();
WebResource webResouce = client.resource("https://access.adelaide.edu.au/sa/login.asp");
ClientResponse response = webResource.post(ClientResponse.class, authBytes);
if (response.getStatus != 302) {
throw new RuntimeException("Failed: HTTP code: " + response.getStatus());
}
However I'm having trouble sending the bytes properly. The server will actually accept any byte data (so you can see if it works without a UID or PASS) and respond with 302, which indicates that it works. Otherwise it'll send a 200 which means it didn't.
I've had a few attempts of sending the UID and PASS in a parameter, getting their bytes and then putting them in a parameter etc etc. but nothing seems to work so far.
Any help would be great, thanks!
You should use Alamofire's custom encoding technique (something like this). This is my 3rd hour of Swift so bear with me.
struct ByteEncoding: ParameterEncoding {
private let data: Data
init(data: Data) {
self.data = data
}
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()
urlRequest.httpBody = data
return urlRequest
}
}
Alamofire.request(url: "url", method: .post, parameters: nil, encoding: ByteEncoding(data: authBytesAsData)
Documentation
https://github.com/Alamofire/Alamofire#custom-encoding
If you use a regular NSURLRequest you can just set the request body:
let URL = NSURL(string: "https://access.adelaide.edu.au/sa/login.asp")!
let request = NSMutableURLRequest(URL: URL)
request.HTTPBody = // NSData you want as your body
Edit
As pointed out by #mattt himself, you can pass an NSURLRequest to Alamofire. No need for the hassle with custom parameter encoding as I answered first. (See below)
I don't exactly know how to do this using Alamofire, but it seems you can use a Custom parameter encoding with a closure. I didn't test this but took it from the Alamofire unit test source:
let encodingClosure: (URLRequestConvertible, [String: AnyObject]?) -> (NSURLRequest, NSError?) = { (URLRequest, parameters) in
let mutableURLRequest = URLRequest.URLRequest.mutableCopy() as NSMutableURLRequest
mutableURLRequest.HTTPBody = parameters["data"]
return (mutableURLRequest, nil)
}
let encoding: ParameterEncoding = .Custom(encodingClosure)
let URL = NSURL(string: "https://access.adelaide.edu.au/sa/login.asp")!
let URLRequest = NSURLRequest(URL: URL)
let data: NSData = // NSData you want as your body
let parameters: [String: AnyObject] = ["data": data]
let URLRequestWithBody = encoding.encode(URLRequest, parameters: parameters).0
Here's a quick example of how you could make this type of request.
import Alamofire
class BytesUploader {
func uploadBytes() {
let URLRequest: NSURLRequest = {
let URL = NSURL(string: "https://access.adelaide.edu.au/sa/login.asp")!
let mutableURLRequest = NSMutableURLRequest(URL: URL)
mutableURLRequest.HTTPMethod = "POST"
let authString = "UID=XXXXX&PASS=XXXXX"
let authData = authString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
mutableURLRequest.HTTPBody = authData
return mutableURLRequest.copy() as NSURLRequest
}()
let request = Alamofire.request(URLRequest)
request.response { request, response, data, error in
if let response = response {
println("Response status code: \(response.statusCode)")
if response.statusCode == 302 {
println("Request was successful")
} else {
println("Request was NOT successful")
}
} else {
println("Error: \(error)")
}
}
}
}
You need to encode your authorization string as an NSData object and then set that as the HTTPBody of the NSURLRequest. This should match your Java code that you posted.

Resources