Difficulty: Advanced
OAuth 2.0 allows users to log in to your application using their existing accounts from providers like Google, GitHub, Facebook, or Twitter. Instead of creating yet another username/password combination, users authenticate through a trusted provider, and your application receives an access token to access their profile information.
The OAuth 2.0 flow for web applications works like this: your app redirects the user to the provider's authorization page. The user grants permission. The provider redirects back to your app with an authorization code. Your app exchanges this code for an access token. Your app uses the token to fetch the user's profile from the provider's API.
Authlib is the recommended library for OAuth integration in Flask. It provides both OAuth client (for logging in via external providers) and OAuth server (for building your own OAuth provider) functionality. Flask-Dance is another popular alternative that provides pre-built integrations for common providers.
To set up OAuth with a provider, you first register your application on the provider's developer console (Google Cloud Console, GitHub Developer Settings, etc.). You receive a client_id and client_secret. You configure the redirect URI (the URL the provider sends users back to after authorization). Then you configure Authlib with these credentials.
After the OAuth callback, you have the user's profile information (email, name, avatar). You need to either find an existing user in your database that matches or create a new one. This is often called 'social login' or 'federated identity'. A common approach is to link OAuth accounts to local user accounts via an OAuthAccount table.
Security considerations include: always validate the state parameter (prevents CSRF), use HTTPS for redirect URIs, store tokens securely, and handle token expiration and refresh. Never expose your client_secret in client-side code.
Implementing social login alongside traditional email/password login requires careful design. Users should be able to link multiple OAuth accounts to a single user. They should also be able to set a password if they initially signed up via OAuth. Consider edge cases like the same email address appearing with different OAuth providers.
from flask import Flask, redirect, url_for, session
from authlib.integrations.flask_client import OAuth
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['GOOGLE_CLIENT_ID'] = 'your-client-id'
app.config['GOOGLE_CLIENT_SECRET'] = 'your-client-secret'
oauth = OAuth(app)
google = oauth.register(
name='google',
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'},
)
@app.route('/login/google')
def google_login():
redirect_uri = url_for('google_callback', _external=True)
return google.authorize_redirect(redirect_uri)
@app.route('/login/google/callback')
def google_callback():
token = google.authorize_access_token()
userinfo = token.get('userinfo')
# userinfo contains: email, name, picture, sub (Google user ID)
# Find or create user
user = User.query.filter_by(email=userinfo['email']).first()
if not user:
user = User(
username=userinfo['name'],
email=userinfo['email'],
oauth_provider='google',
oauth_id=userinfo['sub']
)
db.session.add(user)
db.session.commit()
login_user(user)
return redirect(url_for('dashboard'))
Authlib handles the OAuth flow. google_login redirects to Google's consent screen. google_callback exchanges the code for a token and fetches user info. You then find or create a local user.
from flask import Flask, redirect, url_for
from authlib.integrations.flask_client import OAuth
app = Flask(__name__)
oauth = OAuth(app)
github = oauth.register(
name='github',
client_id='your-github-client-id',
client_secret='your-github-client-secret',
access_token_url='https://github.com/login/oauth/access_token',
authorize_url='https://github.com/login/oauth/authorize',
api_base_url='https://api.github.com/',
client_kwargs={'scope': 'user:email'},
)
@app.route('/login/github')
def github_login():
redirect_uri = url_for('github_callback', _external=True)
return github.authorize_redirect(redirect_uri)
@app.route('/login/github/callback')
def github_callback():
token = github.authorize_access_token()
resp = github.get('user')
profile = resp.json()
# profile contains: login, id, name, email, avatar_url
user = find_or_create_user(
provider='github',
provider_id=str(profile['id']),
username=profile['login'],
email=profile.get('email'),
)
login_user(user)
return redirect(url_for('dashboard'))
GitHub OAuth follows the same pattern but with different URLs and scopes. The github.get('user') call uses the access token to fetch the user's GitHub profile.
from app.extensions import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120), unique=True)
password_hash = db.Column(db.String(256), nullable=True) # Nullable for OAuth-only
oauth_accounts = db.relationship('OAuthAccount', backref='user', cascade='all, delete-orphan')
class OAuthAccount(db.Model):
id = db.Column(db.Integer, primary_key=True)
provider = db.Column(db.String(50), nullable=False) # 'google', 'github'
provider_id = db.Column(db.String(256), nullable=False) # Provider's user ID
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
__table_args__ = (
db.UniqueConstraint('provider', 'provider_id', name='uq_oauth_provider_id'),
)
def find_or_create_user(provider, provider_id, username, email):
"""Find user by OAuth account or create a new one."""
oauth = OAuthAccount.query.filter_by(
provider=provider, provider_id=provider_id
).first()
if oauth:
return oauth.user
# Check if user with this email exists
user = User.query.filter_by(email=email).first() if email else None
if not user:
user = User(username=username, email=email)
db.session.add(user)
oauth = OAuthAccount(provider=provider, provider_id=provider_id, user=user)
db.session.add(oauth)
db.session.commit()
return user
An OAuthAccount model links OAuth identities to local users. One user can have multiple OAuth accounts (Google + GitHub). The find_or_create function handles both new and returning users.
<!-- templates/login.html -->
<h1>Login</h1>
<!-- Traditional login -->
<form method="POST" action="{{ url_for('login') }}">
{{ form.hidden_tag() }}
{{ form.email(placeholder='Email') }}
{{ form.password(placeholder='Password') }}
<button type="submit">Log In</button>
</form>
<hr>
<p>Or continue with:</p>
<!-- Social login buttons -->
<a href="{{ url_for('google_login') }}" class="btn btn-google">
Continue with Google
</a>
<a href="{{ url_for('github_login') }}" class="btn btn-github">
Continue with GitHub
</a>
Offer both traditional and social login. Users choose their preferred authentication method. All paths eventually call login_user() with a local User object.
OAuth 2.0, Flask-OAuthlib, Authlib, Social Login, Google OAuth, GitHub OAuth