[PYTHON] Let's create an app that authenticates with OIDC with Azure AD

Introduction

When considering how Azure AD works with your app, you can do the following:

The information on OpenID Connnect (OIDC) is relatively substantial, but I felt that the information on SCIM was small, so I started writing an article hoping that there would be settings in Azure AD and an implementation example of the application. However, this time we will cover up to the point where SSO can be done with the first OIDC. The app is implemented in Django and the authentication flow uses the authentication code flow. For the time being, I will set it with an eye on automatic provisioning.

Let's do it! (The source code for this time is on github.)

1. About Django

Django is a Python web application framework. I'm using it because it's easy to move it for the time being. I hope the following explanations will give you a feel for it even if you have no experience with Django, but if you have experience with other MVC frameworks, you may be confused by the Django terminology used in this article. So please be careful about the following.

Django uses the MVC pattern, just like most web frameworks. Reference: About MVC model However, each element is called differently and is called MTV (Model, View, Template). It's confusing, but be aware that ** Django's View is a typical MVC Controller **.

Source: [[Python] Django Tutorial-Making a General Purpose Business Web App Fastest](https://qiita.com/okoppe8/items/54eb105c9c94c0960f14#djnago-%E3%81%AE-mvc%E3%83%91 % E3% 82% BF% E3% 83% BC% E3% 83% B3)

2. About Authentication Code Flow

It's one of the most common ways to authenticate and authorize your web app. Details are summarized in the following public information. Again, follow the article below to get a token using the Microsoft Authentication Library (MSAL).

Reference: Microsoft ID Platform and OAuth 2.0 Authentication Code Flow

3. Register your app with Azure AD

There are two screens for registering an app in Azure AD, "App Registration" and "Enterprise Application", but you need to register from "Enterprise Application" for automatic provisioning. You need a license of Azure AD Premium P1 or higher, so if you don't have one, use the free trial version.

3.1. Register your app in ** Azure Portal> Azure Active Directory> Enterprise Applications> New Applications> Non-Gallery Applications **.

3.2. In ** Azure Portal> Azure Active Directory> App Registration> All Applications **, find and open the app created above.

3.3. Make a note of the "Application ID" and "Directory ID" on the ** Overview ** page.

3.4. ** Authenticate ** Add the redirect URI from the page.

ʻAccounts` is the name of the app we are making this time.

3.5. ** Certificates and Secrets> Create a client secret with the new client secret ** and make a note of it.

This time I'm using the client secret for simplicity. A certificate can also be used as a more secure method.

4. Create a Django project

Create a mysite project with the following command:

django-admin startproject mysite

Add a folder to put the Template.

mysite/settings.py


TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  #add to
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

5. Create application ʻaccounts`

Create an app called ʻaccounts in your mysite` project with the following command:

python manage.py startapp accounts

Create ʻurls.py`

accounts/urls.py


from django.urls import include, path

from . import views

urlpatterns = [
    path('', views.index),
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
    path('oidc/callback/', views.callback_view) 
]

/ accounts / oidc / callback is the redirect URI set in step 3.4. After authenticating with Azure AD, the user will be redirected to this URI. It is necessary to implement so that the authentication code passed at that time can be processed by views.callback_view.

Create a custom user model

Allows users authenticated with Azure AD to sign in to this app. To do this, you need to associate Azure AD users with app users. This time, we will associate the user whose ObjectId on the Azure AD side and the external_id of the application are the same.

Create a custom user model and add the external_id attribute.

accounts/models.py


from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    external_id = models.CharField(max_length=50, blank=True)

Modify the administrator screen to show the user's external_id.

accounts/admin.py


from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin

from .models import User


class UserAdmin(BaseUserAdmin):
    list_display = ['username', 'is_staff', 'is_active', 'external_id']


admin.site.register(User, UserAdmin)

Let the custom user model be the authentication user model.

mysite/settings.py


AUTH_USER_MODEL = 'accounts.User'  #add to

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'accounts.apps.AccountsConfig',  #add to
]

Add ʻaccounts` to the project URL.

mysite/urls.py


from django.contrib import admin
from django.shortcuts import redirect  #add to
from django.urls import include, path  #include added

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', lambda req: redirect('accounts/', permanent=False)),  #add to
    path('accounts/', include('accounts.urls')),  #add to
]

6. Implement authentication code flow

Basically, you can authenticate with Azure AD by creating a URL to the Azure AD authentication endpoint and redirecting the user to it. Frequently used features are also available from the library MSAL. The following code is implemented based on Flask + MSAL Sample.

6.1. Add a screen.

6.2. Add settings for authentication and authorization.

mysite/settings.py


# Azure AD

SCOPES = ['User.Read']
TENANT_ID = '{3.3.Directory ID}'
AUTHORITY = f'https://login.microsoftonline.com/{TENANT_ID}/'
CLIENT_ID = '{3.3.Application ID}'
REDIRECT_PATH = '{3.4.Redirect URI}'
CLIENT_SECRET = '{3.5.Client secret}'
ENDPOINT = 'https://graph.microsoft.com/beta/me'

ENDPOINT is an API that is referenced by the authority of the signed-in user. This time, User information acquisition API is executed, so SCOPES ʻUser.Read` is added to.

6.3. Implement View.

accounts/views.py


import uuid
import msal
from django.shortcuts import redirect
from django.contrib.auth import login, logout
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from django.contrib.auth import get_user_model

from mysite2 import settings

User = get_user_model()


