Skip to content

Commit

Permalink
example callback and related changes
Browse files Browse the repository at this point in the history
  • Loading branch information
jensens committed Jan 16, 2025
1 parent 4316324 commit ec03b83
Show file tree
Hide file tree
Showing 19 changed files with 444 additions and 439 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.*
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ __pycache__/
node_modules/
sandbox/
htmlcov/
.ipynb_checkpoints

# venv related
bin/
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ repos:
- id: mypy
args: ['--explicit-package-bases']
additional_dependencies:
- "types-requests"
- "pytest-stub"
- "google-auth-stubs"
- "pydantic"
- "pydantic_settings"
- "fastapi"
- "httpx"
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
Expand Down
30 changes: 30 additions & 0 deletions examples/edutap_wallet_google_example_callback/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Use a Python image with uv pre-installed
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim

# Enable bytecode compilation
ENV UV_COMPILE_BYTECODE=1

# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy

# copy project into container
COPY . /code

# install Python packages
RUN \
uv venv &&\
uv pip install --no-cache-dir --upgrade -e /code &&\
uv pip install --no-cache-dir --upgrade -e /code/examples/edutap_wallet_google_example_callback

# Place executables in the environment at the front of the path
ENV PATH="/.venv/bin:$PATH"

# Create a directory for logs
RUN mkdir /logs
ENV EDUTAP_WALLET_GOOGLE_EXAMPLE_CALLBACK_LOG_FILE=/logs/callback.log

# set working directory
WORKDIR /code/examples/edutap_wallet_google_example_callback

# run fastapi
CMD ["fastapi", "run", "edutap_wallet_google_example_callback.py", "--host", "0.0.0.0", "--port", "8080", "--proxy-headers"]
84 changes: 84 additions & 0 deletions examples/edutap_wallet_google_example_callback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Example callback service for Google

This service logs received data to a file and to stdout.

It logs space separated:
- `class_id`
- `object_id`
- `event_type`
- `exp_time_millis`
- `count`
- `nonce`

# Configuration environment

Environment variables are used for configuration.

- `EDUTAP_WALLET_GOOGLE_EXAMPLE_CALLBACK_LOG_FILE`

The name and location of the log file.
Default is relative to current working directory: `./callback_log.txt`.

From `edutap.wallet_google`:

- `EDUTAP_WALLET_GOOGLE_HANDLER_PREFIX_CALLBACK`

The path prefix of the callback in the browser.
Default: Empty string (no prefix)

- `EDUTAP_WALLET_GOOGLE_HANDLER_CALLBACK_VERIFY_SIGNATURE`

Whether to verify the signature (`1`) in the callback or not (`0`).
Default: `1`


## Local usage (dev)

Installation (execute in this folder)

```shell
uv venv
uv pip install -r requirements.txt
source .venv/bin/activate
```

Run with
```shell
fastapi dev edutap_wallet_google_example_callback.py
```

## As Docker container

### Build image and check container

In the root of the repository (in `../..` relative to the location of this `README.md`), run:

```shell
docker buildx build --progress=plain --no-cache -f examples/edutap_wallet_google_example_callback/Dockerfile -t edutap_wallet_google_example_callback .
```

Then run the container interactive to verify its working:
```shell
docker run -it edutap_wallet_google_example_callback
```
Watch out for errors. Stop with Ctrl-c.


### Run on a server

To get actual callbacks from Google the application has to be accessible from the internet and it need to serve on `https` with a valid TLS certificate.
No self-signed certificates are allowed!

