├── .env.sample.b2c ├── .env.sample.entra-id ├── .env.sample.external-id ├── .env.sample.external-id-custom-domain ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── codeql.yml ├── .gitignore ├── .vscode └── extensions.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── auto-config.json ├── manage.py ├── mysite ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py ├── views.py └── wsgi.py ├── requirements.txt ├── static └── topology.png ├── templates ├── display.html └── index.html ├── tests ├── __init__.py └── test_views.py └── tox.ini /.env.sample.b2c: -------------------------------------------------------------------------------- 1 | # This sample can be configured to work with Azure AD B2C. 2 | # 3 | # If you are using an Azure AD B2C tenant, 4 | # configure the B2C_TENANT_NAME variable with your tenant name, such as "contoso". 5 | B2C_TENANT_NAME= 6 | 7 | # You will also need to configure the following variables with your B2C policies. 8 | SIGNUPSIGNIN_USER_FLOW=B2C_1_signinpolicy 9 | 10 | # Optionally, you may configure the following variables with your B2C policies. 11 | EDITPROFILE_USER_FLOW=B2C_1_ProfileEditPolicy 12 | RESETPASSWORD_USER_FLOW=B2C_1_Password_Reset_Policy 13 | 14 | # The following variables are required for the app to run. 15 | CLIENT_ID= 16 | CLIENT_SECRET= 17 | 18 | # Your project's redirect URI that you registered in Azure Portal. 19 | # For example: http://localhost:5000/redirect 20 | REDIRECT_URI= 21 | 22 | # The following variables are required if the app needs to call an API. 23 | # 24 | # Multiple scopes can be added into the same line, separated by a space. 25 | # Here we use a Microsoft Graph API as an example 26 | # You may need to use your own API's scope. 27 | #SCOPE=User.Read 28 | # 29 | # The sample app will acquire a token to call this API 30 | #ENDPOINT=https://graph.microsoft.com/v1.0/me 31 | 32 | # This one is required if you are deploying to Azure App Service. 33 | POST_BUILD_COMMAND=python manage.py migrate 34 | 35 | -------------------------------------------------------------------------------- /.env.sample.entra-id: -------------------------------------------------------------------------------- 1 | # This sample can be configured to work with Microsoft Entra ID. 2 | # 3 | # If you are using a Microsoft Entra ID tenant, 4 | # configure the AUTHORITY variable as 5 | # "https://login.microsoftonline.com/TENANT_GUID" 6 | # or "https://login.microsoftonline.com/contoso.onmicrosoft.com". 7 | # 8 | # Alternatively, use "https://login.microsoftonline.com/common" for multi-tenant app. 9 | AUTHORITY= 10 | 11 | # The following variables are required for the app to run. 12 | CLIENT_ID= 13 | CLIENT_SECRET= 14 | 15 | # Your project's redirect URI that you registered in Azure Portal. 16 | # For example: http://localhost:5000/redirect 17 | REDIRECT_URI= 18 | 19 | # The following variables are required if the app needs to call an API. 20 | # 21 | # Multiple scopes can be added into the same line, separated by a space. 22 | # Here we use a Microsoft Graph API as an example 23 | # You may need to use your own API's scope. 24 | #SCOPE=User.Read 25 | # 26 | # The sample app will acquire a token to call this API 27 | #ENDPOINT=https://graph.microsoft.com/v1.0/me 28 | 29 | # This one is required if you are deploying to Azure App Service. 30 | POST_BUILD_COMMAND=python manage.py migrate 31 | 32 | -------------------------------------------------------------------------------- /.env.sample.external-id: -------------------------------------------------------------------------------- 1 | # This sample can be configured to work with Microsoft External ID. 2 | # 3 | # If you are using a Microsoft Entra External ID for customers (CIAM) tenant, 4 | # configure AUTHORITY as https://contoso.ciamlogin.com/contoso.onmicrosoft.com 5 | AUTHORITY= 6 | 7 | # The following variables are required for the app to run. 8 | CLIENT_ID= 9 | CLIENT_SECRET= 10 | 11 | # Your project's redirect URI that you registered in Azure Portal. 12 | # For example: http://localhost:5000/redirect 13 | REDIRECT_URI= 14 | 15 | # The following variables are required if the app needs to call an API. 16 | # 17 | # Multiple scopes can be added into the same line, separated by a space. 18 | # Here we use a Microsoft Graph API as an example 19 | # You may need to use your own API's scope. 20 | #SCOPE=User.Read 21 | # 22 | # The sample app will acquire a token to call this API 23 | #ENDPOINT=https://graph.microsoft.com/v1.0/me 24 | 25 | # This one is required if you are deploying to Azure App Service. 26 | POST_BUILD_COMMAND=python manage.py migrate 27 | 28 | -------------------------------------------------------------------------------- /.env.sample.external-id-custom-domain: -------------------------------------------------------------------------------- 1 | # This sample can be configured to work with Microsoft External ID with custom domain. 2 | # 3 | # If you are using a Microsoft External ID tenant with custom domain, 4 | # configure the OIDC_AUTHORITY variable as 5 | # "https://www.contoso.com/TENANT_GUID/v2.0" 6 | OIDC_AUTHORITY= 7 | 8 | # The following variables are required for the app to run. 9 | CLIENT_ID= 10 | CLIENT_SECRET= 11 | 12 | # Your project's redirect URI that you registered in Azure Portal. 13 | # For example: http://localhost:5000/redirect 14 | REDIRECT_URI= 15 | 16 | # The following variables are required if the app needs to call an API. 17 | # 18 | # Multiple scopes can be added into the same line, separated by a space. 19 | # Here we use a Microsoft Graph API as an example 20 | # You may need to use your own API's scope. 21 | #SCOPE=User.Read 22 | # 23 | # The sample app will acquire a token to call this API 24 | #ENDPOINT=https://graph.microsoft.com/v1.0/me 25 | 26 | # This one is required if you are deploying to Azure App Service. 27 | POST_BUILD_COMMAND=python manage.py migrate 28 | 29 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '21 23 * * 0' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 32 | permissions: 33 | # required for all workflows 34 | security-events: write 35 | 36 | # only required for workflows in private repositories 37 | actions: read 38 | contents: read 39 | 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | include: 44 | - language: python 45 | build-mode: none 46 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 47 | # Use `c-cpp` to analyze code written in C, C++ or both 48 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 49 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 50 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 51 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 52 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 53 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 54 | steps: 55 | - name: Checkout repository 56 | uses: actions/checkout@v4 57 | 58 | # Initializes the CodeQL tools for scanning. 59 | - name: Initialize CodeQL 60 | uses: github/codeql-action/init@v3 61 | with: 62 | languages: ${{ matrix.language }} 63 | build-mode: ${{ matrix.build-mode }} 64 | # If you wish to specify custom queries, you can do so here or in a config file. 65 | # By default, queries listed here will override any specified in a config file. 66 | # Prefix the list here with "+" to use these queries and those in the config file. 67 | 68 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 69 | # queries: security-extended,security-and-quality 70 | 71 | # If the analyze step fails for one of the languages you are analyzing with 72 | # "We were unable to automatically build your code", modify the matrix above 73 | # to set the build mode to "manual" for that language. Then modify this step 74 | # to build your code. 75 | # ℹ️ Command-line programs to run using the OS shell. 76 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 77 | - if: matrix.build-mode == 'manual' 78 | run: | 79 | echo 'If you are using a "manual" build mode for one or more of the' \ 80 | 'languages you are analyzing, replace this with the commands to build' \ 81 | 'your code, for example:' 82 | echo ' make bootstrap' 83 | echo ' make release' 84 | exit 1 85 | 86 | - name: Perform CodeQL Analysis 87 | uses: github/codeql-action/analyze@v3 88 | with: 89 | category: "/language:${{matrix.language}}" 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["ms-azuretools.ms-entra"] 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Integrating Microsoft Entra ID with a Python web application written in Django 2 | 3 | This is a multi-purpose [Django](https://www.djangoproject.com/) web app sample. 4 | Write your app like this once, and the same implementation will support 4x2=8 scenarios. 5 | 6 | | | Microsoft Entra ID | External ID | External ID with Custom Domain | Azure AD B2C | 7 | |----------------|--------------------|-------------|--------------------------------|--------------| 8 | | Web App Sign-In & Sign-Out | ✓ | ✓ | ✓ | ✓ | 9 | | Web App Calls a web API | ✓ | ✓ | ✓ | ✓ | 10 | 11 | 12 | ![Topology](static/topology.png) 13 | 14 | ## Getting Started 15 | 16 | ### Prerequisites 17 | 18 | 1. Have [Python](https://python.org) 3.8+ installed 19 | 1. Clone from 20 | [its repo](https://github.com/Azure-Samples/ms-identity-python-webapp-django) 21 | or download its zip package, and then start using it or build on top of it. 22 | 1. `cd project_name` 23 | 1. Run `pip install -r requirements.txt` to install dependencies 24 | 1. Run `python manage.py migrate` to initialize your Django project 25 | 1. Run `python manage.py runserver localhost:5000` and then browse to http://localhost:5000 26 | You may need to change to a different port to match your redirect_uri setup. 27 | 28 | 29 | ### How to configure and use this sample 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 48 | 53 | 58 | 64 | 65 | 66 | 67 | 68 | 74 | 80 | 86 | 92 | 93 | 94 | 95 | 102 | 103 | 104 | 105 | 106 | 112 | 113 | 114 | 115 | 116 | 131 | 132 | 133 | 134 | 140 | 148 | 149 | 150 |
Microsoft Entra IDMicrosoft Entra External IDMicrosoft Entra External ID with Custom DomainAzure AD B2C
App Registration 43 | 44 | Following only the step 1, 2 and 3 of this 45 | [Quickstart: Add sign-in with Microsoft to a Python web app](https://learn.microsoft.com/entra/identity-platform/quickstart-web-app-python-sign-in?tabs=windows) 46 | 47 | 49 | 50 | Follow only the page 1 of this [Tutorial: Prepare your customer tenant ...](https://learn.microsoft.com/entra/external-id/customers/tutorial-web-app-python-flask-prepare-tenant) 51 | 52 | 54 | 55 | Coming soon. 56 | 57 | 59 | 60 | Following only the step 1 and 2 (including 2.1 and 2.2) of this 61 | [Configure authentication in a sample Python web app by using Azure AD B2C](https://learn.microsoft.com/azure/active-directory-b2c/configure-authentication-sample-python-web-app?tabs=linux) 62 | 63 |
Configuration 69 | 70 | Copy this [Entra ID template](.env.sample.entra-id) 71 | as `.env` and then modify `.env` with your app's settings. 72 | 73 | 75 | 76 | Copy this [External ID template](.env.sample.external-id) 77 | as `.env` and then modify `.env` with your app's settings. 78 | 79 | 81 | 82 | Copy this [External ID with Custom Domain template](.env.sample.external-id-custom-domain) 83 | as `.env` and then modify `.env` with your app's settings. 84 | 85 | 87 | 88 | Copy this [Azure AD B2C template](.env.sample.b2c) 89 | as `.env` and then modify `.env` with your app's settings. 90 | 91 |
96 | 97 | Do not reverse the order of the configuration steps above. 98 | If you put your app credentials into the template and then copy it into `.env`, 99 | you risk accidentally committing your templates with credentials into Version Control System. 100 | 101 |
Web App Sign In & Sign Out 107 | 108 | With the basic configuration above, 109 | you can now browse to the index page of this sample to try the sign-in/sign-out experience. 110 | 111 |
Web App Calls a web API 117 | 118 | Add the web API's *endpoint* into your `.env` file. 119 | Also add the *scopes* it needs, separated by space. 120 | The following example is the settings needed to call the Microsoft Graph API. 121 | You may need to replace the their values with your own API endpoint and its scope.. 122 | 123 | ```ini 124 | ENDPOINT=https://graph.microsoft.com/v1.0/me 125 | SCOPE=User.Read 126 | ``` 127 | 128 | Now restart this sample and try its "Call API" experience. 129 | 130 |
135 | 136 | Deploy to 137 | [Azure App Service](https://azure.microsoft.com/en-us/products/app-service) 138 | 139 | 141 | 142 | * Follow the ["Quickstart: Deploy a Python (Django or Flask) web app to Azure App Service"](https://learn.microsoft.com/en-us/azure/app-service/quickstart-python), 143 | but replace its sample app (which does not do user sign-in) with this web app. 144 | * [Configure your app's settings](https://learn.microsoft.com/en-us/azure/app-service/configure-common?tabs=portal#configure-app-settings) 145 | to define environment variables mentioned in the **Configuration** row of this table. 146 | 147 |
151 | 152 | 153 | ## How to build this sample (or a new web project) from scratch 154 | 155 | You can follow the 156 | [instructions for Django, from the underlying library](https://identity-library.readthedocs.io/en/latest/django.html). 157 | 158 | You can refer to the 159 | [source code of this full sample here](https://github.com/Azure-Samples/ms-identity-python-webapp-django/tree/main/mysite) 160 | to pick up other minor details, such as how to modify `urls.py` accordingly, 161 | and how to add templates for this new view (and for the existing `index()` view). 162 | 163 | 164 | ## Contributing 165 | 166 | If you find a bug in the sample, please raise the issue on [GitHub Issues](../../issues). 167 | 168 | If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). 169 | 170 | This project has adopted the 171 | [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 172 | For more information, see the 173 | [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 174 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) 175 | with any additional questions or comments. 176 | 177 | -------------------------------------------------------------------------------- /auto-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "note": "This is an instruction file used by Microsoft tools to generate Quickstart. App developers do not need to use this file.", 3 | "$schema": "https://raw.githubusercontent.com/microsoft/vscode-ms-entra/main/schemas/sampleConfigV1.schema.json", 4 | "_version": "1", 5 | "credential": "secret", 6 | "appUpdates": { 7 | "web": { 8 | "redirectUris": [ 9 | "http://localhost:5000/getAToken" 10 | ] 11 | } 12 | }, 13 | "replacements": { 14 | ".env.sample.entra-id":{ 15 | "out":".env", 16 | "tokens": { 17 | "":"authorityEndpointHost", 18 | "": "appId", 19 | "":"clientSecret", 20 | "":"redirectUri" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /mysite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-python-webapp-django/cdc6ee802f3b610898159dc1e5adbc3c9aae7a70/mysite/__init__.py -------------------------------------------------------------------------------- /mysite/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for mysite project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /mysite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for mysite project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | from pathlib import Path 13 | 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_uri=os.getenv('REDIRECT_URI'), 22 | authority=os.getenv('AUTHORITY'), 23 | oidc_authority=os.getenv('OIDC_AUTHORITY'), 24 | b2c_tenant_name=os.getenv('B2C_TENANT_NAME'), 25 | b2c_signup_signin_user_flow=os.getenv('SIGNUPSIGNIN_USER_FLOW'), 26 | b2c_edit_profile_user_flow=os.getenv('EDITPROFILE_USER_FLOW'), 27 | b2c_reset_password_user_flow=os.getenv('RESETPASSWORD_USER_FLOW'), 28 | ) 29 | 30 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 31 | BASE_DIR = Path(__file__).resolve().parent.parent 32 | 33 | 34 | # Quick-start development settings - unsuitable for production 35 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 36 | 37 | # SECURITY WARNING: keep the secret key used in production secret! 38 | SECRET_KEY = os.getenv("SECRET_KEY", default="".join(random.choices(string.printable, k=64))) 39 | 40 | # SECURITY WARNING: don't run with debug turned on in production! 41 | DEBUG = True 42 | 43 | 44 | if os.getenv("WEBSITE_HOSTNAME"): # Settings for Azure App Service, 45 | # See https://learn.microsoft.com/en-us/azure/app-service/configure-language-python#production-settings-for-django-apps 46 | ALLOWED_HOSTS = [os.getenv("WEBSITE_HOSTNAME")] 47 | 48 | # See https://learn.microsoft.com/en-us/azure/app-service/configure-language-python#detect-https-session 49 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 50 | USE_X_FORWARDED_HOST = True 51 | else: 52 | ALLOWED_HOSTS = [] 53 | 54 | 55 | # Application definition 56 | 57 | INSTALLED_APPS = [ 58 | 'django.contrib.admin', 59 | 'django.contrib.auth', 60 | 'django.contrib.contenttypes', 61 | 'django.contrib.sessions', 62 | 'django.contrib.messages', 63 | 'django.contrib.staticfiles', 64 | "identity", # To utilize the default templates came with the identity package 65 | ] 66 | 67 | MIDDLEWARE = [ 68 | 'django.middleware.security.SecurityMiddleware', 69 | 'django.contrib.sessions.middleware.SessionMiddleware', 70 | 'django.middleware.common.CommonMiddleware', 71 | 'django.middleware.csrf.CsrfViewMiddleware', 72 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 73 | 'django.contrib.messages.middleware.MessageMiddleware', 74 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 75 | ] 76 | 77 | ROOT_URLCONF = 'mysite.urls' 78 | 79 | TEMPLATES = [ 80 | { 81 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 82 | 'DIRS': [ 83 | BASE_DIR / "templates", # Enable this project's templates folder. 84 | # You can also add your own "identity/login.html" and 85 | # "identity/auth_error.html" into this folder 86 | # to override the default templates came with identity package. 87 | ], 88 | 'APP_DIRS': True, 89 | 'OPTIONS': { 90 | 'context_processors': [ 91 | 'django.template.context_processors.debug', 92 | 'django.template.context_processors.request', 93 | 'django.contrib.auth.context_processors.auth', 94 | 'django.contrib.messages.context_processors.messages', 95 | ], 96 | }, 97 | }, 98 | ] 99 | 100 | WSGI_APPLICATION = 'mysite.wsgi.application' 101 | 102 | 103 | # Database 104 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 105 | 106 | DATABASES = { 107 | 'default': { 108 | 'ENGINE': 'django.db.backends.sqlite3', 109 | 'NAME': BASE_DIR / 'db.sqlite3', 110 | } 111 | } 112 | 113 | 114 | # Password validation 115 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 116 | 117 | AUTH_PASSWORD_VALIDATORS = [ 118 | { 119 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 120 | }, 121 | { 122 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 123 | }, 124 | { 125 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 126 | }, 127 | { 128 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 129 | }, 130 | ] 131 | 132 | 133 | # Internationalization 134 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 135 | 136 | LANGUAGE_CODE = 'en-us' 137 | 138 | TIME_ZONE = 'UTC' 139 | 140 | USE_I18N = True 141 | 142 | USE_TZ = True 143 | 144 | 145 | # Static files (CSS, JavaScript, Images) 146 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 147 | 148 | STATIC_URL = 'static/' 149 | 150 | STATICFILES_DIRS = [ 151 | BASE_DIR / "static", 152 | ] 153 | 154 | # Default primary key field type 155 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 156 | 157 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 158 | -------------------------------------------------------------------------------- /mysite/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for mysite project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path 19 | from django.conf import settings 20 | 21 | from . import views 22 | 23 | 24 | urlpatterns = [ 25 | settings.AUTH.urlpattern, 26 | path('', views.index), 27 | path("call_api", views.call_api), 28 | path('admin/', admin.site.urls), 29 | ] 30 | -------------------------------------------------------------------------------- /mysite/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from django.conf import settings 5 | from django.shortcuts import render 6 | import requests 7 | 8 | 9 | __version__ = "0.5.0" 10 | 11 | 12 | @settings.AUTH.login_required 13 | def index(request, *, context): 14 | return render(request, 'index.html', dict( 15 | user=context['user'], 16 | edit_profile_url=settings.AUTH.get_edit_profile_url(), 17 | api_endpoint=os.getenv("ENDPOINT"), 18 | title=f"Microsoft Entra ID Django Web App Sample v{__version__}", 19 | )) 20 | 21 | @settings.AUTH.login_required(scopes=os.getenv("SCOPE", "").split()) 22 | def call_api(request, *, context): 23 | api_result = requests.get( # Use access token to call a web api 24 | os.getenv("ENDPOINT"), 25 | headers={'Authorization': 'Bearer ' + context['access_token']}, 26 | timeout=30, 27 | ).json() if context.get('access_token') else "Did you forget to set the SCOPE environment variable?" 28 | return render(request, 'display.html', { 29 | "title": "Result of API call", 30 | "content": json.dumps(api_result, indent=4), 31 | }) 32 | 33 | -------------------------------------------------------------------------------- /mysite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for mysite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ms_identity_python[django] @ https://github.com/azure-samples/ms-identity-python/archive/refs/heads/0.9.zip 2 | python-dotenv<0.22 3 | requests>=2,<3 4 | -------------------------------------------------------------------------------- /static/topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-python-webapp-django/cdc6ee802f3b610898159dc1e5adbc3c9aae7a70/static/topology.png -------------------------------------------------------------------------------- /templates/display.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | Back 9 |

{{title}}

10 |
{{content}}
11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 |

{{ title }}

9 |

Welcome {{ user.name }}!

10 | 11 | 15 | Topology 16 | 17 |
    18 | {% if api_endpoint %} 19 |
  • Call an API
  • 20 | {% endif %} 21 | 22 | {% if edit_profile_url %} 23 |
  • Edit Profile
  • 24 | {% endif %} 25 | 26 |
  • Logout
  • 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/ms-identity-python-webapp-django/cdc6ee802f3b610898159dc1e5adbc3c9aae7a70/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_views.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.template.exceptions import TemplateDoesNotExist 3 | 4 | 5 | class TemplateAccessibilityTest(TestCase): 6 | def test_login_attempt_should_render_its_template(self): 7 | try: 8 | response = self.client.get("/") # It will trigger the login attempt 9 | print(response.content) 10 | # The response could be rendered by login.html if .env is complete, 11 | # or by auth_error.html otherwise. 12 | # I haven't figured out how to mock the .env to test it decisively. 13 | except TemplateDoesNotExist: 14 | self.fail( 15 | "Template should be accessible, " 16 | "typically came from inside the Identity package.") 17 | 18 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | env_list = 3 | py3 4 | minversion = 4.21.2 5 | 6 | [testenv] 7 | description = run the tests with pytest 8 | package = wheel 9 | wheel_build_env = .pkg 10 | deps = 11 | pytest>=6 12 | -r requirements.txt 13 | skip_install = true # To bypass the error "Multiple top-level packages discovered" 14 | commands = 15 | pip list 16 | python manage.py test 17 | 18 | --------------------------------------------------------------------------------