Introduction
In this post we'll explore how to implement Google OAuth2 with React, Flask, and JWT to create a secure and seamless authentication system for web applications. OAuth2 is a widely used authorization protocol that allows users to grant access to their data to third-party applications, while JWT (JSON Web Tokens) is a popular method for securely transmitting authorization data. Together, OAuth2 and JWT provide a robust solution for both authentication and authorization in web applications.
Authentication vs Authorization
Authentication verifies user identity, while authorization grants access based on permissions. In our system, we will use Google OAuth2
for authentication and JWT
for authorization.
What is Google OAuth2?
Google OAuth2 is an authentication and authorization protocol that allows third-party applications to access user data on Google services securely without sharing usernames and passwords.
What is JWT?
JWT
(JSON Web Token) encodes information about a user's identity and permissions. Typically, when a user logs in, a JWT
token is generated and signed (with a secret key) by the server and returned to the user for further use. Then the token can be used to protect specific routes in an API server, as the client includes the JWT in their requests. The server then decodes the JWT to determine the sender's identity and permissions.
Authentication flow
- The client sends an authentication request to the OAuth2 server (Google OAuth2).
- An access token is returned to the client.
- The Access token is then sent from the client to the API server (Flask).
- API server verifies the token with the OAuth2 server, to confirm the client is authorized to consume that resource.
- OAuth2 server responds with user credentials (name, email, etc).
- API server encrypts a JWT using user credentials and sends it to the client via cookies for security reasons.
- From now on, any request the client sends to the API server will include the JWT cookie.
Setup
To set up Google OAuth2 with React, Flask, and JWT, follow these steps:
Create OAuth client credentials in the Google Developer Console. Go to the console, navigate to credentials, and create a new OAuth client ID for a web application. Add
http://localhost
andhttp://localhost:<port_number>
(in our casehttp://localhost:3000
) to the Authorized JavaScript origins box for local tests or development. Click Save.Clone the repository for our demo project from https://github.com/eli-entelis/Flask-React-Google-OAuth2-Demo.git.
-
Set up Flask by opening a command prompt or terminal in the directory that contains the cloned repository and running the following commands:
cd Flask pip install -r requirements.txt
-
Create a .env file in the Flask directory to configure the
GOOGLE_CLIENT_ID
andGOOGLE_SECRET_KEY
environment variables. You can obtain these values from the Google Developer Console. Click on the credential you just created and on the right side, you will find theClient ID
andClient secret
. Add the following lines to the .env file:
#.env GOOGLE_CLIENT_ID=<insert here client id> GOOGLE_SECRET_KEY=<insert here secret key>
-
To execute the API server, run the following command:
python .\app.py
Go to http://127.0.0.1:5000/ in your web browser, and if
everything works correctly, you should see a "Hello,
World!" response. -
To Set up React, go to
react-client/src/App.js
and insert the GoogleClient ID
to the<GoogleOAuthProvider>
componet:
<GoogleOAuthProvider clientId="<insert here client id>">
This will be explained later.
-
Open a command prompt or terminal in the directory that contains the cloned repository and run the following commands:
cd react-client npm install npm start
This will install the required dependencies and start the React development server. You can access the React application in your browser at http://127.0.0.1:3000/. If everything is set up correctly, you should see this result.
Implementation details
Starting with the client-side code in the getUserInfo
function under the <Auth>
component in src/auth/auth.js
:
async function getUserInfo(codeResponse) {
var response = await fetch("/google_login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ code: codeResponse.code }),
});
return await response.json();
}
This function is an asynchronous function that takes a codeResponse object as an argument, which is the response object returned from the successful Google login flow. It sends a POST request to our API server endpoint (http://127.0.0.1:5000/google_login) with the code property from the codeResponse object as the request body in JSON format. The server response is then parsed as JSON and returned, containing the user information.
In addition to the user information, the function also sets the JWT token as a cookie. I won't dive into the security issues of different storage solution for JWT
, such as cookies, local Storage, session Storage etc. For more information read here.
We configured
"proxy": "http://localhost:5000"
inpackage.json
. this is why we are fetching/google_login
and nothttp://127.0.0.1:5000/google_login
Now let's take a look at the <Auth>
component itself.
export default function Auth() {
const [loggedIn, setLoggedIn] = useState(false);
const [user, setUser] = useState({});
const googleLogin = useGoogleLogin({
flow: "auth-code",
onSuccess: async (codeResponse) => {
var loginDetails = await getUserInfo(codeResponse);
setLoggedIn(true);
setUser(loginDetails.user);
},
});
const handleLogout = () => {
setLoggedIn(false);
setUser(false);
};
return (
<>
{!loggedIn ? (
<IconButton
color="primary"
aria-label="add to shopping cart"
onClick={() => googleLogin()}
>
<GoogleIcon fontSize="large" />
</IconButton>
) : (
<UserAvatar userName={user.name} onClick={handleLogout}></UserAvatar>
)}
</>
);
}
This is the main component for handling authentication. It uses the useGoogleLogin
hook from the @react-oauth/google
library to handle the Google login flow.
The onSuccess
callback is triggered when the login is successful, and it receives a codeResponse
object as an argument, which is passed to the getUserInfo
. Once the user information is received, the loggedIn
state is set to true and the user
state is updated with the user information.
To use the @react-oauth/google
we need to wrap our component with <GoogleOAuthProvider>
and pass it the same Client ID
we used in the previous steps.
❗ Without this step the client wouldn't be able to authenticate. You can see an example at src/App.js
<GoogleOAuthProvider clientId="<insert here client id>">
<Auth></Auth>
</GoogleOAuthProvider>
Lastly, let's take a look at the Flask code.
@app.route('/google_login', methods=['POST'])
def login():
auth_code = request.get_json()['code']
data = {
'code': auth_code,
'client_id': GOOGLE_CLIENT_ID, # client ID from the credential at google developer console
'client_secret': GOOGLE_SECRET_KEY, # client secret from the credential at google developer console
'redirect_uri': 'postmessage',
'grant_type': 'authorization_code'
}
response = requests.post('https://oauth2.googleapis.com/token', data=data).json()
headers = {
'Authorization': f'Bearer {response["access_token"]}'
}
user_info = requests.get('https://www.googleapis.com/oauth2/v3/userinfo', headers=headers).json()
"""
check here if user exists in database, if not, add him
"""
jwt_token = create_access_token(identity=user_info['email']) # create jwt token
response = jsonify(user=user_info)
response.set_cookie('access_token_cookie', value=jwt_token, secure=True)
return response, 200
This Flask API endpoint handles a client request after a successful Google login using OAuth2.0. It receives an authorization code, sends it to Google for token exchange, retrieves user information, creates a JWT
token with the user's email, sets the JWT
token as a cookie
in the response, and returns the user information as JSON in the response body.
Authorization
From now on, you can protect any Flask route with the @jwt_required
decorator and get the decoded JWT with the get_jwt_identity
function. Both are imported from flask_jwt_extended
library.
Here is how:
# Protect a route with jwt_required, which will kick out requests
# without a valid JWT present.
@app.route("/protected", methods=["GET"])
@jwt_required()
def protected():
# Access the identity of the current user with get_jwt_identity
jwt_token = request.cookies.get('access_token_cookie') # Demonstration how to get the cookie
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
ℹ️ INFO |
---|
It's important to set the cookie name to access_token_cookie , by default this is the name that flask_jwt_extended will look for. to override this : app.config['JWT_ACCESS_COOKIE_NAME'] = 'jwt' # Specify the custom cookie name
|
Set app.config['JWT_TOKEN_LOCATION'] = ['cookies'] to make flask_jwt_extended search the JWT in the cookies instead of in the authorization headers which is the default. |
This is how a request to the @/protected
route from our React client looks like:
async function getProtected() {
var response = await fetch("/protected", {
method: "GET",
credentials: "include",
mode: "cors",
headers: {
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((msg) => console.log(msg));
}
Take note that we set credentials: "include"
which allows the browser to include the access_token_cookie
cookie in the request.
Resources
- Github's repository of the Demo.
- react-oauth/google npm package used for google authentication on React's side.
- JWT documentation
✌️ Contact me on:
Linkdin: Eli Entelis
Github: eli-entelis
Email: elientelis1999@gmail.com
Top comments (1)
could you do this without the storing the client secret? where is the client secret used in the code?