I'm using a pretty standard combo Flask + React to develop a web app. Recently I needed to implement a Google SSO auth - which I did. The auth flow is pretty standard - from the frontend app (React), I'm calling the /sso/login endpoint, which initializes the OAuth2 client, generates the authorization URL and redirects the call to said URL. After logging in to the Google, the user gets redirected to /sso/auth, which gets the auth tokens, and in this endpoint (this is important) I'm retrieving the user details, which I save to the session (using Flask-Session and Flask-Login), and then redirect the user again to the URL they originally started the login process from. The final redirect is based on the state parameter passed to the /sso/login endpoint. So the flow is kind of like this:
/app/home/login > /api/sso/login?state=/app/home > accounts.google.com/auth > /api/sso/auth > /app/home
Now, the flow works just fine when calling the API directly, the final redirect is then pointed to the /api/user/info, which correctly shows the details of the currently logged in user. The flow works as well when calling the API from the frontend app when both services run bare-bones from the local machine (so, for the API - simple flask run from the terminal, for the app - yarn start). The problem starts when I want to run the setup in docker containers - that's the setup used in deployment, running on AWS infrastructure. After running two containers (not using docker-compose) every call to the API from the frontend app gets a new session ID, and in result - new session. This is obviously a problem, since the session is crucial for correctly authenticating the user. The outcome is that after clicking the "Log in" button, getting redirected to the Google SSO, the user gets a 401 from the api/sso/auth endpoint, because it fails the state sanity check:
class SSOLogin(Resource):
#staticmethod
def get():
req_state = request.args.get("state"):
session[AUTH_STATE_KEY] = state # first call - the endpoint saves the state to the session
session.permanent = True
return redirect(uri)
...
class SSOAuth(Resource):
#staticmethod
def get():
req_state = request.args.get("state")
session_state = session.get(AUTH_STATE_KEY) # to check if the state is the same as the one returned from Google SSO
if req_state != session_state: # False every time, because the session ID changes between the calls and the session_state is always None
return {
"error": "Invalid state parameter"
}, 401
...
I looked for the answer, of course, but nothing helped. The session is file-based, but I tried using redis with the exact same result. I tried configuring the session config in many ways, tried configuring the CORS extension as well - nothing helps. The session secret key is passed through the environment variable, not randomly generated. I can try using docker-compose, but it's not preferred, since it won't be used on the deployment environment. I'm baffled, guys.
Related
I'm a newbie with authentication! I'm building a web app where users can log in, and the data shown in the web app is different for each user. I'm using Reactjs as my frontend with Firebase authentication. After a user logs into my web app, I'm storing their user ID (UID) and other information into Firestore. I have a collection usersCollection where each document is labelled with the UID. For the backend, I'm using Flask as mostly a REST API with a Postgres database, but I am not storing user credentials there (UID, password, etc.).
For some of my backend functions I need to change the output based on which user is signed in, but I'm not sure how to retrieve the current user's UID. I'm able to make an axios request to send the current user's UID from the frontend to the backend, so I've tried 2 methods with that:
Saving the axios request output as a global variable - this has led to Flask errors like runtimeerror: working outside of application context. and I don't think this is the best solution.
With each GET request that the frontend is making to the backend (every time there's a function whose output changes based on user), I am passing the UID as a parameter, which causes latency problems.
What is the simplest way for me to request the current UID from Firestore from the backend?
Is structuring our frontend, backend, database, and authentication like this recommended? Or is there a simpler way or better system for our situation (JWT?)? We selected Firebase authentication in the first place because we are using a React MUI template that already set up Firebase for us.
Thank you in advance! Happy to provide more information if needed!
I don't know reactjs, but I have the same setup with flutter (iOS / Android apps).
What I did and what worked out well is:
authenticate your client against firebase (which it looks you already achieved)
extract the idToken from the firebase response
send the idToken to your flask backend, which verifies the id token (see below)
in flask backend, log in the user with login_user() from flask_login. This creates a cookie session which is sent back to the client in the response headers
the reactjs client stores the cookie and needs to attach it to every subsequent API request to flask (this might come out of the box for reactjs, but for flutter I needed some custom code for that)
As for the token validation you can…
use the python sdk
use a jwt library such as pyjwt, see documentation
There is flask-firebase which does a good job for the token validation. I wrote a blog post which gives an example how you would use this.
I was trying set up google authentication with react frontend and django rest framework backend. I set up both the frontend and backend using this two part tutorial, PART1 & PART2. When I try to login with google in the frontend I get POST http://127.0.0.1:8000/google-login/ 400 (Bad Request) I think it's because my google api needs an access token and an authorization code to be passed. After debugging the react js, I noticed the response I get from google doesn't have an authorization code. I suspect because responseType is permission(by default), Source:React login props , instead of code. I was wondering how would you change the response type in react? (I'm not even sure if this alone is the issue)
Here's my backend code
In my views.py file
class GoogleLogin(SocialLoginView):
adapter_class = GoogleOAuth2Adapter
callback_url = "http://localhost:3000"
client_class = OAuth2Client
in my urls.py
path('google-login/', GoogleLogin.as_view(), name='google-login'),
for my front end
/Components/login.js
const googleLogin = async (accesstoken,code) => {
console.log(accesstoken)
let res = await cacaDB.post(
`google-login/`,
{
access_token: accesstoken,
code: code
}
);
console.log(res);
return await res.status;
};
const responseGoogle = (response) => {
console.log(response.code);
googleLogin(response.accessToken, response.code);
}
return(
<div className="App">
<h1>LOGIN WITH GOOGLE</h1>
<GoogleLogin
clientId="client_id"
buttonText="LOGIN WITH GOOGLE"
onSuccess={responseGoogle}
onFailure={responseGoogle}
/>
</div>
)
I want to save the user in the database and have them stay logged in, in the front end.
This Post explains the login flow behind the scene. Here's Login flow image I'm basically stuck on returning code and accesstoken(I can return this successfully) step.
Here's my list of questions,
How do I return code from google?
I have knox token set up, can I
use it instead of the JWT tokens?
Does the class GoogleLogin(SocialLoginView), take care of the steps of validating the access token and code with google and creating the user with that email in database?
Would really appreciate your inputs.
After investigating a bit on my end, I think I might have a solution that works for you.
I've messed with OAuth before, and it's quite tricky sometimes because it has to be robust. So a bunch of security policies usually get in the way.
I'll provide my full step-by-step, since I was able to get it working, trying my best to match what you posted.
Firstly, to have a clean slate, I went off the example code linked in the tutorials. I cloned and built the project, and did the following:
Creating a new project on GCP
Configured the OAuth consent screen
I set the User type to "internal". This options may not be available if you're not using an account under GSuite (which I am). "External" should be fine though, just that "internal" is the easiest to test.
Created a OAuth 2.0 Client
Added http://localhost:3000 to the "Authorized JavaScript origins" and "Authorized redirect URIs" sections
Register a Django superuser
Registered a Site, with value of localhost:8000 for both fields.
Went into the admin panel, and added a Social Application with Client ID and Secret Key as the "Client ID" and "Client Secret" from GCP, respectively. I also picked the localhost site that we added earlier and added it to the right hand box. (I left Key blank)
Example of my Application Page
Filled in the clientId field in App.js, in the params of the GoogleLogin component.
Here's where I ran into a bit of trouble, but this is good news as I was able to reproduce your error! Looking at the request in the network inspector, I see that for me, no body was passed, which is clearly the direct cause of the error. But looking at App#responseGoogle(response), it clearly should pass a token of some sort, because we see the line googleLogin(response.accessToken).
So what is happening is that accounts.google.com is NOT returning a proper response, so something is happening on their end, and we get an invalid response, but we fail silently because javascript is javascript.
After examining the response that Google gave back, I found this related SO post that allowed me to fix the issue, and interestingly, the solution to it was quite simple: Clear your cache. I'll be honest, I'm not exactly sure why this works, but I suspect it has something to do with the fact that development is on your local machine (localhost/127.0.0.1 difference, perhaps?).
You can also try to access your site via incognito mode, or another browser, which also worked for me.
I have knox token set up, can I use it instead of the JWT tokens?
I don't think I have enough knowledge to properly answer this, but my preliminary research suggests no. AFAIK, you should just store the token that Google gives you, as the token itself is what you'll use to authenticate. It seems that Knox replaces Django's TokenAuthentication, which means that Knox is in charge of generating the token. If you're offloading the login work to Google, I don't see how you could leverage something like Knox. However, I could be very wrong.
Does the class GoogleLogin(SocialLoginView), take care of the steps of validating the access token and code with google and creating the user with that email in database?
I believe so. After successfully authenticating with Google (and it calls the backend endpoint correctly), it seems to create a "Social Account" model. An example of what it created for me is below. It retrieved all this information (like my name) from Google.
Example of my "Social Accounts" page
As for how to retrieve the login from the browser's local storage, I have no idea. I see no evidence of a cookie, so it must be storing it somewhere else, or you might have to set that up yourself (with React Providers, Services, or even Redux.
I set up a Connected App, a Python application to programmatically access Salesforce objects on behalf of a user (offline access).
The app works and I can generate an access_token:
$ curl https://login.salesforce.com/services/oauth2/token -d "grant_type=password" -d "client_id=MY_APP_CLIENT_ID" -d "client_secret=MY_APP_SECRET" -d "username=my#user.com" -d "password=my_password"
{"access_token":"00D09000000KDIX!AQoAQNi1234","instance_url":"https://my_instance.salesforce.com","id":"https://login.salesforce.com/id/12345/12345","token_type":"Bearer","issued_at":"1606401330889","signature":"abc/def"}
So far so good.
Now I wanted to switch to a web-server-based flow that uses refresh tokens, but I'm stumped. Where do I get the initial refresh_token to send alongside grant_type=refresh_token? The docs seem to assume I already have a refresh_token and just want to generate another access_token based off that, which is not the case.
What are the actual steps and necessary calls, end-to-end?
List of docs that I found and read, but made me no wiser:
https://help.salesforce.com/articleView?id=remoteaccess_oauth_tokens_scopes.htm&type=5
https://help.salesforce.com/articleView?id=remoteaccess_oauth_refresh_token_flow.htm&type=5
https://developer.salesforce.com/forums/?id=906F0000000AgInIAK
https://help.salesforce.com/articleView?id=connected_app_create_api_integration.htm&type=5
Here's the Salesforce documentation on the Web Server OAuth flow. It runs like this; note that user interaction is involved, so curl by itself won't be enough to demonstrate the flow clearly.
You direct the user to the Salesforce login UI, in their web browser, to get yourself an an authorization code:
https://login.salesforce.com/services/oauth2/authorize?client_id=<YOUR CONNECTED APP CLIENT ID>&redirect_uri=<CALLBACK URL ON YOUR SERVER>&response_type=code
The user interacts with the authorization page to approve your application.
The user is then redirected to the callback URL in your application that you provided in the call (note: this also has to be set up as a callback in your Connected App definition), e.g.,
https://YOUR_SERVER.com/oauth2/callback?code=<AUTHORIZATION CODE>
Your app can present UI here if you want but the point is to ingest the authorization code.
The callback URL can be on localhost. That's how, for example, the Salesforce CLI implements authorization of orgs; it spins up a local web server to receive the callback.
At this point, the user interaction is done. Your application makes a POST request to Salesforce's /services/oauth2/token endpoint to exchange the authorization code you received for an access token.
If your Connected App is set up with the refresh_token scope, you'll also get back at that time a refresh token that you can store and use to obtain new access tokens in the future, using the refresh token flow you already identified.
For a headless application, it can be easier to go straight to JWT (if that's your ultimate goal). I have an example of how to pair JWT authentication with the simple_salesforce Python library. It takes a little bit of initial setup to populate the certificate on the Connected App and assign Preapproved Profiles (or better, Permission Sets), but once the setup is done it's very smooth and never requires any user interaction.
My context:
An AngularJS application using the Javascript Facebook SDK, and my distinct server (REST APIs).
Workflow:
User is logged in the client through the FB SDK using the method FB.login(callback).
This later gives a short-lived token that is then sent to the server in order to transform it to a long-lived token.
I'm interested in the mechanism of refreshing the long-lived token after 60 days.
So, reading the doc, we found this:
Even the long-lived access token will eventually expire. At any point,
you can generate a new long-lived token by sending the person back to
the login flow used by your web app - note that the person will not
actually need to login again, they have already authorized your app,
so they will immediately redirect back to your app from the login flow
with a refreshed token - how this appears to the person will vary
based on the type of login flow that you are using, for example if you
are using the JavaScript SDK, this will take place in the background,
if you are using a server-side flow, the browser will quickly redirect
to the Login Dialog and then automatically and immediately back to
your app again.
If I interpret it well, when user is ALREADY logged in through FB.login(callback), a simple redirect to the Angular Application's login flow would allow to get a new short-lived token.
I imagine that the FB.login is immediately run anew in this case, without user interaction, as written.
I want to test it simply, so what I've done is:
Logged in into the application through FB.login(callback).
Clicked on a dummy link making a simple redirect with: window.location.replace('/');
My application being a single page application, every URL should be considered as the authentication page.
But the FB.login isn't run in the background, as I expected from the doc.
What would be the reason?
Does it work only when the domain making the redirect is distinct from the client? (I just can't test this case right now)
Did I misinterpret the doc?
I'm using a very simple GAE instance from a Greasemonkey script. This worked fine for the last months, but now a path is appended to the final 'continue' location, which breaks the login process for me.
The basic workflow, under the assumption that the user is logged into his Google Account, but his token for the GAE instance has timed out:
User opens page A with the GM script installed.
The GM script runs and tries to access the GAE instance with a GM_xmlhttpRequest().
The GAE instance returns "login_needed|<loginurl>". The GM script extracts the loginurl and sets window.location on it.
The user is redirected to the loginurl and eventually back to A. However, this time, actual data is returned by the GM_xmlhttpRequest().
The last step no longer works, as the user is now redirected to the loginurl plus some, which gives a 404 on the target site.
The GAE code is just about half a screen of code. The authentication relevant code is this:
if not users.get_current_user():
self.response.headers['Content-Type'] = 'text/plain'
self.response.out.write('login_needed|'+users.create_login_url(self.request.get('uri')))
The sequence of requests is as follows, all caused by redirects:
GET https://mygaeinstance.appspot.com/?uri=https://targetsite.com/
GET https://www.google.com/accounts/ServiceLogin?service=ah&passive=true&continue=https://appengine.google.com/_ah/conflogin%3Fcontinue%3Dhttps://targetsite.com/<mpl=gm&ahname=MyGAEInstance&sig=<some sig>
GET https://appengine.google.com/_ah/conflogin?continue=https%3A%2F%2Ftargetsite.com%2F&pli=1&auth=<some base64 auth token>
GET https://targetsite.com/_ah/conflogin?state=<some base64 state>
targetsite.com doesn't like that path and as you can see, it wasn't in the initial 'continue' argument passed to appengine.google.com, which was just "https://targetsite.com/". What did I do wrong and how can I fix this?
A recent change to our login flow for App Engine has created an issue whereby a login with a continue URL that's outside the app's own domain will result in an erroneous redirect such as the one you're observing.
We're working on fixing this. In the meantime, a workaround is to set up a redirect handler on your own app. Make that the target of the continue parameter, and have it send a final redirect to your actual target.
This redirect is caused by an expired auth token. To make it work again, you need to invalidate the token on the client, as described here: What is the proper URL to get an Auth Cookie from a GAE based Application