Skip to content

Commit

Permalink
Merge pull request #69 from Intility/graph
Browse files Browse the repository at this point in the history
Documentation, closes #40, #45, #61, #62
  • Loading branch information
JonasKs committed Apr 4, 2022
2 parents 9c68b24 + 3702e93 commit 7c2d3bb
Show file tree
Hide file tree
Showing 28 changed files with 23,431 additions and 113 deletions.
3 changes: 2 additions & 1 deletion demo_project/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# How ever, it can be nice to have this project to run with real credentials, if you want to do manual testing
# with Azure AD.
# Create a new file with these values, and the project should run. (Single tenant, v2 tokens)
SECRET_KEY=
SECRET_KEY=asdasdkjakdsjadkj
APP_CLIENT_ID=
OPENAPI_CLIENT_ID=
TENANT_ID=
GRAPH_SECRET=
4 changes: 3 additions & 1 deletion demo_project/api/api_v1/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from demo_project.api.api_v1.endpoints import hello_world, hello_world_multi_auth
from demo_project.api.api_v1.endpoints import graph, hello_world, hello_world_multi_auth
from fastapi import APIRouter

api_router_azure_auth = APIRouter(tags=['hello'])
api_router_azure_auth.include_router(hello_world.router)
api_router_multi_auth = APIRouter(tags=['hello'])
api_router_multi_auth.include_router(hello_world_multi_auth.router)
api_router_graph = APIRouter(tags=['graph'])
api_router_graph.include_router(graph.router)
53 changes: 53 additions & 0 deletions demo_project/api/api_v1/endpoints/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Any

import httpx
from demo_project.api.dependencies import azure_scheme
from demo_project.core.config import settings
from fastapi import APIRouter, Depends, Request
from httpx import AsyncClient
from jose import jwt

router = APIRouter()


@router.get(
'/hello-graph',
summary='Fetch graph API using OBO',
name='graph',
operation_id='helloGraph',
dependencies=[Depends(azure_scheme)],
)
async def graph_world(request: Request) -> Any: # noqa: ANN401
"""
An example on how to use "on behalf of"-flow to fetch a graph token and then fetch data from graph.
"""
async with AsyncClient() as client:
# Use the users access token and fetch a new access token for the Graph API
obo_response: httpx.Response = await client.post(
f'https://login.microsoftonline.com/{settings.TENANT_ID}/oauth2/v2.0/token',
data={
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'client_id': settings.APP_CLIENT_ID,
'client_secret': settings.GRAPH_SECRET,
'assertion': request.state.user.access_token,
'scope': 'https://graph.microsoft.com/user.read',
'requested_token_use': 'on_behalf_of',
},
)

if obo_response.is_success:
# Call the graph `/me` endpoint to fetch more information about the current user, using the new token
graph_response: httpx.Response = await client.get(
'https://graph.microsoft.com/v1.0/me',
headers={'Authorization': f'Bearer {obo_response.json()["access_token"]}'},
)
graph = graph_response.json()
else:
graph = 'skipped'

# Return all the information to the end user
return (
{'claims': jwt.get_unverified_claims(token=request.state.user.access_token)}
| {'obo_response': obo_response.json()}
| {'graph_response': graph}
)
4 changes: 1 addition & 3 deletions demo_project/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@

azure_scheme = SingleTenantAzureAuthorizationCodeBearer(
app_client_id=settings.APP_CLIENT_ID,
scopes={
f'api://{settings.APP_CLIENT_ID}/user_impersonation': '**No client secret needed, leave blank**',
},
scopes={f'api://{settings.APP_CLIENT_ID}/user_impersonation': '**No client secret needed, leave blank**'},
tenant_id=settings.TENANT_ID,
)

Expand Down
2 changes: 2 additions & 0 deletions demo_project/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class AzureActiveDirectory(BaseSettings):
OPENAPI_CLIENT_ID: str = Field(default='', env='OPENAPI_CLIENT_ID')
APP_CLIENT_ID: str = Field(default='', env='APP_CLIENT_ID')
TENANT_ID: str = Field(default='', env='TENANT_ID')
GRAPH_SECRET: str = Field(default='', env='GRAPH_SECRET')
CLIENT_SECRET: str = Field(default='', env='CLIENT_SECRET')


