Skip to content

Commit

Permalink
Refactor configuration to use only environment variables
Browse files Browse the repository at this point in the history
  • Loading branch information
csc-felipe committed Jun 12, 2024
1 parent a428405 commit 7c4ddc5
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 68 deletions.
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CLIENT_ID=
CLIENT_SECRET=
DEBUG=True
URL_OIDC=https://openid-provider.org/oidc/.well-known/openid-configuration
URL_CALLBACK=http://localhost:8080/callback
URL_REDIRECT=http://localhost:8080/
SCOPE=openid
RESOURCE=resource
COOKIE_DOMAIN=
CORS_DOMAINS=
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ tests/__pycache__
*.json
.tox
.coverage
.env*
!.env.example
28 changes: 8 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,19 @@ pip install -r requirements.txt
```

## Configuration
Configuration variables are set in [config.json](config.json), which resides at the root of the directory.
```
{
"client_id": "",
"client_secret": "",
"url_oidc": "https://openid-provider.org/oidc/.well-known/openid-configuration",
"url_callback": "http://localhost:8080/callback",
"url_redirect": "http://localhost:8080/frontend",
"scope": "openid",
"resource": "something",
"cookie_domain": "",
"cors_domains": [""]
}
```
Configuration variables are set as environment variables in a `.env` file. You can start from `.env.example`.
The app contacts `url_oidc` on startup and retrieves the `authorization_endpoint`, `token_endpoint`, `revocation_endpoint` and `userinfo_endpoint` values, which are used at `/login`, `/callback`, `/logout` and `/userinfo` respectively.


### Environment Variables
- `CONFIG_FILE=config.json` change location of configuration file
- `DEBUG=True` enable debug logging
### Environment Variables for the container
- `APP_HOST=localhost` app hostname that can be passed to container
- `APP_PORT=8080` app port that can be passed to container

## Run
### For Development
```
uvicorn main:app --reload
cp .env.example .env # <- make changes
uvicorn main:app --reload --env-file .env
```
### For Deployment
The docker image copies `config.json` from the current directory, so either edit the values before building the image, or mount a file with correct values into the container.
Expand All @@ -46,7 +32,9 @@ docker build -t cscfi/tiny-rp .
```
Run container
```
docker run -p 8080:8080 cscfi/tiny-rp
cp .env.example .env # <- make changes
docker run -p 8080:8080 --env-file .env cscfi/tiny-rp
```

## Usage
Expand Down
11 changes: 0 additions & 11 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,11 +0,0 @@
{
"client_id": "",
"client_secret": "",
"url_oidc": "https://openid-provider.org/oidc/.well-known/openid-configuration",
"url_callback": "http://localhost:8080/callback",
"url_redirect": "http://localhost:8080/frontend",
"scope": "openid",
"resource": "something",
"cookie_domain": "",
"cors_domains": [""]
}
78 changes: 41 additions & 37 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@
from fastapi.responses import RedirectResponse, JSONResponse, HTMLResponse
from fastapi.middleware.cors import CORSMiddleware

# configuration
ENV_VARS = {
"CLIENT_ID",
"CLIENT_SECRET",
"URL_OIDC",
"URL_CALLBACK",
"URL_REDIRECT",
"SCOPE",
"COOKIE_DOMAIN",
"CORS_DOMAINS",
"DEBUG",
}
# CONFIG will hold environment variables as upper case keys, while later configured variables are lower cased.
CONFIG = {}
for env in ENV_VARS:
CONFIG[env] = os.environ.get(env, "")


# distutils.util.strtobool was deprecated in python 3.12 here is the source code for the simple function
# https://github.com/pypa/distutils/blob/94942032878d431cee55adaab12a8bd83549a833/distutils/util.py#L340-L353
Expand All @@ -38,31 +55,18 @@ def strtobool(val):
# logging
formatting = "[%(asctime)s][%(name)s][%(process)d %(processName)s][%(levelname)-8s] (L:%(lineno)s) %(module)s | %(funcName)s: %(message)s"
logging.basicConfig(
level=logging.DEBUG if bool(strtobool(os.environ.get("DEBUG", "False"))) else logging.INFO, format=formatting
level=logging.DEBUG if bool(strtobool(CONFIG["DEBUG"])) else logging.INFO, format=formatting
)
LOG = logging.getLogger("tiny-rp")

# configuration
config_file = os.environ.get("CONFIG_FILE", "config.json")
CONFIG = {}
try:
with open(config_file, "r") as f:
LOG.info(f"loading configuration file {config_file}")
CONFIG = json.loads(f.read())
LOG.info("configuration loaded")
LOG.debug(CONFIG)
except Exception as e:
LOG.error(f"failed to load configuration file {config_file}, {e}")
sys.exit(e)

DEFAULT_TIMEOUT = httpx.Timeout(15.0, read=60.0)


