Skip to content

OIDC Project — Build a Complete OpenID Connect Authentication System

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about OIDC Project. We cover key concepts, practical examples, and best practices to help you master this topic.

Build a complete Openid Connect authentication system that supports login with multiple providers, ID token verification, UserInfo fetching, session management, and profile display using Python Flask and JWT libraries.

What You'll Build

  • Flask app with multi-provider OIDC login
  • ID token verification with JWKS
  • Session management with secure cookies
  • User profile from ID token and UserInfo
  • Provider discovery and dynamic configuration

Why This Project Matters

Most applications need authentication, but implementing OIDC correctly requires handling many edge cases: token verification, state/nonce management, key rotation, error handling, and session security. This project gives you a reusable, production-ready OIDC auth system.

flowchart LR
    User["User"] --> App["Flask App"]
    App -->|"Login with Google"| Google["Google OIDC"]
    App -->|"Login with GitHub"| GitHub["GitHub OIDC"]
    App -->|"Login with Microsoft"| MS["Microsoft OIDC"]
    App -->|"Verify"| JWKS["JWKS Endpoint"]
    App -->|"Store"| Session["Session Store"]
    App -->|"Profile"| UI["UserInfo Endpoint"]
    style App fill:#dbeafe,stroke:#2563eb

Project Structure

oidc-auth-system/
  app.py
  config.py
  oidc/
    client.py
    provider.py
    verification.py
  templates/
    login.html
    profile.html
    home.html
  requirements.txt

Step 1: Provider Configuration

# config.py
PROVIDERS = {
    "google": {
        "issuer": "https://accounts.google.com",
        "client_id": "google-client-id",
        "client_secret": "google-client-secret",
        "scope": "openid profile email",
    },
    "microsoft": {
        "issuer": "https://login.microsoftonline.com/common/v2.0",
        "client_id": "ms-client-id",
        "client_secret": "ms-client-secret",
        "scope": "openid profile email",
    },
    "github": {
        "issuer": "https://token.actions.githubusercontent.com",
        "client_id": "gh-client-id",
        "client_secret": "gh-client-secret",
        "scope": "openid email",
    },
}

SECRET_KEY = "your-secret-key-change-in-production"
SESSION_TYPE = "filesystem"

Step 2: OIDC Client

# oidc/client.py
import requests
import secrets
import urllib.parse

class OIDCClient:
    def __init__(self, provider_config):
        self.config = provider_config
        self.discovery = self._discover()

    def _discover(self):
        url = f"{self.config['issuer'].rstrip('/')}/.well-known/openid-configuration"
        resp = requests.get(url, timeout=10)
        resp.raise_for_status()
        return resp.json()

    def get_auth_url(self, redirect_uri):
        state = secrets.token_urlsafe(32)
        nonce = secrets.token_urlsafe(16)
        params = {
            "client_id": self.config["client_id"],
            "response_type": "code",
            "redirect_uri": redirect_uri,
            "scope": self.config["scope"],
            "state": state,
            "nonce": nonce,
        }
        url = f"{self.discovery['authorization_endpoint']}?{urllib.parse.urlencode(params)}"
        return url, state, nonce

    def exchange_code(self, code, redirect_uri, client_secret):
        resp = requests.post(self.discovery["token_endpoint"], data={
            "code": code,
            "client_id": self.config["client_id"],
            "client_secret": client_secret,
            "redirect_uri": redirect_uri,
            "grant_type": "authorization_code",
        }, timeout=10)
        resp.raise_for_status()
        return resp.json()

    def get_userinfo(self, access_token):
        resp = requests.get(
            self.discovery["userinfo_endpoint"],
            headers={"Authorization": f"Bearer {access_token}"},
            timeout=10
        )
        resp.raise_for_status()
        return resp.json()

Step 3: Token Verification

# oidc/verification.py
import jwt
from jwt import PyJWKClient

class TokenVerifier:
    def __init__(self, provider_config, discovery):
        self.issuer = provider_config["issuer"]
        self.client_id = provider_config["client_id"]
        self.jwks_uri = discovery["jwks_uri"]
        self.jwks_client = PyJWKClient(self.jwks_uri)

    def verify_id_token(self, id_token, nonce=None):
        signing_key = self.jwks_client.get_signing_key_from_jwt(id_token)
        claims = jwt.decode(
            id_token,
            signing_key.key,
            algorithms=["RS256"],
            audience=self.client_id,
            issuer=self.issuer,
            options={
                "verify_exp": True,
                "verify_iat": True,
                "require": ["iss", "sub", "aud", "exp"],
            }
        )
        if nonce and claims.get("nonce") != nonce:
            raise ValueError("Nonce mismatch")
        return claims

Step 4: Flask Application

