google.auth.exceptions.RefreshError: Invalid Client - python-3.x

I am working on a project to let a client authorize their google ads account, and then use those authorized credentials to download data on their behalf. I have a webapp that successfully Authorizes the app to do things on the clients behalf. This generates an access code that I then trade for two credentials, an access token and a refresh token. This refresh token then gets passed to a database, where a separate app attempts to query the googleAds API.
It is my understanding that the Google Oauth engine only needs the refresh token.
I am trying to authorize by use of load_from_dict() or load_from_env() methods of the GoogleAdsClient class. Both yield the same error: google.auth.exceptions.RefreshError: ('invalid_client: Unauthorized', {'error': 'invalid_client', 'error_description': 'Unauthorized'})
I have verified the developer_token, client_id, and client_secret are all accurate to what is in the API console. I have also verified the refresh_token is being passed correctly to the credential dict.
I am really at a loss on where to go from here. I have read many stack overflow threads with similar titles, and yet I am still stuck at the same place.
Here are some relevant links.
Google Ads API configuration
Google Identity and Server side web apps
Google's example of an API call
Relevant code
class GoogleAds:
def __init__(self):
self.scope = ['https://www.googleapis.com/auth/adwords']
self.client_id = os.getenv('GOOGLE_ADS_CLIENT_ID')
self.client_secret = os.getenv('GOOGLE_ADS_CLIENT_SECRET')
self.developer_token = os.getenv('GOOGLE_ADS_DEVELOPER_TOKEN')
self.refresh_token = os.getenv('GOOGLE_ADS_REFRESH_TOKEN')
def authorize(self):
credentials = {
"developer_token": self.developer_token,
"refresh_token": self.refresh_token,
"client_id": self.client_id,
"client_secret": self.client_secret,
"use_proto_plus":"True",
"grant_type": "refresh_token",
}
print(credentials)
googleads_client = GoogleAdsClient.load_from_dict(credentials)
service = googleads_client.get_service("GoogleAdsService")
request = googleads_client.get_type("SearchGoogleAdsRequest")
return service, request

'error': 'invalid_client', 'error_description': 'Unauthorized' Can be a very hard error to debug. It can mean a number of things.
Currently it Implies to me that the user has not authorized this client.
First ensure that the refresh token has not expired. Second ensure that the client id and client secrete used to create the refresh token are the same one that you are using to request a new access token.
oauth2#expiration

I ended up refreshing the Client_Secret in the google API client and that seemed to have gotten me through.
Q: It is outside the scope of this question, but is it possible to get that value from the authorization step?
A: You can get the customer IDs you have access to with the client.get_service("CustomerService") method. There is also a way to get account hierarchy. I will probably be using (Frankensteining) that to move forward

Related

Cannot make Azure B2C refersh token become invalid

