chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,158 @@
## 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.
![alt](doc_images/adk-auth-all-in-one.svg)
### 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
1. A hotel booking application backend
1. A hotel assistant ADK agent (accessing the application using OpenAPI Tools)
### Details
You can read about the [Auth Code grant / flow type](https://developer.okta.com/blog/2018/04/10/oauth-authorization-code-grant-type) in detail. But for the purpose of this demo, following steps take place
1. The user asks the agent to find hotels in "New York".
1. Agent realizes (based on LLM response) that it needs to call a tool and that the tool needs authentication.
1. Agent redirects the user to the IDP's login page with callback / redirect URL back to ADK UI.
1. The user enters credentials (`john.doe` and `password123`) and accepts the consent.
1. The IDP sends the auth_code back to the redirect URL (from 3).
1. ADK then exchanges this auth_code for an access token.
1. ADK does the API call to get details on hotels and hands over that response to LLM, LLM formats the response.
1. ADK sends a response back to the User.
### Setting up and running
1. Clone this repository
1. 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
```
<details>
<summary><b>Screenshots</b></summary>
Generating JWKS -
![alt](doc_images/jwksgen.png)
Updated `jwks.json` (notice the key is added in the existing array)
![alt](doc_images/jwks_updated.png)
</details>
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.
@@ -0,0 +1,15 @@
# Copyright 2026 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
@@ -0,0 +1,65 @@
# Copyright 2026 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 ...
)
@@ -0,0 +1,243 @@
# Copyright 2026 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.
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
@@ -0,0 +1 @@
google-adk==2.2.0
@@ -0,0 +1,6 @@
# General Agent Configuration
GOOGLE_GENAI_USE_ENTERPRISE=False
GOOGLE_API_KEY=NOT_SET
GOOGLE_MODEL=gemini-flash-latest
OAUTH_CLIENT_ID=abc123
OAUTH_CLIENT_SECRET=secret123
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

@@ -0,0 +1,263 @@
# Copyright 2026 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}"
@@ -0,0 +1,266 @@
# Copyright 2026 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
# ideally 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)
@@ -0,0 +1,243 @@
# Copyright 2026 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.
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
@@ -0,0 +1,569 @@
# Copyright 2026 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 request'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"invalid 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)
@@ -0,0 +1,14 @@
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=""
@@ -0,0 +1,5 @@
{
"keys": [
"Replace with JWKS from jwkset.com/generate"
]
}
@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IDP Admin Portal</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.container {
max-width: 960px;
margin-top: 50px;
}
.card {
border-radius: 1rem;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
pre {
background-color: #e9ecef;
border-radius: 0.25rem;
padding: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="card p-4">
<h1 class="text-center mb-4">IDP Administration Portal</h1>
<div id="alert-container"></div>
<!-- OpenID Configuration Section -->
<section class="mb-5">
<h2>OpenID Configuration</h2>
<form id="configForm">
<div class="mb-3">
<label for="issuer" class="form-label">Issuer</label>
<input type="text" class="form-control" id="issuer" value="{{ openid_config.issuer }}">
</div>
<div class="mb-3">
<label for="authorization_endpoint" class="form-label">Authorization Endpoint</label>
<input type="text" class="form-control" id="authorization_endpoint" value="{{ openid_config.authorization_endpoint }}">
</div>
<div class="mb-3">
<label for="token_endpoint" class="form-label">Token Endpoint</label>
<input type="text" class="form-control" id="token_endpoint" value="{{ openid_config.token_endpoint }}">
</div>
<div class="mb-3">
<label for="jwks_uri" class="form-label">JWKS URI</label>
<input type="text" class="form-control" id="jwks_uri" value="{{ openid_config.jwks_uri }}">
</div>
<button type="submit" class="btn btn-primary">Update Configuration</button>
</form>
</section>
<hr>
<!-- Registries Section -->
<div class="row mt-4">
<!-- User Registry -->
<div class="col-md-6">
<h2>User Registry</h2>
<pre><code id="userRegistry">{{ user_registry }}</code></pre>
<h4 class="mt-4">Add New User</h4>
<form id="addUserForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required>
</div>
<div class="mb-3">
<label for="sub" class="form-label">Subject (sub)</label>
<input type="text" class="form-control" id="sub" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email">
</div>
<div class="mb-3">
<label for="profile" class="form-label">Profile</label>
<input type="text" class="form-control" id="profile">
</div>
<button type="submit" class="btn btn-success">Add User</button>
</form>
</div>
<!-- Client Registry -->
<div class="col-md-6">
<h2>Client Registry</h2>
<pre><code id="clientRegistry">{{ client_registry }}</code></pre>
<h4 class="mt-4">Add New Client</h4>
<form id="addClientForm">
<div class="mb-3">
<label for="client_id" class="form-label">Client ID</label>
<input type="text" class="form-control" id="client_id" required>
</div>
<div class="mb-3">
<label for="client_name" class="form-label">Client Name</label>
<input type="text" class="form-control" id="client_name" required>
</div>
<div class="mb-3">
<label for="client_secret" class="form-label">Client Secret</label>
<input type="text" class="form-control" id="client_secret">
</div>
<div class="mb-3">
<label for="redirect_uri" class="form-label">Redirect URIs (space-separated)</label>
<input type="text" class="form-control" id="redirect_uri">
</div>
<div class="mb-3">
<label for="allowed_scopes" class="form-label">Allowed Scopes (space-separated)</label>
<input type="text" class="form-control" id="allowed_scopes">
</div>
<div class="mb-3">
<label for="response_types" class="form-label">Response Types (space-separated)</label>
<input type="text" class="form-control" id="response_types">
</div>
<div class="mb-3">
<label for="grant_types" class="form-label">Grant Types (space-separated)</label>
<input type="text" class="form-control" id="grant_types">
</div>
<button type="submit" class="btn btn-success">Add Client</button>
</form>
</div>
</div>
</div>
</div>
<script>
const alertContainer = document.getElementById('alert-container');
function showAlert(message, type = 'success') {
const wrapper = document.createElement('div');
wrapper.innerHTML = [
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
` <div>${message}</div>`,
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
'</div>'
].join('');
alertContainer.append(wrapper);
}
// Update Config
document.getElementById('configForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
issuer: document.getElementById('issuer').value,
authorization_endpoint: document.getElementById('authorization_endpoint').value,
token_endpoint: document.getElementById('token_endpoint').value,
jwks_uri: document.getElementById('jwks_uri').value
};
const response = await fetch('/admin/update-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
showAlert(result.message, result.success ? 'success' : 'danger');
});
// Add User
document.getElementById('addUserForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
username: document.getElementById('username').value,
password: document.getElementById('password').value,
sub: document.getElementById('sub').value,
email: document.getElementById('email').value,
profile: document.getElementById('profile').value,
};
const response = await fetch('/admin/add-user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
showAlert(result.message, result.success ? 'success' : 'danger');
if (result.success) location.reload();
});
// Add Client
document.getElementById('addClientForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
client_id: document.getElementById('client_id').value,
client_name: document.getElementById('client_name').value,
client_secret: document.getElementById('client_secret').value,
redirect_uri: document.getElementById('redirect_uri').value,
allowed_scopes: document.getElementById('allowed_scopes').value,
response_types: document.getElementById('response_types').value,
grant_types: document.getElementById('grant_types').value,
};
const response = await fetch('/admin/add-client', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
showAlert(result.message, result.success ? 'success' : 'danger');
if (result.success) location.reload();
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Consent</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.consent-container {
max-width: 600px;
margin-top: 50px;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
background-color: #fff;
}
.scope-list li {
font-size: 1rem;
margin-bottom: 5px;
}
</style>
</head>
<body>
<div class="container d-flex justify-content-center">
<div class="consent-container">
<h2 class="text-center mb-4">Authorize Application</h2>
<p class="lead">The application <strong>{{ auth_request.get('client_name', 'Unknown Client') }}</strong> is requesting access to your data. Do you want to grant it?</p>
<h4 class="mt-4">Requested Scopes:</h4>
<ul class="list-unstyled scope-list">
{% for scope in auth_request.get('scope', '').split() %}
<li>- {{ scope }}</li>
{% endfor %}
</ul>
<form method="post" action="/consent" class="mt-4">
<input type="hidden" name="consent" value="true">
<div class="d-grid gap-2">
<button type="submit" class="btn btn-success">Allow Access</button>
<a href="/consent?consent=false" class="btn btn-outline-danger">Deny Access</a>
</div>
</form>
</div>
</div>
</body>
</html>
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
.login-container {
max-width: 400px;
padding: 2rem;
border-radius: 1rem;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="login-container">
<h1 class="text-center mb-4">Log in</h1>
<p class="text-center text-muted">to continue to {{ client_name }}</p>
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}
<form method="POST" action="{{ url_for('authorize') }}">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</body>
</html>
@@ -0,0 +1,6 @@
google-adk==2.2.0
Flask==3.1.3
flask-cors==6.0.1
python-dotenv==1.2.2
PyJWT[crypto]==2.10.1
requests==2.33.0