# app.py
from flask import Flask, request, redirect, session, render_template
from oidc.client import OIDCClient
from oidc.verification import TokenVerifier
from config import PROVIDERS, SECRET_KEY

app = Flask(__name__)
app.secret_key = SECRET_KEY

@app.route("/")
def home():
    user = session.get("user")
    return render_template("home.html", user=user)

@app.route("/login/<provider>")
def login(provider):
    if provider not in PROVIDERS:
        return "Unknown provider", 400

    client = OIDCClient(PROVIDERS[provider])
    auth_url, state, nonce = client.get_auth_url(
        redirect_uri=f"https://{request.host}/callback/{provider}"
    )
    session["oauth_state"] = state
    session["oauth_nonce"] = nonce
    session["oauth_provider"] = provider
    return redirect(auth_url)

@app.route("/callback/<provider>")
def callback(provider):
    if request.args.get("state") != session.get("oauth_state"):
        return "Invalid state", 400
    if provider != session.get("oauth_provider"):
        return "Provider mismatch", 400

    client = OIDCClient(PROVIDERS[provider])
    verifier = TokenVerifier(PROVIDERS[provider], client.discovery)

    tokens = client.exchange_code(
        code=request.args["code"],
        redirect_uri=f"https://{request.host}/callback/{provider}",
        client_secret=PROVIDERS[provider]["client_secret"],
    )

    claims = verifier.verify_id_token(
        tokens["id_token"],
        nonce=session.get("oauth_nonce")
    )

    userinfo = client.get_userinfo(tokens["access_token"])

    session["user"] = {
        "id": claims["sub"],
        "provider": provider,
        "name": claims.get("name") or userinfo.get("name"),
        "email": claims.get("email") or userinfo.get("email"),
        "picture": claims.get("picture") or userinfo.get("picture"),
    }
    session.permanent = True
    return redirect("/")

@app.route("/logout")
def logout():
    session.clear()
    return redirect("/")

Step 5: Testing

# Run the app
flask run --host=0.0.0.0 --port=5000

# Test login flow (requires registered providers)
# Visit http://localhost:5000/login/google
# Login with Google account
# Check profile and session

Common Mistakes

1. Not Handling Token Expiry

Sessions should expire when the access/refresh token expires. Implement token refresh or force re-login.

2. Storing Sensitive Data in Session

Do not store tokens in client-side sessions. Store a session ID server-side and keep tokens in server memory/database.

3. Not Validating iss Claim

Without issuer validation, a token from a malicious provider can authenticate on your app.

4. Confusing Multiple Provider Sessions

If a user logs in with Google then Microsoft, handle session merging or use separate identities.

5. No Error Page for Failed Authentication

When OIDC fails (user denies consent, network error), show a user-friendly error page instead of a raw traceback.

Practice Questions

  1. Why should tokens not be stored in client-side sessions?
  2. How does the project handle multiple OIDC providers?
  3. What is the purpose of the issuer field in provider config?
  4. Why is PKCE not used in this server-side implementation?
  5. How would you add token refresh to this project?

Answers:

  1. Client-side sessions are stored in browser cookies and can be decoded or manipulated. Store session IDs only and keep tokens server-side.
  2. Each provider has its own config entry. The /login/<provider> route dynamically selects the correct OIDC client.
  3. The issuer is used to construct the discovery URL and to validate the iss claim in ID tokens.
  4. Server-side apps can securely store a client secret. PKCE is needed for public clients like SPAs and mobile apps.
  5. Store the refresh token server-side. On token expiry, call the token endpoint with grant_type=refresh_token.

Challenge: Extend this project to support token refresh, remember-me functionality (30-day sessions), and account linking where one user can connect multiple providers.

FAQ

How do I register my app with an OIDC provider?

: Each provider has a developer console (Google Cloud Console, Azure Portal, GitHub Settings). Create an application and add your redirect URI.

What is the `redirect_uri` and why must it match exactly?

: The redirect URI is where the provider sends the user after authentication. It must match exactly what is registered, including protocol, port, and trailing slash.

How do I handle users who deny consent?

: The provider redirects with error=access_denied. Show a message like "Login cancelled" and offer alternative login methods.

Can I use this project in production?

: Yes, with additional hardening: HTTPS-only, secure session cookies, Rate Limiting on login routes, and proper secret management.

How do I add more OIDC providers?

: Add a new entry to the PROVIDERS config with the provider's issuer URL, client ID, client secret, and desired scopes.

Mini Project

Complete the OIDC authentication system with at least two providers (Google and Microsoft). Implement user session management, ID token verification with JWKS, UserInfo fetching, and a profile page. Add error handling for all failure modes.

What's Next

Review the OIDC vs OAuth2 Differences to reinforce core concepts, or explore API Gateway Complete Guide for integrating OIDC with gateway authentication.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro