mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
chore: Added ADK Authentication End2End Samples
Merge https://github.com/google/adk-python/pull/2960 1. All in one authentication sample (has an IDP, Agent and the application) under `contributing/samples/authn-adk-all-in-one/` 2. Documented for all the steps. 3. OAuth 2.0 Authorization Code Grant type used by the agent. COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2960 from nikhilpurwant:main dfcc821602d265c4ae7cc42eb1f5739beaad6f87 PiperOrigin-RevId: 808672120
This commit is contained in:
committed by
Copybara-Service
parent
25958242db
commit
21c26f92d4
@@ -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
|
||||
```
|
||||
<details>
|
||||
|
||||
<summary><b>Screenshots</b></summary>
|
||||
Generating JWKS -
|
||||
|
||||

|
||||
|
||||
Updated `jwks.json` (notice the key is added in the existing array)
|
||||
|
||||

|
||||
|
||||
</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 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
|
||||
@@ -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 ...
|
||||
)
|
||||
+229
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
google-adk==1.12
|
||||
@@ -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
|
||||
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 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}"
|
||||
@@ -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)
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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=""
|
||||
|
||||
@@ -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==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
|
||||
Reference in New Issue
Block a user