Skip to content

Commit

Permalink
Implemented client and updated README
Browse files Browse the repository at this point in the history
  • Loading branch information
hvalfangst committed Nov 7, 2024
1 parent efdd681 commit 24687a8
Show file tree
Hide file tree
Showing 55 changed files with 233 additions and 242 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/.env_oauth
client/.env_oauth
/infra/terraform.tfvars
/.idea/.gitignore
/infra/.terraform.lock.hcl
Expand Down
112 changes: 100 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,54 @@ The file structure is as follows:
![screenshot](images/terraform_tfvars.png)


## Register server on Azure Entra ID

Before deploying our server we need to create an app registration that we have deployed our API to Azure Web Apps, we need to register it on Microsoft Entra ID.

### Create a new app registration

Navigate to the **App registrations** blade and click on **New registration** button in the top left tab

![screenshot](images/azuread_app_registrations.png)

Choose a fitting name. Here I have set the name to "Hvalfangst Server" as the registration will be utilized by the API we just deployed to Azure Web Apps. The client which is to interact with our server resource will **NOT** be deployed. It will merely run locally. The fact that
both the newly deployed server and the not-to-be-deployed local client are both APIs may seem confusing, but this is just for demonstration purposes. We do not need to set up a redirect URI for our server as it merely validates token received in the authentication header and calls the underlying
service if the associated token had the necessary scopes. We will set the redirect URL for our client in later sections, as it will have to execute a callback from browser to a specified endpoint.


![screenshot](images/azure_entra_id_register_hvalfangst_server_api.png)

Once the app registration has been created, store the application and tenant id for later use. We will make use of these when setting up the CI/CD pipeline - which deploys the server API to Azure Web Apps.

![screenshot](images/hvalfangst_server_api_app_registration.png)


### Create Scope

We will now proceed to create scopes. Scopes are in essence fully customizable access right labels, meaning that you may are free to pick any name. It is, however common to conform to the following pattern: **{RESOURCE}.{ACCESS_LEVEL}**.
Say that you for sake of argument have implemented a CRUD API in the domain of wines. Since the domain is wine, the prefix would naturally be **Wines**. Access levels could be **READ**, **WRITE**, **UPDATE** and **DELETE**.
For instance, the scope **Wines.Read** grants you access to **read** wines - which in the API translates to the right to perform any **HTTP GET** requests, which commonly would be actions such as listing all heroes or to get a specific hero.

Click on the **Add a scope** button under the **Expose an API** section, which is accessible from the **Expose an API** blade under **Manage**.

![screenshot](images/hvalfangst_server_api_expose_api.png)

Set the scope name to **Heroes.Read**. Clients which has this scope grants the ability to list and view heroes. Choose **Admins only** for consent.
Choose something relevant to the scope name for the remainder of fields.

![screenshot](images/hvalfangst_server_api_add_scope.png)

Do the same for **Heroes.Write** and **Heroes.Delete**.

![screenshot](images/hvalfangst_server_api_all_scopes.png)