def get_configs():
"""Request OpenID configuration from OpenID provider."""
with httpx.Client(verify=False, timeout=DEFAULT_TIMEOUT) as client:
LOG.debug(f"requesting OpenID configuration from {CONFIG['url_oidc']}")
response = client.get(CONFIG["url_oidc"])
LOG.debug(f"requesting OpenID configuration from {CONFIG['URL_OIDC']}")
response = client.get(CONFIG["URL_OIDC"])
if response.status_code == 200:
# store URLs for later use
LOG.debug("OpenID configuration received")
Expand All @@ -85,7 +89,7 @@ def get_configs():
# add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=CONFIG["cors_domains"],
allow_origins=CONFIG["CORS_DOMAINS"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
Expand Down Expand Up @@ -122,15 +126,15 @@ async def login_endpoint():
state = secrets.token_hex()
LOG.debug(f"state: {state}")
params = {
"client_id": CONFIG["client_id"],
"client_id": CONFIG["CLIENT_ID"],
"response_type": "code",
"state": state,
"redirect_uri": CONFIG["url_callback"],
"scope": CONFIG["scope"],
"redirect_uri": CONFIG["URL_CALLBACK"],
"scope": CONFIG["SCOPE"],
}
# optional param for special cases
if "resource" in CONFIG:
params["resource"] = CONFIG["resource"]
params["resource"] = CONFIG["RESOURCE"]

# prepare the redirection response
url = CONFIG["url_auth"] + "?" + urlencode(params)
Expand All @@ -139,7 +143,7 @@ async def login_endpoint():

# store state cookie for callback verification
response.set_cookie(
key="oidc_state", value=state, max_age=300, httponly=True, secure=True, domain=CONFIG.get("cookie_domain", None)
key="oidc_state", value=state, max_age=300, httponly=True, secure=True, domain=CONFIG.get("COOKIE_DOMAIN", None)
)

# redirect user to sign in at OpenID provider
Expand Down Expand Up @@ -181,16 +185,16 @@ async def callback_endpoint(oidc_state: str = Cookie(""), state: str = "", code:
id_token, access_token = await request_tokens(code)
LOG.debug(f"id_token={id_token}, access_token={access_token}")

if CONFIG["url_redirect"] == "":
if CONFIG["URL_REDIRECT"] == "":
# display tokens
LOG.debug("redirect address is not set, display tokens in JSON")
return {"id_token": id_token, "access_token": access_token}
else:
# save tokens to cookies and redirect
LOG.debug(f"save tokens to cookies and redirect user to {CONFIG['url_redirect']}")
LOG.debug(f"save tokens to cookies and redirect user to {CONFIG['URL_REDIRECT']}")

# prepare the redirection response
response = RedirectResponse(CONFIG["url_redirect"])
response = RedirectResponse(CONFIG["URL_REDIRECT"])

# store tokens to cookies
response.set_cookie(
Expand All @@ -199,27 +203,27 @@ async def callback_endpoint(oidc_state: str = Cookie(""), state: str = "", code:
max_age=3600,
httponly=True,
secure=True,
domain=CONFIG.get("cookie_domain", None),
domain=CONFIG.get("COOKIE_DOMAIN", None),
)
response.set_cookie(
key="access_token",
value=access_token,
max_age=3600,
httponly=True,
secure=True,
domain=CONFIG.get("cookie_domain", None),
domain=CONFIG.get("COOKIE_DOMAIN", None),
)
response.set_cookie(
key="logged_in",
value="True",
max_age=3600,
httponly=False,
secure=True,
domain=CONFIG.get("cookie_domain", None),
domain=CONFIG.get("COOKIE_DOMAIN", None),
)

# redirect user
LOG.debug(f"redirecting to {CONFIG['url_redirect']}")
LOG.debug(f"redirecting to {CONFIG['URL_REDIRECT']}")
return response


Expand All @@ -232,21 +236,21 @@ async def logout_endpoint(id_token: str = Cookie(""), access_token: str = Cookie
await revoke_token(access_token)

# prepare the redirection response
response = RedirectResponse(CONFIG["url_redirect"])
response = RedirectResponse(CONFIG["URL_REDIRECT"])

# overwrite cookies with instantly expiring ones
response.set_cookie(
key="id_token", value="", max_age=0, httponly=True, secure=True, domain=CONFIG.get("cookie_domain", None)
key="id_token", value="", max_age=0, httponly=True, secure=True, domain=CONFIG.get("COOKIE_DOMAIN", None)
)
response.set_cookie(
key="access_token", value="", max_age=0, httponly=True, secure=True, domain=CONFIG.get("cookie_domain", None)
key="access_token", value="", max_age=0, httponly=True, secure=True, domain=CONFIG.get("COOKIE_DOMAIN", None)
)
response.set_cookie(
key="logged_in", value="", max_age=0, httponly=False, secure=True, domain=CONFIG.get("cookie_domain", None)
key="logged_in", value="", max_age=0, httponly=False, secure=True, domain=CONFIG.get("COOKIE_DOMAIN", None)
)

# redirect user
LOG.debug(f"redirecting to {CONFIG['url_redirect']}")
LOG.debug(f"redirecting to {CONFIG['URL_REDIRECT']}")
return response


Expand All @@ -255,9 +259,9 @@ async def request_tokens(code: str) -> Tuple[str, str]:
LOG.debug(f"set up token request using code: {code}")

# set up basic auth and payload
auth = httpx.BasicAuth(username=CONFIG["client_id"], password=CONFIG["client_secret"])
auth = httpx.BasicAuth(username=CONFIG["CLIENT_ID"], password=CONFIG["CLIENT_SECRET"])
LOG.debug("basic auth is set")
data = {"grant_type": "authorization_code", "code": code, "redirect_uri": CONFIG["url_callback"]}
data = {"grant_type": "authorization_code", "code": code, "redirect_uri": CONFIG["URL_CALLBACK"]}
LOG.debug(f"post payload: {data}")

async with httpx.AsyncClient(auth=auth, verify=False, timeout=DEFAULT_TIMEOUT) as client:
Expand All @@ -283,7 +287,7 @@ async def revoke_token(token: str) -> None:
if not CONFIG["url_revoke"]:
# some AAI systems might not provide a revocation endpoint
return
auth = httpx.BasicAuth(username=CONFIG["client_id"], password=CONFIG["client_secret"])
auth = httpx.BasicAuth(username=CONFIG["CLIENT_ID"], password=CONFIG["CLIENT_SECRET"])
params = {"token": token}

async with httpx.AsyncClient(auth=auth, verify=False, timeout=DEFAULT_TIMEOUT) as client:
Expand Down

0 comments on commit 7c4ddc5

Please sign in to comment.