Unsupported grant type in Google OAuth - oauth

I am getting an error "unsupported grant type" when I try to request an OAuth token for a service account using curl. I'm following the example for OAuth 2.0 for service accounts (https://developers.google.com/identity/protocols/OAuth2ServiceAccount) and I think I have everything setup correctly. I have a service account setup in Google Cloud and I'm using that email address in the OAuth request.
The documentation says to use the URL encoded grant type "urn:ietf:params:oauth:grant-type:jwt-bearer" but it isn't clear if this is the only option for the grant type or what other options might be.
I am sending the the base64 encoded header
{"alg":"RS256","typ":"JWT"}
and "."
and base64 encoded claims
{
"iss":"chargepubadmin#xxxxxxxx.iam.gserviceaccount.com",
"scope":"https://www.googleapis.com/auth/pubsub",
"aud":"https://www.googleapis.com/oauth2/v4/token",
"exp":1497159875,
"iat":1497156275
}
and "."
and base64 encoded signature
{base64 header}.{base64 claims}
.
curl -X POST -d 'grant_type=http%3A%2F%2Foauth.net%2Fgrant_type%2Fdevice%2F1.0%26assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.ew0KICAiaXNzIjoiY2.......' "https://www.googleapis.com/oauth2/v4/token"
I'm using an online base64 encoding tool which matches the example base64 encoding.
Can anyone enlighten me as to the what the grant type is or should be?

The grant type should be set as urn:ietf:params:oauth:grant-type:jwt-bearer documented here under the REST API Making the access token request section.
Working example using google-auth library
It will be very easy and simple, if you used the google-auth library which automatically takes care of parsing the private key json file, fetching access tokens, refreshing them and actually including them as part of the requests.
You only need to provide the request URL and body, the library takes care of the rest. Here is a simplified example:
#!/usr/bin/env python
from google.auth.transport.requests import AuthorizedSession
from google.oauth2.service_account import Credentials
# BEGIN CONFIGURATION - change as needed.
# Path to the JSON file containing the service account private key and email.
PRIVATE_KEY_JSON = '/path/to/json/file'
# The API scope this token will be valid for.
API_SCOPES = ['https://www.googleapis.com/auth/pubsub']
# END CONFIGURATION
if __name__ == '__main__':
credentials = Credentials.from_service_account_file(
PRIVATE_KEY_JSON, scopes=API_SCOPES)
authed_session = AuthorizedSession(credentials)
url = 'https://pubsub.googleapis.com/v1/<SOMETHING>'
response = authed_session.get(url)
print str(response.content)
Working example without additional libraries
If you do not want to use any additional libraries but can use the standard python libraries, here is a working sample (tested personally with a service account of my own) in Python (supports both 2.x and 3.x versions) which takes care of all the steps:
#!/usr/bin/env python
import Crypto.PublicKey.RSA as RSA
import Crypto.Hash.SHA256 as SHA
import Crypto.Signature.PKCS1_v1_5 as PKCS1_v1_5
import base64
import json
import time
try:
from urllib.request import urlopen
except ImportError:
from urllib2 import urlopen
try:
from urllib.parse import urlencode
except ImportError:
from urllib import urlencode
# BEGIN CONFIGURATION - change as needed.
# Path to the JSON file containing the service account private key and email.
PRIVATE_KEY_JSON = '/path/to/json/file'
# The API scope this token will be valid for.
API_SCOPE = 'https://www.googleapis.com/auth/pubsub'
# The validity of the token in seconds. Max allowed is 3600s.
ACCESS_TOKEN_VALIDITY_SECS = 3600
# END CONFIGURATION
class OauthAccessTokenGetter:
"""Fetches a new Google OAuth 2.0 access token.
The code is based on the steps described here: https://developers.go
ogle.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests
"""
ACCESS_TOKEN_AUD = 'https://www.googleapis.com/oauth2/v4/token'
REQUEST_URL = 'https://www.googleapis.com/oauth2/v4/token'
GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
def __init__(self, private_key_json_file, scope, token_valid_secs=3600):
self.private_key_json = self.LoadPrivateKeyJsonFromFile(
private_key_json_file)
self.scope = scope
self.token_valid_secs = token_valid_secs
#classmethod
def Base64UrlEncode(cls, data):
"""Returns the base64url encoded string for the specified data."""
return base64.urlsafe_b64encode(data)
#classmethod
def LoadPrivateKeyJsonFromFile(cls, private_key_json_file):
"""Returns JSON object by parsing the specified private key JSON
file."""
with open(private_key_json_file) as private_key_json_file:
return json.load(private_key_json_file)
def GetPrivateKey(self):
"""Returns the imported RSA private key from the JSON data."""
return RSA.importKey(self.private_key_json['private_key'])
def GetSigner(self):
"""Returns a PKCS1-V1_5 object for signing."""
return PKCS1_v1_5.new(self.GetPrivateKey())
#classmethod
def GetEncodedJwtHeader(cls):
"""Returns the base64url encoded JWT header."""
return cls.Base64UrlEncode(json.dumps({'alg': 'RS256', 'typ': 'JWT'}).encode('utf-8'))
def GetEncodedJwtClaimSet(self):
"""Returns the base64url encoded JWT claim set."""
current_time_secs = int(time.time())
jwt_claims = {
'iss': self.private_key_json['client_email'],
'scope': self.scope,
'aud': self.ACCESS_TOKEN_AUD,
'exp': current_time_secs + self.token_valid_secs,
'iat': current_time_secs
}
return self.Base64UrlEncode(json.dumps(jwt_claims).encode('utf-8'))
def GetJwtSignature(self, message):
"""Returns signature of JWT as per JSON Web Signature (JWS) spec."""
signed_message = self.GetSigner().sign(SHA.new(message))
return self.Base64UrlEncode(signed_message)
def GetSignedJwt(self):
"""Returns signed JWT."""
header = self.GetEncodedJwtHeader()
jwt_claim_set = self.GetEncodedJwtClaimSet()
signature = self.GetJwtSignature(header + b'.' + jwt_claim_set)
return header + b'.' + jwt_claim_set + b'.' + signature
def SendRequest(self, body):
"""Returns the response by sending the specified request."""
return urlopen(self.REQUEST_URL, urlencode(body).encode('utf-8')).read()
def GetAccessToken(self):
"""Returns the access token."""
body = {
'grant_type': self.GRANT_TYPE,
'assertion': self.GetSignedJwt()
}
response = json.loads(self.SendRequest(body))
return response['access_token']
if __name__ == '__main__':
print (OauthAccessTokenGetter(PRIVATE_KEY_JSON, API_SCOPE,
ACCESS_TOKEN_VALIDITY_SECS).GetAccessToken())
After you get the access token, you need to include it as the Bearer header in the requests you send as described here.
GET /drive/v2/files HTTP/1.1
Authorization: Bearer <access_token>
Host: www.googleapis.com/
Equivalently in curl as:
curl -H "Authorization: Bearer <access_token>" https://www.googleapis.com/drive/v2/files
Although it is described here that you can specify the token using access_token= parameter, I could not get it working at least for Google Compute Engine APIs, may be it works with PubSub, but the Bearer header approach has worked always in my experience.
UPDATE: As per the discovery doc for PubSub API, there seems to be a query parameter for access_token=, so it might very well work too.
"access_token": {
"description": "OAuth access token.",
"type": "string",
"location": "query"
},
And the discovery doc for Compute Engine APIs indicate the use of oauth_token query parameter instead and I did verify that it worked.
"oauth_token": {
"type": "string",
"description": "OAuth 2.0 token for the current user.",
"location": "query"
},