It goes without saying that the chosen scopes are just simple examples that does not necessarily conform to best practices when it comes to naming and even usages of scopes.
Feel free to adapt the scopes in the app registration and code as you see fit. It is also important to mention, now that we venture into the more technical aspects, that
the newly created scopes are absolutely junk in and of itself. You **must** reference the scopes exactly as defined in your server code for it to have any effect whatsoever.
That is, you must implement logic in your endpoints which verifies the signature associated with the token included in the auth header, ensures that the
audience is the client id associated with the server app registration and that the scopes included in the claims (after the token's signature has been verified)
matches that of what is required by said endpoint.

## Set up CI/CD via Deployment Center

Now that we have our new Web App resource up and running on Azure, we may proceed to set up our means of deploying our code to the
Expand Down Expand Up @@ -56,7 +104,17 @@ For the CI/CD workflow script to actually work, we have to make some adjustments
which are located in their own directories. The autogenerated script assumes that the files are located in the root folder, which is not the case here.
Thus, we need to change the script to reference files located under the server directory, as we are to deploy our server.

The final pipeline definition should look like [this](.github/workflows/main_hvalfangstlinuxwebapp.yml).
We are storing configuration values for our API in a class named [AzureConfig](server/config/config.py). Notice how the values for fields **TENANT_ID**
and **SERVER_CLIENT_ID** are retrieved from the runtime environment - which means that these environment variables must be set somehow. When running the
API locally for sake of testing one should **NOT** hardcode the associated values due to the risk of accidentally committing to SCM. Instead, you should
either set the environment values on your system or retrieve them from an .env file, which, naturally, **HAS** to be added your .gitignore.

Proceed to add two new GitHub Action secrets. These should be your tenant ID and the client ID associated with your newly created **Hvalfangst Server API** app registration.

![screenshot](images/github_actions_hvalfangst_secrets.png)

We now need to modify our GitHub Actions Workflow script to set the environment variables in our Azure Web App itself. We do so by the use of the az CLI
command **az webapp config appsettings set** where the associated values are retrieved from our repository secrets we set above.

## Deploy API

Expand All @@ -72,42 +130,72 @@ Navigate to the **Deployment Center** section of your Azure Web App. A new deplo

![screenshot](images/deployment_center_post_action.png)

Click on the **Environment variables** section of your Web App to ensure that the App setting environment variables **HVALFANGST_TENANT_ID** and **HVALFANGST_SERVER_CLIENT_ID**
have been set. The environment variable **SCM_DO_BUILD_DURING_DEPLOYMENT** was set by our [Terraform script](infra/terraform.tf) when creating the Azure Web App. It instructs our container to
build the virtual environment based on our [requirements](server/requirements.txt) file on deploy as opposed to utilizing some pre-built virtual environment that has been transmitted.

![screenshot](images/hvalfangstlinuxwebapp_environment_variables.png)

Now that we know that it deployed successfully it is finally time to access the API. Click on URI associated with **Default Domain**

![screenshot](images/overview_default_domain.png)

You will be prompted with the following default page, which indicates that the API is up and running.
You will be prompted with the following index page, which indicates that the API is up and running.

![screenshot](images/firefox_api_home.png)

The index page is available for all users and as such is not protected by any token validation logic. What is protected by token validation logic is our [heroes route](server/routers/heroes.py).
This route exposes 4 endpoints: "POST /heroes/", "GET /heroes/", "GET /heroes{hero_id}" and "DELETE /heroes/{hero_id}".
Notice how one in each endpoint always start by awaiting a function called [authorize](server/security/auth.py), passing in a token and a scope.
The scope names referenced in aforementioned function call are exactly what was defined earlier. Hence, my little
rant about scopes in and of itself being useless unless there is logic in place in the server code to actually enforce
this. The token is a bearer token, which should be passed in the authorization header. The authorize function will first and foremost attempt to verify the signature associated with the token by
utilizing public keys by use of the keys discovery endpoint exposed by Azure. A handful of keys unique to our tenant will be retrieved when calling the discovery endpoint. The Key ID used to sign the token
is actually included in the header of the token (remember that clients calling the server API first has to get a token from Azure Entra ID authorization server)

## Register API on Azure AD

Now that we have deployed our API to Azure Web Apps, we need to register it on Microsoft Entra ID.
## Register server on Azure Entra ID

### Create a new app registration

Navigate to the **App registrations** blade and click on **New registration** button in the top left tab
In order for our client to be abl

![screenshot](images/azuread_app_registrations.png)
![screenshot](images/hvalfangst_api_client_app_reg.png)

![screenshot](images/azure_entra_id_register_hvalfangst_server_api.png)
![screenshot](images/hvalfangst_client.png)

![screenshot](images/hvalfangst_server_api_app_registration.png)
### Create Secret

![screenshot](images/hvalfangst_client_new_secret.png)

### Expose API
![screenshot](images/hvalfangst_client_add_secret.png)

![screenshot](images/hvalfangst_client_secrets.png)

![screenshot](images/hvalfangst_server_api_expose_api.png)
### Add Redirect URL

![screenshot](images/hvalfangst_client_authentication.png)

![screenshot](images/hvalfangst_server_api_add_scope.png)
![screenshot](images/hvalfangst_client_api_configure_web.png)

![screenshot](images/hvalfangst_server_api_all_scopes.png)
### Add API permissions

![screenshot](images/hvalfangst_client_api_permissions.png)

![screenshot](images/hvalfangst_client_request_permission_graph.png)

![screenshot](images/hvalfangst_client_api_permissions_graph_openid.png)

![screenshot](images/hvalfangst_client_api_permissions_hvalfangst_search.png)


![screenshot](images/hvalfangst_client_api_permissions_hvalfangst_server_heroes_read.png)

![screenshot](images/hvalfangst_client_all_permissions_added.png)

![screenshot](images/hvalfangst_client_grant_admin_consent_prompt.png)

![screenshot](images/hvalfangst_client_permissions_granted_admin_consent_for.png)


## Running API
Expand Down
1 change: 0 additions & 1 deletion client/__init__.py

This file was deleted.

18 changes: 10 additions & 8 deletions client/config/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from dotenv import load_dotenv
from fastapi import HTTPException
from pydantic_settings import BaseSettings
from client import logger
from logger import logger

load_dotenv()


class OAuthSettings(BaseSettings):
AZURE_CLIENT_ID: str
AZURE_CLIENT_SECRET: str
Expand All @@ -21,21 +22,22 @@ class Config:
def initialize_oauth_settings():
try:
# Create an instance of OAuthSettings
internal_oauth_settings = OAuthSettings()
settings = OAuthSettings()

# Check if the required OAuth fields are set
if not internal_oauth_settings.AZURE_CLIENT_ID or not internal_oauth_settings.AZURE_CLIENT_SECRET or not internal_oauth_settings.AZURE_TENANT_ID or not internal_oauth_settings.API_SCOPE:
logger.logger.error("One or more required OAuth environment variables are missing.")
if not settings.AZURE_CLIENT_ID or not settings.AZURE_CLIENT_SECRET \
or not settings.AZURE_TENANT_ID or not settings.API_SCOPE:
logger.error("One or more required OAuth environment variables are missing.")
raise HTTPException(status_code=500,
detail="Configuration error: Required OAuth environment variables are missing.")

logger.logger.info("OAuth settings loaded successfully.")
return internal_oauth_settings
logger.info("OAuth settings loaded successfully.")
return settings
except FileNotFoundError:
logger.logger.critical(".env file not found.")
logger.critical(".env file not found.")
raise HTTPException(status_code=500, detail="Configuration error: .env file not found.")
except Exception as e:
logger.logger.critical(f"Error loading OAuth settings: {e}")
logger.critical(f"Error loading OAuth settings: {e}")
raise HTTPException(status_code=500,
detail="Configuration error: An error occurred while loading OAuth settings.")

Expand Down
3 changes: 3 additions & 0 deletions client/logger/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .logger import logger

__all__ = ["logger"]
5 changes: 1 addition & 4 deletions client/logger.py → client/logger/logger.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# client/logger.py

import logging

# Configure logging
Expand All @@ -8,5 +6,4 @@
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)

