Skip to content

Commit

Permalink
Improve use of env variables and documentation
Browse files Browse the repository at this point in the history
- Combine function of CLIENT_ID and OAUTH_CLIENT_ID
- Remove defaults for some values in the config file
- Improve documentation on env variable values
  • Loading branch information
kipparker committed Jul 30, 2024
1 parent 6638017 commit 43331f1
Show file tree
Hide file tree
Showing 13 changed files with 62 additions and 41 deletions.
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ It may also be a useful reference for developers who are creating secure data en

## Authentication API

The authentication app is in the [authentication](authentication) directory. It provides endpoints for authenticating and identifying users, and for handling and passing on requests from the client API to the FAPI API. It uses a
The authentication app is in the [authentication](authentication) directory. It provides endpoints for authenticating and identifying users, and for handling and passing on requests from the client API to the FAPI API. It uses the Ory Hydra service to handle most of the OAuth2 flow, with additional endpoints added to handle the FAPI specific requirements.

Authentication API documentation is available at https://perseus-demo-authentication.ib1.org/api-docs.

Expand All @@ -42,16 +42,33 @@ Resource API documentation is available at https://perseus-demo-energy.ib1.org/a

## Environment variables

Both apps have example `.env.template` files in their root directories. These should be copied to `.env` and edited as required, filling CLIENT_ID and CLIENT_SECRET with the values provided by Ory Hydra, or on request from ib1 for the demo apps.
Both apps have example `.env.template` files in their root directories. These should be copied to `.env` and edited as required. The following environment variables are used in the authentication app:

- `REDIS_HOST`: a local redis instance is used to store PAR requests
- `OAUTH_CLIENT_ID`: Client ID for the Ory Hydra client
- `OAUTH_URL`: URL for the Ory Hydra client
- `OAUTH_CLIENT_SECRET`: Client secret for the Ory Hydra client
- `REDIRECT_URI`: The page to return to after authentication and authorisation eg. for local development http://127.0.0.1:3000/callback
- `ISSUER_URL`: URL of the Oauth issuer eg. for docker compose https://authentication_web

The following environment variables are used in the resource app:

- `OAUTH_CLIENT_ID`: Client ID for the Ory Hydra client (same as for authentication)
- `OAUTH_CLIENT_SECRET`: Client secret for the Ory Hydra client (same as for authentication)
- `ISSUER_URL`: URL of the Oauth issuer eg. for docker compose https://authentication_web

## Running a dev server

The fastapi servers for each app can be run using:

```bash
cd authentication|resource
pipenv install --dev
pipenv run uvicorn api.main:app --reload
```

**nb** the recommended way to run the apps is using the docker compose environment, as the apps require a redis instance and the resource app requires the authentication app to be running.

## Creating self-signed certificates

The docker compose and client.py scripts require a set of self-signed certificates in a certs/ folder. These can be generated using the `certmaker.sh` script in the `scripts` directory.
Expand Down
8 changes: 3 additions & 5 deletions authentication/.env.template
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
REDIS_HOST=localhost
OAUTH_CLIENT_ID=<ory-client-id>
OAUTH_URL=https://<ory-supplied-url>
CLIENT_ID=<perseus-client-id>
CLIENT_SECRET=<perseus-client-secret>
REDIRECT_URI=http://127.0.0.1:3000/callback
ISSUER_URL=https://authentication_web
OAUTH_CLIENT_SECRET=<ory-client-secret>