When a user logs out of Azure B2C using the MSAL library on a mobile device this only clears the local cache. The remote session on the server still exists which means any existing refresh tokens could still be used.
From searching I know that the Microsoft Graph API can be used to revoke the current user's sign in session, and therefore invalidate all current refresh tokens. I believe I am doing this, but the refresh tokens keep remaining active.
Here is my flow:
I get a token for user A (I tried this with auth code flow and ROPC but I don't believe that should make a differnce).
I confirmed that I can get a new access token by using the current refresh token that is returned in a Postman call -
{{b2c_login_url}}/B2C_1_ROPC_SignIn/oauth2/v2.0/token?grant_type=refresh_token&client_id={{b2c_ropc_client_id}}&refresh_token=xxxxx&scope={{b2c_scopes}}&redirect_uri={{b2c_api_redirect_uri}}
This returns a new access token as expected.
I then take the azure userId value ("oid" property in the access token) and pass that through to my API that then runs the following code.
var graphClient = GetGraphClient();
var result = await graphClient.Users["{" + userId + "}"]
.RevokeSignInSessions()
.Request()
.PostAsync();
return result.GetValueOrDefault();
I can see that the result of this expression is true. I can also go onto the Azure B2C user details and see that "StsRefreshTokensValidFrom" has been updated to the current date time as expected.
Now, I run the exact same http request I ran previously using the refresh token to get another access token, but this time, it should fail. However, I continue to get new access tokens.
The strange thing is that I am sure I tested this previously, tried to get a new token, and it failed as I'd expect. But now it will always return me new tokens.
I feel I am missing something here. Any advice?
I tried to reproduce the same in my environment and got below results:
I generated token for a B2C user using ROPC flow via Postman with parameters as below:
POST https://<tenant_name>.b2clogin.com/<tenant_name>.onmicrosoft.com/<policy>/oauth2/v2.0/token
client_id : xxxxx-xxxx-xxxx-xxxxxxxx
grant_type : password
scope : https://<tenant_name>.onmicrosoft.com/web_api/api.read offline_access
username : b2c_username
password : password
Response:
Using the above refresh token, I'm able to generate access token successfully like below:
POST https://<tenant_name>.b2clogin.com/<tenant_name>.onmicrosoft.com/<policy>/oauth2/v2.0/token
client_id : xxxxx-xxxx-xxxx-xxxxxxxx
grant_type:refresh_token
scope:https://<tenant_name>.onmicrosoft.com/web_api/api.read
redirect_uri:https://jwt.ms
refresh_token:paste_refresh_token
Response:
To revoke refresh tokens, I ran below query via Graph Explorer like this:
POST https://graph.microsoft.com/beta/users/<user_id>/invalidateAllRefreshTokens
Response:
Code Sample in C#:
GraphServiceClient graphClient = new GraphServiceClient( authProvider );
await graphClient.Users["userid"]
.InvalidateAllRefreshTokens()
.Request()
.PostAsync();
To confirm that, I checked user's details in Portal like below:
When I tried to get access token with same refresh token, I got error saying token is revoked like below:
After revoking the tokens from Graph API, it may take up to 5 minutes to work.
If you run the query for access token as soon as you revoked the refresh tokens, you may still get access token.
So, wait for 5-10 minutes and try to get the access token with same query. Then, you won't be getting access token as the refresh token will be revoked at that time.

Can an open id connect id token be used to authenticate to an api

I am building a mern application.
the backend built using express exposes an api which users can create data and access the data they have created.
I want to allow users to sign in with google and get authorization to create and access the resources on this api which i control (not on google apis).
I keep coming across oauth 2 / open id connect articles stating that an Id token is for use by a client and a access token provided by a resource server should be used to get access to an api.
e.g. https://auth0.com/blog/why-should-use-accesstokens-to-secure-an-api/
the reason stated for this is that the aud property on the id token wont be correct if used on the api.
I realise that some sources say: that if the spa and api are served from same server and have same client id and therefore audience I can use and id token to authenticate to the api, but I am looking to understand what I can do when this is not the case?
I feel using oauth2 for authorization is overkill for my app and I cant find any information about how to use open id connect to authenticate to my api.
Surely when you sign in to Auth0 authourization server using google it is just requesting an open id connect id token from google?
I am wondering if using Authorization Code Grant flow to receive an id token on the api server would allow me to authenticate a user to my api?
in this case would the api server be the client as far as open id connect is concerned and therefore the aud value would be correct?
I can generate an url to visit the google oauth server using the node googleapis library like so:
const { google } = require("googleapis");
const oauth2Client = new google.auth.OAuth2(
'clientid','clientsecret',
"http://localhost:3000/oauthcallback",//this is where the react app is served from
);
const calendar = google.calendar({ version: "v3", auth: oauth2Client });
const scopes = ["openid"];
const url = oauth2Client.generateAuthUrl({
// 'online' (default) or 'offline' (gets refresh_token)
access_type: "offline",
// If you only need one scope you can pass it as a string
scope: scopes,
});
async function getUrl(req, res) {
console.log(url)
res.status(200).json({
url,
});
}
and use the following flow.
You are not supposed to access any API's using the ID-Token. First of all the life-time of the ID-token is very short, typically like 5 minutes.
You should always use the access-token to access API's and you can using the refresh token get new access-tokens. The ID-token you can only get one time and you use that to create the local user and local cookie session.
If you are using a SPA application, you should also consider using the BFF pattern, to avoid using any tokens in the SPA-Application
see The BFF Pattern (Backend for Frontend): An Introduction
I agree with one of the commenters that you should follow the principle of separation of concern and keep the authorization server as a separate service. Otherwise it will be a pin to debug and troubleshoot when it does not work.

passing properties in callback function nodejs

I'm using paypal-rest-sdk. Problem I'm facing is, when I'm making an authorizationUrl call, I want to pass some parameters which can be accessed in the redirected URL.
Below is my code
import paypal from 'paypal-rest-sdk';
const openIdConnect = paypal.openIdConnect;
paypal.configure({
mode: "sandbox"
client_id: //MyClientId,
client_secret: //MySecretId,
openid_redirect_uri: `http://myRedirectionEndpoint/account/domestic/paypal/callback?state={accountId:5e8c2291d69ed1407ec86221}`
});
openIdConnect.authorizeUrl({scope: 'openid profile'});
Adding query parameter state gives the error as invalid redirectUri
What is the best way to pass the data that needs to be used after redirection
I think you are slightly misunderstanding how oauth authorization works. Basically if you want to get any data you need to do this AFTER you consume the callback and validate the user in your system as well.
Have you ever seen for Google/github etc openid auth provider returning some data that corresponds to the caller system's data? It's not possible.
You are probably confusing this with webhook where the caller system calls a webhook with some data internally and you capture it. Which is commonly used in payment transactions.
But the auth is slightly different. For auth there are 3 systems.
the actual auth provider (Paypal/google/github) etc.
an Identity provider which basically gets profile data etc and other than for enterprise systems these two systems are simply same.
the caller system which is your NodeJS service in this case.
=> Now caller-system calls the auth provider to get some kind of code generally an auth code. This means the user exists in auth system let's say Google.
=> Then the caller-system calls the identity provider with that auth code checking if the user is there in identity provider(idp) as well and the idp returns access_token, id_token, refresh_token etc (as I said most of the time these are same systems). But consider amazon, let's say you want to login to Amazon with your Google account. You have a Google account alright but you don't have amazon account. So you will get the auth code but will not get the id_token.
=> Now the id_token most of the time contains some basic info of the user in JWT format. But Now the ACCESS_TOKEN is used to do all the other calls to your system(caller system). Now as I said id_token some kind of user data. You can have a db table mapping userid with account number in your NodeJs service.
=> Make an endpoint to get the account number or something which takes access_token and id_token. First validate the access_token and verify the signature of the id_token then decrypt the token to get basic user info. and use that id to fetch the data from your table and use that data.
After Edit:
You can see in the doc:
paypal.configure({
'openid_client_id': 'CLIENT_ID',
'openid_client_secret': 'CLIENT_SECRET',
'openid_redirect_uri': 'http://example.com' });
// Authorize url
paypal.openIdConnect.authorizeUrl({'scope': 'openid profile'});
// Get tokeninfo with Authorize code
paypal.openIdConnect.tokeninfo.create("Replace with authorize code", function(error, tokeninfo){
console.log(tokeninfo);
});
// Get userinfo with Access code
paypal.openIdConnect.userinfo.get("Replace with access_code", function(error, userinfo){
console.log(userinfo);
});
When you get the auth code, you use it to call the paypal.openIdConnect.tokeninfo.create and get the tokens. Then use those tokens to call the paypal.openIdConnect.userinfo.get to get the user Info. Now when you get the userinfo you will be able to create the db row that you wanted to create.
You can add those two below calls in your /callback route.

JWT Token Storage

I have been going through some of my .NET Core2 services and adding some JWT authentication to them to provide some basic security.
I created a new ProvisioningService which has an endpoint that builds a token and returns it:
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
I altered one of my existing services (which I'll refer to as TestService) by adding AddAuthentication in the StartUp. The endpoint for this call has the [HttpPost(), Authorize] attributes. I deployed these changes to my Test server.
When I call TestService/api/updateSomething I am returned a 401 Unauthorized as expected. On my local machine, I create a new token via ProvisioningService/api/buildToken and add the token from the response to my TestService call via the Authorization header. To my surprise...this worked.
Why does my TestService (on a completely different server) view a token created on my local machine as a valid token and allow the call to work? I was expecting this to return the same 401 because I assumed this token was going to be invalid on my Test server. My inexperience with JWT is probably showing....but I am not understanding how these tokens are being stored/shared between servers.
I failed to understand that the token itself has what it needs to authorize itself after it is decrypted. This question is no longer needed.

Token based authentication with flask-security extension

I am currently looking for a way to secure a REST API using token based authentication. I am developing the API in Python using Flask and have discovered the flask-security extension which seems to have a lot of interesting features.
One of the features mentioned in the documentation is Token Authentication.
According to the documentation:
Token based authentication is enabled by retrieving the user auth
token by performing an HTTP POST with the authentication details as
JSON data against the authentication endpoint. A successful call to
this endpoint will return the user’s ID and their authentication
token. This token can be used in subsequent requests to protected
resources.
I am however still a bit confused on how to implement this feature using flask-security.
Some online research has led me to using things such as #auth_token_required but I am having some trouble to put everything together. The flask-security documentation itself is not very helpful.
For example, how can a user get an authentication token? what is the authentication endpoints?
It would be great if you could lead me in the right direction. Code examples would be awesome too :-)
Endpoint is /login, you post your credentials as json request body:
{'email':'john#smit.com', 'password':'1234'}
However for this to work you need to disable the csrf tokens in your flask app (thanks Mandar Vaze):
app.config['WTF_CSRF_ENABLED'] = False
Then you do each request with the token in the HTTP headers:
Authentication-Token:WyI1NTE1MjhmNDMxY2Q3NTEwOTQxY2ZhYTgiLCI2Yjc4NTA4MzBlYzM0Y2NhZTdjZjIxNzlmZjhiNTA5ZSJd.B_bF8g.t1oUMxHr_fQfRUAF4aLpn2zjja0
Or as query string:
http://localhost:5000/protected?auth_token=WyI1NTE1MjhmNDMxY2Q3NTEwOTQxY2ZhYTgiLCI2Yjc4NTA4MzBlYzM0Y2NhZTdjZjIxNzlmZjhiNTA5ZSJd.B_bF8g.t1oUMxHr_fQfRUAF4aLpn2zjja0
Client example in python 3:
import requests
import json
#do the login
r = requests.post('http://localhost:5000/login',
data=json.dumps({'email':'john#smit.com', 'password':'1234'}),
headers={'content-type': 'application/json'})
response = r.json()
print(response) #check response
token = response['response']['user']['authentication_token'] #set token value
#Now you can do authorised calls
r = requests.get('http://localhost:5000/protected',
headers={'Authentication-Token': token})
print(r.text)
Angular example snippet to obtain the token:
$http.post('/login', {"email": $scope.formdata.login,"password":$scope.formdata.password}).
success(function(results) {
$window.sessionStorage.token = results.response.user.authentication_token;
});
Angular example snippet to visit protected pages:
if ($window.sessionStorage.getItem('token')) {
config.headers['Authentication-Token'] = $window.sessionStorage.getItem('token');
}
I found Flask-Security's token-based not a good candidate for my project. I recommend using JWT token instead.
The problems with Flask-Security's token based authentication.
Need to disable CSRF globally, this is not good when you also have a traditional web application in which CSRF token is desirable
No easy way to renew the token ( without submitting password again )
Can not control the payload of the token, there's no API to put/get data to/from the token
That token, by design, only works with one Flask app. So if your frontend app needs to talk with multiple restful apis, this wont work well
Check out JWT (pyjwt or flask-jwt) token, it solves all the above problems and more.
Authentication endpoint is /login
Look at the code of flask-security here specifically views.py: _render_json()
login() calls _render_json which in turn calls get_auth_token() - and returns the auth token.
Problem (for me) is to get this to work.
For me request.json seems empty (hence this does not work)
{"email": "test#example.com", "password": "test123"}
Hopefully this helps you move forward a little.

Resources