Related

OpenAPI V2 (Swagger) how to force client_id field to be URL encoded before being Base64 encoded an passed into authorization header?

I'm using springfox-swagger 3.0.0 to generate the Swagger UI (so I don't have much control over the Swagger UI code), and for my oauth2 authorization I've defined an application flow.
security: {
key: "OAuth2";
value: {
type: TYPE_OAUTH2;
flow: FLOW_APPLICATION;
token_url: "/oauth2/token";
scopes: {
scope: {
key: "api"
value: "Grants access to api"
}
}
}
}
Now my problem is that the client ids my system uses, contains colon ":" characters, which are also use to separate client id from client secret in the authorization header. This messed up the logic at the token endpoint.
authorization: Basic YXBpOjk1NmZkYmEzLWE1ZmEtNDk0MS1iZDAzLWY3NGY0ZmNhYjM1ZjpzWFNXVlBpcklSN1dsUkRvOG9lNFM2VTR3OEI0VFg4VUUwNm9QR2FyWmVn
If I URL encode my client id value before passing it to the client_id field in the swagger console, everything works ok.
authorization: Basic YXBpJTNBOTU2ZmRiYTMtYTVmYS00OTQxLWJkMDMtZjc0ZjRmY2FiMzVmOnNYU1dWUGlySVI3V2xSRG84b2U0UzZVNHc4QjRUWDhVRTA2b1BHYXJaZWc=
Any idea how can I instruct swagger to URL encode the client id field value before packaging it in the header?
Thanks
You'll have to fork Swagger UI and implement the necessary change in your fork. Then, since you're using Springfox, you'll need to point it to your custom Swagger UI fork instead of the bundled version.
The code that constructs the Authorization header for OAuth requests lives here:
https://github.com/swagger-api/swagger-ui/blob/master/src/core/plugins/auth/actions.js
Tweak either the authorizePassword or authorizeApplication function (depending on your grant type) to percent-encode the clientId:
Authorization: "Basic " + btoa(encodeURIComponent(clientId) + ":" + clientSecret)