A kind of simple way to get an environment up and running is with Docker Swarm, Traefik Web-Proxy with Lets-Encrypt and our container running in there.
Since this is out of scope of this README we point the dear reader to the tutorial website [Docker Swarm Rocks](https://dockerswarm.rocks/traefik/).
The following examples are meant to run in such a cluster, or to be adapted to different environment.
We hope you get the idea.

There is an example swarm deployment in here in `swarm.yml`.
It can be deployed on the cluster.
The public domain is configured using the environment variable `EDUTAP_WALLET_GOOGLE_EXAMPLE_DOMAIN`.
A TLS certificate will be issued automatically using Lets Encrypt.

```shell
docker stack deploy swarm.yml -c swarm.yml edutap_wallet_google_example_callback
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from edutap.wallet_google.handlers.fastapi import router_callback
from fastapi import FastAPI
from fastapi.logger import logger

import os
import pathlib


app = FastAPI()
app.include_router(router_callback)


class LoggingCallbackHandler:
"""
Implementation of edutap.wallet_google.protocols.CallbackHandler
"""

async def handle(
self,
class_id: str,
object_id: str,
event_type: str,
exp_time_millis: int,
count: int,
nonce: str,
) -> None:
pathlib.Path(
os.environ.get(
"EDUTAP_WALLET_GOOGLE_EXAMPLE_CALLBACK_LOG_FILE", "./callback_log.txt"
)
)
line = f'"{class_id}", "{object_id}", "{event_type}", "{exp_time_millis}", "{count}", "{nonce}"\n'
logger.info(line)
with open("callback_log.txt", "a") as file:
file.write(line)
10 changes: 10 additions & 0 deletions examples/edutap_wallet_google_example_callback/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
name = "edutap_wallet_google_example_callback"
version = "1.0"
dependencies = [
"edutap.wallet_google[fastapi]",
"fastapi[standard]",
]

[project.entry-points.'edutap.wallet_google.plugins']
CallbackHandler = 'edutap_wallet_google_example_callback:LoggingCallbackHandler'
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-e ../..
-e .
47 changes: 47 additions & 0 deletions examples/edutap_wallet_google_example_callback/swarm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
version: '3.7'

volumes:
logs:
driver_opts:
type: none
device: /data/wallet_google_example_callback
o: bind

networks:
traefik-public:
external: true
driver: overlay

services:
callback:
image: 'edutap_wallet_google_example_callback:latest'
volumes:
- logs:/logs
networks:
- traefik-public
environment:
EDUTAP_WALLET_GOOGLE_HANDLER_CALLBACK_VERIFY_SIGNATURE: "0"
deploy:
replicas: 1
resources:
limits:
cpus: '1'
memory: 128M
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.constraint-label=traefik-public
# SERVICE
- traefik.http.services.wallet_google_example_callback.loadbalancer.server.port=8080
# DOMAIN TLS
- traefik.http.routers.wallet_google_example_callback-domain.rule=Host(`${EDUTAP_WALLET_GOOGLE_EXAMPLE_DOMAIN?Unset}`)
- traefik.http.routers.wallet_google_example_callback-domain.entrypoints=https
- traefik.http.routers.wallet_google_example_callback-domain.tls=true
- traefik.http.routers.wallet_google_example_callback-domain.tls.certresolver=le
- traefik.http.routers.wallet_google_example_callback-domain.service=wallet_google_example_callback
- traefik.http.routers.wallet_google_example_callback-domain.middlewares=gzip
# DOMAIN insecure
- traefik.http.routers.wallet_google_example_callback-domain-ins.rule=Host(`${EDUTAP_WALLET_GOOGLE_EXAMPLE_DOMAIN?Unset}`)
- traefik.http.routers.wallet_google_example_callback-domain-ins.entrypoints=http
- traefik.http.routers.wallet_google_example_callback-domain-ins.service=wallet_google_example_callback
- traefik.http.routers.wallet_google_example_callback-domain-ins.middlewares=gzip
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,23 @@ Issues = "https://github.com/edutap-eu/edutap.wallet_google/issues"
Documentation = "https://docs.edutap.eu/packages/edutap_wallet_google/index.html"

[project.optional-dependencies]
fastapi = [
callback = [
"fastapi",
"httpx",
]
test = [
"pytest",
"pytest-cov",
"pytest-explicit",
"requests-mock",
"tox",
"fastapi",
"httpx",
"edutap.wallet_google[callback]",
]
typecheck = [
"google-auth-stubs",
"mypy",
"pytest-stub",
"types-cryptography",
"types-requests",
]
develop = [
"pdbpp",
Expand Down
14 changes: 9 additions & 5 deletions src/edutap/wallet_google/_vendor/google_pay_token_decryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@


ECv2_PROTOCOL_VERSION = "ECv2"
ECv2_PROTOCOL_VERSION_SIGNING = "ECv2SigningOnly"


class GooglePayError(Exception):
Expand Down Expand Up @@ -208,9 +209,12 @@ def verify_signature(self, data: dict) -> None:
:raises: An exception if the signatures could not be verified.
"""
if data["protocolVersion"] != ECv2_PROTOCOL_VERSION:
if data["protocolVersion"] not in [
ECv2_PROTOCOL_VERSION,
ECv2_PROTOCOL_VERSION_SIGNING,
]:
raise GooglePayError(
f"Only {ECv2_PROTOCOL_VERSION}-signed tokens are supported, but token is {data['protocolVersion']}-signed."
f"Only {ECv2_PROTOCOL_VERSION} or {ECv2_PROTOCOL_VERSION_SIGNING}-signed tokens are supported, but token is {data['protocolVersion']}-signed."
)

self._verify_intermediate_signing_key(data)
Expand Down Expand Up @@ -284,21 +288,21 @@ def _verify_message_signature(self, signed_key: dict, data: dict) -> None:
except Exception:
raise GooglePayError("Could not verify message signature")

def _filter_root_signing_keys(self) -> None:
def _filter_root_signing_keys(self, protocol=ECv2_PROTOCOL_VERSION) -> None:
"""
Filter the root signing keys to get only the keys that use ECv2 protocol
and that either doesn't expire or has an expiry date in the future.
"""
self.root_signing_keys = [
key
for key in self.root_signing_keys
if key["protocolVersion"] == ECv2_PROTOCOL_VERSION
if key["protocolVersion"] == protocol
and (
"keyExpiration" not in key
or check_expiration_date_is_valid(key["keyExpiration"])
)
]
if len(self.root_signing_keys) == 0:
raise GooglePayError(
f"At least one root signing key must be {ECv2_PROTOCOL_VERSION}-signed and have a valid expiration date."
f"At least one root signing key must be {protocol}-signed and have a valid expiration date."
)
4 changes: 2 additions & 2 deletions src/edutap/wallet_google/handlers/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
# define routers for all use cases: callback, images, and the combined router (at bottom of file)
router_callback = APIRouter(
prefix=session_manager.settings.handler_prefix_callback,
tags=["edutap", "google_wallet"],
tags=["edutap.wallet_google"],
)
router_images = APIRouter(
prefix=session_manager.settings.handler_prefix_images,
tags=["edutap", "google_wallet"],
tags=["edutap.wallet_google"],
)


Expand Down
Loading

0 comments on commit ec03b83

Please sign in to comment.