class Settings(AzureActiveDirectory):
Expand Down
9 changes: 7 additions & 2 deletions demo_project/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from argparse import ArgumentParser

import uvicorn
from demo_project.api.api_v1.api import api_router_azure_auth, api_router_multi_auth
from demo_project.api.api_v1.api import api_router_azure_auth, api_router_graph, api_router_multi_auth
from demo_project.api.dependencies import azure_scheme
from demo_project.core.config import settings
from fastapi import FastAPI, Security
Expand All @@ -16,7 +16,7 @@
swagger_ui_init_oauth={
'usePkceWithAuthorizationCodeGrant': True,
'clientId': settings.OPENAPI_CLIENT_ID,
# 'additionalQueryStringParams': {'prompt': 'consent'},
'additionalQueryStringParams': {'prompt': 'consent'},
},
version='1.0.0',
description='## Welcome to my API! \n This is my description, written in `markdown`',
Expand Down Expand Up @@ -53,6 +53,11 @@ async def load_config() -> None:
prefix=settings.API_V1_STR,
# Dependencies specified on the API itself
)
app.include_router(
api_router_graph,
prefix=settings.API_V1_STR,
# Dependencies specified on the API itself
)


if __name__ == '__main__':
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/settings/_category_.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"label": "Settings",
"position": 4,
"position": 5,
"collapsible": true
}
22 changes: 0 additions & 22 deletions docs/docs/single-tenant/locking_down_on_roles.mdx

This file was deleted.

5 changes: 5 additions & 0 deletions docs/docs/usage-and-faq/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"label": "Usage and FAQ",
"position": 4,
"collapsible": true
}
54 changes: 54 additions & 0 deletions docs/docs/usage-and-faq/accessing_the_user.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: Accessing the user object
sidebar_position: 1
---

You can access your user object in two ways, either with `Depends(<schema name>)` or with `request.state.user`.

### `Depends(<schema name>)`

```python title="depends_api_example.py"
from fastapi import APIRouter, Depends

from demo_project.api.dependencies import azure_scheme
from fastapi_azure_auth.user import User

router = APIRouter()


@router.get(
'/hello-user',
response_model=User,
operation_id='helloWorldApiKey',
)
async def hello_user(user: User = Depends(azure_scheme)) -> dict[str, bool]:
"""
Wonder how this auth is done?
"""
return user.dict()
```


### `request.state.user`

```python title="request_state_user_api_example.py"
from fastapi import APIRouter, Depends, Request

from demo_project.api.dependencies import azure_scheme
from fastapi_azure_auth.user import User

router = APIRouter()


@router.get(
'/hello-user',
response_model=User,
operation_id='helloWorldApiKey',
dependencies=[Depends(azure_scheme)]
)
async def hello_user(request: Request) -> dict[str, bool]:
"""
Wonder how this auth is done?
"""
return request.state.user.dict()
```
18 changes: 18 additions & 0 deletions docs/docs/usage-and-faq/admin_consent_when_logging_in.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Approval required on login
sidebar_position: 5
---

If you're met by this screen when attempting to log in:

![approval_required](../../static/img/usage-and-faq/approval_required.png)

