Skip to content

Commit ecc9610

Browse files
committed
An all-in-one sample for Django web project
1 parent a973c5d commit ecc9610

File tree

8 files changed

+299
-38
lines changed

8 files changed

+299
-38
lines changed

.env.sample

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# The following variables are required for the app to run.
2+
CLIENT_ID=<client id>
3+
CLIENT_SECRET=<client secret>
4+
5+
# This sample can be configured as a Microsoft Entra ID app,
6+
# or a Microsoft Entra External ID app.
7+
8+
# 1. If you are using a Microsoft Entra ID tenent,
9+
# configure the AUTHORITY variable as
10+
# "https://login.microsoftonline.com/TENANT_GUID"
11+
# or "https://login.microsoftonline.com/subdomain.onmicrosoft.com".
12+
#
13+
# Alternatively, use the ".../common" if you are building a multi-tenant AAD app
14+
# in world-wide cloud
15+
AUTHORITY=https://login.microsoftonline.com/common
16+
#
17+
#
18+
# 2. If you are using a Microsoft Entra External ID for customers (CIAM) tenant,
19+
# configure AUTHORITY as "https://subdomain.ciamlogin.com"
20+
#AUTHORITY=<authority url>
21+
22+
REDIRECT_VIEW=getAToken # Used for forming an absolute URL to your redirect URI.
23+
# The absolute URL must match the redirect URI you set
24+
# in the app's registration in the Azure portal.
25+
26+
# You can use your own API's scope. Here we use a Microsoft Graph API as an example
27+
SCOPE=User.ReadBasic.All
28+
29+
# The sample app will acquire a token to call this API
30+
ENDPOINT=https://graph.microsoft.com/v1.0/users # This resource requires no admin consent

README.md

+163-35
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,185 @@
1-
# Project Name
1+
# Sample: A Python Django web project to sign in users and call APIs with the Microsoft Entra ID
22

