OAuth authentication with ruby

Is there example code for oauth2 authentication with ruby for a console application?

so far i’ve got:


require ‘oauth2’

data = {}

client_id = “”
client_secret = “”

client = OAuth2::Client.new(
client_id,
client_secret,
:authorize_url => “/v2/approve_app”,
:site => ‘https://api.freeagent.com’,
)

p client.auth_code.authorize_url

This spits out a url which I can use to approve the app for my account, however after that I get bounced to the google oauth playground. There’s a button that says “exchange authorisation code for tokens” but when I click on it I get “Malformed auth code”.

I’d rather not use google services in general. Do you have any example code for authentication with oauth2?

Thanks,
Mark

Ok so I’ve made a bit of progress but now when I pass the authorisation code to the /v2/token_endpoint endpoint I just get a 401 basic authorization error. I’m pretty sure the authorisation credentials are correct, I’ve even tried passing them with the curl -u parameter.

The client id and secret are removed in this example but in my code they are correct. Why is this failing?

curl -i https://api.freeagent.com/v2/token_endpoint -X POST -H 'Accept: application/json' -H 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8' -d 'grant_type=authorization_code&code=1hqncAfxC7SJrPpAYnS0ucihiaVkxHM-6ddp8VS-v&redirect_uri=https%3A%2F%2Fa.rkw.io' -u 'CLIENT_ID:CLIENT_SECRET'
HTTP/1.1 401 Unauthorized
Server: nginx
Date: Wed, 18 Apr 2018 06:40:03 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-store
Pragma: no-cache
WWW-Authenticate: Basic realm="Application"
X-Request-Id: c85db9ac-0e41-475f-95b0-ad7728ef333d
X-Rev: 71e64648db5921c2924e205882bc6d45bcce4a7a
X-Host: web3-gc
X-Runtime: 0.041215
Content-Security-Policy: default-src data: 'unsafe-eval' 'unsafe-inline' *; plugin-types application/x-shockwave-flash; script-src 'self' 'unsafe-eval' 'unsafe-inline' data: *.google-analytics.com *.googleapis.com *.optimizely.com api.stripe.com bam.nr-data.net connect.facebook.net freeagent-assets.s3.amazonaws.com freeagent-videos.s3.amazonaws.com googleads.g.doubleclick.net js.honeybadger.io js.stripe.com js-agent.newrelic.com p.typekit.net sdr.totango.com use.typekit.com use.typekit.net website.freeagent.com www.freeagent.com www.google.co.uk www.google.com www.googleadservices.com www.googletagmanager.com tagmanager.google.com www.snapengage.com; report-uri /csp_violations
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Vary: Origin
Strict-Transport-Security: max-age=31536000;

HTTP Basic: Access denied.

Hello! So here is a very simple Ruby app using OAuth2.

require 'oauth2'

client_id = "OAuth identifier from your app set up"
client_secret = "OAuth secret from your app set up"

client = OAuth2::Client.new(
  client_id,
  client_secret,
  authorize_url: "/v2/approve_app",
  token_url: "/v2/token_endpoint",
  site: 'https://api.sandbox.freeagent.com',
)

# redirect_uri must match one of those listed when you created your app
auth_url = client.auth_code.authorize_url(redirect_uri: "https://developers.google.com/oauthplayground")

# Once you've built the auth url, you will need to follow it and approve the app.  Once you've done that you'll get
# redirected and you'll get a token.

# In the case of this example it is using Google so the page it takes me to will be the same one in our documentation.  In
# the slider on the left under "Step 2" you'll have an Authorization code there.  You use that in the `auth_code` field below.

# If you don't want to use Google, you need to have some TLS secured URL you can redirect to (see this for more information -
# https://aaronparecki.com/oauth-2-simplified/).  You could do this for local testing by, say, creating a simple app running
# SSL with your own self-signed certs that just spits out the code from the URL params.

auth_code = "code from the above authorize URL redirect"
token = client.auth_code.get_token(auth_code, redirect_uri: "https://developers.google.com/oauthplayground")

response = token.get("https://api.sandbox.freeagent.com/v2/company")