Generating a JWT for Apple's DeviceCheck API

I'm attempting to use the DeviceCheck API from Apple. I can't seem to craft a request that doesn't fail with a 401 Unable to verify authorization token I've tried a handful of minor variations.
import java.security.KeyFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64
import io.jsonwebtoken.{Jwts, SignatureAlgorithm}
val deviceCheckPrivateKey = "<Key in plaintext without the key-guards>"
val privateKey = KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder.decode(deviceCheckPrivateKey)))
val builder = Jwts
.builder()
.setHeaderParam("kid", "<key-id-from-file>")
.signWith(SignatureAlgorithm.ES256, privateKey)
.claim("iss", "<team-id>")
.claim("iat", System.currentTimeMillis())
println(builder.compact())
I take the output of this scratch file and plug it in here:
curl -i -H "Authorization: Bearer <Output>" -X POST --data-binary #ValidQueryRequest.json https://api.development.devicecheck.apple.com/v1/query_two_bits
as recommended by Apple's documentation.
Is the overall structure of this right? I'm trying to follow this tutorial which implies this structuring:
But this blurb from Apple:
Each request you send to the query and update endpoints must include an authorization header that contains your authentication key. The authentication key must must use the ES256 algorithm and be in the Base 64 URL–encoded JSON web token format. If your token doesn't use this format, you receive a BAD_AUTHENTICATION_TOKEN HTTP error.
Suggests that rather than signing using the key, my request should "contain my authentication key".
According to: https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6
val builder = Jwts
.builder()
.setHeaderParam("kid", "<key-id-from-file>")
.signWith(SignatureAlgorithm.ES256, privateKey)
.claim("iss", "<team-id>")
.claim("iat", System.currentTimeMillis()) // <--- Should be seconds, not milliseconds

How to authenticate a Rails API with Google JWTs

On the frontend, using React and the Google Login Button, the user logs in to my application. Google returns the user's JWT to a callback URL on the frontend, encoded. I then deliver this JWT on each request to a Rails API and want the rails API to decode and verify this JWT, and if it is verified successfully, return the data that API endpoint is supposed to give.
I am currently using this code:
def google_public_key
x509 = OpenSSL::X509::Certificate.new ENV["GOOGLE_CERT"]
x509.public_key
end
The env var is described like this: https://gist.github.com/Connorelsea/c6b91a4b4b6889294fd4e2fcacb06564
I am getting this error: OpenSSL::X509::CertificateError (nested asn1 error)
If I do not verify, I can read the content of the JWT. From the JWT website it can be decoded, but not verified, as well.
Had the same problem and solved by setting the public_key as Pkey object not as a string in the constructor, try:
def google_public_key
x509 = OpenSSL::X509::Certificate.new
x509.public_key = OpenSSL::PKey::RSA.new ENV["GOOGLE_CERT"]
end

Authenticating to an API with a token

I'm working with the Zendesk API, an HTTPS-only, JSON API and authentication is required to update a resource, and the API supplies an API token to use when authenticating as different users. When updating a resource, I issue a PUT request over SSL, assign the appropriate JSON content to the request body, and specify the Content-Type request header as application/json.
Next, the API instructs its users to authenticate as the end-user by either using the user's email and password (which I can't do for several reasons) or to use the user's email address along with the API token. The following is my attempt to authorize to the API with the Authorization header:
#id = params[:id]
#comment_body = params[:comment]
uri = URI.parse "https://{subdomain}.zendesk.com/api/v2/requests/#{#id}.json"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Put.new(uri.request_uri)
req.body = '{"request": {"comment":{"value":' + "\"#{#comment_body}\"" + '}}}'
req['Content-Type'] = 'application/json'
#The following two lines do not work!
credentials = Base64::encode64("{user_email}/token:{api_token}")
request.headers['Authorization'] = "Basic #{credentials}"
response = http.request(req)
The API specifies that the format for authentication using the API token is {user_email}/token:{api_token}. I encoded that format with Base64::encode64 and passed it to the Authorization Header preceded with Basic, but the response is a 401 Unauthorized. However, replacing those two lines with req.basic_auth {user_email}, {user_password} works fine.
So my question is, how can I authenticate as a different user using the email and the given API token as authentication instead of supplying the user's email and password to req.basic_auth?
The googling I've done on the topic has revealed very little; apparently it's a lot more common to use the normal {username}:{password} format when dealing with the Authorization header than an API token.
Thanks in advance!!
Update: Weirdly, trying to authenticate as the end-user with req['Authorization'] = "Basic #{credentials}" does not return a 401 Unauthorized Error or a WWW-Authenticate header while trying to authorize as request.headers['Authorize'] = "Basic #{credentials}" does.
Finally figured it out after much head-banging and nearly throwing my laptop out the window. Suddenly, the answer seems incredibly obvious.
When using Net::HTTP, its basic_auth method can also accept tokens depending on the API, and the Zendesk API specifies that the format for using the API token is {email}/token:{token}. Basic authentication uses the format {username}:{password}, where the two fields are separated by a colon, meaning in Zendesk's case I can place {email}/token as the first argument and {token} as the second argument (instead of the username as the first argument and the password as the second argument), so the following code is correct:
req.basic_auth "{email}/token", "{api_token}"
I hope anyone who found this useful could leave a comment. Nice to know I spared someone from this frustration.

