Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
kipparker committed Feb 6, 2024
0 parents commit 17bf5a3
Show file tree
Hide file tree
Showing 25 changed files with 1,677 additions and 0 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Deploy Dev

on:
workflow_run:
workflows: ["Python tests"]
branches: [main]
types:
- completed
workflow_dispatch:
branches:
- kip/*

env:
AWS_REGION: eu-west-2 # set this to your preferred AWS region, e.g. us-west-1

jobs:
deploy-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Install Copilot CLI
uses: ksivamuthu/aws-copilot-github-action@v0.0.1
with:
command: install
- run: |
copilot --version
copilot deploy
26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Python tests

on: [push]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pipenv
pipenv install --dev
- name: Lint with ruff
run: |
# stop the build if there are Python syntax errors or undefined names
pipenv run ruff .
# - name: Run tests
# run: |
# # stop the build if there are Python syntax errors or undefined names
# pipenv run pytest .
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
*.sqlite
*.pyc
venv/*
instance/
.venv/
.mypy_cache/
.env

.env*
.pytest_cache
__pycache__
.DS_Store
.ruff_cache
.mypy_cache

certs
requirements.txt
.python-version
11 changes: 11 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]

[dev-packages]

[requires]
python_version = "3.12"
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Perseus demo authentication api

Emulates authentication endpoints for the Perseus demo. These endpoints in production will be provided by an energy provider's authentication platform. Api documentation is available at https://perseus-demo-authentication.ib1.org/api-docs.

The authentication api is responsible for authenticating and identifying its own users, and for handling and passing on requests from the client API to the FAPI API.

## Run the dev server

```bash
pipenv install --dev
pipenv run uvicorn api.main:app --reload
```

## Running the local docker environment

```bash
docker-compose up
```

The docker environment uses nginx to proxy requests to uvicorn, with nginx configuration to pass through client certificates to the backend, using the same header as used by AWS ALB (`x-amzn-mtls-clientcert`). It requires a set of certificates as generated by [scripts/certmaker.sh], which should be available in the `certs` directory.
4 changes: 4 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/*
!Pipfile
!Pipfile.lock
!api/
8 changes: 8 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:3.12-slim
RUN pip install pipenv
COPY Pipfile* /code/
WORKDIR /code
RUN pipenv install --system --deploy --ignore-pipfile
COPY ./api /code/api
EXPOSE 8000
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8080"]
24 changes: 24 additions & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
fastapi = "*"
pyjwt = "*"
uvicorn = "*"
requests = "*"
python-jose = {extras = ["cryptography"] }
passlib = {extras = ["bcrypt"] }
python-multipart = "*"

[dev-packages]
black = "*"
mypy = "*"
ruff = "*"
types-python-jose = "*"
types-passlib = "*"
types-requests = "*"

[requires]
python_version = "3.12"
700 changes: 700 additions & 0 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

Empty file added backend/api/__init__.py
Empty file.
99 changes: 99 additions & 0 deletions backend/api/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from datetime import datetime, timedelta, timezone
from typing import Annotated
import bcrypt

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt

from . import models

# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "059300339d8e2d0bc4405ceaf1c28f95c0f81ec6b73d7bce2cb36bd1a597512e"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30


fake_users_db = {
"platform_user": {
"username": "platform_user",
"full_name": "Platform User",
"email": "user@platform.org",
"hashed_password": "$2b$12$P1c1ltZ37o8db.cXooGn7.w9GjLhbx/y7uAvirrkm1NtnFwL7Lgpm", # 'perseus'
"disabled": False,
}
}


oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


# Hash a password using bcrypt
def get_password_hash(password):
pwd_bytes = password.encode("utf-8")
salt = bcrypt.gensalt()
hashed_password = bcrypt.hashpw(password=pwd_bytes, salt=salt)
return hashed_password


# Check if the provided password matches the stored password (hashed)
def verify_password(plain_password, hashed_password):
password_byte_enc = plain_password.encode("utf-8")
return bcrypt.checkpw(password=password_byte_enc, hashed_password=hashed_password)


def get_user(db, username: str):
if username in db:
user_dict = db[username]
return models.UserInDB(**user_dict)


def authenticate_user(username: str, password: str):
user = get_user(fake_users_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password.encode("utf-8")):
return False
return user


def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt


async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str | None = payload.get("sub")
if username is None:
raise credentials_exception
token_data = models.UserTokenData(username=username)
except JWTError:
raise credentials_exception
if token_data.username is None:
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
return user


async def get_current_active_user(
current_user: Annotated[models.User, Depends(get_current_user)]
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
5 changes: 5 additions & 0 deletions backend/api/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os

FAPI_API = os.environ.get("FAPI_API", "https://perseus-demo-fapi.ib1.org")
CLIENT_ID = "21653835348762"
CLIENT_SECRET = "uE4NgqeIpuSV_XejQ7Ds3jsgA1yXhjR1MXJ1LbPuyls"
73 changes: 73 additions & 0 deletions backend/api/examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from pydantic.config import JsonDict

CLIENT_ID = "21653835348762"
CLIENT_SECRET = "uE4NgqeIpuSV_XejQ7Ds3jsgA1yXhjR1MXJ1LbPuyls"

CLIENT_CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIDPDCCAiQCCQDWNMOIuzwDfzANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJK\nUDEOMAwGA1UECAwFVG9reW8xEzARBgNVBAcMCkNoaXlvZGEta3UxDzANBgNVBAoM\nBkNsaWVudDEbMBkGA1UEAwwSY2xpZW50LmV4YW1wbGUub3JnMB4XDTE5MTAyODA3\nMjczMFoXDTIwMTAyNzA3MjczMFowYDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRv\na3lvMRMwEQYDVQQHDApDaGl5b2RhLWt1MQ8wDQYDVQQKDAZDbGllbnQxGzAZBgNV\nBAMMEmNsaWVudC5leGFtcGxlLm9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAK2Oyc+BV4N5pYcp47opUwsb2NaJq4X+d5Itq8whpFlZ9uCCHzF5TWSF\nXrpYscOp95veGPF42eT1grfxYyvjFotE76caHhBLCkIbBh6Vf222IGMwwBbSZfO9\nJ3eURtEADBvsZ117HkPVdjYqvt3Pr4RxdR12zG1TcBAoTLGchyr8nBqRADFhUTCL\nmsYaz1ADiQ/xbJN7VUNQpKhzRWHCdYS03HpbGjYCtAbl9dJnH2EepNF0emGiSPFq\ndf6taToyCr7oZjM7ufmKPjiiEDbeSYTf6kbPNmmjtoPNNLeejHjP9p0IYx7l0Gkj\nmx4kSMLp4vSDftrFgGfcxzaMmKBsosMCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA\nqzdDYbntFLPBlbwAQlpwIjvmvwzvkQt6qgZ9Y0oMAf7pxq3i9q7W1bDol0UF4pIM\nz3urEJCHO8w18JRlfOnOENkcLLLntrjOUXuNkaCDLrnv8pnp0yeTQHkSpsyMtJi9\nR6r6JT9V57EJ/pWQBgKlN6qMiBkIvX7U2hEMmhZ00h/E5xMmiKbySBiJV9fBzDRf\nmAy1p9YEgLsEMLnGjKHTok+hd0BLvcmXVejdUsKCg84F0zqtXEDXLCiKcpXCeeWv\nlmmXxC5PH/GEMkSPiGSR7+b1i0sSotsq+M3hbdwabpJ6nQLLbKkFSGcsQ87yL+gr\nSo6zun26vAUJTu1o9CIjxw==\n-----END CERTIFICATE-----\n"
CLIENT_PUSHED_AUTHORIZATION_REQUEST: JsonDict = {
"response_type": "code",
"client_id": "3280859750204",
"redirect_uri": "https://mobile.example.com/cb",
"code_challenge": "W78hCS0q72DfIHa...kgZkEJuAFaT4",
"code_challenge_method": "S256",
}

PUSHED_AUTHORIZATION_REQUEST: JsonDict = {
"parameters": "response_type=code&client_id=3280859750204&redirect_uri=https%3A%2F%2Fmobile.example.com%2Fcb&code_challenge=W78hCS0q72DfIHa...kgZkEJuAFaT4&code_challenge_method=S256",
"clientId": CLIENT_ID,
"clientCertificate": CLIENT_CERTIFICATE,
}


PUSHED_AUTHORIZATION_RESPONSE: JsonDict = {
"expires_in": 600,
"request_uri": "urn:ietf:params:oauth:request_uri:UymBrux4ZEMrBRKx9UyKyIm98zpX1cHmAPGAGNofmm4",
}

AUTHORIZATION_REQUEST: JsonDict = {
"client_id": 3280859750204,
"request_uri": "urn:ietf:params:oauth:request_uri:UymBrux4ZEMrBRKx9UyKyIm98zpX1cHmAPGAGNofmm4",
}

AUTHORIZATION_RESPONSE: JsonDict = {
"message": "Authorisation code request issued",
"ticket": "b0JGD-ZkT8ElBGw2ck-T-t87Z033jXvhqC2omPT1bQ4",
}

ISSUE_RESPONSE: JsonDict = {
"type": "authorizationIssueResponse",
"resultCode": "A040001",
"resultMessage": "[A040001] The authorization request was processed successfully.",
"accessTokenDuration": 0,
"accessTokenExpiresAt": 0,
"action": "LOCATION",
"authorizationCode": "DxiKC0cOc_46nzVjgr41RWBQtMDrAvc0BUbMJ_v7I70",
"idToken": "eyJraWQiOiIxIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0dXNlcjAxIiwiYXVkIjpbIjU5MTIwNTk4NzgxNjQ5MCJdLCJjX2hhc2giOiJqR2kyOElvYm5HcjNNQ3Y0UUVQRTNnIiwiaXNzIjoiaHR0cHM6Ly9hcy5leGFtcGxlLmNvbSIsImV4cCI6MTU3MjQxMjY4MiwiaWF0IjoxNTcyMzI2MjgyLCJub25jZSI6Im4tMFM2X1d6QTJNaiJ9.1PFmc0gAsBWtLBriq3z9a4Tsi_ioEYlOqOYbicGEXWIS1WGX5ffGOyZNSzVBMamZbltZmSys0jlYmmYYLqgGsg",
"responseContent": (
"https://client.example.org/cb/example.com#"
"code=DxiKC0cOc_46nzVjgr41RWBQtMDrAvc0BUbMJ_v7I70&"
"id_token=eyJraWQiOiIxIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0dXNlcjAxIiwiYXVkIjpbIjU5MTIwN"
"Tk4NzgxNjQ5MCJdLCJjX2hhc2giOiJqR2kyOElvYm5HcjNNQ3Y0UUVQRTNnIiwiaXNzIjoiaHR0cHM6L"
"y9hcy5leGFtcGxlLmNvbSIsImV4cCI6MTU3MjQxMjY4MiwiaWF0IjoxNTcyMzI2MjgyLCJub25jZSI6I"
"m4tMFM2X1d6QTJNaiJ9.1PFmc0gAsBWtLBriq3z9a4Tsi_ioEYlOqOYbicGEXWIS1WGX5ffGOyZNSzVB"
"MamZbltZmSys0jlYmmYYLqgGsg"
),
}


TOKEN_REQUEST: JsonDict = {
"client_id": f"{CLIENT_ID}",
"parameters": "grant_type=authorization_code&redirect_uri=https://client.example.org/cb/example.com&code=DxiKC0cOc_46nzVjgr41RWBQtMDrAvc0BUbMJ_v7I70",
"client_certificate": CLIENT_CERTIFICATE,
}

TOKEN_RESPONSE: JsonDict = {
"access_token": "SUtEVc3Tj3D3xOdysQtssQxe9egAhI4fimexNVMjRyU",
"id_token": (
"eyJraWQiOiIxIiwiYWxnIjoiRVMyNTYifQ.eyJzdWIiOiJ0ZXN0dXNlcjAxIiwiYXVkIjpbIjU5MTIwN"
"Tk4NzgxNjQ5MCJdLCJpc3MiOiJodHRwczovL2FzLmV4YW1wbGUuY29tIiwiZXhwIjoxNTcyNDEyNzY5L"
"CJpYXQiOjE1NzIzMjYzNjksIm5vbmNlIjoibi0wUzZfV3pBMk1qIn0.9EQojck-Cf2hnKAZWR164kr21"
"o5lPKehvIHyViZgRg4CY_ZGmnyFooG4FCwlZxu-QOTtaDCffCsuCdz4GqknTA"
),
"refresh_token": "tXZjYfoK35I-djg9V3n6s58zsrVqRIzTNMXKIS_wkj8",
}
Loading

0 comments on commit 17bf5a3

Please sign in to comment.