3-
(short, 1-3 sentenced, description of the project)
3+
This is a Python web application that uses the
4+
[Django framework](https://www.djangoproject.com/)
5+
and the
6+
[Microsoft Entra ID](https://www.microsoft.com/security/business/microsoft-entra)
7+
to sign in users and make authenticated calls to the Microsoft Graph API.
48

5-
## Features
6-
7-
This project framework provides the following features:
8-
9-
* Feature 1
10-
* Feature 2
11-
* ...
129

1310
## Getting Started
1411

1512
### Prerequisites
1613

17-
(ideally very short, if any)
18-
19-
- OS
20-
- Library version
21-
- ...
14+
- Register your web application in the Microsoft Entra admin center,
15+
by following step 1, 2 and 3 of this
16+
[Quickstart: Add sign-in with Microsoft to a Python web app](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-web-app-python-sign-in?tabs=windows)
17+
- Have [Python](https://python.org) 3.8+ installed
2218

2319
### Installation
2420

25-
(ideally very short)
26-
27-
- npm install [package name]
28-
- mvn install
29-
- ...
21+
1. This sample already implements sign-in and calling downstream API.
22+
You can clone
23+
[its repo](https://github.com/Azure-Samples/ms-identity-python-webapp-django)
24+
or download its zip package, and then start using it or build on top of it.
25+
(Alternatively, you can follow our [tutorial](#tutorial) to learn
26+
how to build this from scratch, or how to add auth to your existing project.
27+
2. `cd project_name`
28+
3. Run `pip install -r requirements.txt` to install dependencies
29+
4. Run `python manage.py migrate` to initialize your Django project
30+
5. Copy [`.env.sample`](https://github.com/Azure-Samples/ms-identity-python-webapp-django/blob/main/.env.sample) as `.env`,
31+
and then modify its content based on your application's registration.
3032

3133
### Quickstart
32-
(Add steps to get up and running quickly)
3334

34-
1. git clone [repository clone url]
35-
2. cd [repository name]
36-
3. ...
35+
1. After finishing installation, now you can run
36+
`python manage.py runserver localhost:5000` to start your development server.
37+
You may need to change to a different port to match your redirect_uri setup.
38+
2. Now visit http://localhost:5000
39+
40+
41+
## Tutorial
42+
43+
> Note: You do not have to read this tutorial.
44+
>
45+
> * If you are starting a new project, you can begin with
46+
> [our sample](https://github.com/Azure-Samples/ms-identity-python-webapp-django)
47+
> and build on top of it.
48+
49+
The following chapters teach you
50+
how to build a Django project with Microsoft Entra ID from scratch.
51+
After that, you will also know how to modify an existing Python Django project
52+
to sign in users and call APIs with the Microsoft Entra ID.
53+
54+
### Preface: Have a Django web project as a starting point
55+
56+
You can use
57+
[Django's own tutorial, part 1](https://docs.djangoproject.com/en/5.0/intro/tutorial01/)
58+
as a reference. What we need are these steps:
59+
60+
1. `django-admin startproject mysite`
61+
2. `python manage.py migrate`
62+
3. `python manage.py runserver localhost:5000`
63+
You may need to change to a different port to match your redirect_uri setup.
64+
65+
4. Now, add an `index` view to your project.
66+
For now, it can simply return a "hello world" page to any visitor.
67+
68+
```python
69+
from django.http import HttpResponse
70+
def index(request):
71+
return HttpResponse("Hello, world. Everyone can read this line.")
72+
```
73+
74+
### Chapter 1: Enable your Python Django web app to sign in users
75+
76+
1. At the beginning of `mysite/settings.py` file, create a global Auth helper like this:
77+
78+
```python
79+
from identity.django import Auth
80+
AUTH = Auth("your_client_id", client_credential=..., authority=..., redirect_view="xyz")
81+
```
82+
83+
Generally speaking, your redirect_uri shall contain a top-level path such as
84+
`http://localhost:5000/redirect`,
85+
then your setting here shall be `..., redirect_view="redirect")`.
86+
87+
2. Inside the same `mysite/settings.py` file,
88+
add `"identity",` into the `INSTALLED_APPS` list,
89+
to enable the default templates came with the `identity` package.
90+
91+
```python
92+
INSTALLED_APPS = [
93+
...,
94+
"identity",
95+
]
96+
```
97+
98+
3. Modify the `mysite/urls.py` to add these content:
99+
100+
```python
101+
...
102+
from django.urls import path, include
103+
from django.conf import settings
104+
105+
urlpatterns = [
106+
path("", include(settings.AUTH.urlpatterns)),
107+
...
108+
]
109+
```
110+
111+
4. Now, inside the `mysite/views.py`,
112+
for each view that you would like to enforce user login,
113+
simply add a one-liner `@settings.AUTH.login_required` before each view.
114+
For example,
115+
116+
```python
117+
...
118+
from django.conf import settings
119+
120+
@settings.AUTH.login_required
121+
def index(request):
122+
return HttpResponse("Hello, if you can read this, you're signed in.")
123+
```
124+
125+
That is it. Now visit `http://localhost:5000` again, you will see the sign-in experience.
126+
127+
128+
### Chapter 2: Get an Access Token and call Microsoft Graph
129+
130+
This chapter begins where chapter 1 left off.
131+
We will add the following new view which will call a downstream API.
132+
133+
```python
134+
import json
135+
from django.shortcuts import redirect, render
136+
import requests
137+
138+
...
139+
140+
def call_downstream_api(request):
141+
token = settings.AUTH.get_token_for_user(["your_scope1", "your_scope2"])
142+
if "error" in token:
143+
return redirect(settings.AUTH.login)
144+
api_result = requests.get( # Use access token to call downstream api
145+
"https://example.com/your/api",
146+
headers={'Authorization': 'Bearer ' + token['access_token']},
147+
timeout=30,
148+
).json() # Here we assume the response format is json
149+
return render(request, 'display.html', {
150+
"title": "Result of downstream API call",
151+
"content": json.dumps(api_result, indent=4),
152+
})
153+
```
154+
155+
The `settings.AUTH.get_token_for_user(...)` will also implicitly enforce sign-in.
156+
157+
You can refer to our
158+
[full sample here](https://github.com/Azure-Samples/ms-identity-python-webapp-django)
159+
to pick up other minor details, such as how to modify `urls.py` accordingly,
160+
and how to add templates for this new view (and for the existing `index()` view).
37161

38162

39-
## Demo
163+
### What is next?
40164

41-
A demo app is included to show how to use the project.
165+
You may notice that, this sample's code base contains no templates used by sign-in.
166+
That is because the upstream helper library provides built-in minimalist templates.
167+
That is convenient, but at some point you may want to customize the look-and-feel.
168+
You can do so by copying
169+
[the upstream templates](https://github.com/rayluo/identity/tree/dev/identity/templates/identity)
170+
into your own project's `templates/identity` sub-directory, and then start hacking.
42171

43-
To run the demo, follow these steps:
44172

45-
(Add steps to start up the demo)
173+
## Contributing
46174

47-
1.
48-
2.
49-
3.
175+
If you find a bug in the sample, please raise the issue on [GitHub Issues](../../issues).
50176

51-
## Resources
177+
If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md).
52178

53-
(Any additional resources or related projects)
179+
This project has adopted the
180+
[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
181+
For more information, see the
182+
[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
183+
184+
with any additional questions or comments.
54185

55-
- Link to supporting information
56-
- Link to similar sample
57-
- ...

mysite/settings.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,20 @@
99
For the full list of settings and their values, see
1010
https://docs.djangoproject.com/en/4.2/ref/settings/
1111
"""
12-
import os, random, string
1312
from pathlib import Path
1413

14+
import os, random, string
15+
from dotenv import load_dotenv
16+
from identity.django import Auth
17+
load_dotenv()
18+
AUTH = Auth(
19+
os.getenv('CLIENT_ID'),
20+
client_credential=os.getenv('CLIENT_SECRET'),
21+
redirect_view=os.getenv('REDIRECT_VIEW'),
22+
scopes=os.getenv('SCOPE', "").split(),
23+
authority=os.getenv('AUTHORITY'),
24+
)
25+
1526
# Build paths inside the project like this: BASE_DIR / 'subdir'.
1627
BASE_DIR = Path(__file__).resolve().parent.parent
1728

@@ -37,6 +48,7 @@
3748
'django.contrib.sessions',
3849
'django.contrib.messages',
3950
'django.contrib.staticfiles',
51+
"identity", # To utilize the default templates came with the identity package
4052
]
4153

4254
MIDDLEWARE = [
@@ -54,7 +66,12 @@
5466
TEMPLATES = [
5567
{
5668
'BACKEND': 'django.template.backends.django.DjangoTemplates',
57-
'DIRS': [],
69+
'DIRS': [
70+
BASE_DIR / "templates", # Enable this project's templates folder.
71+
# You can also add your own "identity/login.html" and
72+
# "identity/auth_error.html" into this folder
73+
# to override the default templates came with identity package.
74+
],
5875
'APP_DIRS': True,
5976
'OPTIONS': {
6077
'context_processors': [

mysite/urls.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@
1515
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
1616
"""
1717
from django.contrib import admin
18-
from django.urls import path
18+
from django.urls import path, include
19+
from django.conf import settings
20+
21+
from . import views
22+
1923

2024
urlpatterns = [
25+
path("", include(settings.AUTH.urlpatterns)),
26+
path('', views.index, name="index"),
27+
path("call_downstream_api", views.call_downstream_api),
2128
path('admin/', admin.site.urls),
2229
]

mysite/views.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import os
2+
import json
3+
4+
from django.conf import settings
5+
from django.shortcuts import redirect, render
6+
import requests
7+
8+
9+
__version__ = "0.1.0"
10+
11+
12+
@settings.AUTH.login_required
13+
def index(request):
14+
user = settings.AUTH.get_user(request)
15+
assert user # User would not be None since we decorated this view with @login_required
16+
return render(request, 'index.html', dict(
17+
user=user,
18+
version=__version__,
19+
edit_profile_url=settings.AUTH.get_edit_profile_url(request),
20+
downstream_api=os.getenv("ENDPOINT"),
21+
))
22+
23+
# We choose to not decorate this view with @login_required,
24+
# because its get_token_for_user() could ask for more scopes than initial login,
25+
# so we want to handle the error separately.
26+
def call_downstream_api(request):
27+
token = settings.AUTH.get_token_for_user(request, os.getenv("SCOPE", "").split())
28+
if "error" in token:
29+
return redirect(settings.AUTH.login)
30+
api_result = requests.get( # Use access token to call downstream api
31+
os.getenv("ENDPOINT"),
32+
headers={'Authorization': 'Bearer ' + token['access_token']},
33+
timeout=30,
34+
).json() # Here we assume the response format is json
35+
return render(request, 'display.html', {
36+
"title": "Result of downstream API call",
37+
"content": json.dumps(api_result, indent=4),
38+
})
39+

requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
# Since we currently still supports Python 3.7, we choose Django version accordingly.
22
# See https://docs.djangoproject.com/en/5.0/faq/install/#what-python-version-can-i-use-with-django
33
django>=3.2,<6
4+
5+
identity>=0.4,<0.5
6+
python-dotenv<0.22
7+
requests>=2,<3

templates/display.html

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>{{title}}</title>
6+
</head>
7+
<body>
8+
<a href="javascript:window.history.go(-1)">Back</a> <!-- Displayed on top of a potentially long page, so it will remain visible -->
9+
<h1>{{title}}</h1>
10+
<pre>{{content}}</pre> <!-- Just a generic viewer to show the content as-is -->
11+
</body>
12+
</html>

templates/index.html

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Microsoft Entra ID Django Web App Sample: Index</title>
6+
</head>
7+
<body>
8+
<h1>Microsoft Entra ID Django Web App Sample {{version}}</h1>
9+
<h2>Welcome {{ user.name }}!</h2>
10+
11+
<ul>
12+
{% if downstream_api %}
13+
<li><a href='/call_downstream_api'>Call a downstream API</a></li>
14+
{% endif %}
15+
16+
{% if edit_profile_url %}
17+
<li><a href='{{ edit_profile_url }}'>Edit Profile</a></li>
18+
{% endif %}
19+
20+
<li><a href="/logout">Logout</a></li>
21+
</ul>
22+
</body>
23+
</html>
24+

0 commit comments

Comments
 (0)