In this quickstart, you download a Python FastAPI web API code sample, and review the way it restricts resource access to authorized accounts only. The sample supports authorization of personal Microsoft accounts and accounts in any Azure Active Directory (Azure AD) organization.
- Azure account with an active subscription. Create an account for free.
- Azure Active Directory tenant
- Python 3.8 or higher
- Visual Studio Code (or equivalent)
Clone the sample from your shell or command line:
git clone https://github.com/TessFerrandez/fastapi_with_aad_auth.git
Register your web API in App Registrations in the Azure portal.
- Sign in to the Azure Portal
- If you have access to multiple tenants, use the Directory + subscription filter in the top menu to select the tenant in which you want to register an application.
- Find and select Azure Active Directory.
- Under Manage, select App registrations > New registration.
- Enter a Name for your application, for example
TODO-API
. Users of your app might see this name and you can change it later. - For Supported account types, select Accounts in any organizational directory.
- Select Register to create the application.
- On the Overview page, look for the Application (client) ID value, and then record it for later use. You'll need it to configure the API (that is,
API_CLIENT_ID
in the .env file). - Under Manage, select Expose an API > Add a scope. Accept the proposed Application ID URI (api://{clientId}) by selecting Save and continue, and then enter the following information:
- For Scope name, enter
access_as_user
- For Who can consent, ensure that the Admins and users option is selected.
- In the Admin consent display name box, enter
Access TODO API as a user
. - In the Admin consent description box, enter
Access TODO API as a user
. - In the User consent display name box, enter
Access TODO API as a user
. - In the User consent description box, enter
Access TODO API as a user
. - For State, keep Enabled.
- For Scope name, enter
- Select Add scope.
Add roles to the API App registration
- Under Manage, select App Roles and Create app role
- For Display name, enter
Admin
- For Allowed member types, select Users/Groups
- For Value, enter
Admin
- For Description, enter
Administrator
- Ensure that the checkbox for Do you want to enable this app role? is checked
- For Display name, enter
- Repeat the same steps to create a
User
app role
Add a secret to the API App registration
- Under Manage, select Certificates & secrets and New client secret
- For Description, enter
API Client Secret
- For Expires, leave it at 6 months
- For Description, enter
- Select Add
- On the Certificates & Secrets page, save the secret Value. You'll need it to configure the API (that is
API_CLIENT_SECRET
in the .env file.).
NOTE: You will not be able to access this value later so it is important that you save it. If you missed saving it, you can remove it and create a new secret.
Create an App registration for Swagger
- Back at App registrations, select New registration
- For Name, enter
TODO-API-SWAGGER
- For Supported account types, select Accounts in any organizational directory.
- For Redirect URI, select Single-page application (SPA) and enter
http://localhost:8000/oauth2-redirect
- For Name, enter
- Select Register to create the application.
- On the Overview page, look for the Application (client) ID value, and then record it for later use. You'll need it to configure the API (that is,
SWAGGER_UI_CLIENT_ID
in the .env file).
- In Azure Portal, find and select Azure Active Directory
- Under Manage, select Enterprise applications, and select the
TODO-API
application - Select Assign users and groups and then Add user/group
- Under Users, select your own user, and select Select to make your choice.
- Under Select a role, select either
Admin
orUser
and select Select to make your choice. Depending on which you choose, you will have access to different API endpoints - Select Assign to finish assigning the role to the user.
Configure environment variables
- Copy the
.env.sample
file and rename to.env
- Set the
API_CLIENT_ID
,API_CLIENT_SECRET
andSWAGGER_UI_CLIENT_ID
to the values you gathered above - Set the
AAD_TENANT_ID
to your Azure Tenant ID
Install required libraries
-
Open a command prompt and navigate to the directory where you cloned the repository
-
Create a new virtual environment to install your python libraries
python -m venv .venv
-
Activate your virtual environment
.\.venv\Scripts\activate # Windows source .venv/Scripts/activate # Linux
-
Upgrade pip to the latest version
python -m pip install --upgrade pip
-
Install the required libraries
pip install -r requirements.txt
pip install -r requirements.txt
Open Visual Studio Code and set the interpreter
-
In the terminal window, in the project directory, launch visual studio code
code .
-
Visual Studio Code may recognize that there is a virtual environment and ask you if you want to activate it. If this does not happen, use View->Command Palette->Python:Select Interpreter and select the
.venv:venv
interpreter (in rare cases you may need to manually select the.\.venv\Scripts\python.exe
if Visual Studio Code does not recommend it). -
Close down any open terminals and start a new one from Terminal->New Terminal. This ensures that any commands you run will be using the new interpreter.
Run the API from Visual Studio Code
- Open the file
app/main.py
- Press F5 to run under a debugger (or CTRL+F5 to run without a debugger)
- Under Debug Configuration select Python File
This will serve the api locally on your machine.
NOTE: The output suggests for you to browse to http://localhost:8000 - if you browse there you will see {"detail": "Not Found"}, this is normal as we don't have a default endpoint for our API.
- Browse to http://localhost:8000/health to reach the health endpoint. If all is working correctly, you should be greeted with "OK"
- Browse to http://localhost:8000/docs to see the Swagger UI and the available endpoints.
- Try an endpoint - for example [GET]/todoitems->Try it out->Execute. This should result in a
401:Unauthorized
Log in to use the API
- Log in using the Authorize button at the top right of the page.
- client_id: should be pre-filled, leave it as is
- client_secret: should be empty, leave it as is
- scopes: select the
Access API as user
scope - Select Authorize to log in
- Follow the prompts to log in with your account.
- In the Permissions requested dialog box, check the box to Consent on behalf of your organization and select Accept - you will only need to consent once for the API.
- In the Available authorizations dialog box, select Close
Access the [POST]/todoitems
- Select the [POST]/todoitems endpoint
- Select Try it out. You can change the request body, and give it another name than "Walk the dog" if you want
- Select Execute
- Verify that you receive a 201 result, and the resulting json for the created item.
You can find the code for the available routes in /app/api/routes/api.py
Endpoint | Request Method | Description | Authentication | Auth method |
---|---|---|---|---|
/health | GET | Get health status | No authentication | |
/todoitems | GET | Get my todo items | User | Depends(get_user) |
/todoitems | POST | Create todo item | User | Depends(get_user) |
/todoitems | DELETE | Delete all todo items | Admin | Depends(get_admin_user) |
/todoitems/{id} | GET | Get todo item | User (owner of item or admin) | Depends(get_todo_item_by_id_from_path) |
/todoitems/{id} | DELETE | Delete todo item | User (owner of item or admin) | Depends(get_todo_item_by_id_from_path) |
FastAPI has a powerful Dependency Injection system, that allows us to enforce security, authentication, role requirements etc.
In our case, we have created a simple dependency function in /app/api/dependencies/auth.py
to ensure that the user is logged in for the GET /todoitems
endpoint for example.
def get_user(user: User = Depends(authorize)) -> User:
return user
This, in turn, depends on authorize, defined in app/services/AzureADAuthorization.py
. authorize is an instance of the AzureADAuthorization, that when called (through the __call__
method) validates and decodes the authentication token against the Azure AD App and required scopes, and further generates a User instance based on the contents of the token.
If the token is invalid, or can't be processed, the AzureADAuthorization class returns a 401 UNAUTHORIZED HTTP status.
Because the AzureADAuthorization class derives from OAuth2AuthorizationCodeBearer, FastAPI (and Swagger) understands that the endpoint requires authentication, and displays the padlock in the Swagger UI.
There are multiple ways to protect the endpoints, and the various endpoints implemented in this sample, show some of these varieties.
By passing in user = Depends(get_user)
as a parameter to our endpoint function, we require the user to be authenticated and also get the user info, so that we can filter the todo items that belong to the user.
@router.get('/todoitems', status_code=status.HTTP_200_OK, name="Get My Todos [Admin or Owner of todo]")
async def get_my_todos(user: User = Depends(get_user)) -> TodoItemsInList:
items: List[TodoItem] = todo_repository.get_items_for_user(user)
return TodoItemsInList(items=items)
We can create more specialized dependency functions, that both validates that the user is authenticated, and validates that the user has the correct role.
def get_admin_user(user: User = Depends(authorize)) -> User:
if 'Admin' in user.roles:
return user
raise ForbiddenAccess('Admin privileges required')
We can then use the get_admin_user dependency function exactly as the get_user function.
The example below shows this usage with a slight modification. If you don't need to use the returned user for further processing, you can simply add the dependency to the router decorator.
@router.delete('/todoitems', status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_admin_user)], name="Delete all Todos [Admin]")
async def delete_all_todo_items() -> None:
todo_repository.delete_all_items()
We can also create more intricate dependencies, that don't only validate authorization, but also validate access to items.
def get_todo_item_by_id_from_path(id: int = Path(...), user: User = Depends(get_user)):
try:
todo: TodoItem = todo_repository.get_item(id)
except EntityNotFound:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Item does not exist')
if 'Admin' in user.roles or todo.owner_id == user.id:
return todo
raise HTTPException(status.HTTP_403_FORBIDDEN, detail='User is not allowed to access the item')
Because get_user indirectly depends on OAuth2AuthorizationCodeBearer, and get_todo_item_by_id_from_path depends on get_user, FastAPI and the Swagger UI will still understand that authorization is required.
@router.get('/todoitems/{id}', status_code=status.HTTP_200_OK, name="Get Todo by Id [Admin or Owner of todo]")
async def get_todo_by_id(id: int, todo: TodoItem = Depends(get_todo_item_by_id_from_path)) -> TodoItemInResponse:
return TodoItemInResponse(item=todo)