@login_required
def index(request):
    context = {'user': request.user}
    return render(request, 'accounts/index.html', context)


def logout_view(request):
    logout(request)
    return redirect('/')


def login_view(request):
    request.session['state'] = str(uuid.uuid4())
    auth_url = _build_auth_url(
        scopes=settings.SCOPES, state=request.session['state'])
    context = {'auth_url': auth_url}
    return render(request, 'accounts/login.html', context)


def callback_view(request):
    if request.GET.get('state') != request.session.get('state'):
        # 'state'Does not match the request
        return redirect('/')
    if 'error' in request.GET:
        #Azure AD authenticates/Returned an authorization error
        return render(request, 'accounts/auth_error.html', request.GET)
    if 'code' in request.GET:
        cache = _load_cache(request)
        result = _build_msal_app(cache=cache).acquire_token_by_authorization_code(
            request.GET['code'],
            scopes=settings.SCOPES,  # Misspelled scope would cause an HTTP 400 error here
            redirect_uri=settings.REDIRECT_PATH)
        if 'error' in result:
            return render(request, 'accounts/auth_error.html', result)
        request.session['user'] = result.get('id_token_claims')

    try:
        #Oid on the Azure AD side and external on the app side_Check if the user has the same id
        oid = request.session['user']['oid']
        user = User.objects.get(external_id=oid)
        login(request, user)
    except User.DoesNotExist as e:
        context = {'error': 'User.DoesNotExist', 'error_description': str(e)}
        return render(request, 'accounts/auth_error.html', context)

    return redirect('/')


def _build_auth_url(authority=None, scopes=None, state=None):
    return _build_msal_app(authority=authority).get_authorization_request_url(
        scopes or [],
        state=state or str(uuid.uuid4()),
        redirect_uri=settings.REDIRECT_PATH)


def _build_msal_app(cache=None, authority=None):
    return msal.ConfidentialClientApplication(
        settings.CLIENT_ID, authority=settings.AUTHORITY,
        client_credential=settings.CLIENT_SECRET, token_cache=cache)


def _load_cache(request):
    cache = msal.SerializableTokenCache()
    if request.session.get('token_cache'):
        cache.deserialize(request.session['token_cache'])
    return cache


def _save_cache(request, cache):
    if cache.has_state_changed:
        request.session['token_cache'] = cache.serialize()

The following are the points.

7. Run the app

#Create a DB
python manage.py makemigrations
python manage.py migrate

#Create an admin user
python manage.py createsuperuser

#Start the server
python manage.py runserver

You can access the app by opening http: // localhost: 8000 / in your browser. / admin is the admin console.

8. Authenticate with Azure AD and sign in to the app

Now let's sign in to the app as an Azure AD user.

First, assign users to sign in on the ** Azure Portal> Azure Active Directory> Enterprise Applications> App App> Users and Groups ** page.

Next, since we haven't configured auto-provisioning this time, launch the admin shell with python manage.py shell and manually create the app-side user as shown below.

from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.create(username='{Azure AD user username}', external_id='{Azure AD user ObjectId}') 
user.save()

Now, when the above user accesses the app and presses the login button, the Azure AD authentication screen will be required. If you enter your credentials there, you will be asked to agree to the app permissions as shown below. This consent screen only requires first-time access. In addition, the administrator can give consent on a tenant-by-tenant basis, in which case the consent screen will not be displayed to general users.

2019-12-24_07h04_10.png

If you accept here, you've successfully signed in!

2019-12-24_07h21_12.png

The app can also access APIs protected by the permissions of the signed-in user.

2019-12-24_07h21_58.png

in conclusion

I was able to sign in to the app as an Azure AD user.

The purpose of having the externalId of the custom user created this time is for authentication and to implement [SCIM API](user provisioning of SCIM in Azure Active Directory (Azure AD)). However, it is quite difficult to implement one by one, so if you have a library for SCIM API, you should use it. This time Django seems to have a library called django-scim2, but it seems that the custom user model also needs to be recreated for the library. If you use the SCIM API, you may need to think from the design stage. I will try to make an app that supports automatic provisioning next time!

Recommended Posts

Let's create an app that authenticates with OIDC with Azure AD
Create an app that guesses students with python
Create an English word app with python
Create an image composition app with Flask + Pillow
[kotlin] Create an app that recognizes photos taken with a camera on android
Create an app that works well with people's reports using the COTOHA API
Let's make an app that can search similar images with Python and Flask Part1
Let's make an app that can search similar images with Python and Flask Part2
Create an environment with virtualenv
Create an API with Django
Let's create a script that registers with Ideone.com in Python.
Let's create an external specification that is easy to handle
Create a Todo app with Django ① Build an environment with Docker
Create an app that notifies LINE of the weather every morning
Create an Excel file with Python3
Create an age group with pandas
Let's recognize emotions with Azure Face
Create a web app that can be easily visualized with Plotly Dash
Create an application by classifying with Pygame
Creating an image splitting app with Tkinter
Create an image processing viewer with PySimpleGUI
Let's create a free group with Python
Quickly create an excel file with Python #python
Create a GUI app with Python's Tkinter
Create an update screen with Django Updateview
Create a simple web app with flask
Create your first app with Django startproject
[Python] Quickly create an API with Flask
Create an add-in-enabled Excel instance with xlwings
Let's develop an investment algorithm with Python 1
Create an upgradeable msi file with cx_Freeze
With LINEBot, I made an app that informs me of the "bus time"