3

I have a vapor3 app within which I am trying to connect to the Xero Api, the process is fairly simple. We send the user to a xero web page where they login with the details and authorise our connection, it then redirects back to my vapor 3 app. In the app we then connect to the xero server with the code provided in the redirect, and then xero should be issue an access token to be used going forward. The process is described here

The issue is I can't connect to xero to get the access token. I have tried 2 approaches, the first is to use HTTPClient, with this code:

 let m = try decoder.decode(Master.self, from: masterDoc!)
 let ci = m.xeroAppKey.data(using: .utf8)?.base64EncodedString() //convert from string to base encoded
 let xs = m.xeroAppSec.data(using: .utf8)?.base64EncodedString() //convert from string to base encoded
 let authorization = "Basic " + ci! + ":" + xs!
 print("authorisation is \(authorization)")
 return HTTPClient.connect(hostname: "identity.xero.com", on: req).flatMap{client in
       var httpReq = HTTPRequest(method: .POST, url: "https://identity.xero.com/connect/token")
       httpReq.headers.add(name: "authorization", value: authorization)
       httpReq.headers.add(name: "Content-Type", value: "x-www-form-urlencoded")
       httpReq.body = HTTPBody(string: "grant_type=authorization_code&code=\(code)&redirect_uri=http://localhost:8080/XeroAuthRedirect")

       return client.send(httpReq).flatMap{resp in
         print("response is \(resp) with status \(resp.status)")
         return req.future().map{
           return .ok
         }  
      }
    } 

With this I get the following response:

HTTP/1.1 301 Moved Permanently
Server: AkamaiGHost
Content-Length: 0
Location: https://identity.xero.com/connect/token
Expires: Tue, 02 Jun 2020 07:59:59 GMT
Cache-Control: max-age=0, no-cache, no-store
Pragma: no-cache
Date: Tue, 02 Jun 2020 07:59:59 GMT
Connection: keep-alive

Which suggests the end point has moved but the location suggested is the same one that I am trying to connect to. I can't find anywhere in the docs which suggested the end point has changed. It also seems HTTPClient wont follow redirects.

So I tried to connect using URLSession instead using this code:

let url = URL(string:"https://identity.xero.com/connect/token")!
                let payload = "grant_type=authorization_code&code=\(code)&redirect_uri=http://localhost:8080/XeroAuthRedirect".data(using: .utf8)

                let promise = req.eventLoop.newPromise(HTTPStatus.self)

                var request = URLRequest(url: url)
                request.httpMethod = "POST"
                request.addValue(authorization, forHTTPHeaderField: "authorization")
                request.addValue("x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
                request.httpBody = payload

                URLSession.shared.dataTask(with: request) {(data, response, error) in

                    if let error = error {
                        print(error.localizedDescription)
                        promise.fail(error: error) // (!)
                        return
                    }

                    guard let data = data else {
                        print("empty data")
                        promise.fail(error: SumUpError.invalidResponse) // (!)
                        return
                    }

                    guard let str = String(data: data, encoding: .utf8) else {
                        print("invalid data")
                        promise.fail(error: SumUpError.invalidResponse) // (!)
                        return
                    }

                    print(str)


                    print("response is \(String(describing: response))")


                }.resume()

Where i get the following error message.

{"error":"invalid_request"}

Any ideas what is going on here or how I can get this connection up and running are greatly apprecitated.

Joby Ingram-Dodd
  • 730
  • 5
  • 23
  • I doubt it's anything relevant, but in my token refresh code my content-type header is set to `application/x-www-form-urlencoded` which is slightly different to yours. – droopsnoot Jun 02 '20 at 08:46
  • @droopsnoot no joy with that. I guess in your app you are going to the idenitity.xero.com endpoint? – Joby Ingram-Dodd Jun 02 '20 at 09:51
  • Yes, but mine is a Windows service m2m connection, so I don't do the initial call to the Xero login page, I use their XOAuth tool instead to get the tokens. So I'm only refreshing a token that I've already got. My endpoint is the same, and the post body content is different. – droopsnoot Jun 02 '20 at 09:57
  • The other thing I have is "Authorization" with a capital "a", but it also contains the word "Bearer" prior to the token, so it's "Authorization: Bearer " & access-token. Again, not sure if it's case-sensitive or whether the extra word is only required in my scenario. (Oh, now I see you have "Basic" in there, maybe that's correct for your usage). – droopsnoot Jun 02 '20 at 10:00
  • @droopsnoot in the docs, it shows the authorisation should be in this format ```authorization: "Basic " + base64encode(client_id + ":" + client_secret)``` which I have taken to mean that i should encode the client id and secret and add them to a string with Basic at the start. Do you see it any differently? – Joby Ingram-Dodd Jun 02 '20 at 10:59
  • That sounds about right, but I'm doing things a little differently. First off, I use PKCE so I don't have a client secret. Second because I use XOAuth I just store the access token and refresh token, then I keep refreshing it. I don't base64encode it, though, because it comes back from XOAuth and from the call to the identity endpoint (for a refresh) already encoded. That may be because it's coming back as a JSON object, and perhaps the parsing is doing that. (Sorry, I don't know the exact ins and outs of it, I stopped messing when it started working). – droopsnoot Jun 02 '20 at 11:45

1 Answers1

1

Ok so the answer in the end was to use the vapor client service found at req.client()

I used the following code. I also made a rookie error of encodeing the client id and client secret and then combing them, rather then cobining them first then encoding them. The docs do show this but I guess not clear enough. Anyway here is the code that worked to connect to the Xero Api with Vapor 3.


    let toBase = "\(clientId):\(clientSecret)".data(using: .utf8)?.base64EncodedString()
    let authorization = "Basic " + toBase!           
    let payload = "grant_type=authorization_code&code=\(code)&redirect_uri=http://localhost:8080/XeroAuthRedirect".data(using: .utf8)!
    let request = Request( using: req)
    request.http.url = URL(string:"https://identity.xero.com/connect/token")!
    request.http.body = payload.convertToHTTPBody()
    request.http.method = HTTPMethod.POST
    request.http.headers.add(name: "authorization", value: authorization)
    request.http.headers.add(name: "Content-Type", value: "application/x-www-form-urlencoded")
 return try req.client().send(request).flatMap{resp in 

}

Hopefully this is helpful to anyone trying to add xero with Vapor.

Joby Ingram-Dodd
  • 730
  • 5
  • 23