Implementation Tutorial: Python Authentication with Dex
This tutorial walks through setting up a Python web application that uses Dex as an identity provider to implement OIDC authentication.
Prerequisites
- Docker and Docker Compose (for running Dex)
- Python 3.8+ installed
- Basic understanding of web concepts
- macOS environment (can be adapted for other platforms)
Step 1: Set Up Dex Identity Provider
Dex is an identity service that uses OpenID Connect to provide authentication. We'll run it in Docker for simplicity.
Create Docker Compose Configuration
First, create a docker-compose.yml file in the dex directory:
version: '3'
services:
dex:
image: ghcr.io/dexidp/dex:v2.35.3
container_name: dex
ports:
- "5556:5556"
volumes:
- ./config.yaml:/etc/dex/config.yaml
restart: unless-stopped
Configure Dex
Create a config.yaml file in the dex directory:
issuer: http://localhost:5556/dex
storage:
type: sqlite3
config:
file: /var/dex/dex.db
web:
http: 0.0.0.0:5556
staticClients:
- id: python-app
redirectURIs:
- 'http://localhost:8000/callback'
name: 'Python App'
secret: python-app-secret
oauth2:
skipApprovalScreen: true
enablePasswordDB: true
staticPasswords:
- email: "admin@example.com"
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" # password: password
username: "admin"
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
This configuration:
- Sets up Dex to run on localhost:5556
- Creates a static client for our Python application
- Enables a simple password database with one user (admin@example.com)
- Configures SQLite storage for persistence
Step 2: Create the Python Web Application
We'll create a Flask application that authenticates users via Dex using OIDC.
Install Required Packages
Create a requirements.txt file in the app directory:
flask==2.0.1
authlib==1.0.1
requests==2.26.0
python-dotenv==0.19.1
Create Application Structure
Set up the following files in the app directory:
.env- Environment variables
CLIENT_ID=python-app
CLIENT_SECRET=python-app-secret
ISSUER_URL=http://localhost:5556/dex
APP_SECRET_KEY=your-secret-key
app.py- Main application file
import os
from flask import Flask, redirect, url_for, session, request, jsonify, render_template
from authlib.integrations.flask_client import OAuth
from urllib.parse import urlencode
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Flask app setup
app = Flask(__name__)
app.secret_key = os.getenv("APP_SECRET_KEY")
# OAuth setup
oauth = OAuth(app)
oauth.register(
name='dex',
client_id=os.getenv("CLIENT_ID"),
client_secret=os.getenv("CLIENT_SECRET"),
server_metadata_url=f'{os.getenv("ISSUER_URL")}/.well-known/openid-configuration',
client_kwargs={
'scope': 'openid email profile'
}
)
@app.route('/')
def home():
user = session.get('user')
return render_template('home.html', user=user)
@app.route('/login')
def login():
redirect_uri = url_for('callback', _external=True)
return oauth.dex.authorize_redirect(redirect_uri)
@app.route('/callback')
def callback():
token = oauth.dex.authorize_access_token()
user = oauth.dex.parse_id_token(token)
session['user'] = user
return redirect('/')
@app.route('/logout')
def logout():
session.pop('user', None)
return redirect('/')
@app.route('/protected')
def protected():
if 'user' not in session:
return redirect('/login')
return render_template('protected.html', user=session['user'])
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=True)
- Create a
templatesdirectory with template files:
templates/home.html:
<!DOCTYPE html>
<html>
<head>
<title>OIDC Demo</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
.card { border: 1px solid #ddd; padding: 20px; border-radius: 5px; }
.btn { display: inline-block; padding: 10px 15px; background: #4285f4; color: white;
text-decoration: none; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<h1>OIDC Authentication Demo</h1>
{% if user %}
<div class="card">
<h2>Logged in as {{ user.email }}</h2>
<p>User ID: {{ user.sub }}</p>
<p><a href="/protected" class="btn">View Protected Page</a></p>
<p><a href="/logout" class="btn">Logout</a></p>
</div>
{% else %}
<p>You are not logged in.</p>
<p><a href="/login" class="btn">Login with Dex</a></p>
{% endif %}
</div>
</body>
</html>
templates/protected.html:
<!DOCTYPE html>
<html>
<head>
<title>Protected Page</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
.card { border: 1px solid #ddd; padding: 20px; border-radius: 5px; }
.btn { display: inline-block; padding: 10px 15px; background: #4285f4; color: white;
text-decoration: none; border-radius: 4px; }
pre { background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; }
</style>
</head>
<body>
<div class="container">
<h1>Protected Resource</h1>
<div class="card">
<h2>This page is only accessible to authenticated users</h2>
<h3>Your User Profile:</h3>
<pre>{{ user|tojson(indent=4) }}</pre>
<p><a href="/" class="btn">Back to Home</a></p>
</div>
</div>
</body>
</html>
Step 3: Running the Application
Start Dex
- Navigate to the dex directory:
cd ~/learn/auth-demo/dex
- Start the Dex container:
docker-compose up -d
- Verify Dex is running:
curl http://localhost:5556/dex/.well-known/openid-configuration
Start the Python Application
- Navigate to the app directory:
cd ~/learn/auth-demo/app
- Install dependencies:
pip install -r requirements.txt
- Start the application:
python app.py
- Open http://localhost:8000 in your browser
Step 4: Testing the Authentication Flow
- Open the application at http://localhost:8000
- Click "Login with Dex"
- You'll be redirected to the Dex login page
- Log in with:
- Email: admin@example.com
- Password: password
- After successful authentication, you'll be redirected back to the application
- You can now access the protected page, which shows your user information
Authentication Flow Explained
The authentication flow follows the OIDC Authorization Code Flow:
- User initiates login: By clicking the login button, the user is redirected to Dex's authorization endpoint
- Authorization request: The app requests authorization from Dex with client ID, redirect URI, and requested scopes
- User authentication: User authenticates with Dex using credentials
- Authorization grant: Dex redirects back to the app's callback URL with an authorization code
- Token exchange: The app exchanges the code for ID and access tokens by making a request to Dex's token endpoint
- Identity verification: The app verifies the ID token and extracts user information
- Session creation: The app creates a session for the authenticated user
- Protected resource access: The user can now access protected resources
Next Steps and Enhancements
- Add more authentication providers: Configure Dex to connect to external providers like Google, GitHub, etc.
- Implement token refresh: Add logic to refresh tokens when they expire
- Add role-based access control: Extend the application to use roles or groups from the ID token
- Improve security: Add CSRF protection and secure session handling
- Implement logout with OIDC: Use OIDC's end_session_endpoint for proper logout
Troubleshooting
- Connection issues: Ensure Dex is running and accessible at http://localhost:5556
- Callback errors: Verify that the redirect URI in Dex config exactly matches the application callback URL
- Token validation errors: Check that the client ID and secret match between Dex and the application
- Invalid state errors: This usually indicates session problems in the application
Conclusion
You've successfully set up a Python web application that uses OpenID Connect for authentication through Dex. This demonstrates the core concepts of modern authentication while providing a foundation you can build upon for more complex scenarios.