When designing the routing system for a web application that supports both native password authentication and OAuth social logins, you should consider the various steps involved in each authentication process. Here's a sample route setup:
Native Password Authentication:
POST /signup
: This route would handle the creation of a new user account. It would accept a payload with username and password, perform validation, hash the password, and store the user in the database.POST /login
: This route would handle user login with username and password. It would validate the username and password, compare the hashed password with the one stored in the database, and if they match, create a session or return a JWT token.OAuth Social Login:
GET /auth/{provider}
: This route would initiate the OAuth process for the specified provider (e.g., GET /auth/google
, GET /auth/twitter
). It would redirect the user to the provider's login page.GET /auth/{provider}/callback
: This route would handle the callback from the OAuth provider. The provider will redirect the user to this route with an authorization code in the URL. This route would exchange the authorization code for an access token, retrieve the user's profile information from the provider, and create or update the user in your database. After this, it would create a session or return a JWT token.Logout:
POST /logout
: This route would handle user logout. It would invalidate the user's session or token.Here's a basic diagram of how the routes could be set up:
POST /signup -----> Create new user account with username and password
POST /login -----> Authenticate user with username and password
GET /auth/{provider} ------------------> Redirect user to OAuth provider login page
GET /auth/{provider}/callback ---------> Handle callback from OAuth provider, authenticate user
POST /logout --------------------------> Logout user, invalidate session or token
In the scenario where you're using a JavaScript framework like Next.js for your frontend, you would handle form submission and session storage on the frontend, and make API calls to the backend.
Here's a general approach:
Frontend (Next.js): The user fills out a form with the required information. When they submit the form, the frontend stores this information in the session (you could use next-iron-session
or similar) and then redirects the user to the backend route to initiate the OAuth process.
Backend (Flask): The backend handles the OAuth process, as before. When the OAuth provider redirects the user back to the callback URL, the backend exchanges the authorization code for an access token, retrieves the user's social account details, and then redirects back to the frontend with this information.
Frontend (Next.js): The frontend retrieves the temporarily stored information from the session and combines it with the social account details from the backend. It then makes a final API call to the backend to create the user account with all of this information.
For this to work, your backend would need to support CORS (Cross-Origin Resource Sharing) since the frontend and backend are likely running on different origins. You can use Flask-CORS for this.
Here's an example of how your backend code would look like. Note that we're removing the /signup
route and adding CORS support:
from flask import Flask, redirect, url_for
from flask_dance.contrib.google import make_google_blueprint, google
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
app = Flask(__name__)
# Configurations
app.config['SECRET_KEY'] = 'YOUR_SECRET_KEY'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' # Use your actual database URI
# Set up Flask-SQLAlchemy
db = SQLAlchemy(app)
# Set up Flask-Dance
blueprint = make_google_blueprint(
client_id="YOUR_GOOGLE_CLIENT_ID",
client_secret="YOUR_GOOGLE_CLIENT_SECRET",
scope=["profile", "email"],
redirect_url="/login/google"
)
app.register_blueprint(blueprint, url_prefix="/login")
# Set up Flask-CORS
CORS(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
@app.route('/login/google')
def google_login():
if not google.authorized:
return redirect(url_for('google.login'))
resp = google.get("/oauth2/v2/userinfo")
if resp.ok:
user_info = resp.json()
email = user_info["email"]
return "You are {email} on Google.".format(email=email) # You will need to pass this information back to the frontend
return "Failed to fetch user info."
if __name__ == "__main__":
db.create_all() # Create database tables
app.run(host='localhost', port=5000)
You would then handle the form submission, session storage, and user account creation on your Next.js frontend. Here's a very basic example of how you might handle this:
import { useState } from 'react';
import { ironSession } from 'next-iron-session';
// Form to collect user information
export default function Home() {
const [username, setUsername] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
// Store username in session
await ironSession.set('username', username);
// Redirect to backend to initiate OAuth process
window.location.href = 'http://localhost:5000/login/google';
};
return (
<form onSubmit={handleSubmit}>
<label>
Username:
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
</label>
<input type="submit" value="Sign up with Google" />
</form>
);
}
Remember to replace 'http://localhost:5000/login/google'
with your actual backend URL, and to properly handle the OAuth process and user account creation. This is a very basic example and doesn't include error handling or other best practices you should follow in a real-world application.
When using OAuth and the signup process needs to be gated (like requiring an invite or checking eligibility criteria), the typical flow does not change much. Instead, additional steps are added to the OAuth process.
Here's a possible approach:
Initiate OAuth Process: When the user initiates the OAuth process by clicking on "Sign up with Google/Twitter/etc.", they get redirected to the social provider's page to authorize your application.
OAuth Callback: After authorization, the provider redirects the user back to your callback URL. Here, you get an authorization code which you can exchange for access tokens.
Check Eligibility: Instead of creating an account immediately, you would check if the user meets the necessary criteria for account creation. This could be checking if the user's email is in an invite list, checking if the user's age is above a certain threshold, or any other gating criteria.
Create Account or Show Error: If the user meets the gating criteria, you can proceed to create the account. Otherwise, you can redirect them to a page that informs them of why they cannot create an account.
So, instead of creating another signup route, you add additional steps to your OAuth callback route. The flow would look something like this:
GET /auth/{provider} ---------------> Redirect user to OAuth provider login page
GET /auth/{provider}/callback -----> Handle callback from OAuth provider, check gating criteria, and either authenticate user or show error
Remember to securely handle the user data received during the OAuth process, especially when gating the signup process. If the user doesn't meet the gating criteria, any data received from the OAuth provider should be discarded to respect the user's privacy.
Read More about Flask Sessions here: https://testdriven.io/blog/flask-sessions/
Note: Since session data is stored in cookies that are cryptographically-signed (not encrypted!), sessions should NOT be used for storing any sensitive information. You should never include passwords or personal information in session data.
In the scenario where the user provides information on your frontend prior to authorizing their account with a social OAuth provider, one common way to handle this is by using sessions or a similar mechanism to temporarily store the information the user has provided.
In the scenario where you're using a JavaScript framework like Next.js for your frontend, you would handle form submission and session storage on the frontend, and make API calls to the backend.
Here's a general approach:
Frontend (Next.js): The user fills out a form with the required information. When they submit the form, the frontend stores this information in the session (you could use next-iron-session
or similar) and then redirects the user to the backend route to initiate the OAuth process.
Backend (Flask): The backend handles the OAuth process, as before. When the OAuth provider redirects the user back to the callback URL, the backend exchanges the authorization code for an access token, retrieves the user's social account details, and then redirects back to the frontend with this information.
Frontend (Next.js): The frontend retrieves the temporarily stored information from the session and combines it with the social account details from the backend. It then makes a final API call to the backend to create the user account with all of this information.
For this to work, your backend would need to support CORS (Cross-Origin Resource Sharing) since the frontend and backend are likely running on different origins. You can use Flask-CORS for this.
Here's an example of how your backend code would look like. Note that we're removing the /signup
route and adding CORS support:
from flask import Flask, redirect, url_for
from flask_dance.contrib.google import make_google_blueprint, google
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
app = Flask(__name__)
# Configurations
app.config['SECRET_KEY'] = 'YOUR_SECRET_KEY'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' # Use your actual database URI
# Set up Flask-SQLAlchemy
db = SQLAlchemy(app)
# Set up Flask-Dance
blueprint = make_google_blueprint(
client_id="YOUR_GOOGLE_CLIENT_ID",
client_secret="YOUR_GOOGLE_CLIENT_SECRET",
scope=["profile", "email"],
redirect_url="/login/google"
)
app.register_blueprint(blueprint, url_prefix="/login")
# Set up Flask-CORS
CORS(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
@app.route('/login/google')
def google_login():
if not google.authorized:
return redirect(url_for('google.login'))
resp = google.get("/oauth2/v2/userinfo")
if resp.ok:
user_info = resp.json()
email = user_info["email"]
return "You are {email} on Google.".format(email=email) # You will need to pass this information back to the frontend
return "Failed to fetch user info."
if __name__ == "__main__":
db.create_all() # Create database tables
app.run(host='localhost', port=5000)
You would then handle the form submission, session storage, and user account creation on your Next.js frontend. Here's a very basic example of how you might handle this:
import { useState } from 'react';
import { ironSession } from 'next-iron-session';
// Form to collect user information
export default function Home() {
const [username, setUsername] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
// Store username in session
await ironSession.set('username', username);
// Redirect to backend to initiate OAuth process
window.location.href = 'http://localhost:5000/login/google';
};
return (
<form onSubmit={handleSubmit}>
<label>
Username:
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
</label>
<input type="submit" value="Sign up with Google" />
</form>
);
}
Remember to replace 'http://localhost:5000/login/google'
with your actual backend URL, and to properly handle the OAuth process and user account creation. This is a very basic example and doesn't include error handling or other best practices you should follow in a real-world application.