diff --git a/contributing/samples/authn-adk-all-in-one/README.md b/contributing/samples/authn-adk-all-in-one/README.md
new file mode 100644
index 0000000000..fa5db4e772
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/README.md
@@ -0,0 +1,152 @@
+## ADK Authentication Demo (All in one - Agent, IDP and The app)
+
+This folder contains everything you need to run the ADK's `auth-code`
+ grant type authentication demo completely locally
+
+Here's the high level diagram.
+
+
+
+### Introduction
+More often than not the agents use some kind of system identity
+ (especially for OpenAPI and MCP tools).
+ But obviously this is insecure in that multiple end users
+ are using the same identity with permissions to access ALL users' data on the
+ backend.
+
+ADK provides various [authentication mechanisms](https://google.github.io/adk-docs/tools/authentication/) to solve this.
+
+However to properly test it you need various components.
+We provide everything that is needed so that you can test and run
+ ADK authentication demo locally.
+
+This folder comes with -
+
+1. An IDP
+2. A hotel booking application backend
+3. A hotel assistant ADK agent (accessing the application using OpenAPI Tools)
+
+### Details
+
+You can read about the Auth Code grant / flow type in detail [here](https://developer.okta.com/blog/2018/04/10/oauth-authorization-code-grant-type). But for the purpose of this demo, following steps take place
+
+1. The user asks the agent to find hotels in "New York".
+2. Agent realizes (based on LLM response) that it needs to call a tool and that the tool needs authentication.
+3. Agent redirects the user to the IDP's login page with callback / redirect URL back to ADK UI.
+4. The user enters credentials (`john.doe` and `password123`) and accepts the consent.
+5. The IDP sends the auth_code back to the redirect URL (from 3).
+6. ADK then exchanges this auth_code for an access token.
+7. ADK does the API call to get details on hotels and hands over that response to LLM, LLM formats the response.
+8. ADK sends a response back to the User.
+
+### Setting up and running
+
+1. Clone this repository
+2. Carry out following steps and create and activate the environment
+```bash
+# Go to the cloned directory
+cd adk-python
+# Navigate to the all in one authentication sample
+cd contributing/samples/authn-adk-all-in-one/
+
+python3 -m venv .venv
+
+. .venv/bin/activate
+
+pip install -r requirements.txt
+
+```
+3. Configure and Start the IDP. Our IDP needs a private key to sign the tokens and a JWKS with public key component to verify them. Steps are provided for that (please check the screenshots below)
+
+🪧 **NOTE:**
+It is recommended that you execute the key pair creation and public
+ key extraction commands (1-3 and 5 below) on Google cloud shell.
+
+```bash
+cd idp
+
+# Create .env file by copying the existing one.
+cp sample.env .env
+cp sample.jwks.json jwks.json
+
+
+# Carry out following steps
+# 1. Generate a key pair, When asked about passphrase please press enter (empty passphrase)
+ssh-keygen -t rsa -b 2048 -m PEM -f private_key.pem
+
+# 2. Extract the public key
+openssl rsa -in private_key.pem -pubout > pubkey.pub
+
+# 3. Generate the jwks.json content using https://jwkset.com/generate and this public key (choose key algorithm RS256 and Key use Signature) (Please check the screenshot)
+# 4. Update the jwks.json with the key jwks key created in 3 (please check the screenshot)
+# 5. Update the env file with the private key
+cat private_key.pem | tr -d "\n"
+# 6. Carefully copy output of the command above into the .env file to update the value of PRIVATE_KEY
+# 7. save jwks.json and .env
+
+# Start the IDP
+python app.py
+```
+
+
+Screenshots
+Generating JWKS -
+
+
+
+Updated `jwks.json` (notice the key is added in the existing array)
+
+
+
+
+
+4. In a separate shell - Start the backend API (Hotel Booking Application)
+```bash
+# Go to the cloned directory
+cd adk-python
+# Navigate to the all in one authentication sample
+cd contributing/samples/authn-adk-all-in-one/
+
+# Activate Env for this shell
+. .venv/bin/activate
+
+cd hotel_booker_app/
+
+# Start the hotel booker application
+python main.py
+
+```
+
+5. In a separate shell - Start the ADK agent
+```bash
+# Go to the cloned directory
+cd adk-python
+# Navigate to the all in one authentication sample
+cd contributing/samples/authn-adk-all-in-one/
+
+# Activate Env for this shell
+. .venv/bin/activate
+
+cd adk_agents/
+
+cp sample.env .env
+
+# ⚠️ Make sure to update the API KEY (GOOGLE_API_KEY) in .env file
+
+# Run the agent
+adk web
+
+```
+6. Access the agent on http://localhost:8000
+
+🪧 **NOTE:**
+
+After first time authentication,
+it might take some time for the agent to respond,
+subsequent responses are significantly faster.
+
+### Conclusion
+
+You can exercise the ADK Authentication
+without any external components using this demo.
+
diff --git a/contributing/samples/authn-adk-all-in-one/adk_agents/agent_openapi_tools/__init__.py b/contributing/samples/authn-adk-all-in-one/adk_agents/agent_openapi_tools/__init__.py
new file mode 100644
index 0000000000..c48963cdc7
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/adk_agents/agent_openapi_tools/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from . import agent
diff --git a/contributing/samples/authn-adk-all-in-one/adk_agents/agent_openapi_tools/agent.py b/contributing/samples/authn-adk-all-in-one/adk_agents/agent_openapi_tools/agent.py
new file mode 100644
index 0000000000..db956ea454
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/adk_agents/agent_openapi_tools/agent.py
@@ -0,0 +1,65 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import os
+
+from google.adk.tools.openapi_tool.auth.auth_helpers import openid_url_to_scheme_credential
+from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_toolset import OpenAPIToolset
+
+credential_dict = {
+ "client_id": os.environ.get("OAUTH_CLIENT_ID"),
+ "client_secret": os.environ.get("OAUTH_CLIENT_SECRET"),
+}
+auth_scheme, auth_credential = openid_url_to_scheme_credential(
+ openid_url="http://localhost:5000/.well-known/openid-configuration",
+ credential_dict=credential_dict,
+ scopes=[],
+)
+
+
+# Open API spec
+file_path = "./agent_openapi_tools/openapi.yaml"
+file_content = None
+
+try:
+ with open(file_path, "r") as file:
+ file_content = file.read()
+except FileNotFoundError:
+ # so that the execution does not continue when the file is not found.
+ raise FileNotFoundError(f"Error: The API Spec '{file_path}' was not found.")
+
+
+# Example with a JSON string
+openapi_spec_yaml = file_content # Your OpenAPI YAML string
+openapi_toolset = OpenAPIToolset(
+ spec_str=openapi_spec_yaml,
+ spec_str_type="yaml",
+ auth_scheme=auth_scheme,
+ auth_credential=auth_credential,
+)
+
+from google.adk.agents import LlmAgent
+
+root_agent = LlmAgent(
+ name="hotel_agent",
+ instruction=(
+ "Help user find and book hotels, fetch their bookings using the tools"
+ " provided."
+ ),
+ description="Hotel Booking Agent",
+ model=os.environ.get("GOOGLE_MODEL"),
+ tools=[openapi_toolset], # Pass the toolset
+ # ... other agent config ...
+)
diff --git a/contributing/samples/authn-adk-all-in-one/adk_agents/agent_openapi_tools/openapi.yaml b/contributing/samples/authn-adk-all-in-one/adk_agents/agent_openapi_tools/openapi.yaml
new file mode 100644
index 0000000000..8adda49623
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/adk_agents/agent_openapi_tools/openapi.yaml
@@ -0,0 +1,229 @@
+openapi: 3.0.0
+info:
+ title: Hotel Booker API
+ description: A simple API for managing hotel bookings, with a custom client credentials authentication flow.
+ version: 1.0.0
+servers:
+ - url: http://127.0.0.1:8081
+paths:
+ /hotels:
+ get:
+ summary: Get available hotels
+ description: Retrieves a list of available hotels, optionally filtered by location.
+ security:
+ - BearerAuth: []
+ parameters:
+ - in: query
+ name: location
+ schema:
+ type: string
+ description: The city to filter hotels by (e.g., 'New York').
+ responses:
+ '200':
+ description: Successfully retrieved hotels.
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: boolean
+ example: false
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/Hotel'
+ message:
+ type: string
+ example: "Successfully retrieved hotels."
+ '401':
+ description: Unauthorized. Invalid or expired token.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /book:
+ post:
+ summary: Book a room
+ description: Books a room in a specified hotel.
+ security:
+ - BearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BookingRequest'
+ responses:
+ '200':
+ description: Booking successful.
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: boolean
+ example: false
+ data:
+ type: object
+ properties:
+ booking_id:
+ type: string
+ example: "HB-1"
+ message:
+ type: string
+ example: "Booking successful!"
+ '400':
+ description: Bad request. Missing information or invalid booking details.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Unauthorized. Invalid or expired token.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /booking_details:
+ get:
+ summary: Get booking details
+ description: Retrieves details for a specific booking by ID or guest name.
+ security:
+ - BearerAuth: []
+ parameters:
+ - in: query
+ name: booking_id
+ schema:
+ type: string
+ description: The custom booking ID (e.g., 'HB-1').
+ - in: query
+ name: guest_name
+ schema:
+ type: string
+ description: The name of the guest to search for (partial and case-insensitive).
+ responses:
+ '200':
+ description: Booking details retrieved successfully.
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: boolean
+ example: false
+ data:
+ type: object
+ properties:
+ custom_booking_id:
+ type: string
+ example: "HB-1"
+ hotel_name:
+ type: string
+ example: "Grand Hyatt"
+ hotel_location:
+ type: string
+ example: "New York"
+ guest_name:
+ type: string
+ example: "John Doe"
+ check_in_date:
+ type: string
+ example: "2025-10-01"
+ check_out_date:
+ type: string
+ example: "2025-10-05"
+ num_rooms:
+ type: integer
+ example: 1
+ total_price:
+ type: number
+ format: float
+ example: 1000.0
+ message:
+ type: string
+ example: "Booking details retrieved successfully."
+ '400':
+ description: Bad request. Missing parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Unauthorized. Invalid or expired token.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Booking not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+components:
+ securitySchemes:
+ BearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: CustomAuthToken
+ schemas:
+ ErrorResponse:
+ type: object
+ properties:
+ error:
+ type: boolean
+ example: true
+ data:
+ type: object
+ nullable: true
+ message:
+ type: string
+ example: "Invalid access token."
+ Hotel:
+ type: object
+ properties:
+ id:
+ type: integer
+ example: 1
+ name:
+ type: string
+ example: "Grand Hyatt"
+ location:
+ type: string
+ example: "New York"
+ available_rooms:
+ type: integer
+ example: 10
+ price_per_night:
+ type: number
+ format: float
+ example: 250.0
+ BookingRequest:
+ type: object
+ properties:
+ hotel_id:
+ type: integer
+ example: 1
+ guest_name:
+ type: string
+ example: "John Doe"
+ check_in_date:
+ type: string
+ format: date
+ example: "2025-10-01"
+ check_out_date:
+ type: string
+ format: date
+ example: "2025-10-05"
+ num_rooms:
+ type: integer
+ example: 1
+ required:
+ - hotel_id
+ - guest_name
+ - check_in_date
+ - check_out_date
+ - num_rooms
\ No newline at end of file
diff --git a/contributing/samples/authn-adk-all-in-one/adk_agents/requirements.txt b/contributing/samples/authn-adk-all-in-one/adk_agents/requirements.txt
new file mode 100644
index 0000000000..f490d72da0
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/adk_agents/requirements.txt
@@ -0,0 +1 @@
+google-adk==1.12
diff --git a/contributing/samples/authn-adk-all-in-one/adk_agents/sample.env b/contributing/samples/authn-adk-all-in-one/adk_agents/sample.env
new file mode 100644
index 0000000000..e448864ea1
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/adk_agents/sample.env
@@ -0,0 +1,6 @@
+# General Agent Configuration
+GOOGLE_GENAI_USE_VERTEXAI=False
+GOOGLE_API_KEY=NOT_SET
+GOOGLE_MODEL=gemini-2.5-flash
+OAUTH_CLIENT_ID=abc123
+OAUTH_CLIENT_SECRET=secret123
\ No newline at end of file
diff --git a/contributing/samples/authn-adk-all-in-one/doc_images/adk-auth-all-in-one.svg b/contributing/samples/authn-adk-all-in-one/doc_images/adk-auth-all-in-one.svg
new file mode 100644
index 0000000000..37bfec651f
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/doc_images/adk-auth-all-in-one.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/contributing/samples/authn-adk-all-in-one/doc_images/jwks_updated.png b/contributing/samples/authn-adk-all-in-one/doc_images/jwks_updated.png
new file mode 100644
index 0000000000..cc376ea19c
Binary files /dev/null and b/contributing/samples/authn-adk-all-in-one/doc_images/jwks_updated.png differ
diff --git a/contributing/samples/authn-adk-all-in-one/doc_images/jwksgen.png b/contributing/samples/authn-adk-all-in-one/doc_images/jwksgen.png
new file mode 100644
index 0000000000..b7f553e240
Binary files /dev/null and b/contributing/samples/authn-adk-all-in-one/doc_images/jwksgen.png differ
diff --git a/contributing/samples/authn-adk-all-in-one/hotel_booker_app/hotelbooker_core.py b/contributing/samples/authn-adk-all-in-one/hotel_booker_app/hotelbooker_core.py
new file mode 100644
index 0000000000..3f6916034f
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/hotel_booker_app/hotelbooker_core.py
@@ -0,0 +1,263 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+import logging
+import sqlite3
+
+
+class HotelBooker:
+ """
+ Core business logic for hotel booking, independent of any web framework.
+ """
+
+ def __init__(self, db_name="data.db"):
+ self.db_name = db_name
+ self._initialize_db()
+
+ def _get_db_connection(self):
+ """Helper to get a new, independent database connection."""
+ conn = sqlite3.connect(self.db_name)
+ conn.row_factory = sqlite3.Row
+ return conn
+
+ def _initialize_db(self):
+ """
+ Drops, creates, and populates the database tables with sample data.
+ """
+ conn = None
+ try:
+ conn = self._get_db_connection()
+ cursor = conn.cursor()
+
+ cursor.execute("DROP TABLE IF EXISTS bookings")
+ cursor.execute("DROP TABLE IF EXISTS hotels")
+ conn.commit()
+
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS hotels (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ location TEXT NOT NULL,
+ total_rooms INTEGER NOT NULL,
+ available_rooms INTEGER NOT NULL,
+ price_per_night REAL NOT NULL
+ )
+ """)
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS bookings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ custom_booking_id TEXT UNIQUE,
+ hotel_id INTEGER NOT NULL,
+ guest_name TEXT NOT NULL,
+ check_in_date TEXT NOT NULL,
+ check_out_date TEXT NOT NULL,
+ num_rooms INTEGER NOT NULL,
+ total_price REAL NOT NULL,
+ FOREIGN KEY (hotel_id) REFERENCES hotels(id)
+ )
+ """)
+
+ conn.commit()
+
+ sample_hotels = [
+ ("Grand Hyatt", "New York", 200, 150, 250.00),
+ ("The Plaza Hotel", "New York", 150, 100, 350.00),
+ ("Hilton Chicago", "Chicago", 300, 250, 180.00),
+ ("Marriott Marquis", "San Francisco", 250, 200, 220.00),
+ ]
+ cursor.executemany(
+ """
+ INSERT INTO hotels (name, location, total_rooms, available_rooms, price_per_night)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ sample_hotels,
+ )
+ conn.commit()
+
+ initial_bookings_data = [
+ (1, "Alice Smith", "2025-08-10", "2025-08-15", 1, 1250.00),
+ (3, "Bob Johnson", "2025-09-01", "2025-09-03", 2, 720.00),
+ ]
+ for booking_data in initial_bookings_data:
+ cursor.execute(
+ """
+ INSERT INTO bookings (hotel_id, guest_name, check_in_date, check_out_date, num_rooms, total_price)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ booking_data,
+ )
+ booking_id_int = cursor.lastrowid
+ custom_id = f"HB-{booking_id_int}"
+ cursor.execute(
+ "UPDATE bookings SET custom_booking_id = ? WHERE id = ?",
+ (custom_id, booking_id_int),
+ )
+ conn.commit()
+ except sqlite3.Error as e:
+ if conn:
+ conn.rollback()
+ finally:
+ if conn:
+ conn.close()
+
+ def is_token_valid(self, conn, token):
+ """Checks if a given token is valid and not expired."""
+ logging.info("not implemented")
+ return True
+
+ def get_available_hotels(self, cursor, location=None):
+ """Retrieves a list of available hotels, optionally filtered by location."""
+ query = (
+ "SELECT id, name, location, available_rooms, price_per_night FROM"
+ " hotels WHERE available_rooms > 0"
+ )
+ params = []
+ if location:
+ query += " AND location LIKE ?"
+ params.append(f"%{location}%")
+ try:
+ cursor.execute(query, params)
+ rows = cursor.fetchall()
+ return [dict(row) for row in rows], None
+ except sqlite3.Error as e:
+ return None, f"Error getting available hotels: {e}"
+
+ def book_a_room(
+ self, conn, hotel_id, guest_name, check_in_date, check_out_date, num_rooms
+ ):
+ """Books a room in a specified hotel."""
+ cursor = conn.cursor()
+ try:
+ cursor.execute(
+ "SELECT available_rooms, price_per_night FROM hotels WHERE id = ?",
+ (hotel_id,),
+ )
+ hotel_info = cursor.fetchone()
+
+ if not hotel_info:
+ return None, f"Hotel with ID {hotel_id} not found."
+
+ available_rooms, price_per_night = (
+ hotel_info["available_rooms"],
+ hotel_info["price_per_night"],
+ )
+ if available_rooms < num_rooms:
+ return (
+ None,
+ (
+ f"Not enough rooms available at hotel ID {hotel_id}. Available:"
+ f" {available_rooms}, Requested: {num_rooms}"
+ ),
+ )
+
+ try:
+ check_in_dt = datetime.datetime.strptime(check_in_date, "%Y-%m-%d")
+ check_out_dt = datetime.datetime.strptime(check_out_date, "%Y-%m-%d")
+ except ValueError:
+ return None, "Invalid date format. Please use YYYY-MM-DD."
+
+ num_nights = (check_out_dt - check_in_dt).days
+ if num_nights <= 0:
+ return None, "Check-out date must be after check-in date."
+
+ total_price = num_rooms * price_per_night * num_nights
+
+ cursor.execute(
+ "UPDATE hotels SET available_rooms = ? WHERE id = ?",
+ (available_rooms - num_rooms, hotel_id),
+ )
+
+ cursor.execute(
+ """
+ INSERT INTO bookings (hotel_id, guest_name, check_in_date, check_out_date, num_rooms, total_price)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """,
+ (
+ hotel_id,
+ guest_name,
+ check_in_date,
+ check_out_date,
+ num_rooms,
+ total_price,
+ ),
+ )
+
+ booking_id_int = cursor.lastrowid
+ custom_booking_id = f"HB-{booking_id_int}"
+
+ cursor.execute(
+ "UPDATE bookings SET custom_booking_id = ? WHERE id = ?",
+ (custom_booking_id, booking_id_int),
+ )
+
+ conn.commit()
+ return custom_booking_id, None
+ except sqlite3.Error as e:
+ conn.rollback()
+ return None, f"Error booking room: {e}"
+
+ def get_booking_details(self, cursor, booking_id=None, guest_name=None):
+ """Retrieves details for a specific booking."""
+ query = """
+ SELECT
+ b.custom_booking_id,
+ h.name AS hotel_name,
+ h.location AS hotel_location,
+ b.guest_name,
+ b.check_in_date,
+ b.check_out_date,
+ b.num_rooms,
+ b.total_price
+ FROM
+ bookings b
+ JOIN
+ hotels h ON b.hotel_id = h.id
+ """
+ params = []
+ result_type = "single"
+
+ if booking_id:
+ query += " WHERE b.custom_booking_id = ?"
+ params.append(booking_id)
+ elif guest_name:
+ query += " WHERE LOWER(b.guest_name) LIKE LOWER(?)"
+ params.append(f"%{guest_name}%")
+ result_type = "list"
+ else:
+ return (
+ None,
+ (
+ "Please provide either a booking ID or a guest name to retrieve"
+ " booking details."
+ ),
+ )
+
+ try:
+ cursor.execute(query, params)
+ rows = cursor.fetchall()
+
+ if not rows:
+ return (
+ None,
+ (
+ f"No booking found for the given criteria (ID: {booking_id},"
+ f" Name: {guest_name})."
+ ),
+ )
+
+ bookings = [dict(row) for row in rows]
+ return bookings if result_type == "list" else bookings[0], None
+ except sqlite3.Error as e:
+ return None, f"Error getting booking details: {e}"
diff --git a/contributing/samples/authn-adk-all-in-one/hotel_booker_app/main.py b/contributing/samples/authn-adk-all-in-one/hotel_booker_app/main.py
new file mode 100644
index 0000000000..bd4c771140
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/hotel_booker_app/main.py
@@ -0,0 +1,266 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from functools import wraps
+import os
+import sqlite3
+
+from dotenv import load_dotenv
+from flask import Flask
+from flask import g
+from flask import jsonify
+from flask import request
+from hotelbooker_core import HotelBooker
+import jwt
+import requests
+
+# Load environment variables from .env file
+load_dotenv()
+
+app = Flask(__name__)
+# Instantiate the core logic class
+hotel_booker = HotelBooker()
+app.config["DATABASE"] = hotel_booker.db_name
+
+OIDC_CONFIG_URL = os.environ.get(
+ "OIDC_CONFIG_URL", "http://localhost:5000/.well-known/openid-configuration"
+)
+
+# Cache for OIDC discovery and JWKS
+oidc_config = None
+jwks = None
+
+
+def get_oidc_config():
+ """Fetches and caches the OIDC configuration."""
+ global oidc_config
+ if oidc_config is None:
+ try:
+ response = requests.get(OIDC_CONFIG_URL)
+ response.raise_for_status()
+ oidc_config = response.json()
+ except requests.exceptions.RequestException as e:
+ return None, f"Error fetching OIDC config: {e}"
+ return oidc_config, None
+
+
+def get_jwks():
+ """Fetches and caches the JSON Web Key Set (JWKS)."""
+ global jwks
+ if jwks is None:
+ config, error = get_oidc_config()
+ if error:
+ return None, error
+ jwks_uri = config.get("jwks_uri")
+ if not jwks_uri:
+ return None, "jwks_uri not found in OIDC configuration."
+ try:
+ response = requests.get(jwks_uri)
+ response.raise_for_status()
+ jwks = response.json()
+ except requests.exceptions.RequestException as e:
+ return None, f"Error fetching JWKS: {e}"
+ return jwks, None
+
+
+def get_db():
+ """Manages a per-request database connection."""
+ if "db" not in g:
+ g.db = sqlite3.connect(app.config["DATABASE"])
+ g.db.row_factory = sqlite3.Row
+ return g.db
+
+
+@app.teardown_appcontext
+def close_db(exception):
+ db = g.pop("db", None)
+ if db is not None:
+ db.close()
+
+
+def is_token_valid(token: str):
+ """
+ Validates a JWT token using the public key from the OIDC jwks_uri.
+ """
+ if not token:
+ return False, "Token is empty."
+
+ jwks_data, error = get_jwks()
+ if error:
+ return False, f"Failed to get JWKS: {error}"
+
+ try:
+ header = jwt.get_unverified_header(token)
+ kid = header.get("kid")
+ if not kid:
+ return False, "Token header missing 'kid'."
+
+ key = next(
+ (k for k in jwks_data.get("keys", []) if k.get("kid") == kid), None
+ )
+ if not key:
+ return False, "No matching key found in JWKS."
+
+ public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
+
+ # The decoding happens just so that we are able to
+ # check if there were any exception decoding the token
+ # which indicate it being not valid.
+ # Also you could have verify_aud and verify_iss as False
+ # But when they are true issuer and audience are needed in the jwt.decode call
+ # they are checked against the values from the token
+ # idealy token validation should also check whether the API being called is part of
+ # audience so for example localhost:8081/api should cover localhost:8081/api/hotels
+ # but should not cover localhost:8000/admin
+ # so this middleware (decorator - is_token_valid, can check the request url and do that check, but we are
+ # skipping that as the audience will always be localhost:8081)
+ decoded_token = jwt.decode(
+ token,
+ key=public_key,
+ issuer="http://localhost:5000",
+ audience="http://localhost:8081",
+ algorithms=[header["alg"]],
+ options={"verify_exp": True, "verify_aud": True, "verify_iss": True},
+ )
+ return True, "Token is valid."
+ except jwt.ExpiredSignatureError:
+ return False, "Token has expired."
+ except jwt.InvalidAudienceError:
+ return False, "Invalid audience."
+ except jwt.InvalidIssuerError:
+ return False, "Invalid issuer."
+ except jwt.InvalidTokenError as e:
+ return False, f"Invalid token: {e}"
+ except Exception as e:
+ return False, f"An unexpected error occurred during token validation: {e}"
+
+
+# Decorator to check for a valid access token on protected routes
+def token_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ auth_header = request.headers.get("Authorization")
+ if not auth_header or not auth_header.startswith("Bearer "):
+ return {
+ "error": True,
+ "data": None,
+ "message": "Missing or invalid Authorization header.",
+ }, 401
+
+ token = auth_header.split(" ")[1]
+ is_valid, message = is_token_valid(token)
+
+ if not is_valid:
+ return {"error": True, "data": None, "message": message}, 401
+
+ return f(*args, **kwargs)
+
+ return decorated_function
+
+
+@app.route("/hotels", methods=["GET"])
+@token_required
+def get_hotels():
+ location = request.args.get("location")
+ hotels, error_message = hotel_booker.get_available_hotels(
+ get_db().cursor(), location
+ )
+
+ if hotels is not None:
+ return (
+ jsonify({
+ "error": False,
+ "data": hotels,
+ "message": "Successfully retrieved hotels.",
+ }),
+ 200,
+ )
+ else:
+ return jsonify({"error": True, "data": None, "message": error_message}), 500
+
+
+@app.route("/book", methods=["POST"])
+@token_required
+def book_room():
+ conn = get_db()
+ data = request.json
+ hotel_id = data.get("hotel_id")
+ guest_name = data.get("guest_name")
+ check_in_date = data.get("check_in_date")
+ check_out_date = data.get("check_out_date")
+ num_rooms = data.get("num_rooms")
+
+ if not all([hotel_id, guest_name, check_in_date, check_out_date, num_rooms]):
+ return (
+ jsonify({
+ "error": True,
+ "data": None,
+ "message": "Missing required booking information.",
+ }),
+ 400,
+ )
+
+ booking_id, error_message = hotel_booker.book_a_room(
+ conn, hotel_id, guest_name, check_in_date, check_out_date, num_rooms
+ )
+
+ if booking_id:
+ return (
+ jsonify({
+ "error": False,
+ "data": {"booking_id": booking_id},
+ "message": "Booking successful!",
+ }),
+ 200,
+ )
+ else:
+ return jsonify({"error": True, "data": None, "message": error_message}), 400
+
+
+@app.route("/booking_details", methods=["GET"])
+@token_required
+def get_details():
+ conn = get_db()
+ booking_id = request.args.get("booking_id")
+ guest_name = request.args.get("guest_name")
+
+ if not booking_id and not guest_name:
+ return (
+ jsonify({
+ "error": True,
+ "data": None,
+ "message": "Please provide either a booking ID or a guest name.",
+ }),
+ 400,
+ )
+
+ details, error_message = hotel_booker.get_booking_details(
+ get_db().cursor(), booking_id=booking_id, guest_name=guest_name
+ )
+
+ if details:
+ return (
+ jsonify({
+ "error": False,
+ "data": details,
+ "message": "Booking details retrieved successfully.",
+ }),
+ 200,
+ )
+ else:
+ return jsonify({"error": True, "data": None, "message": error_message}), 404
+
+
+if __name__ == "__main__":
+ app.run(debug=True, port=8081)
diff --git a/contributing/samples/authn-adk-all-in-one/hotel_booker_app/openapi.yaml b/contributing/samples/authn-adk-all-in-one/hotel_booker_app/openapi.yaml
new file mode 100644
index 0000000000..8adda49623
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/hotel_booker_app/openapi.yaml
@@ -0,0 +1,229 @@
+openapi: 3.0.0
+info:
+ title: Hotel Booker API
+ description: A simple API for managing hotel bookings, with a custom client credentials authentication flow.
+ version: 1.0.0
+servers:
+ - url: http://127.0.0.1:8081
+paths:
+ /hotels:
+ get:
+ summary: Get available hotels
+ description: Retrieves a list of available hotels, optionally filtered by location.
+ security:
+ - BearerAuth: []
+ parameters:
+ - in: query
+ name: location
+ schema:
+ type: string
+ description: The city to filter hotels by (e.g., 'New York').
+ responses:
+ '200':
+ description: Successfully retrieved hotels.
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: boolean
+ example: false
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/Hotel'
+ message:
+ type: string
+ example: "Successfully retrieved hotels."
+ '401':
+ description: Unauthorized. Invalid or expired token.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /book:
+ post:
+ summary: Book a room
+ description: Books a room in a specified hotel.
+ security:
+ - BearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/BookingRequest'
+ responses:
+ '200':
+ description: Booking successful.
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: boolean
+ example: false
+ data:
+ type: object
+ properties:
+ booking_id:
+ type: string
+ example: "HB-1"
+ message:
+ type: string
+ example: "Booking successful!"
+ '400':
+ description: Bad request. Missing information or invalid booking details.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Unauthorized. Invalid or expired token.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /booking_details:
+ get:
+ summary: Get booking details
+ description: Retrieves details for a specific booking by ID or guest name.
+ security:
+ - BearerAuth: []
+ parameters:
+ - in: query
+ name: booking_id
+ schema:
+ type: string
+ description: The custom booking ID (e.g., 'HB-1').
+ - in: query
+ name: guest_name
+ schema:
+ type: string
+ description: The name of the guest to search for (partial and case-insensitive).
+ responses:
+ '200':
+ description: Booking details retrieved successfully.
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ error:
+ type: boolean
+ example: false
+ data:
+ type: object
+ properties:
+ custom_booking_id:
+ type: string
+ example: "HB-1"
+ hotel_name:
+ type: string
+ example: "Grand Hyatt"
+ hotel_location:
+ type: string
+ example: "New York"
+ guest_name:
+ type: string
+ example: "John Doe"
+ check_in_date:
+ type: string
+ example: "2025-10-01"
+ check_out_date:
+ type: string
+ example: "2025-10-05"
+ num_rooms:
+ type: integer
+ example: 1
+ total_price:
+ type: number
+ format: float
+ example: 1000.0
+ message:
+ type: string
+ example: "Booking details retrieved successfully."
+ '400':
+ description: Bad request. Missing parameters.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Unauthorized. Invalid or expired token.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Booking not found.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+components:
+ securitySchemes:
+ BearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: CustomAuthToken
+ schemas:
+ ErrorResponse:
+ type: object
+ properties:
+ error:
+ type: boolean
+ example: true
+ data:
+ type: object
+ nullable: true
+ message:
+ type: string
+ example: "Invalid access token."
+ Hotel:
+ type: object
+ properties:
+ id:
+ type: integer
+ example: 1
+ name:
+ type: string
+ example: "Grand Hyatt"
+ location:
+ type: string
+ example: "New York"
+ available_rooms:
+ type: integer
+ example: 10
+ price_per_night:
+ type: number
+ format: float
+ example: 250.0
+ BookingRequest:
+ type: object
+ properties:
+ hotel_id:
+ type: integer
+ example: 1
+ guest_name:
+ type: string
+ example: "John Doe"
+ check_in_date:
+ type: string
+ format: date
+ example: "2025-10-01"
+ check_out_date:
+ type: string
+ format: date
+ example: "2025-10-05"
+ num_rooms:
+ type: integer
+ example: 1
+ required:
+ - hotel_id
+ - guest_name
+ - check_in_date
+ - check_out_date
+ - num_rooms
\ No newline at end of file
diff --git a/contributing/samples/authn-adk-all-in-one/idp/app.py b/contributing/samples/authn-adk-all-in-one/idp/app.py
new file mode 100644
index 0000000000..86388efc1e
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/idp/app.py
@@ -0,0 +1,569 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+from datetime import datetime
+from datetime import timedelta
+from datetime import timezone
+import hashlib
+import json
+import logging
+import os
+import time
+from urllib.parse import urlencode
+from urllib.parse import urlparse
+from urllib.parse import urlunparse
+
+from dotenv import load_dotenv
+from flask import Flask
+from flask import jsonify
+from flask import redirect
+from flask import render_template
+from flask import request
+from flask import session
+from flask_cors import CORS
+import jwt
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+# Load environment variables from .env file
+load_dotenv()
+
+app = Flask(__name__, template_folder="templates")
+CORS(app)
+app.secret_key = os.urandom(24)
+
+# Load JWKS and private key from files and environment variables
+try:
+ with open("jwks.json", "r") as f:
+ JWKS = json.load(f)
+except FileNotFoundError:
+ JWKS = None
+ logging.error(
+ "jwks.json not found. The server will not be able to generate JWTs."
+ )
+
+PRIVATE_KEY = os.getenv("PRIVATE_KEY")
+GENERATE_JWT = os.getenv("GENERATE_JWT", "true").lower() == "true"
+
+if GENERATE_JWT and not PRIVATE_KEY:
+ raise ValueError(
+ "PRIVATE_KEY environment variable must be set when GENERATE_JWT is true."
+ )
+
+# A simple user registry for demonstration purposes
+USER_REGISTRY = {
+ "john.doe": {
+ "password": "password123",
+ "sub": "john.doe",
+ "profile": "I am John Doe.",
+ "email": "john.doe@example.com",
+ },
+ "jane.doe": {
+ "password": "password123",
+ "sub": "jane.doe",
+ "profile": "I am Jane Doe.",
+ "email": "jane.doe@example.com",
+ },
+}
+
+OPENID_CONFIG = {
+ "issuer": "http://localhost:5000",
+ "authorization_endpoint": "http://localhost:5000/authorize",
+ "token_endpoint": "http://localhost:5000/generate-token",
+ "jwks_uri": "http://localhost:5000/jwks.json",
+ "response_types_supported": ["code", "token", "id_token", "id_token token"],
+ "grant_types_supported": [
+ "client_credentials",
+ "implicit",
+ "authorization_code",
+ ],
+ "token_endpoint_auth_methods_supported": ["client_secret_post"],
+ "scopes_supported": ["openid", "profile", "email", "api:read", "api:write"],
+ "id_token_signing_alg_values_supported": ["RS256"],
+ "subject_types_supported": ["public"],
+ "code_challenge_methods_supported": ["S256"],
+}
+
+# A simple client registry
+CLIENT_REGISTRY = {
+ "abc123": {
+ "client_secret": "secret123",
+ "allowed_scopes": [
+ "api:read",
+ "api:write",
+ "openid",
+ "profile",
+ "email",
+ ],
+ "redirect_uri": [
+ "http://localhost:8081/callback_implicit.html",
+ "http://localhost:8081/callback_authcode.html",
+ "http://localhost:8081/callback_pkce.html",
+ "http://localhost:8000/dev-ui/",
+ ],
+ "response_types": ["token", "id_token", "code"],
+ "grant_types": ["client_credentials", "implicit", "authorization_code"],
+ "client_name": "ADK Agent",
+ }
+}
+
+# A simple "database" to store temporary authorization codes
+AUTHORIZATION_CODES = {}
+
+
+def generate_jwt(payload, key, alg="RS256"):
+ if not JWKS:
+ raise ValueError("JWKS not loaded, cannot generate JWT.")
+
+ kid = JWKS["keys"][0]["kid"]
+ headers = {"kid": kid, "alg": alg}
+
+ return jwt.encode(payload, key, algorithm=alg, headers=headers)
+
+
+def create_access_token(client_id, scopes, user_sub=None):
+ if GENERATE_JWT:
+ payload = {
+ "iss": "http://localhost:5000", # who issued this token?
+ # aud - What client API is this token for? - please check comment in hotel booker is_token_valid
+ # ideally the reqeust's resource parameter (part of OAuth spec extension)
+ # Here is an example of such request inbound to this IDP
+ # GET http://localhost:5000/authorize?
+ # response_type=code&
+ # client_id=client123&
+ # redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fdev-ui&
+ # scope=openid%20profile%20api%3Aread&
+ # state=XYZ789&
+ # resource=http%3A%2F%2Flocalhost%3A8081%2Fapi
+ "aud": "http://localhost:8081",
+ "sub": user_sub if user_sub else client_id,
+ "exp": (
+ datetime.now(timezone.utc).timestamp()
+ + timedelta(hours=1).total_seconds()
+ ),
+ "iat": datetime.now(timezone.utc).timestamp(),
+ "scope": " ".join(scopes),
+ }
+ return generate_jwt(payload, PRIVATE_KEY)
+ else:
+ return os.urandom(32).hex()
+
+
+def create_id_token(client_id, user_data, scopes, nonce=None):
+ if not GENERATE_JWT:
+ return None
+
+ payload = {
+ "iss": "http://localhost:5000",
+ "sub": user_data.get("sub"),
+ "aud": client_id,
+ "exp": (
+ datetime.now(timezone.utc).timestamp()
+ + timedelta(hours=1).total_seconds()
+ ),
+ "iat": datetime.now(timezone.utc).timestamp(),
+ "auth_time": datetime.now(timezone.utc).timestamp(),
+ "email": user_data.get("email"),
+ "profile": user_data.get("profile"),
+ "scope": " ".join(scopes),
+ }
+ if nonce:
+ payload["nonce"] = nonce
+ return generate_jwt(payload, PRIVATE_KEY)
+
+
+@app.route("/.well-known/openid-configuration")
+def openid_configuration():
+ return jsonify(OPENID_CONFIG)
+
+
+@app.route("/jwks.json")
+def jwks_endpoint():
+ return jsonify(JWKS)
+
+
+@app.route("/authorize", methods=["GET", "POST"])
+def authorize():
+ if request.method == "GET":
+ client_id = request.args.get("client_id")
+ redirect_uri = request.args.get("redirect_uri")
+ client = CLIENT_REGISTRY.get(client_id)
+
+ if not client or redirect_uri not in client.get("redirect_uri", []):
+ return "Invalid client or redirect URI", 400
+
+ auth_request = request.args.to_dict()
+ auth_request["client_name"] = client["client_name"]
+ session["auth_request"] = auth_request
+ return render_template("login.html", client_name=client["client_name"])
+
+ if request.method == "POST":
+ username = request.form.get("username")
+ password = request.form.get("password")
+ auth_request = session.get("auth_request")
+ user = USER_REGISTRY.get(username)
+
+ if not user or user["password"] != password:
+ return render_template(
+ "login.html",
+ error="Invalid username or password",
+ client_name=auth_request["client_name"],
+ )
+
+ session["user"] = user
+
+ return render_template("consent.html", auth_request=auth_request)
+
+
+@app.route("/consent", methods=["POST"])
+def consent():
+ auth_request = session.get("auth_request")
+ user = session.get("user")
+
+ if not auth_request or not user:
+ return "Invalid session", 400
+
+ logging.debug(f"consent screen POST call auth_request => {auth_request}")
+ client_id = auth_request.get("client_id")
+ redirect_uri = auth_request.get("redirect_uri")
+ scopes = auth_request.get("scope", "").split(" ")
+ response_type = auth_request.get("response_type")
+ state = auth_request.get("state")
+
+ if request.form.get("consent") == "true":
+ if response_type == "token id_token" or response_type == "id_token token":
+ access_token = create_access_token(client_id, scopes, user.get("sub"))
+ id_token = create_id_token(client_id, user, scopes)
+
+ parsed = urlparse(redirect_uri)
+ fragment_params = {
+ "access_token": access_token,
+ "id_token": id_token,
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": " ".join(scopes),
+ "state": state,
+ }
+ new_uri = urlunparse((
+ parsed.scheme,
+ parsed.netloc,
+ parsed.path,
+ parsed.params,
+ parsed.query,
+ urlencode(fragment_params),
+ ))
+
+ session.pop("auth_request", None)
+ session.pop("user", None)
+ return redirect(new_uri)
+
+ elif response_type == "code":
+ auth_code = os.urandom(16).hex()
+ AUTHORIZATION_CODES[auth_code] = {
+ "client_id": client_id,
+ "user": user,
+ "scopes": scopes,
+ "redirect_uri": redirect_uri,
+ "expires_at": time.time() + 300,
+ "code_challenge": auth_request.get("code_challenge"),
+ "code_challenge_method": auth_request.get("code_challenge_method"),
+ }
+
+ parsed = urlparse(redirect_uri)
+ query_params = {"code": auth_code, "state": state}
+ new_uri = urlunparse((
+ parsed.scheme,
+ parsed.netloc,
+ parsed.path,
+ parsed.params,
+ urlencode(query_params),
+ parsed.fragment,
+ ))
+
+ session.pop("auth_request", None)
+ session.pop("user", None)
+ return redirect(new_uri)
+
+ # User denied consent or invalid response
+ parsed = urlparse(redirect_uri)
+ query_params = {
+ "error": "access_denied",
+ "error_description": "User denied access",
+ "state": state,
+ }
+ new_uri = urlunparse((
+ parsed.scheme,
+ parsed.netloc,
+ parsed.path,
+ parsed.params,
+ urlencode(query_params),
+ parsed.fragment,
+ ))
+ return redirect(new_uri)
+
+
+@app.route("/generate-token", methods=["POST"])
+def generate_token():
+ auth_header = request.headers.get("Authorization")
+ client_id = None
+ client_secret = None
+
+ # Client id and secret can come in body or in header (Authorization : Basic base64(client_id_value:client_secret_value))
+ if auth_header and auth_header.startswith("Basic "):
+ try:
+ encoded_credentials = auth_header.split(" ")[1]
+ decoded_credentials = base64.b64decode(encoded_credentials).decode(
+ "utf-8"
+ )
+ client_id, client_secret = decoded_credentials.split(":", 1)
+ except (IndexError, ValueError):
+ pass # Fallback to form data
+
+ if not client_id or not client_secret:
+ client_id = request.form.get("client_id")
+ client_secret = request.form.get("client_secret")
+
+ grant_type = request.form.get("grant_type")
+
+ # logging.debug(f"Grant Type = {grant_type}")
+ # logging.debug(f"Request => {request.__dict__}")
+
+ client = CLIENT_REGISTRY.get(client_id)
+
+ if not client:
+ logging.error(f"invlid client {client_id}")
+ return (
+ jsonify(
+ {"error": "invalid_client", "error_description": "Client not found"}
+ ),
+ 401,
+ )
+
+ if client["client_secret"] != client_secret:
+ logging.error(f"Client authentication failed")
+ return (
+ jsonify({
+ "error": "invalid_client",
+ "error_description": "Client authentication failed",
+ }),
+ 401,
+ )
+
+ if grant_type == "client_credentials":
+ scopes = request.form.get("scope", "").split(" ")
+ for scope in scopes:
+ if scope not in client["allowed_scopes"]:
+ logging.error(f"Invalid_scope")
+ return jsonify({"error": "invalid_scope"}), 400
+
+ access_token = create_access_token(client_id, scopes)
+
+ return jsonify({
+ "access_token": access_token,
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": " ".join(scopes),
+ })
+
+ elif grant_type == "authorization_code":
+ code = request.form.get("code")
+ redirect_uri = request.form.get("redirect_uri")
+ code_verifier = request.form.get("code_verifier")
+
+ auth_code_data = AUTHORIZATION_CODES.pop(code, None)
+
+ if not auth_code_data:
+ logging.error(f"Invalid or expired authorization code.")
+ return (
+ jsonify({
+ "error": "invalid_grant",
+ "error_description": "Invalid or expired authorization code.",
+ }),
+ 400,
+ )
+
+ if (
+ auth_code_data["redirect_uri"] != redirect_uri
+ or auth_code_data["client_id"] != client_id
+ ):
+ logging.error(f"Redirect URI or client ID mismatch")
+ return (
+ jsonify({
+ "error": "invalid_grant",
+ "error_description": "Redirect URI or client ID mismatch",
+ }),
+ 400,
+ )
+
+ if time.time() > auth_code_data["expires_at"]:
+ logging.error(f"Authorization code has expired")
+ return (
+ jsonify({
+ "error": "invalid_grant",
+ "error_description": "Authorization code has expired",
+ }),
+ 400,
+ )
+
+ if "code_challenge" in auth_code_data and auth_code_data["code_challenge"]:
+ if not code_verifier:
+ logging.error(f"Code verifier is required for PKCE flow.")
+ return (
+ jsonify({
+ "error": "invalid_request",
+ "error_description": "Code verifier is required for PKCE flow.",
+ }),
+ 400,
+ )
+
+ computed_challenge = (
+ base64.urlsafe_b64encode(
+ hashlib.sha256(code_verifier.encode("utf-8")).digest()
+ )
+ .decode("utf-8")
+ .replace("=", "")
+ )
+ if computed_challenge != auth_code_data["code_challenge"]:
+ logging.error(f"PKCE code challenge mismatch.")
+ return (
+ jsonify({
+ "error": "invalid_grant",
+ "error_description": "PKCE code challenge mismatch.",
+ }),
+ 400,
+ )
+
+ # Create tokens based on the stored user data
+ user = auth_code_data["user"]
+ access_token = create_access_token(
+ client_id, auth_code_data["scopes"], user["sub"]
+ )
+ id_token = create_id_token(client_id, user, auth_code_data["scopes"])
+
+ return jsonify({
+ "access_token": access_token,
+ "id_token": id_token,
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "scope": " ".join(auth_code_data["scopes"]),
+ })
+ logging.error(f"Unsupported_grant_type")
+ return jsonify({"error": "unsupported_grant_type"}), 400
+
+
+@app.route("/")
+def index():
+ return render_template("index.html")
+
+
+# --- ADMIN ROUTES START ---
+@app.route("/admin")
+def admin_portal():
+ return render_template(
+ "admin.html",
+ openid_config=OPENID_CONFIG,
+ user_registry=json.dumps(USER_REGISTRY),
+ client_registry=json.dumps(CLIENT_REGISTRY),
+ )
+
+
+@app.route("/admin/update-config", methods=["POST"])
+def admin_update_config():
+ try:
+ data = request.json
+ OPENID_CONFIG["issuer"] = data.get("issuer", OPENID_CONFIG["issuer"])
+ OPENID_CONFIG["authorization_endpoint"] = data.get(
+ "authorization_endpoint", OPENID_CONFIG["authorization_endpoint"]
+ )
+ OPENID_CONFIG["jwks_uri"] = data.get("jwks_uri", OPENID_CONFIG["jwks_uri"])
+ OPENID_CONFIG["token_endpoint"] = data.get(
+ "token_endpoint", OPENID_CONFIG["token_endpoint"]
+ )
+ return jsonify(
+ {"success": True, "message": "OpenID configuration updated."}
+ )
+ except Exception as e:
+ return jsonify({"success": False, "message": str(e)}), 400
+
+
+@app.route("/admin/add-user", methods=["POST"])
+def admin_add_user():
+ try:
+ data = request.json
+ username = data.get("username")
+ password = data.get("password")
+ sub = data.get("sub")
+ profile = data.get("profile")
+ email = data.get("email")
+
+ if not username or not password or not sub:
+ return (
+ jsonify({
+ "success": False,
+ "message": "Username, password, and sub are required.",
+ }),
+ 400,
+ )
+
+ USER_REGISTRY[username] = {
+ "password": password,
+ "sub": sub,
+ "profile": profile,
+ "email": email,
+ }
+ return jsonify({"success": True, "message": f"User '{username}' added."})
+ except Exception as e:
+ return jsonify({"success": False, "message": str(e)}), 400
+
+
+@app.route("/admin/add-client", methods=["POST"])
+def admin_add_client():
+ try:
+ data = request.json
+ client_id = data.get("client_id")
+ client_secret = data.get("client_secret")
+ allowed_scopes = data.get("allowed_scopes", "").split()
+ redirect_uri = data.get("redirect_uri", "").split()
+ response_types = data.get("response_types", "").split()
+ grant_types = data.get("grant_types", "").split()
+ client_name = data.get("client_name")
+
+ if not client_id or not client_name:
+ return (
+ jsonify({
+ "success": False,
+ "message": "Client ID and Client Name are required.",
+ }),
+ 400,
+ )
+
+ CLIENT_REGISTRY[client_id] = {
+ "client_secret": client_secret,
+ "allowed_scopes": allowed_scopes,
+ "redirect_uri": redirect_uri,
+ "response_types": response_types,
+ "grant_types": grant_types,
+ "client_name": client_name,
+ }
+ return jsonify({"success": True, "message": f"Client '{client_id}' added."})
+ except Exception as e:
+ return jsonify({"success": False, "message": str(e)}), 400
+
+
+# --- ADMIN ROUTES END ---
+
+if __name__ == "__main__":
+ app.run(port=5000)
diff --git a/contributing/samples/authn-adk-all-in-one/idp/sample.env b/contributing/samples/authn-adk-all-in-one/idp/sample.env
new file mode 100644
index 0000000000..825c230807
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/idp/sample.env
@@ -0,0 +1,15 @@
+GENERATE_JWT=true
+
+# Steps -
+# 1. ssh-keygen -t rsa -b 2048 -m PEM -f private_key.pem
+# 2. When asked about passphrase please press enter (empty passphrase)
+# 3. openssl rsa -in private_key.pem -pubout > pubkey.pub
+# 4. Generate the jwks.json content using https://jwkset.com/generate and this public key (choose key algorithm RS256 and Key use Signature)
+# 5. Update the jwks.json with the jwks key created in 4
+
+# Add key from step 1 here
+# make sure you add it in single line. You can use the following command to get a single line key
+# cat private_key.pem | tr -d "\n"
+
+PRIVATE_KEY=""
+
diff --git a/contributing/samples/authn-adk-all-in-one/idp/sample.jwks.json b/contributing/samples/authn-adk-all-in-one/idp/sample.jwks.json
new file mode 100644
index 0000000000..127a7b346b
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/idp/sample.jwks.json
@@ -0,0 +1,5 @@
+{
+ "keys": [
+ "Replace with JWKS from jwkset.com/generate"
+ ]
+}
\ No newline at end of file
diff --git a/contributing/samples/authn-adk-all-in-one/idp/templates/admin.html b/contributing/samples/authn-adk-all-in-one/idp/templates/admin.html
new file mode 100644
index 0000000000..e7b0fb5748
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/idp/templates/admin.html
@@ -0,0 +1,210 @@
+
+
+
+
+
+ IDP Admin Portal
+
+
+
+
+
+
+
IDP Administration Portal
+
+
+
+
+
OpenID Configuration
+
+
+
+
+
+
+
+
+
+
User Registry
+
{{ user_registry }}
+
Add New User
+
+
+
+
+
Client Registry
+
{{ client_registry }}
+
Add New Client
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/contributing/samples/authn-adk-all-in-one/idp/templates/consent.html b/contributing/samples/authn-adk-all-in-one/idp/templates/consent.html
new file mode 100644
index 0000000000..5996353483
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/idp/templates/consent.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+Consent
+
+
+
+
+
+
+
Authorize Application
+
The application {{ auth_request.get('client_name', 'Unknown Client') }} is requesting access to your data. Do you want to grant it?
+
+
Requested Scopes:
+
+ {% for scope in auth_request.get('scope', '').split() %}
+
- {{ scope }}
+ {% endfor %}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/contributing/samples/authn-adk-all-in-one/idp/templates/login.html b/contributing/samples/authn-adk-all-in-one/idp/templates/login.html
new file mode 100644
index 0000000000..c460ec41c1
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/idp/templates/login.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+Login
+
+
+
+
+
+
Log in
+
to continue to {{ client_name }}
+
+ {% if error %}
+
{{ error }}
+ {% endif %}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/contributing/samples/authn-adk-all-in-one/requirements.txt b/contributing/samples/authn-adk-all-in-one/requirements.txt
new file mode 100644
index 0000000000..6cd3c4bb52
--- /dev/null
+++ b/contributing/samples/authn-adk-all-in-one/requirements.txt
@@ -0,0 +1,6 @@
+google-adk==1.12
+Flask==3.1.1
+flask-cors==6.0.1
+python-dotenv==1.1.1
+PyJWT[crypto]==2.10.1
+requests==2.32.4
\ No newline at end of file