Skip to main content

Confidential Clients (Backends)

Use this guide if your application has a secure backend (e.g., written in Python, Node.js, etc.) that can safely store a client_secret. This approach is for traditional server-side web applications or services that manage their own user sessions and do not have a built-in OIDC client library. You will need to implement two endpoints in your backend to handle the OIDC flow.

Integration Steps

The process involves your backend initiating the login, handling the redirect from Visual Passcodes, and securely exchanging the authorization code for tokens on the server-side.

  1. Initiate Login: From a backend endpoint (e.g., /login), construct the full Visual Passcodes authorization URL and redirect the user's browser to it. You must generate and store a unique state value in the user's session to validate the callback later.
  2. Create a Callback Endpoint: Create an endpoint in your application that matches one of your registered redirect_uri values (e.g., /auth/callback). Visual Passcodes redirects the user here after they authenticate.
  3. Handle the Callback:
    • The request from Visual Passcodes contains an authorization_code and the state as query parameters.
    • Crucially, you must verify that the received state matches the value you stored in the user's session to prevent CSRF attacks.
  4. Exchange Code for Tokens:
    • With the validated authorization_code, your backend makes a direct, server-to-server POST request to the Visual Passcodes Token Endpoint.
    • The request body must include grant_type=authorization_code, the code, your client_id, and your client_secret.
  5. Process Tokens and Establish Session:
    • Visual Passcodes responds with an id_token, access_token, and refresh_token.
    • Your backend must validate the id_token's signature and claims (iss, aud, exp, nonce) to ensure its authenticity.
    • Use the information from the validated id_token to identify the user and create a local session in your application.

Code Examples

1. Prerequisites:

Before you begin, ensure you have the following:

  • Python 3.9+ installed.
  • FastAPI and Uvicorn (ASGI server) installed: pip install fastapi "uvicorn[standard]"
  • httpx for making HTTP requests in Python: pip install httpx
  • jwcrypto for JWT handling in Python: pip install jwcrypto
  • Visual Passcodes Client Registration: You must have a confidential client registered with your Visual Passcodes instance. This will provide you with a client_id and a client_secret.
  • Visual Passcodes' Discovery URL: The URL for Visual Passcodes' OpenID Connect discovery endpoint (e.g., https://your-server.com/api/oidc/default/.well-known/openid-configuration).
  • Helper File: The Python helper file (oidc_helper.py or similar) containing the generate_authorize_request, Oid2Fa, PromptValue, and IdTokenData functions.

2. Project Setup:

Create a project directory and place the provided helper file (let's assume it's named oidc_helper.py) inside it. Create a main.py file for your FastAPI application.

your-fastapi-app/
├── main.py
├── oidc_helper.py
└── settings.py

3. Configuration (settings.py - Example):

For demonstration purposes, let's assume you have a settings.py file or environment variables for your Visual Passcodes configuration.

settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
VISUAL_AUTH_DISCOVERY_URL: str = "https://your-server.com/api/oidc/default/.well-known/openid-configuration"
VISUAL_AUTH_CLIENT_ID: str = "your_confidential_client_id"
VISUAL_AUTH_CLIENT_SECRET: str = "your_confidential_client_secret"
APP_REDIRECT_URI: str = "http://localhost:8000/auth/callback" # Must match registered redirect_uri in Visual Passcodes

model_config = SettingsConfigDict(env_file=".env")

settings = Settings()

4. FastAPI Application (main.py):

main.py
from fastapi import FastAPI, Request, Response, HTTPException, status
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from urllib.parse import urlencode

# Assuming oidc_helper.py is in the same directory
from oidc_helper import generate_authorize_request, Oid2Fa, PromptValue, IdTokenData
from settings import settings # Import your settings

app = FastAPI()

# Add SessionMiddleware to manage user sessions.
# In a real application, use a strong secret key from environment variables.
app.add_middleware(SessionMiddleware, secret_key="super-secret-key-that-should-be-in-env")

# Initialize Oid2Fa client (perform discovery once at startup)
# In a production environment, handle potential network errors during discovery.
try:
oidc_client = Oid2Fa(discover_url=settings.VISUAL_AUTH_DISCOVERY_URL)
except Exception as e:
print(f"Failed to initialize Oid2Fa client: {e}")
# Depending on your application, you might want to exit or log a critical error
# sys.exit(1) # Example for exiting on critical error

@app.get("/")
async def read_root(request: Request):
"""
Root endpoint. Checks if the user is authenticated and displays a welcome message
or a login link.
"""
if "user_info" in request.session:
user = request.session["user_info"]
return f"Welcome, {user['name']} ({user['email']})! <a href='/logout'>Logout</a>"
return "Hello! <a href='/login'>Login with Visual Passcodes</a>"

@app.get("/login")
async def login(request: Request):
"""
Initiates the OIDC authorization code flow.
Generates an authorization request URL and redirects the user to Visual Passcodes.
Stores nonce and code_verifier in the session for later validation.
"""
# Generate authorization request parameters
auth_request = generate_authorize_request(
redirect_uri=settings.APP_REDIRECT_URI,
client_id=settings.VISUAL_AUTH_CLIENT_ID,
scope="openid profile email", # Request standard OIDC scopes
state=secrets.token_urlsafe(16), # Recommended to prevent CSRF
prompt=PromptValue.LOGIN, # Force login prompt
tenant_id="default" # Or your specific tenant_id
)

# Store necessary data in session for callback validation
request.session["oidc_state"] = auth_request.request_params.split("state=")[-1].split("&")[0] # Extract state
request.session["oidc_nonce"] = auth_request.nonce
request.session["oidc_code_verifier"] = auth_request.code_verifier

# Construct the full authorization URL
authorization_url = f"{oidc_client._authorization_uri}?{auth_request.request_params}"

return RedirectResponse(authorization_url)

@app.get("/auth/callback")
async def auth_callback(request: Request, code: str, state: str | None = None):
"""
Handles the callback from Visual Passcodes after user authentication.
Exchanges the authorization code for an ID token and validates it.
"""
# 1. Validate the 'state' parameter to prevent CSRF attacks
expected_state = request.session.pop("oidc_state", None)
if not state or state != expected_state:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid state parameter.")

# Retrieve nonce and code_verifier from session
nonce = request.session.pop("oidc_nonce", None)
code_verifier = request.session.pop("oidc_code_verifier", None)

if not nonce or not code_verifier:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing OIDC session data.")

try:
# 2. Exchange the authorization code for an ID token
id_token_data: IdTokenData = oidc_client.get_id_token(
redirect_uri=settings.APP_REDIRECT_URI,
code=code,
client_id=settings.VISUAL_AUTH_CLIENT_ID,
client_secret=settings.VISUAL_AUTH_CLIENT_SECRET, # Sent from server-side
code_verifier=code_verifier,
nonce=nonce # Pass nonce for validation within the helper
)

# Store user info in session
request.session["user_info"] = {
"sub": id_token_data.sub,
"email": id_token_data.email,
"name": id_token_data.name
}

# Redirect to a protected page or home page
return RedirectResponse("/")

except Exception as e:
# Log the error for debugging (e.g., invalid token, network issues)
print(f"Authentication failed: {e}")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication failed.")

@app.get("/logout")
async def logout(request: Request):
"""
Logs out the user by clearing the session.
"""
request.session.clear()
return RedirectResponse("/")

Explanation of the Code:

  • main.py: This is your FastAPI application.
    • SessionMiddleware: Essential for managing user sessions and storing temporary data like nonce and code_verifier between the authorization request and callback.
    • oidc_client = Oid2Fa(...): The Oid2Fa class from your helper file is initialized at application startup. This performs the OpenID Connect discovery, fetching necessary endpoints and the JWKS (JSON Web Key Set) for token validation.
    • /login endpoint:
      • Calls generate_authorize_request from oidc_helper to create the parameters for the authorization URL. This function automatically handles PKCE by generating the code_verifier and code_challenge.
      • Stores the state, nonce, and code_verifier in the FastAPI session. These are crucial for validating the callback response and ensuring security.
      • Redirects the user's browser to Visual Passcodes' authorization endpoint.
    • /auth/callback endpoint:
      • This is the redirect_uri that Visual Passcodes will send the user back to after authentication.
      • It receives the code (authorization code) and state from Visual Passcodes as query parameters.
      • State Validation: It first validates the state parameter against the one stored in the session to prevent CSRF attacks.
      • Token Exchange: It retrieves the nonce and code_verifier from the session and calls oidc_client.get_id_token(). This function makes a POST request to Visual Passcodes' token endpoint, including the client_id, client_secret, code, and code_verifier.
      • ID Token Validation: The get_id_token function (within Oid2Fa) handles the validation of the ID token's signature, claims (issuer, audience, expiration), and the nonce.
      • Session Management: On successful authentication, the user's information (sub, email, name) from the ID token is stored in the FastAPI session, marking the user as logged in.
      • Redirects the user to the root (/) or a protected page.
    • /logout endpoint: Clears the user's session, effectively logging them out.

This example demonstrates a secure and functional integration of a FastAPI confidential client with the Visual Passcodes service, adhering to OpenID Connect best practices including PKCE and state/nonce validation.