# If you are making the request as part of a Practice, you'll need to add the X-Subdomain header specifying subdomain of
# the Practice's company you're making the request for.

# response = token.get("https://api.sandbox.freeagent.com/v2/company", headers: { "X-Subdomain": "companysubdomain" }

require 'json'
puts JSON.parse(response.body)

Now, as to your latest update. You can definitely do it via cUrl. You can use basic auth like you did or user/pass in the query string. I see you’ve opened a ticket with Support already. If, as part of that ticket, you can give them your Client ID, that can help us with debugging. (If you post here when you’ve done that, I can follow up with Support).

cUrl using Basic Auth:

$ curl -iv https://api.sandbox.freeagent.com/v2/token_endpoint -X POST -H 'Accept: application/json' -d 'grant_type=authorization_code&redirect_uri=https%3A%2F%2Fdevelopers.google.com%2Foauthplayground&code=1kNpW2UjSDvpTS6srjACxL0U-0C-Ig821eiWhg6rX' -u CLIENTID:CLIENTSECRET

Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 109.73.126.98...
* TCP_NODELAY set
* Connected to api.sandbox.freeagent.com (109.73.126.98) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.sandbox.freeagent.com
* Server certificate: COMODO RSA Domain Validation Secure Server CA
* Server certificate: COMODO RSA Certification Authority
* Server auth using Basic with user '_RtoSa5LKU8d1tPXPG9ieg'
> POST /v2/token_endpoint HTTP/1.1
> Host: api.sandbox.freeagent.com
> Authorization: Basic XXXXXX
> User-Agent: curl/7.54.0
> Accept: application/json
> Content-Length: 143
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 143 out of 143 bytes
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Server: nginx
Server: nginx
< Date: Wed, 18 Apr 2018 13:33:21 GMT
Date: Wed, 18 Apr 2018 13:33:21 GMT
< Content-Type: application/json;charset=UTF-8
Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Connection: keep-alive
Connection: keep-alive
< Cache-Control: no-store
Cache-Control: no-store
< Pragma: no-cache
Pragma: no-cache
< ETag: W/"2fc62bd6f4077b03fb05a2fbbda5d369"
ETag: W/"2fc62bd6f4077b03fb05a2fbbda5d369"
< X-Request-Id: d9c5479d-3f8d-4d8d-9447-9155a4a571de
X-Request-Id: d9c5479d-3f8d-4d8d-9447-9155a4a571de
< X-Rev: a249062cac1bfa6930614473f6aa7b39fa05c2af
X-Rev: a249062cac1bfa6930614473f6aa7b39fa05c2af
< X-Host: web1-gc
X-Host: web1-gc
< X-Runtime: 0.044827
X-Runtime: 0.044827
< Content-Security-Policy: default-src data: 'unsafe-eval' 'unsafe-inline' *; plugin-types application/x-shockwave-flash; script-src 'self' 'unsafe-eval' 'unsafe-inline' data: *.google-analytics.com *.googleapis.com *.optimizely.com api.stripe.com bam.nr-data.net connect.facebook.net freeagent-assets.s3.amazonaws.com freeagent-videos.s3.amazonaws.com googleads.g.doubleclick.net js.honeybadger.io js.stripe.com js-agent.newrelic.com p.typekit.net sdr.totango.com use.typekit.com use.typekit.net website.freeagent.com www.freeagent.com www.google.co.uk www.google.com www.googleadservices.com www.googletagmanager.com tagmanager.google.com www.snapengage.com; report-uri /csp_violations
Content-Security-Policy: default-src data: 'unsafe-eval' 'unsafe-inline' *; plugin-types application/x-shockwave-flash; script-src 'self' 'unsafe-eval' 'unsafe-inline' data: *.google-analytics.com *.googleapis.com *.optimizely.com api.stripe.com bam.nr-data.net connect.facebook.net freeagent-assets.s3.amazonaws.com freeagent-videos.s3.amazonaws.com googleads.g.doubleclick.net js.honeybadger.io js.stripe.com js-agent.newrelic.com p.typekit.net sdr.totango.com use.typekit.com use.typekit.net website.freeagent.com www.freeagent.com www.google.co.uk www.google.com www.googleadservices.com www.googletagmanager.com tagmanager.google.com www.snapengage.com; report-uri /csp_violations
< X-Frame-Options: SAMEORIGIN
X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 1; mode=block
X-XSS-Protection: 1; mode=block
< Vary: Origin
Vary: Origin
< X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff
< X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff
< Strict-Transport-Security: max-age=31536000;
Strict-Transport-Security: max-age=31536000;