Then please follow the steps provided in [issue 45](https://github.com/Intility/fastapi-azure-auth/issues/45):

1. Navigate to [Azure -> Azure Active Directory -> App registrations](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps)
and find your backend application registration
2. Go to `Expose an API`
3. Under `Authorized client applications` click `Add a client application`
4. Add the client ID of your OpenAPI application registration (saved as `OPENAPI_CLIENT_ID` in your `.env` file)
5. Select the `api://<client id>/user_impersonation` checkbox
6. Click `Add Application`
55 changes: 55 additions & 0 deletions docs/docs/usage-and-faq/calling_your_apis_from_python.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: Calling your APIs from Python
sidebar_position: 3
---

In order to call your APIs from Python (or any other backend), you should use the [Client Credential Flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow).

1. Navigate to [Azure -> Azure Active Directory -> App registrations](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps)
and find your **OpenAPI application registration** (ideally you'd create a new one for your new application, but we'll reuse this for now)
2. Navigate over to `Certificate & secrets`
3. Click `New client secret`
4. Give it a name and an expiry time
5. Click `Add`

![secret_picture](../../static/img/usage-and-faq/secret_picture.png)


:::info
You can use client certificates too, but we won't cover this here.
:::

6. Copy the secret and save it for later.

![copy_secret](../../static/img/usage-and-faq/copy_secret.png)


7. Fetch the token from Azure, and then call your own API endpoint:

```python title="my_script.py"
from httpx import AsyncClient
from demo_project.core.config import settings

async with AsyncClient() as client:
azure_response = await client.post(
url=f'https://login.microsoftonline.com/{settings.TENANT_ID}/oauth2/v2.0/token',
data={
'grant_type': 'client_credentials',
'client_id': settings.OPENAPI_CLIENT_ID,
'client_secret': settings.CLIENT_SECRET,
'scope': f'api://{settings.APP_CLIENT_ID}/.default', # note: NOT .user_impersonation
}
)
token = azure_response.json()['access_token']

my_api_response = await client.get(
'http://localhost:8000/api/v1/hello-graph',
headers={'Authorization': f'Bearer {token}'},
)
print(my_api_response.json())
```

:::info
If you install `ipython` you can use `asyncio` code directly in your terminal.
(`poetry add ipython --dev`)
:::
62 changes: 62 additions & 0 deletions docs/docs/usage-and-faq/graph_usage.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Using Microsoft Graph
sidebar_position: 4
---

[Microsoft Graph](https://docs.microsoft.com/en-us/graph/overview) can be used together with the
[On Behalf Flow (OBO)](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow),
but in order to make this work you must alter your app registration configuration a bit.

:::info
This documentation is based off [issue #40](https://github.com/Intility/fastapi-azure-auth/issues/40)
:::


### Backend API App Registration
1. Head over to [Azure -> Azure Active Directory -> App registrations](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps),
and select your **Backend API** Application Registration
2. Navigate to the `Manifest` in the menu on the left
3. Add your OpenAPI/Swagger ClientID to the `knownClientApplications` (saved as `OPENAPI_CLIENT_ID` in your `.env`)

![manifest](../../static/img/usage-and-faq/manifest.png)


4. Select `API permissions` and ensure `User.Read` is there. If not, follow the steps in the picture below:
1. `Add a permission`
2. Select `Microsoft Graph` under `Microsoft APIs`
3. Select `Delegated permissions`
4. Search for and select `User.Read`
5. Click add permission

![user_read](../../static/img/usage-and-faq/user_read.png)


5. Select `Certificates & Secrets` and create a secret for your backend to use in order to fetch a Graph token
1. `New client secret`
2. Give it a name
3. Add

![graph_secret](../../static/img/usage-and-faq/graph_secret.png)



### OpenAPI App Registration

1. Head back to [Azure -> Azure Active Directory -> App registrations](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps),
and select your **OpenAPI/Swagger** Application Registration
2. Select `API permissions` in the menu on the left
3. Add `email`, `offline_access`, `openid`, `profile` scopes
1. `Add a permission`
2. Select `Microsoft Graph` under `Microsoft APIs`
3. Select `Delegated permissions`
4. Select the permissions
5. Click add permission

![user_read](../../static/img/usage-and-faq/openapi_scopes.png)


### Code
You can now fetch a graph token using the
[OBO flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow).
A full code example of an API using Graph can be found in the
[demo project](https://github.com/Intility/fastapi-azure-auth/blob/main/demo_project/api/api_v1/endpoints/graph.py).
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Locking down on roles
sidebar_position: 4
sidebar_position: 2
---

You can lock down on roles by creating your own wrapper dependency:
Expand Down
Loading

0 comments on commit 7c2d3bb

Please sign in to comment.