Skip to content

Commit efdd681

Browse files
committed
Added terraform scripts for provisioning an app service plan containing an Azure Web App. Implemented our server API, which is deployed to Azure Web Apps on push to main via a GitHub Actions Workflow script generated by Azure.
1 parent 760e40b commit efdd681

File tree

82 files changed

+1400
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+1400
-1
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
2+
# More GitHub Actions for Azure: https://github.com/Azure/actions
3+
# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions
4+
5+
name: Build and deploy Python app to Azure Web App - hvalfangstlinuxwebapp
6+
7+
on:
8+
push:
9+
branches:
10+
- main
11+
workflow_dispatch:
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set up Python version
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: '3.10'
24+
25+
- name: Create and start virtual environment
26+
run: |
27+
python -m venv venv
28+
source venv/bin/activate
29+
30+
- name: Install dependencies
31+
run: pip install -r server/requirements.txt
32+
33+
# Optional: Add step to run tests here (PyTest, Django test suites, etc.)
34+
35+
- name: Zip artifact for deployment
36+
run: cd server && zip -r ../release.zip ./*
37+
38+
- name: Upload artifact for deployment jobs
39+
uses: actions/upload-artifact@v4
40+
with:
41+
name: python-app
42+
path: |
43+
release.zip
44+
!venv/
45+
46+
deploy:
47+
runs-on: ubuntu-latest
48+
needs: build
49+
environment:
50+
name: 'Production'
51+
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
52+
permissions:
53+
id-token: write #This is required for requesting the JWT
54+
env:
55+
HVALFANGST_TENANT_ID: ${{ secrets.HVALFANGST_TENANT_ID }}
56+
HVALFANGST_API_SERVER_CLIENT_ID: ${{ secrets.HVALFANGST_API_SERVER_CLIENT_ID }}
57+
58+
steps:
59+
- name: Download artifact from build job
60+
uses: actions/download-artifact@v4
61+
with:
62+
name: python-app
63+
64+
- name: Unzip artifact for deployment
65+
run: unzip release.zip -d .
66+
67+
- name: Login to Azure
68+
uses: azure/login@v2
69+
with:
70+
client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_40531D048E714BC9BF1FF2DB2DD35753 }}
71+
tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_336AC753681745D78B7B262945914F63 }}
72+
subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_97162121CF214550B81AF6E0B11ADA2C }}
73+
74+
- name: Check and set environment variables on Azure App Service if not already set
75+
run: |
76+
# Check for HVALFANGST_TENANT_ID and set it if missing
77+
existing_tenant_id=$(az webapp config appsettings list --name hvalfangstlinuxwebapp \
78+
--resource-group hvalfangstresourcegroup \
79+
--query "[?name=='HVALFANGST_TENANT_ID'].value" \
80+
--output tsv)
81+
if [ -z "$existing_tenant_id" ]; then
82+
echo "Setting HVALFANGST_TENANT_ID..."
83+
az webapp config appsettings set --name hvalfangstlinuxwebapp \
84+
--resource-group hvalfangstresourcegroup \
85+
--settings HVALFANGST_TENANT_ID=${{ secrets.HVALFANGST_TENANT_ID }}
86+
else
87+
echo "HVALFANGST_TENANT_ID is already set."
88+
fi
89+
90+
# Check for HVALFANGST_API_SERVER_CLIENT_ID and set it if missing
91+
existing_client_id=$(az webapp config appsettings list --name hvalfangstlinuxwebapp \
92+
--resource-group hvalfangstresourcegroup \
93+
--query "[?name=='HVALFANGST_API_SERVER_CLIENT_ID'].value" \
94+
--output tsv)
95+
if [ -z "$existing_client_id" ]; then
96+
echo "Setting HVALFANGST_API_SERVER_CLIENT_ID..."
97+
az webapp config appsettings set --name hvalfangstlinuxwebapp \
98+
--resource-group hvalfangstresourcegroup \
99+
--settings HVALFANGST_API_SERVER_CLIENT_ID=${{ secrets.HVALFANGST_API_SERVER_CLIENT_ID }}
100+
else
101+
echo "HVALFANGST_API_SERVER_CLIENT_ID is already set."
102+
fi
103+
104+
105+
- name: 'Deploy to Azure Web App'
106+
uses: azure/webapps-deploy@v3
107+
id: deploy-to-webapp
108+
with:
109+
app-name: 'hvalfangstlinuxwebapp'
110+
slot-name: 'Production'

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/.env_oauth
2+
/infra/terraform.tfvars
3+
/.idea/.gitignore
4+
/infra/.terraform.lock.hcl
5+
/.idea/azure-oauth2-auth-code-flow-fastapi.iml
6+
/.idea/azure-oauth2-auth-code-flow-fastapi2.iml
7+
/.idea/codeStyles/codeStyleConfig.xml
8+
/infra/.terraform/providers/registry.terraform.io/hashicorp/azurerm/4.4.0/windows_amd64/LICENSE.txt
9+
/.idea/misc.xml
10+
/.idea/modules.xml
11+
/.idea/codeStyles/Project.xml
12+
/pu.xml
13+
/infra/terraform.tfstate
14+
/infra/terraform.tfstate.backup
15+
/infra/.terraform/providers/registry.terraform.io/hashicorp/azurerm/4.4.0/windows_amd64/terraform-provider-azurerm_v4.4.0_x5.exe
16+
/.idea/vcs.xml

README.md

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,116 @@
1-
# oauth
1+
# Azure OAuth2 OIDC Auth Code Flow demonstration
2+
3+
The goal of this repository is to demonstrate how to incorporate [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749) on Azure **WITHOUT** the use of [MSAL](https://learn.microsoft.com/en-us/entra/identity-platform/msal-overview) for educational purposes.
4+
In a production environment one should **ALWAYS** use MSAL or similar battle-tested libraries, but it is vital for any engineer to understand what is going on under the hood instead of just blindly calling a library which
5+
automagically solves all your needs.
6+
7+
The repo contains code for both the client and the server. The client is utilizing [OpenID Connect (OIDC)](https://auth0.com/docs/authenticate/protocols/openid-connect-protocol) with
8+
Auth code flow. A comprehensive step-by-step guide is included on how to register the client and server on Azure Entra ID.
9+
10+
## Requirements
11+
12+
- **Platform**: x86-64, Linux/WSL
13+
- **Programming Language**: [Python 3](https://www.python.org/downloads/)
14+
- **Azure Account**: Access to [Azure Subscription](https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account)
15+
- **IAC Tool**: [Terraform](https://www.terraform.io/)
16+
17+
18+
## Allocate resources
19+
20+
The script [up](up.sh) provisions Azure resources by applying our [Terraform script](infra/terraform.tf).
21+
22+
It is necessary to create a file named **terraform.tfvars** in the [infra](infra) directory. This file holds sensitive information
23+
necessary for terraform to be able to interact with your cloud resources, namely that of your tenant and subscription id.
24+
An exemption for this file has been added in our [.gitignore](.gitignore) so that you do not accidentally commit it.
25+
26+
The file structure is as follows:
27+
28+
![screenshot](images/terraform_tfvars.png)
29+
30+
31+
## Set up CI/CD via Deployment Center
32+
33+
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
34+
aforementioned Web App. We will do so by connecting our Web App to our GitHub repository. Azure Web Apps has the ability
35+
to create a fully fledged CI/CD pipeline in the form of a GitHub Action Workflows script, which it commits on our behalf. As part of this pipeline a managed identify
36+
will be created in Azure in order to authenticate requests. Secrets will be automatically created and referenced in the CI/CD script by Azure.
37+
38+
Click on the **Deployment Center** section under the **Deployment** blade. Choose GitHub as source and set the appropriate organization, repository and branch.
39+
For authentication keep it as is (user-assigned identity). Click on the **Save** button in the top left corner.
40+
41+
![screenshot](images/deployment_center.png)
42+
43+
After the changes have persisted, navigate to your GitHub repository. A new commit which contains the CI/CD workflows file should be present. As mentioned earlier,
44+
this has been committed by Azure on our behalf.
45+
46+
![screenshot](images/github_workflow_commit.png)
47+
48+
Navigate to the bottom of the workflow file. Take notice of the three secrets being referenced.
49+
50+
![screenshot](images/github_workflow_secrets.png)
51+
52+
If you navigate to your secrets and variables associated with your GitHub Actions you will see that there are three new secrets, the same referenced above. Again,
53+
these have been set by Azure on your behalf in order to set up authentication with our managed identity which was created as part of the Deployment Center rollout.
54+
55+
For the CI/CD workflow script to actually work, we have to make some adjustments. Remember, this repo contains code for both the client and server -
56+
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.
57+
Thus, we need to change the script to reference files located under the server directory, as we are to deploy our server.
58+
59+
The final pipeline definition should look like [this](.github/workflows/main_hvalfangstlinuxwebapp.yml).
60+
61+
## Deploy API
62+
63+
In order to deploy our code to our Azure Web App slot, we need to trigger the newly registered GitHub Actions Workflow manually. Head over to the **Actions** section of your repository. Click on the **Run workflow** button located in the right corner.
64+
65+
![screenshot](images/github_actions.png)
66+
67+
Running said action should result in the following:
68+
69+
![screenshot](images/github_actions_dispatched_task.png)
70+
71+
Navigate to the **Deployment Center** section of your Azure Web App. A new deployment will be visible. Commit author and message will be equal to that of GitHub.
72+
73+
![screenshot](images/deployment_center_post_action.png)
74+
75+
Now that we know that it deployed successfully it is finally time to access the API. Click on URI associated with **Default Domain**
76+
77+
![screenshot](images/overview_default_domain.png)
78+
79+
You will be prompted with the following default page, which indicates that the API is up and running.
80+
81+
![screenshot](images/firefox_api_home.png)
82+
83+
84+
## Register API on Azure AD
85+
86+
Now that we have deployed our API to Azure Web Apps, we need to register it on Microsoft Entra ID.
87+
88+
### Create a new app registration
89+
90+
Navigate to the **App registrations** blade and click on **New registration** button in the top left tab
91+
92+
![screenshot](images/azuread_app_registrations.png)
93+
94+
![screenshot](images/azure_entra_id_register_hvalfangst_server_api.png)
95+
96+
![screenshot](images/hvalfangst_server_api_app_registration.png)
97+
98+
99+
### Expose API
100+
101+
102+
![screenshot](images/hvalfangst_server_api_expose_api.png)
103+
104+
105+
![screenshot](images/hvalfangst_server_api_add_scope.png)
106+
107+
![screenshot](images/hvalfangst_server_api_all_scopes.png)
108+
109+
110+
111+
112+
113+
## Running API
114+
```bash
115+
python -m uvicorn app.main:app --reload
116+
```

client/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# client/__init__.py

client/config/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# client/config/__init__.py
2+
3+
from .oauth import oauth_settings
4+
5+
__all__ = ["oauth_settings"]

client/config/oauth.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# client/config/oauth.py
2+
3+
from dotenv import load_dotenv
4+
from fastapi import HTTPException
5+
from pydantic_settings import BaseSettings
6+
from client import logger
7+
8+
load_dotenv()
9+
10+
class OAuthSettings(BaseSettings):
11+
AZURE_CLIENT_ID: str
12+
AZURE_CLIENT_SECRET: str
13+
AZURE_TENANT_ID: str
14+
API_SCOPE: str
15+
REDIRECT_URI: str
16+
17+
class Config:
18+
env_file = ".env_oauth"
19+
20+
21+
def initialize_oauth_settings():
22+
try:
23+
# Create an instance of OAuthSettings
24+
internal_oauth_settings = OAuthSettings()
25+
26+
# Check if the required OAuth fields are set
27+
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:
28+
logger.logger.error("One or more required OAuth environment variables are missing.")
29+
raise HTTPException(status_code=500,
30+
detail="Configuration error: Required OAuth environment variables are missing.")
31+
32+
logger.logger.info("OAuth settings loaded successfully.")
33+
return internal_oauth_settings
34+
except FileNotFoundError:
35+
logger.logger.critical(".env file not found.")
36+
raise HTTPException(status_code=500, detail="Configuration error: .env file not found.")
37+
except Exception as e:
38+
logger.logger.critical(f"Error loading OAuth settings: {e}")
39+
raise HTTPException(status_code=500,
40+
detail="Configuration error: An error occurred while loading OAuth settings.")
41+
42+
43+
# Initialize OAuth settings
44+
oauth_settings = initialize_oauth_settings()

client/logger.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# client/logger.py
2+
3+
import logging
4+
5+
# Configure logging
6+
logging.basicConfig(
7+
level=logging.INFO,
8+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
9+
)
10+
11+
# Create a logger object that can be imported across the application
12+
logger = logging.getLogger(__name__)

client/main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# client/main.py
2+
3+
from fastapi import FastAPI
4+
from client.routers import auth, heroes
5+
6+
app = FastAPI(
7+
title="Hero API",
8+
description="An API to manage heroes secure by OAuth 2.0 auth code flow",
9+
version="1.0.0"
10+
)
11+
12+
# Register the oauth and heroes router
13+
app.include_router(auth.router, prefix="/auth", tags=["OAuth2 Back-channel"])
14+
app.include_router(heroes.router, prefix="/api", tags=["Heroes"])

client/models/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# client/models/__init__.py
2+
3+
from .dnd_hero import DnDHero, AbilityScores, SkillProficiencies, Equipment, Spell
4+
5+
__all__ = ["DnDHero", "AbilityScores", "SkillProficiencies", "Equipment", "Spell"]

client/models/ability_scores.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# client/models/ability_scores.py
2+
3+
from pydantic import BaseModel
4+
5+
6+
class AbilityScores(BaseModel):
7+
strength: int
8+
dexterity: int
9+
constitution: int
10+
intelligence: int
11+
wisdom: int
12+
charisma: int

client/models/dnd_hero.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# client/models/dnd_hero.py
2+
3+
from pydantic import BaseModel
4+
from typing import List, Optional
5+
from client.models.ability_scores import AbilityScores
6+
from client.models.equipment import Equipment
7+
from client.models.skill_proficiencies import SkillProficiencies
8+
from client.models.spell import Spell
9+
10+
11+
class DnDHero(BaseModel):
12+
id: str
13+
name: str
14+
race: str
15+
class_: str # Avoids conflict with the Python `class` keyword
16+
level: int
17+
background: Optional[str] = None
18+
alignment: Optional[str] = None
19+
20+
# Nested fields
21+
ability_scores: AbilityScores
22+
skill_proficiencies: SkillProficiencies
23+
equipment: Equipment
24+
spells: Optional[List[Spell]] = None # Optional, only for spellcasters
25+
26+
hit_points: int
27+
armor_class: int
28+
speed: int
29+
30+
# Additional optional features
31+
personality_traits: Optional[str] = None
32+
ideals: Optional[str] = None
33+
bonds: Optional[str] = None
34+
flaws: Optional[str] = None

client/models/equipment.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# client/models/equipment.py
2+
3+
from typing import Optional, List
4+
from pydantic import BaseModel
5+
6+
7+
class Equipment(BaseModel):
8+
weapon: Optional[str] = None
9+
armor: Optional[str] = None
10+
items: List[str] = []

0 commit comments

Comments
 (0)