<
* Connection #0 to host api.sandbox.freeagent.com left intact
{"access_token":"1DxYR5OpkzwWIKuyujarSYp1z1_dIKz3aqVTe-n","token_type":"bearer","expires_in":604800,"refresh_token":"1MznVDe7ESXFw1_p7YVhogPjzBMdARGueK-N8FS"}%

Hi, thanks for the response. What you’ve pasted seems to be exactly what I was doing, I only switched to a raw curl request for the token exchange in order to rule out some kind of issue with the ruby gem being the cause and also to see exactly what the http response was. I’ve given support my client id so hopefully they’ll get back to me soon. The response to the curl command seemed to suggest the basic authentication was failing. I was not using the sandbox but the live endpoint, is there some special activation that needs to occur before an application can use that? I didn’t see anything in the freeagent oauth documentation about it but I’ve encountered this with other services before.

Thanks,
Mark

So this is the code I was using with the ruby gem, I think it’s essentially the same as yours.

require 'oauth2'
require 'base64'

client_id = ""
client_secret = ""

client = OAuth2::Client.new(
  client_id,
  client_secret,
  :authorize_url => "/v2/approve_app",
  :token_url => "/v2/token_endpoint",
  :site => 'https://api.freeagent.com',
)

puts client.auth_code.authorize_url

# at this point browse to the url and copy/paste the code from the url

print "code: "

code = $stdin.readline.chomp

token = client.auth_code.get_token(
  code,
  :redirect_uri => 'https://a.rkw.io'
)

p token

result:


/var/lib/gems/2.3.0/gems/oauth2-1.4.0/lib/oauth2/client.rb:119:in `request': invalid_grant:  (OAuth2::Error)
{"error":"invalid_grant"}
	from /var/lib/gems/2.3.0/gems/oauth2-1.4.0/lib/oauth2/client.rb:146:in `get_token'
	from /var/lib/gems/2.3.0/gems/oauth2-1.4.0/lib/oauth2/strategy/auth_code.rb:30:in `get_token'
	from ./test.rb:27:in `<main>'

Thanks for the code, Mark. No, there’s no activation that needs doing for a non-Practice app. The two environments (sandbox and production) are completely separate.

If I had to guess, I’d say the id and secret you’re using don’t match what we have here. If you don’t mind, can you please regenerate your client tokens (via the https://dev.freeagent.com/apps site) and try again.

It may be worthwhile to create a company on the sandbox (https://signup.sandbox.freeagent.com/signup) and a sandbox app (https://dev.staging.fre.ag/) to make sure our continued debugging doesn’t access any real data.

Hi Pat

I’ve already tried destroying and re-creating the app to no avail, also I can see that the client id and secret on the application page are correct and match what’s in the code.

I’ve just regenerated them and the result is the same.

Mark

Hi Pat

Are there not logs you can inspect to determine the reason why the requests are failing? That would seem like a much quicker path to resolving this than guessing.

Thanks,
Mark

For anyone looking at this in the future, the problem was due to using a redirect_uri in get_token and not authorize_url. If you use it in one, you need to use it in the other.

Thanks for your help Pat. Is this a generic oauth2 requirement or is it a feature of your implementation? Is there any scope for making the error response a bit more intuitive? I was thinking perhaps inject a header into the 401 response to indicate why it failed, but you can’t really say it’s a uri mismatch because the uri was essentially the same for both requests, it was just implicit in one and explicit in the other. I guess exposing the details of that could introduce security issues.

I didn’t see this reply, Mark. Sorry about that.

After a brief look at the OAuth spec, I’m thinking this is an eccentricity with the Ruby oauth2 gem. Nothing that we (FreeAgent) are doing.