2 changes: 1 addition & 1 deletion authentication/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def create_id_token(subject="platform_user") -> str:
claims = {
"iss": f"{conf.ISSUER_URL}",
"sub": subject,
"aud": conf.CLIENT_ID,
"aud": conf.OAUTH_CLIENT_ID,
"exp": int(time.time()) + 3600,
"iat": int(time.time()),
"kid": 1,
Expand Down
10 changes: 5 additions & 5 deletions authentication/api/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@


ISSUER_URL = os.environ.get("ISSUER_URL", "https://perseus-demo-authentication.ib1.org")
CLIENT_ID = os.environ.get("CLIENT_ID", "21653835348762")
CLIENT_SECRET = os.environ.get(
"CLIENT_SECRET", "uE4NgqeIpuSV_XejQ7Ds3jsgA1yXhjR1MXJ1LbPuyls"

OAUTH_CLIENT_SECRET = os.environ.get(
"OAUTH_CLIENT_SECRET"
)
OAUTH_URL = os.environ.get(
"OAUTH_URL", "https://vigorous-heyrovsky-1trvv0ikx9.projects.oryapis.com"
"OAUTH_URL"
)
OAUTH_CLIENT_ID = os.environ.get(
"OAUTH_CLIENT_ID", "f67916ce-de33-4e2f-a8e3-cbd5f6459c30"
"OAUTH_CLIENT_ID"
)
AUTHORIZATION_ENDPOINT = os.environ.get(
"AUTHORIZATION_ENDPOINT",
Expand Down
12 changes: 6 additions & 6 deletions authentication/api/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
from . import conf


CLIENT_ID = "21653835348762"
CLIENT_ID = "abc123-addefg-4e2f-a8e3-cbd5f6459c30"
CLIENT_SECRET = "uE4NgqeIpuSV_XejQ7Ds3jsgA1yXhjR1MXJ1LbPuyls"

CLIENT_CERTIFICATE = "-----BEGIN%20CERTIFICATE-----%0AMIIDkzCCAnugAwIBAgIUerCGLrDY6aCYaB6nj9HivLJQtCAwDQYJKoZIhvcNAQEL%0ABQAwVzELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjETMBEGA1UECgwKUGVy%0Ac2V1cyBDQTEiMCAGA1UEAwwZcGVyc2V1cy1kZW1vLWZhcGkuaWIxLm9yZzAeFw0y%0ANDAxMzAxNTA2NDRaFw0yNTAxMjkxNTA2NDRaMGwxCzAJBgNVBAYTAkdCMQ8wDQYD%0AVQQIDAZMb25kb24xITAfBgNVBAoMGFBlcnNldXMgRGVtbyBBY2NvdW50YW5jeTEp%0AMCcGA1UEAwwgcGVyc2V1cy1kZW1vLWFjY291bnRhbmN5LmliMS5vcmcwggEiMA0G%0ACSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC06LPNmROgBhDrfpUnfYaoqMENiqcy%0ArbqMGGWiRtRQO87ngL96iCMi30kPQjg4kD8PK49522ZZJ%2BVTqErtD6reS5y6mWkB%0AydsoWRKTi7UyHdyyK9D%2FHc%2FxK1JtFwlAZd4pMaN6KWJLEEfvmZxhOI5pfzKwi6Jj%0A%2BOfedpVJSAeXkI6CYY6qq33KRI4KQau5E7PbzbcNYICvLj1Vhs2bUz8a0tOJG5r8%0A06MCr%2FtteTYMjbb6x3yVlSL3b3LOtj4n8RCYyLlQ3S4ni1MSyHdnqngUGGyrnSVI%0AJF3lR9xJ6AK96RNgiRSzjCWOjOFZjnpuFlGa%2FXxn7buIrDehuKlmVDm3AgMBAAGj%0AQjBAMB0GA1UdDgQWBBRgUt%2BadzZuWnp9bEQLx6qhSm0T%2BTAfBgNVHSMEGDAWgBQP%0Ad2ICahMnEqfEp%2FLCaoBcGQufojANBgkqhkiG9w0BAQsFAAOCAQEArGVkNNfH9Zct%0Ak68YUrky3jPc3L714CjsW3l7yCWCqmkuB6VWIggNqltdKQDzdXDIwFLmN%2Fi7D57K%0AFqDaQboKUumeF3vsIi4LYlRqwGTuN7uGTghcqrpPozM7m%2BYTdPObY%2F8FtL6MqmqJ%0Adv61MYERRl3iLuR6UIbBaQr4YvgThe9WGotqknFOyxrD1yuunlYutOQpF2tXR8hk%0AES5XhvdLQfiuGStM4MPB0%2FWlMwc7mVge5aOVOixJeB0yGNmSSJWeEMQB0ETW3BbF%0AySdR2NroAWqouPWaJMZIpjxldeeTOBmc8k6%2BDLBvRIFcDUgpuLfaoAFTZKtkWbao%0A2NHw6nQAMQ%3D%3D%0A-----END%20CERTIFICATE-----%0A"
CLIENT_PUSHED_AUTHORIZATION_REQUEST: JsonDict = {
"response_type": "code",
"client_id": f"{conf.CLIENT_ID}",
"client_id": CLIENT_ID,
"redirect_uri": "https://mobile.example.com/cb",
"code_challenge": "W78hCS0q72DfIHa...kgZkEJuAFaT4",
"code_challenge_method": "S256",
}

PUSHED_AUTHORIZATION_REQUEST: JsonDict = {
"parameters": f"response_type=code&client_id={conf.CLIENT_ID}&redirect_uri=https%3A%2F%2Fmobile.example.com%2Fcb&code_challenge=W78hCS0q72DfIHa...kgZkEJuAFaT4&code_challenge_method=S256",
"parameters": f"response_type=code&client_id={CLIENT_ID}&redirect_uri=https%3A%2F%2Fmobile.example.com%2Fcb&code_challenge=W78hCS0q72DfIHa...kgZkEJuAFaT4&code_challenge_method=S256",
"client_id": CLIENT_ID,
"client_certificate": CLIENT_CERTIFICATE,
}
Expand All @@ -27,7 +27,7 @@
}

AUTHORIZATION_REQUEST: JsonDict = {
"client_id": conf.CLIENT_ID,
"client_id": CLIENT_ID,
"request_uri": "urn:ietf:params:oauth:request_uri:UymBrux4ZEMrBRKx9UyKyIm98zpX1cHmAPGAGNofmm4",
}

Expand All @@ -38,7 +38,7 @@


TOKEN_REQUEST: JsonDict = {
"client_id": f"{CLIENT_ID}",
"client_id": CLIENT_ID,
"parameters": "grant_type=authorization_code&redirect_uri=https://client.example.org/cb/example.com&code=DxiKC0cOc_46nzVjgr41RWBQtMDrAvc0BUbMJ_v7I70",
"client_certificate": CLIENT_CERTIFICATE,
"redirect_uri": "https://client.example.org/cb/",
Expand All @@ -64,7 +64,7 @@
INTROSPECTION_FAILED_RESPONSE: JsonDict = {"active": False}
INTROSPECTION_RESPONSE = {
"aud": [],
"client_id": "f67916ce-de33-4e2f-a8e3-cbd5f6459c30",
"client_id": CLIENT_ID,
"exp": 1713285925,
"ext": {},
"iat": 1713282325,
Expand Down
16 changes: 11 additions & 5 deletions authentication/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def get_authentication_credentials(
):
"""
The resource server will use client credentials to connect to the introspection endpoint
This function is a placeholder. Authentication will be provided
by Perseus Directory issued client certificates in production, and necessary client
information will be available in the certificate.
"""
if not credentials:
raise HTTPException(
Expand All @@ -46,11 +50,11 @@ def get_authentication_credentials(
)
current_username_bytes = credentials.username.encode("utf8")
is_correct_username = secrets.compare_digest(
current_username_bytes, conf.CLIENT_ID.encode("utf8")
current_username_bytes, conf.OAUTH_CLIENT_ID.encode("utf8")
)
current_password_bytes = credentials.password.encode("utf8")
is_correct_password = secrets.compare_digest(
current_password_bytes, conf.CLIENT_SECRET.encode("utf8")
current_password_bytes, conf.OAUTH_CLIENT_SECRET.encode("utf8")
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
Expand All @@ -66,7 +70,9 @@ async def docs() -> dict:
return {"docs": "/api-docs"}


@app.post("/api/v1/par", response_model=models.PushedAuthorizationResponse, status_code=201)
@app.post(
"/api/v1/par", response_model=models.PushedAuthorizationResponse, status_code=201
)
async def pushed_authorization_request(
response_type: Annotated[str, Form()],
client_id: Annotated[str, Form()],
Expand Down Expand Up @@ -172,7 +178,7 @@ async def token(
redirect_uri: Annotated[str, Form()],
code_verifier: Annotated[str, Form()],
code: Annotated[str, Form()],
x_amzn_mtls_clientcert: Annotated[str, Header()],
x_amzn_mtls_clientcert: Annotated[str|None, Header()],
) -> models.TokenResponse:
"""
Token issuing endpoint
Expand All @@ -191,7 +197,7 @@ async def token(
"code_verifier": code_verifier,
}
session = requests.Session()
session.auth = (conf.CLIENT_ID, conf.CLIENT_SECRET)
session.auth = (conf.OAUTH_CLIENT_ID, conf.CLIENT_SECRET)
response = requests.post(
f"{conf.TOKEN_ENDPOINT}",
data=payload,
Expand Down
6 changes: 3 additions & 3 deletions authentication/copilot/backend/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ network:
# Optional fields for more advanced use-cases.
#
variables: # Pass environment variables as key value pairs.
CLIENT_ID: f67916ce-de33-4e2f-a8e3-cbd5f6459c30
OAUTH_CLIENT_ID: f67916ce-de33-4e2f-a8e3-cbd5f6459c30
REDIS_HOST: redis.${COPILOT_ENVIRONMENT_NAME}.${COPILOT_APPLICATION_NAME}.local
OAUTH_URL: https://vigorous-heyrovsky-1trvv0ikx9.projects.oryapis.com

Expand All @@ -52,9 +52,9 @@ environments:
secrets:
SERVER_KEY: /copilot/perseus-demo-fapi/prod/secrets/server_key
SERVER_CERT: /copilot/perseus-demo-fapi/prod/secrets/server_cert
CLIENT_SECRET: /copilot/perseus-demo-authentication/prod/secrets/client_secret
OAUTH_CLIENT_SECRET: /copilot/perseus-demo-authentication/prod/secrets/client_secret
dev:
secrets:
SERVER_KEY: /copilot/perseus-demo-fapi/dev/secrets/server_key
SERVER_CERT: /copilot/perseus-demo-fapi/dev/secrets/server_cert
CLIENT_SECRET: /copilot/perseus-demo-authentication/dev/secrets/client_secret
OAUTH_CLIENT_SECRET: /copilot/perseus-demo-authentication/dev/secrets/client_secret
10 changes: 6 additions & 4 deletions client.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def pushed_authorization_request() -> tuple[str, dict]:
f"{AUTHENTICATION_API}/api/v1/par",
data={
"response_type": "code",
"client_id": f"{conf.CLIENT_ID}",
"client_id": f"{conf.OAUTH_CLIENT_ID}",
"redirect_uri": f"{conf.REDIRECT_URI}",
"state": generate_state(),
"code_challenge": code_challenge,
Expand Down Expand Up @@ -88,10 +88,12 @@ def client_side_decoding(token: str):
jwks_client = jwt.PyJWKClient(jwks_url)
header = jwt.get_unverified_header(token)
key = jwks_client.get_signing_key(header["kid"]).key
decoded = jwt.decode(token, key, [header["alg"]], audience=f"{conf.CLIENT_ID}")
decoded = jwt.decode(
token, key, [header["alg"]], audience=f"{conf.OAUTH_CLIENT_ID}"
)
print(decoded, conf.ISSUER_URL)
# Example of tests to apply
if decoded["aud"] != conf.CLIENT_ID:
if decoded["aud"] != conf.OAUTH_CLIENT_ID:
raise ValueError("Invalid audience")
if decoded["iss"] != conf.ISSUER_URL:
raise ValueError("Invalid issuer")
Expand Down Expand Up @@ -140,7 +142,7 @@ def auth():
response = session.get(
f"{AUTHENTICATION_API}/api/v1/authorize",
params={
"client_id": f"{conf.CLIENT_ID}",
"client_id": f"{conf.OAUTH_CLIENT_ID}",
"request_uri": par_response["request_uri"],
},
verify=False,
Expand Down
2 changes: 0 additions & 2 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ services:
stop_signal: SIGINT
env_file: authentication/.env
environment:
- FAPI_API=http://host.docker.internal:8020
- REDIS_HOST=redis
# - FAPI_API=https://perseus-demo-fapi.ib1.org
command:
[
"uvicorn",
Expand Down
4 changes: 2 additions & 2 deletions resource/.env.template
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
CLIENT_ID=<perseus-client-id>
CLIENT_SECRET=<perseus-client-secret>
OAUTH_CLIENT_ID=<ory-client-id>
OAUTH_CLIENT_SECRET=<ory-client-secret>
ISSUER_URL=https://authentication_web
2 changes: 1 addition & 1 deletion resource/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def check_token(token: str, client_certificate: str) -> dict:
response = requests.post(
url=introspection_endpoint,
json={"token": token, "client_certificate": client_certificate},
auth=(conf.CLIENT_ID, conf.CLIENT_SECRET),
auth=(conf.OAUTH_CLIENT_ID, conf.CLIENT_SECRET),
verify=False,
)
if response.status_code != 200:
Expand Down
6 changes: 3 additions & 3 deletions resource/api/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
ENV = os.environ.get("ENV", "dev")
DIRNAME = os.path.dirname(os.path.realpath(__file__))
ISSUER_URL = os.environ.get("ISSUER_URL", "")
CLIENT_ID = os.environ.get("CLIENT_ID", "21653835348762")
CLIENT_SECRET = os.environ.get(
"CLIENT_SECRET", "uE4NgqeIpuSV_XejQ7Ds3jsgA1yXhjR1MXJ1LbPuyls"
OAUTH_CLIENT_ID = os.environ.get("OAUTH_CLIENT_ID")
OAUTH_CLIENT_SECRET = os.environ.get(
"OAUTH_CLIENT_SECRET"
)
OPEN_API_ROOT = "/dev" if ENV == "prod" else ""
4 changes: 2 additions & 2 deletions resource/copilot/perseus-resource/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ network:
variables: # Pass environment variables as key value pairs.
LOG_LEVEL: info
ISSUER_URL: "https://perseus-demo-authentication.ib1.org"
CLIENT_ID: f67916ce-de33-4e2f-a8e3-cbd5f6459c30
OAUTH_CLIENT_ID: f67916ce-de33-4e2f-a8e3-cbd5f6459c30


# You can override any of the values defined above by environment.
secrets:
CLIENT_SECRET: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/client_secret
OAUTH_CLIENT_SECRET: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/client_secret

environments:
prod:
Expand Down

0 comments on commit 43331f1

Please sign in to comment.