# Create a logger object that can be imported across the application
logger = logging.getLogger(__name__)
logger = logging.getLogger("logger")
2 changes: 1 addition & 1 deletion client/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# client/main.py

from fastapi import FastAPI
from client.routers import auth, heroes
from routers import auth, heroes

app = FastAPI(
title="Hero API",
Expand Down
6 changes: 2 additions & 4 deletions client/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# client/models/__init__.py
from .hero import Hero

from .dnd_hero import DnDHero, AbilityScores, SkillProficiencies, Equipment, Spell

__all__ = ["DnDHero", "AbilityScores", "SkillProficiencies", "Equipment", "Spell"]
__all__ = ["Hero"]
12 changes: 0 additions & 12 deletions client/models/ability_scores.py

This file was deleted.

34 changes: 0 additions & 34 deletions client/models/dnd_hero.py

This file was deleted.

10 changes: 0 additions & 10 deletions client/models/equipment.py

This file was deleted.

23 changes: 23 additions & 0 deletions client/models/hero.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Optional
from pydantic import BaseModel


class Hero(BaseModel):
id: str
name: str
race: str
class_: str # Avoids conflict with the Python `class` keyword
level: int
background: Optional[str] = None
alignment: Optional[str] = None

# Core combat stats
hit_points: int
armor_class: int
speed: int

# Optional personality fields
personality_traits: Optional[str] = None
ideals: Optional[str] = None
bonds: Optional[str] = None
flaws: Optional[str] = None
24 changes: 0 additions & 24 deletions client/models/skill_proficiencies.py

This file was deleted.

13 changes: 0 additions & 13 deletions client/models/spell.py

This file was deleted.

2 changes: 1 addition & 1 deletion client/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ config~=0.5.1
dotenv~=0.0.5
python-dotenv==1.0.1
httpx==0.27.2
jwt==1.3.1
pyjwt==2.9.0
pydantic_settings==2.6.0
Loading

0 comments on commit 24687a8

Please sign in to comment.