{"id":18233271,"url":"https://github.com/hvalfangst/azure-entraid-oauth2-client-server-python","last_synced_at":"2026-05-09T07:03:13.535Z","repository":{"id":259048884,"uuid":"876202691","full_name":"hvalfangst/azure-entraid-oauth2-client-server-python","owner":"hvalfangst","description":"Oauth2 on Azure Entra ID demonstrated with client and server FastAPI applications in Python. The server is deployed to Azure Web Apps via a GitHub Actions Workflow script. Client utilizes OIDC with authorization code flow.","archived":false,"fork":false,"pushed_at":"2024-11-03T09:35:22.000Z","size":1595,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-11-03T10:21:31.848Z","etag":null,"topics":["az-204","azure","azure-entra-id","entra-id","fastapi","github-actions","microsoft-entra-id","oauth2","oauth2-authorization-code-flow","openid-connect","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hvalfangst.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-10-21T15:10:12.000Z","updated_at":"2024-11-03T09:35:26.000Z","dependencies_parsed_at":"2024-11-03T10:22:05.512Z","dependency_job_id":"69080294-cd11-4a95-9594-c137f27b0cea","html_url":"https://github.com/hvalfangst/azure-entraid-oauth2-client-server-python","commit_stats":null,"previous_names":["hvalfangst/azure-oauth2-auth-code-flow-fastapi","hvalfangst/azure-oauth2-auth-code-flow-fastapi2","hvalfangst/azure-oauth2-oidc-auth-code-flow-homebrew-python","hvalfangst/azure-entraid-oauth2-client-server-python"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hvalfangst%2Fazure-entraid-oauth2-client-server-python","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hvalfangst%2Fazure-entraid-oauth2-client-server-python/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hvalfangst%2Fazure-entraid-oauth2-client-server-python/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hvalfangst%2Fazure-entraid-oauth2-client-server-python/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hvalfangst","download_url":"https://codeload.github.com/hvalfangst/azure-entraid-oauth2-client-server-python/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247838415,"owners_count":21004576,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["az-204","azure","azure-entra-id","entra-id","fastapi","github-actions","microsoft-entra-id","oauth2","oauth2-authorization-code-flow","openid-connect","python"],"created_at":"2024-11-04T15:03:34.856Z","updated_at":"2026-05-09T07:03:08.512Z","avatar_url":"https://github.com/hvalfangst.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Azure OAuth2 OIDC Auth Code Flow demonstration\n\nThe 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.\nIn 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\nautomagically solves all your needs.\n\nThe 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 \nAuth code flow. A comprehensive step-by-step guide is included on how to register the client and server on Azure Entra ID. \n\n## Requirements\n\n- **Platform**: x86-64, Linux/WSL\n- **Programming Language**: [Python 3](https://www.python.org/downloads/)\n- **Azure Account**: Access to [Azure Subscription](https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account)\n- **IAC Tool**: [Terraform](https://www.terraform.io/) \n\n\n## Allocate resources\n\nThe script [up](up.sh) provisions Azure resources by applying our [Terraform script](infra/terraform.tf).\n\nIt is necessary to create a file named **terraform.tfvars** in the [infra](infra) directory. This file holds sensitive information\nnecessary for terraform to be able to interact with your cloud resources, namely that of your tenant and subscription id. \nAn exemption for this file has been added in our [.gitignore](.gitignore) so that you do not accidentally commit it. \n\nThe file structure is as follows:\n\n![screenshot](images/terraform_tfvars.png)\n\n\n## Register server on Entra ID\n\nBefore deploying our server we need to create an app registration on Entra ID. \n\n### Create a new app registration\n\nLog into your Azure account on the web. Search for **Microsoft Entra ID**. Navigate to the **App registrations** blade and click on **New registration** button in the top left tab.\n\n![screenshot](images/azuread_app_registrations.png)\n\nChoose a suitable name. Here I have chosen **Hvalfangst Server** as the registration will be utilized by an API we are to deploy to Azure Web Apps in the coming sections. The client which is to interact with our server resource will **NOT** be deployed. It will merely run locally. The fact that\nboth the to-be-deployed server and the local client are both Python APIs (using the FastAPI framework) may seem confusing, but this is just for demonstration purposes. The client could have been a React SPA and the sever an Axum API for all you care. We do not need to set up a redirect URI for our server. This will be done for the client in later sections. \n\n![screenshot](images/azure_entra_id_register_hvalfangst_server_api.png)\n\nOnce 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.\n\n![screenshot](images/hvalfangst_server_api_app_registration.png)\n\n\n### Create Scope\n\nWe will now proceed to create scopes. Scopes are in essence fully customizable access right labels, meaning that you are free to choose **ANY** name. It is, however common to conform to the following pattern: **Resource.Access**. \nSay that you 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** and **DELETE**.\nFor 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. Example GET requests are to list all wines and to view details about a specific wine based on ID.\n\nClick on the **Add a scope** button under the **Expose an API** section, which is accessible from the **Expose an API** blade under **Manage**.\n\n![screenshot](images/hvalfangst_server_api_expose_api.png)\n\nSet the scope name to **Heroes.Read**. Clients with this scope may list and view heroes. As for consent, choose **Admins only**.\nFor the remainder of fields you are free to choose whatever you find descriptive.\n\n![screenshot](images/hvalfangst_server_api_add_scope.png)\n\nRepeat the above for subsequent scopes **Heroes.Write** and **Heroes.Delete**.\n\n![screenshot](images/hvalfangst_server_api_all_scopes.png)\n\nIt goes without saying that the chosen scopes are just simple examples. Feel free to adapt as you see fit. It is also important to mention that the newly created scopes \nare absolutely junk on its own. You **must** reference the scope names exactly as defined in your [server code](server/security/auth.py) for it to have any effect.\nThat is, you must implement logic in your endpoints which verifies the signature associated with the token derived from the auth header, ensure that the\naudience is the client id of the server app registration and that the scopes included in the decoded claims matches that of what is required for that specific endpoint.\nIn order to [create heroes](server/services/hero_service.py) one must have the scope **Heroes.Create** as specified in the [router](server/routers/heroes.py). \nSo nothing new under the sun for those who have set up JWT-based authorization before (without magic that is, major caveat).\n\n## Set up CI/CD via Deployment Center\n\nNow that we have provisioned necessary infrastructure and created an app registration for the server, we may proceed to create the pipeline used to deploy our code to Azure Web Apps.\nWe will do so by integrating our Web App to our **GitHub repository**. Azure Web Apps has the ability\nto 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\nwill be created in Azure in order to authenticate requests. Secrets will be created automatically and referenced in the CI/CD script. Once the\npipeline script has been created, we must adapt it slightly for it to work. More on this later.\n\nClick on the **Deployment Center** section under the **Deployment** blade. Choose GitHub as source and set the appropriate organization, repository and branch.\nFor authentication keep it as is (user-assigned identity). Click on the **Save** button in the top left corner.\n\n![screenshot](images/deployment_center.png)\n\nAfter 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,\nthis has been committed by Azure on our behalf.\n\n![screenshot](images/github_workflow_commit.png)\n\nNavigate to the bottom of the workflow file. Take notice of the three secrets being referenced.\n\n![screenshot](images/github_workflow_secrets.png)\n\nIf you navigate to your secrets and variables associated with your GitHub Actions you will see that there are three new secrets, which are the same as referenced above. Again,\nthese 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.\n\nFor 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 -\nwhich 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.\nThus, we need to change the script to reference files located under the server directory, as we are to deploy our server. \n\nWe are storing configuration values for our API in a class named [AzureConfig](server/config/config.py). Notice how the values for fields **TENANT_ID**\nand **SERVER_CLIENT_ID** are retrieved from the runtime environment - which means that these environment variables must be set somehow. When running the\nAPI locally for sake of testing one should **NOT** hardcode the associated values due to the risk of accidentally committing to SCM. Instead, you should\neither set environment variables on your system or retrieve them from an .env file, which, naturally, **HAS** to be added your .gitignore.\n\nProceed 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.\n\n![screenshot](images/github_actions_hvalfangst_secrets.png)\n\nWe 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\ncommand **az webapp config appsettings set** where the associated values are retrieved from our repository secrets we set above. \n\n## Deploy API \n\nIn order to deploy our code, we need to perform a manual GitHub Actions trigger. Head over to the **Actions** section of your repository. Click on the **Run workflow** button located in the right corner.\n\n![screenshot](images/github_actions.png)\n\nRunning the workflow should result in the following:\n\n![screenshot](images/github_actions_dispatched_task.png)\n\nNavigate 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.\n\n![screenshot](images/deployment_center_post_action.png)\n\nClick 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**\nhave 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\nbuild 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.\n\n![screenshot](images/hvalfangstlinuxwebapp_environment_variables.png)\n\nNow that we know that it deployed successfully it is finally time to access the API. Click on URI associated with **Default Domain**.\n\n![screenshot](images/overview_default_domain.png)\n\nYou will be prompted with the following index page, which indicates that the API is up and running.\n\n![screenshot](images/firefox_api_home.png)\n\nThe 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).\nThis route exposes 4 endpoints: **POST /heroes/**, **GET /heroes/**, **GET /heroes{hero_id}** and **DELETE /heroes/{hero_id}**.\nTake note of how one in every endpoint start by awaiting a function called [authorize](server/security/auth.py), with the arguments **token** and a **scope**.\nThe scope names referenced in aforementioned function call are exactly what was defined earlier. Hence, my little\nrant about scopes in and of itself being useless unless there is logic in place to actually enforce\nrequired scopes. We will utilize our [local client](client/main.py) to make HTTP calls to the server we deployed in previous sections. But first we must register it on MS Entra ID\nand assign it the appropriate permissions so that the scopes contained in its tokens  matches that of being specified in the protected server endpoints.\n\n\n## Register client on Azure Entra ID\n\nNow that have our server deployed and configured, it is time to talk about the client. As mentioned before, the client is **NOT** deployed to Azure - it is merely a local API.\n\n\n### Create a new app registration\n\nAs usual, one must create a new app registration akin to what was done with the server.\n\n![screenshot](images/hvalfangst_api_client_app_reg.png)\n\nAgain, take note of the **Client ID**.\n\n![screenshot](images/hvalfangst_client.png)\n\n### Create Secret\n\nRecall earlier when we mentioned that the server does not need secrets? Well, the client does. It is called **Client Secret** for a reason (naturally).\n\nHead over to the **Certificates \u0026 secrets** blade. Click on the **New client secret** button.\n\n![screenshot](images/hvalfangst_client_new_secret.png)\n\nPick a suitable name.\n\n![screenshot](images/hvalfangst_client_add_secret.png)\n\nStore the secret value for later.\n\n![screenshot](images/hvalfangst_client_secrets.png)\n\n### Add Redirect URL\n\nNow it's time for the confusing part, or at least, as Bane would put it - **FOR YOU**.\n\nHead over to the **Authentication** section of your client app registration. Click on **Add a platform** under **Platform configurations**. \nYou will be prompted with the choice between daemon, SPA or web. Pick web.\n\n![screenshot](images/hvalfangst_client_authentication.png)\n\nDoing so will prompt you with yet another screen with two input fields. The first input field is for setting the redirect URI. This may also be referred to as the callback URI or the back-channel URI. More on this below. The front-channel logout URL will not be\nused in this example, but is typically needed for applications which utilize Single Sign On (SSO). As for the **Implicit grant and hybrid flows**, check off both boxes so that the authorization server may issue both access and ID tokens. We do this\non purpose in order to demonstrate what goes on under the covers.\n\n![screenshot](images/hvalfangst_client_api_configure_web.png)\n\nWhen the client application starts, it opens a browser window and directs the user to Azure's authorization endpoint. \nThis prompts the user to log in with their Microsoft account and to authorize specific permissions, which in our case are **openid**, **Heroes.Read**, **Heroes.Write** and **Heroes.Delete**.\nThese are mandatory permissions for the Open ID Connect flow to work.\nIf the user successfully logs in and consents, Azure redirects the user to a pre-configured URL known as the redirect, or callback URI.\nWhen the user authorizes the application, Azure needs a secure location to send the authorization code. The redirect URI is this location. \nThe client application must \"listen\" at this URI to receive the code, which is necessary for exchanging it for tokens.\nIn practical terms, this is quite simple as one merely has to expose an HTTP endpoint in the API. In our case we have chosen the redirect URI of **http://localhost:8000/auth/callback**, \nbecause our client application runs locally on port 8000, the name of our router is called auth and our designated endpoint to handle the logic is named **callback**. \nAs is evident from peeking at our [callback endpoint](client/routers/auth.py), it extracts a value from the query parameter named **code**. \nIt then proceeds to call the OIDC flow handler function named [handle_openid_connect_flow](client/services/auth_service.py) with the aforementioned code retrieved from the authorization server in the callback endpoint. \nThe OIDC function calls yet another function named **get_access_token**, which attempt to exchange our authorization code for an actual access token by issuing a POST request to the token endpoint exposed by MS Entra ID.\nAs may be observed, the request body must be populated with our client id \u0026 secret, the authorization code and the grant type. Access and ID tokens will be fetched from the response on success and stored in our\n[token_storage](client/services/token_storage.py) class so that we may utilize the token in the endpoint associated with the [heroes router](client/routers/heroes.py). \nIt goes without saying that this is just a silly example on how to easily store the tokens in-memory. The MSAL library has their own, proper, caching implementation baked in.\n\n### Create .env file\n\nNow we have successfully deployed our API to Azure Web Apps, set up a CI/CD pipeline, deployed our server code to the Azure Web Apps slot and configured our client and server on Microsoft Entra ID.\nIt is finally time to run our Client.\n\nFor the local client to work, one must create a file named **.env_oauth**, which is to hold client and tenant id, secret and callback URI. This information\nmay be retrieved from your Client app registration. If you forgot to copy the client secret to your clipboard you may create a new one and use that instead.\nThe fields will be mapped to our [OAuthSettings](client/config/oauth.py) on startup and used when making calls to the authorization server in order to obtain tokens. \n\nThe final file should look as follows.\n\n![screenshot](images/env_oauth.png)\n\nNote that the scopes has to be the fully-qualified unique names associated with your server app registration. That is, they should be prefixed with api://{tenant_id}.\nThis is only done when requesting the scopes in our initial query. In our server we merely reference the last portion, that is **Heroes.Read**.\n\n\n## Running local Client API\nI have included a shell script named [run_client](client/run_client.sh) which may be used to serve our API on localhost.\n\nIt may be invoked as follows:\n```bash\nsh client/run_client.sh\n```\n\n## Permission request\n \nA new browser window will be opened on startup, which will prompt you to consent to a set of permissions. These will then in turn be set in you client app registration.\n\nClick on **Accept**.\n\n\n![screenshot](images/oidc_consent_part_1.png)\n\n![screenshot](images/oidc_consent_part_2.png)\n\nIn this repository I have for **sake of demonstration** decided to return the decoded tokens as JSON in the\ncallback function - which results in this information being rendered in your browser as part of the final redirect.\nThis comes in handy when debugging. \n\nYou will be prompted with the following on consenting to the above:\n\n![screenshot](images/tokens.png)\n\nI have highlighted the most important parts. The URI matches that of our registered redirect URI for the client app registration.\nThe query parameter **code** was provided to us by the authorization server when we accepted the consent. \nIt was then exchanged for actual tokens in our client's [get_access_token](client/services/auth_service.py) function.\nThe tokens were then [stored](client/services/token_storage.py), decoded and returned as JSON in the\nfinal part of our [handle_openid_connect_flow](client/services/auth_service.py) function. \n\nThe values contained in field **scp** correspond to our requested scopes of **Heroes.Read**\nand **Heroes.Write**. The value contained in field **aud** under key **access_token** should match that of the Client ID of your server app registration\nas this is indeed the audience of the token. Remember, we do **NOT** verify anything related to the token in our client code. Instead, we issue a POST request\nto our resource server with the token. Our resource server will verify that the signature and audience before even thinking about evaluating the scopes\nin order to protect endpoints.\n\nWe can now make authorized requests onto the resource server via our local API. Open up the **Postman** tool. A\nPostman collection [has been provided](client/postman/azure-entraid-oauth2-client-server-python.postman_collection.json) in this repository for ease of burden.\n\n![screenshot](images/postman_create_hero.png)\n\n![screenshot](images/server_web_app_log_stream.png)\n\n![screenshot](images/postman_get_hero.png)\n\n![screenshot](images/postman_list_heroes.png)\n\n\n![screenshot](images/postman_delete_hero.png)\n\n![screenshot](images/server_logs_deletion.png)\n\n![screenshot](images/postman_list_heroes_after_deletion.png)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhvalfangst%2Fazure-entraid-oauth2-client-server-python","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhvalfangst%2Fazure-entraid-oauth2-client-server-python","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhvalfangst%2Fazure-entraid-oauth2-client-server-python/lists"}