OAuth2 Authorization Code Flow (with OIDC): How Your Code Works

Absolutely! Let’s review the OAuth2 Authorization Code Flow as implemented in your identity-backend and Echo client app, step by step, with code context and real-world best practices.





OAuth2 Authorization Code Flow (with OIDC): How Your Code Works






1. What Is It? (And Why Use It?)



  • OAuth2 Authorization Code Flow (with OpenID Connect) is the industry gold standard for user SSO.
  • It securely lets your client app (Echo API Gateway) authenticate real users without ever seeing their password—identity-backend handles the sensitive bits.
  • Result: You get a signed JWT as proof of login, and your app can trust it.






2. Step-by-Step Flow (Echo-Style)




Step 1: User Attempts to Access a Protected Resource



  • User browses to http://localhost:5000.
  • Echo client app checks: “Do I have a valid JWT for this session?”
    • No JWT: Redirect to /login
    • Yes: Show the app



Code:

@app.route("/")

def home():

    if not session.get("id_token"):

        return redirect(url_for("login"))

    return render_template("index.html")





Step 2: Echo App Redirects User to Identity-Backend /authorize



  • Client app generates a random state (for CSRF defense), sets OIDC params.
  • Redirects to:


/identity-backend/authorize?client_id=browser-ui&redirect_uri=...&response_type=code&scope=openid&state=...



Code:

@app.route("/login")

def login():

    state = str(uuid.uuid4())

    session["state"] = state

    params = {

        "client_id": CLIENT_ID,

        "redirect_uri": REDIRECT_URI,

        "response_type": "code",

        "scope": "openid",

        "state": state

    }

    return redirect(f"{IDP_URL}/authorize?{urlencode(params)}")





Step 3: User Logs in on Identity-Backend



  • Identity-backend shows login form at /authorize.
  • User submits username/password to /login.
  • If credentials are valid:
    • Generate a single-use authorization code.
    • Store it in authcodes.db.
    • Redirects user back to client app:


/callback?code=<auth_code>&state=<original_state>



Code:

@app.route("/authorize")

def authorize():

    # ... parameter validation

    session["client_id"] = client_id

    # Show login form


@app.route("/login", methods=["POST"])

def handle_login():

    # ...validate credentials

    auth_code = str(uuid.uuid4())

    with sqlite3.connect(AUTH_CODE_DB) as conn:

        conn.execute("INSERT INTO codes (code, username, ...) VALUES (?, ...)", (auth_code, ...))

    # Redirect with ?code=...&state=...





Step 4: Client App Handles Callback, Verifies State, Exchanges Code for JWT



  • Client app verifies that state matches what was originally sent.
  • POSTs to /token with:
    • code, client_id, client_secret, redirect_uri

  • If code is valid, identity-backend returns:


{ "id_token": "<jwt>", "token_type": "Bearer", ... }



Code:

@app.route("/callback")

def callback():

    code = request.args.get("code")

    state = request.args.get("state")

    if state != session["state"]:

        abort(400, "State mismatch")

    token_resp = requests.post(

        f"{IDP_URL}/token",

        data={

            "code": code,

            "client_id": CLIENT_ID,

            "client_secret": CLIENT_SECRET,

            "redirect_uri": REDIRECT_URI

        }

    )

    id_token = token_resp.json()["id_token"]

    session["id_token"] = id_token

    return redirect(url_for("home"))





Step 5: User is Authenticated—JWT is Used for All Future Requests



  • JWT (id_token) is stored in user’s session (not in localStorage!).
  • Every protected request checks this JWT for validity/expiration.






3. Security Details: How You Got it Right



  • State param checked—prevents CSRF attacks.
  • Single-use auth codes—never reused; deleted from DB after token exchange.
  • Short-lived tokens—JWT expires quickly (15 min default).
  • Signature, audience, issuer all checked—no “just decode and trust.”
  • No credentials go to the client app—all handled by identity-backend.
  • All failures and suspicious events are logged.






4. What Could Be Even Stronger



  • Enforce HTTPS everywhere. (Don’t even allow HTTP except in dev.)
  • Consider PKCE for public clients.
  • Multi-factor can be plugged into the login step.
  • Explicit session expiry/rotation on logout or login from new device.






5. Sequence Diagram (PlantUML)


@startuml

actor User as U

participant "Browser (Client App)\n(API Gateway)" as C

participant "Identity Service\n(identity-backend)" as I


U -> C : GET /

C -> C : Check session for JWT

alt No valid JWT

    C -> I : GET /authorize?client_id=...&redirect_uri=...&state=...

    I -> U : Show login form

    U -> I : POST /login

    I -> I : Validate, issue code, redirect

    I -> C : Redirect /callback?code=...&state=...

    C -> I : POST /token {code, ...}

    I -> C : {id_token (JWT)}

    C -> C : Store JWT, login session valid

end

U -> C : Use app as logged-in user

@enduml





Summary



  • OAuth2 Auth Code Flow (with OIDC) is how your user SSO works, end-to-end.
  • No passwords or codes ever leak to the client app.
  • Your code gets it right: state checked, codes single-use, JWTs signed and validated.
  • Add PKCE, MFA, and strict secret handling for enterprise, and you’re audit-proof.





Let me know if you want to walk through the code line-by-line or see how to plug in advanced features like PKCE or federated login!

 

Comments

Popular posts from this blog

Feature: Audit log for one login, and identity service

Getting started - Build your data science lab environment

QA - Run #1 - Results