Siesta REST login - ios

How to translate my login user URLSession code into Siesta framework code? My current attempt isn't working.
I've looked at the example in the GithubBrowser but the API I have doesn't work like that.
The issue is that the user structure is kind of split by how the endpoint in the API I'm consuming works. The endpoint is http://server.com/api/key. Yes, it really is called key and not user or login. Its called that by the authors because you post a user/pass pair and get a key back. So it takes in (via post) a json struct like:
{"name": "bob", "password": "s3krit"}
and returns as a response:
{"token":"AEWasBDasd...AAsdga"}
I have a SessionUser struct:
struct SessionUser: Codable
{
let name: String
let password: String
let token: String
}
...which encapsulates the state (the "S" in REST) for the user. The trouble is name & password get posted and token is the response.
When this state changes I do my:
service.invalidateConfiguration() // So requests get config change
service.wipeResources() // Scrub all unauthenticated data
An instance is stored in a singleton, which is picked up by the configure block so that the token from the API is put in the header for all other API requests:
configure("**") {
// This block ^ is run AGAIN when the configuration is invalidated
// even if loadManifest is not called again.
if let haveToken = SessionManager.shared.currentUser?.token
{
$0.headers["Authorization"] = haveToken
}
}
That token injection part is already working well, by the way. Yay, Siesta!
URLSession version
This is bloated compared to Siesta, and I'm now not using this but here is what it used to be:
func login(user: SessionUser, endpoint: URL)
{
DDLogInfo("Logging in: \(user.name) with \(user.password)")
let json: [String: Any] = ["name": user.name, "password": user.password]
let jsonData = try? JSONSerialization.data(withJSONObject: json)
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.httpBody = jsonData
_currentStatus = .Unknown
weak var welf = self
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
handleLogin(error: error, message: "No data from login attempt")
return
}
let jsonData:Any
do {
jsonData = try JSONSerialization.jsonObject(with: data, options: [])
}
catch let jsonDecodeError {
handleLogin(error: jsonDecodeError, message: "Could not get JSON from login response data")
return
}
guard let jsonDecoded = jsonData as? [String: Any] else {
handleLogin(error: error, message: "Could not decode JSON as dictionary")
return
}
guard let token = jsonDecoded["token"] as? String else {
handleLogin(error: error, message: "No auth token in login response")
return
}
let newUser = SessionUser(name: user.name, password: "", token: token)
welf?.currentUser = newUser
welf?.saveCurrentSession()
welf?._currentStatus = .LoggedIn
DDLogInfo("User \(newUser.name) logged in")
loginUpdate(user: newUser, status: .LoggedIn, message: nil, error: nil)
}
task.resume()
}
Siesta Version
Here is my attempt right now:
func login(user: String, pass: String, status: #escaping (String?) -> ())
{
let json = [ "name": user, "password": pass]
let req = ManifestCloud.shared.keys.request(.post, json: json)
req.onSuccess { (tokenInfo) in
if let token = tokenInfo.jsonDict["token"] as? String
{
let newUser = SessionUser(name: user, password: pass, token: token)
self.currentUser = newUser
}
status("success")
}
req.onFailure { (error) in
status(error.userMessage)
}
req.onCompletion { (response) in
status(nil)
}
}
Its sort of working, but the log in credentials are not saved by Siesta and I've had to rig up a new notification system for login state which I'd hoped Siesta would do for me.
I want to use Siesta's caching so that the SessionUser object is cached locally and I can use it to get a new token, if required, using the cached credentials. At the moment I have a jury-rigged system using UserDefaults.
Any help appreciated!

The basic problem here is that you are requesting but not loading the resource. Siesta draws a distinction between those two things: the first is essentially a fancied-up URLSession request; the second means that Siesta hangs on to some state and notifies observers about it.
Funny thing, I just answered a different but related question about this a few minutes ago! You might find that answer a helpful starting point.
In your case, the problem is here:
let req = ManifestCloud.shared.keys.request(.post, json: json)
That .request(…) means that only your request hooks (onSuccess etc.) receive a notification when your POST request finishes, and Siesta doesn’t keep the state around for others to observe.
You would normally accomplish that by using .load(); however, that creates a GET request and you need a POST. You probably want to promote your POST to be a full-fledge load request like this:
let keysResource = ManifestCloud.shared.keys
let req = keysResource.load(using:
keysResource.request(.post, json: json))
This will take whatever that POST request returns and make it the (observable) latestData of ManifestCloud.shared.keys, which should give you the “notification system for login state” that you’re looking for.

Related

login user with GET request swift

I have created a screen with a text field called customer_number text field and another screen with a text field called password text field. I want to integrate my app with an existing API made by the backend developers. I am new to IOS Development and I don't know how to go about it. How do I make a get request and pass the login credentials for the user to login?
I want to get the customer number from the API and pass it to the app and enable the customer to log in.
I think this question is too big and complex to be replied exhaustively. You didn't tell us about the API. What kind of input does it take? What kind of response?
Supposing the simplest case. You API expects JSON objects as input and respond with another JSON object containing the information you request.
I usually do tasks like this using the NSURLRequest.
let js = ["Username":username, "Password":password]
let session = URLSession.init(configuration: .default)
let url = URL(...)
var req = URLRequest.init(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 10)
req.httpMethod = "POST"
// Add some header key-value pairs
req.addValue(..., forHTTPHeaderField: ...)
...
let task = session.dataTask(with: request) { (data, response, error) in
guard error == nil else { return }
guard let responseData = data else { return }
let code = (response as! HTTPURLResponse).statusCode
// Checking for code == 200 states for authorised user. Generally log-in APIs should return some 4xx code if not allowed or non-authorised user.
if code == 200 {
// Now we try to convert returned data as a JSON object
do {
let json = try JSONSerialization.jsonObject(with: responseData, options: [])
// use your json object here, for example checking if contains the user number...
} catch {
// handle errors
}
}
}
task.resume()
I coded this very quickly, please check the correctness of al mechanism!

swift:receive AWS id_token after sending facebook current token to AWS Cognito?

I want to send the facebook access token to the AWS Cognito and then receive an authorization token which can further be sent as an Authorization header in HTTP Put request.
However, I always get "unauthorized" response from the AWS end point.
When I try to print :
credentialsProvider.credentials().continueOnSuccessWith(executor: AWSExecutor.default()) { (task) -> Any? in
print(task.error)
return true
}
I get the following output:
Optional(Error Domain=com.amazonaws.AWSJSONBuilderErrorDomain Code=4 "serialized object is neither a valid json Object nor NSData object: {
IdentityPoolId = "******";
Logins = {
"graph.facebook.com" = "<FBSDKAccessToken: *******>";
};
}" UserInfo={NSLocalizedDescription=serialized object is neither a valid json Object nor NSData object: {
IdentityPoolId = "*****+*";
Logins = {
"graph.facebook.com" = "<FBSDKAccessToken: ******>";
};
}})
This is my code:
import AWSCognito
class FacebookProvider: NSObject, AWSIdentityProviderManager {
func logins() -> AWSTask<NSDictionary> {
if let token = FBSDKAccessToken.current() {
return AWSTask(result: [AWSIdentityProviderFacebook:token])
}
return AWSTask(error:NSError(domain: "Facebook Login", code: -1 , userInfo: ["Facebook" : "No current Facebook access token"]))
}
}
class API {
..............
public func putOrder(when fbLogin: Bool, _ order: Order, onSuccess: #escaping(JSON) -> Void,
on Failure: #escaping(Error)-> Void) {
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .EUCentral1 ,
identityPoolId:"*****", identityProviderManager:FacebookProvider())
let configuration = AWSServiceConfiguration(region: AWSRegionType.EUCentral1, credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
let url = "\(serverURL)\(API.loginOrderPath)"
let urlRequest: NSMutableURLRequest = NSMutableURLRequest(url: NSURL(string: url)! as URL)
urlRequest.httpMethod = API.apiMethodPut
urlRequest.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
urlRequest.setValue("\(credentialsProvider.credentials())", forHTTPHeaderField: "Authorization")
do {
var json: JSON
json = ["companyId": order.companyId, "drinks": order.drinksId, "payment": order.payment, "tip": order.tip]
urlRequest.httpBody = try json.rawData()
let task = URLSession.shared.dataTask(with: urlRequest as URLRequest, completionHandler: {data, response, error -> Void in
if error != nil {
Failure(error!)
} else {
if let response = try? JSON(data: data!) {
onSuccess(response)
} else {
}
}
})
task.resume()
} catch _ {
}
}
}
Expected Result: JSON response from the AWS Server
Actual Result : unauthorised
The reason why your are receiving an unauthorized response from API Gateway is double :
credentialsProvider.credentials() is not serialized to JSON and can not be "as is" for the authorization headers.
Looks like you are trying to manually call API Gateway, by managing your self the low level details of the URL Request. I don't see code to add a signature to the request. All authenticated API Gateway requests must be signed (see https://docs.aws.amazon.com/apigateway/api-reference/making-http-requests/) and the Authorization header must contain the credentials used to compute the signature.
Managing the low level details of AWS Signature is not trivial. You should not write code to do that but use the AWS iOS SDK instead. In particular, if you're trying to call API Gateway with Cognito User Pool authorisation, have a look at this example : https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-generate-sdk-ios-swift.html.
The API Gateway console will generate the client side code required to run this example (see https://docs.aws.amazon.com/apigateway/latest/developerguide/genearte-ios-sdk-of-an-api.html)
This should remove a lot of boiler plate from your code, making it easier to read and to maintain.
The flow posted in your comment above, the iOS SDK will take care of 2/ 3/ and 4/ steps for you - automatically.

POST API call using APIGateway returns "Internal Server Error" in swift 4, but works everywhere else

I created a nodejs lambda function in AWS and exposed it using APIGateway with methods GET, PUT, POST, and DELETE (all setup with proxy). All methods have been tested and work in AWS using APIGateway, and then outside of AWS using Postman.
First, I called the GET method for the endpoint in my Swift 4 project, and it is successful.
BUT I have tried just about everything to call the POST method in swift and cannot get it to execute successfully. This is what I am currently trying after researching online:
let awsEndpoint: String = "https://host/path"
guard let awsURL = URL(string: awsEndpoint) else {
print("Error: cannot create URL")
return
}
var postUrlRequest = URLRequest(url: awsURL)
postUrlRequest.httpMethod = "POST"
postUrlRequest.addValue("John Doe", forHTTPHeaderField: "name")
postUrlRequest.addValue("imageurl.com", forHTTPHeaderField: "imageUrl")
URLSession.shared.dataTask(with: postUrlRequest) { data, response, error in
guard let data = data else { return }
do {
guard let receivedTodo = try JSONSerialization.jsonObject(with: data,
options: []) as? [String: Any] else {
print("Error")
return
}
} catch let err{
print(err)
}
}.resume()
The response I get is ["message":"Internal Server Error"]. When I look at the logs in CloudWatch they are not very descriptive. The error log for the post call is:
"Execution failed due to configuration error: Malformed Lambda proxy response"
After researching this issue aws suggests to format the response in a specific way and I have updated my nodejs lambda function to mimmic this.
case "POST":
pool.getConnection(function(err, connection) {
const groupName = event.headers.name;
const imageUrl = event.headers.imageUrl;
var group = {Name: groupName, ImageUrl: imageUrl, IsActive:true, Created:date, Updated:date};
var query = "INSERT INTO Groups SET ?";
connection.query(query,group, function (error, results, fields) {
var responseBody = {
"key3": "value3",
"key2": "value2",
"key1": "value1"
};
var response = {
"statusCode": 200,
"headers": {
"my_header": "my_value"
},
"body": JSON.stringify(responseBody),
"isBase64Encoded": true
};
if (error) callback(error);
else callback(null, response)
connection.release();
});
});
break;
Like I said previously, this works when testing everywhere except swift 4. My GET call works with swift 4, so I do not think it is an issue with allowing anything in the info.plist but I could be wrong. I have tried just about everything, but cannot seem to get past this error.
I fixed this issue myself. After allowing ALL log output in API Gateway for that endpoint, I found that somewhere along the way my headers were being converted to all lowercase.
'imageUrl' became 'imageurl'
It was throwing an error because in my lambda function, it could not find 'imageUrl'
I think this is a conversion that is happening in APIGateway because I have never come across this issue with swift.

Getting token from an asp.net web api in iOS / swift

I am a .net developer but very new to iOS and swift development, just need help with consuming Web API using Swift2
The Asp.net Web API has been built with OAuth2 authentication, published to my Azure VM server with SSL certificate installed. The API site itself works properly, tested through Postman
However I got stuck when started writing first few lines of code in Swift trying to get Authentication token. After reading some online tutorials I decided to engage Alamofire, and produced below codes snippet:
func GetToken() {
let params = [
"grant_type" : "password",
"username" : "123456#qq.com",
"password" : "averygoodpassword"
]
let headers = [
"Content-Type" : "application/x-www-form-urlencoded"
]
request(.POST, "https://api.example.com/token",
parameters: params,
headers: headers,
encoding: .JSON)
.responseJSON { request, response, result in
print (request)
print (response?.description)
print (result)
switch result {
case .Success(let JSON):
print("Success with JSON: \(JSON)")
case .Failure(let data, let error):
print("Request failed with error: \(error)")
if let data = data {
print("Response data: \(NSString(data: data, encoding: NSUTF8StringEncoding)!)")
}
}
}
}
It ends up with below output in Xcode which didn't seem to be OK. The error = unsupported_grant_type told me that the request were sent to server but the parameters were not sent with request properly. I really cannot figure out the reason and solution, had been digging on Internet for a few days but still feeling desperate with it. Can anyone help please? Even if someone can provide a pure swift solution without any 3rd party library will be greatly helpful. Thanks!
Xcode output:
Optional( { URL: https://api.example.com/token })
Optional(" { URL: https://api.example.com/token } { status code: 400, headers {\n \"Access-Control-Allow-Headers\" = \"Content-Type\";\n \"Access-Control-Allow-Methods\" = \"GET, POST, PUT, DELETE, OPTIONS\";\n \"Access-Control-Allow-Origin\" = \"*\";\n \"Cache-Control\" = \"no-cache\";\n \"Content-Length\" = 34;\n \"Content-Type\" = \"application/json;charset=UTF-8\";\n Date = \"Fri, 30 Sep 2016 10:30:31 GMT\";\n Expires = \"-1\";\n Pragma = \"no-cache\";\n Server = \"Microsoft-IIS/8.5\";\n \"X-Powered-By\" = \"ASP.NET\";\n} }")
SUCCESS
Success with JSON: {
error = "unsupported_grant_type";
}
I had a similar problem trying to POST to MailGun for some automated emails I was implementing in an app.
I was able to get this working properly with a large HTTP response. I put the full path into Keys.plist so that I can upload my code to github and broke out some of the arguments into variables so I can have them programmatically set later down the road.
// Email the FBO with desired information
// Parse our Keys.plist so we can use our path
var keys: NSDictionary?
if let path = NSBundle.mainBundle().pathForResource("Keys", ofType: "plist") {
keys = NSDictionary(contentsOfFile: path)
}
if let dict = keys {
// variablize our https path with API key, recipient and message text
let mailgunAPIPath = dict["mailgunAPIPath"] as? String
let emailRecipient = "bar#foo.com"
let emailMessage = "Testing%20email%20sender%20variables"
// Create a session and fill it with our request
let session = NSURLSession.sharedSession()
let request = NSMutableURLRequest(URL: NSURL(string: mailgunAPIPath! + "from=FBOGo%20Reservation%20%3Cscheduler#<my domain>.com%3E&to=reservations#<my domain>.com&to=\(emailRecipient)&subject=A%20New%20Reservation%21&text=\(emailMessage)")!)
// POST and report back with any errors and response codes
request.HTTPMethod = "POST"
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
if let error = error {
print(error)
}
if let response = response {
print("url = \(response.URL!)")
print("response = \(response)")
let httpResponse = response as! NSHTTPURLResponse
print("response code = \(httpResponse.statusCode)")
}
})
task.resume()
}
The Mailgun Path is in Keys.plist as a string called mailgunAPIPath with the value:
https://API:key-<my key>#api.mailgun.net/v3/<my domain>.com/messages?
I'm slightly opposed to using 3rd party libraries, especially for small things like a http POST and this seems like a much more maintainable solution to me. Anyways, hope this helps, let me know if you have any questions!

Error handling in Alamofire

I have the HTTP code in an AngularJS controller:
$http.post('/api/users/authenticate', {email: $scope.email, password: $scope.password})
.success(function (data, status, headers, config) {
authService.login($scope.email);
$state.go('home');
})
.error(function (data, status, headers, config) {
$scope.errorMessages = data;
$scope.password = "";
});
In the success case, the server will respond with a JSON representation of a user. In the error case the server will respond with a simple string such as User not found which can be accessed through the data parameter.
I'm having trouble figuring out how to do something similar in Alamofire. Here's what I have right now:
#IBAction func LoginPressed(sender: AnyObject) {
let params: Dictionary<String,AnyObject> = ["email": emailField.text, "password": passwordField.text]
Alamofire.request(.POST, "http://localhost:3000/api/users/authenticate", parameters: params)
.responseJSON {(request, response, data, error) in
if error == nil {
dispatch_async(dispatch_get_main_queue(), {
let welcome = self.storyboard?.instantiateViewControllerWithIdentifier("login") as UINavigationController;
self.presentViewController(welcome, animated: true, completion: nil);
})
}
else{
dispatch_async(dispatch_get_main_queue(), {
// I want to set the error label to the simple message which I know the server will return
self.errorLabel.text = "something went wrong"
});
}
}
}
I have no idea if I'm handling the non-error case correctly either and would appreciate input on that as well.
You are are on the right track, but you are going to run into some crucial issues with your current implementation. There are some low level Alamofire things that are going to trip you up that I want to help you out with. Here's an alternative version of your code sample that will be much more effective.
#IBAction func loginPressed(sender: AnyObject) {
let params: [String: AnyObject] = ["email": emailField.text, "password": passwordField.text]
let request = Alamofire.request(.POST, "http://localhost:3000/api/users/authenticate", parameters: params)
request.validate()
request.response { [weak self] request, response, data, error in
if let strongSelf = self {
let data = data as? NSData
if data == nil {
println("Why didn't I get any data back?")
strongSelf.errorLabel.text = "something went wrong"
return
} else if let error = error {
let resultText = NSString(data: data!, encoding: NSUTF8StringEncoding)
println(resultText)
strongSelf.errorLabel.text = "something went wrong"
return
}
var serializationError: NSError?
if let json: AnyObject = NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments, error: &serializationError) {
println("JSON: \(json)")
let welcome = self.storyboard?.instantiateViewControllerWithIdentifier("login") as UINavigationController
self.presentViewController(welcome, animated: true, completion: nil)
} else {
println("Failed to serialize json: \(serializationError)")
}
}
}
}
Validation
First off, the validate function on the request will validate the following:
HTTPStatusCode - Has to be 200...299
Content-Type - This header in the response must match the Accept header in the original request
You can find more information about the validation in Alamofire in the README.
Weakify / Strongify
Make sure to weak self and strong self your closure to make sure you don't end up creating a retain cycle.
Dispatch to Main Queue
Your dispatch calls back to the main queue are not necessary. Alamofire guarantees that your completion handler in the response and responseJSON serializers is called on the main queue already. You can actually provide your own dispatch queue to run the serializers on if you wish, but neither your solution or mine are currently doing so making the dispatch calls to the main queue completely unnecessary.
Response Serializer
In your particular case, you don't actually want to use the responseJSON serializer. If you do, you won't end up getting any data back if you don't pass validation. The reason is that the response from the JSON serialization is what will be returned as the AnyObject. If serialization fails, the AnyObject will be nil and you won't be able to read out the data.
Instead, use the response serializer and try to parse the data manually with NSJSONSerialization. If that fails, then you can rely on the good ole NSString(data:encoding:) method to print out the data.
Hopefully this helps shed some light on some fairly complicated ways to get tripped up.
So Alamofire treats all requests successful. This really comes down to the API server http headers being returned.
You could use Alamofire.Request.validate()
It'll allow you to validate http headers, etc. Check out the example
https://github.com/Alamofire/Alamofire#validation
I am assuming the the error message will be in the data object.
to access the values from data you could do something like
I am not really sure about your api response looks but in this example
{
"message": "Could not authenticate"
}
let message: String? = data?.valueForKey("message") as String

Resources