Twitter Application Only Auth

I'm trying to get an Application Only Auth token following the steps of this link:
https://dev.twitter.com/docs/auth/application-only-auth
I'm using Ruby on Rails and Rest Client to make the POST request needed and I'm setting the headers (I think) properly.
The step-by-step says:
URL encode the consumer key and the consumer secret according to RFC
1738. Note that at the time of writing, this will not actually change the consumer key and secret, but this step should still be performed
in case the format of those values changes in the future.
Concatenate the encoded consumer key, a colon character ":", and the
encoded consumer secret into a single string.
Base64 encode the string from the previous step.
And my code is:
require 'rest_client'
key = URI::encode('app_key')
secret = URI::encode('app_secret')
encoded = Base64.encode64("#{key}:#{secret}")
res = RestClient::Resource.new "https://api.twitter.com/oauth2/token/"
response = ''
options = {}
options['Authorization'] = "Basic #{encoded}"
options['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
res.post('grant_type=client_credentials', options) do |response, request, result|
response << "#{CGI::escapeHTML(response.inspect)}<br /><br />"
response << "#{CGI::escapeHTML(request.inspect)}<br /><br />"
response << "#{CGI::escapeHTML(result.inspect)}<br />"
end
render :text => txt
And I print out this:
"{\"errors\":[{\"label\":\"authenticity_token_error\",\"code\":99,\"message\":\"Unable to verify your credentials\"}]}"
#<RestClient::Request:0x9ece5d8 #method=:post, #headers={"Authorization"=>"Basic bXlfa2V5Om15X3NlY3JldA==\n", "Content-Type"=>"application/x-www-form-urlencoded;charset=UTF-8"}, #url="https://api.twitter.com/oauth2/token/", #cookies={}, #payload="", #user=nil, #password=nil, #timeout=nil, #open_timeout=nil, #block_response=nil, #raw_response=false, #verify_ssl=false, #ssl_client_cert=nil, #ssl_client_key=nil, #ssl_ca_file=nil, #tf=nil, #max_redirects=10, #processed_headers={"Accept"=>"*/*; q=0.5, application/xml", "Accept-Encoding"=>"gzip, deflate", "Authorization"=>"Basic bXlfa2V5Om15X3NlY3JldA==\n", "Content-Type"=>"application/x-www-form-urlencoded;charset=UTF-8", "Content-Length"=>"29"}, #args={:method=>:post, :url=>"https://api.twitter.com/oauth2/token/", :payload=>"grant_type=client_credentials", :headers=>{"Authorization"=>"Basic bXlfa2V5Om15X3NlY3JldA==\n", "Content-Type"=>"application/x-www-form-urlencoded;charset=UTF-8"}}>
#<Net::HTTPForbidden 403 Forbidden readbody=true>
My key and secret are valid.
Am I missing something?
Thanks!
EDIT:
Updating with the solution I've found.
The problem was on the Base64 convertion and string encoding.
I had to add a forced encoding parameter to the key+secret combination, for UTF-8 convertion:
encoded = Base64.encode64("#{key}:#{secret}".force_encoding('UTF-8'))
The Rails Base64.encode64 inserts a line break every 60 encoded characters.
The workaround was:
For Ruby 1.9+ (strict_ was included in Ruby 1.9)
Base64.strict_encode64(string)
For Ruby 1.9-
Base64.encode64(string).gsub('/\n/') # To remove the line break
Are you trying to implement Authorization with Tweeter (as OAuth Provider). Instead of writing it from the scratch following the API documentation, I would suggest to use OmniAuth. The setup & boilerplate code is fairly easy to use.
Read more about it at http://www.omniauth.org/ & https://github.com/intridea/omniauth/wiki
Let us know, if that helped you or not.

Resources