ADFS Authentication for Django

Documentation Status https://img.shields.io/pypi/v/django-auth-adfs.svg https://img.shields.io/pypi/pyversions/django-auth-adfs.svg https://img.shields.io/pypi/djversions/django-auth-adfs.svg https://codecov.io/github/snok/django-auth-adfs/coverage.svg?branch=master

A Django authentication backend for Microsoft ADFS and Azure AD

Features

  • Integrates Django with Active Directory on Windows 2012 R2, 2016 or Azure AD in the cloud.
  • Provides seamless single sign on (SSO) for your Django project on intranet environments.
  • Auto creates users and adds them to Django groups based on info received from ADFS.
  • Django Rest Framework (DRF) integration: Authenticate against your API with an ADFS access token.

Contents

Installation

Requirements

  • Python 3.5 and above
  • Django 1.11 and above

You will also need the following:

  • A properly configured Microsoft Windows server 2012 R2 or 2016 with the AD FS role installed or an Azure Active Directory setup.
  • A root CA bundle containing the root CA that signed the webserver certificate of your ADFS server if signed by an enterprise CA.

Note

When using Azure AD, beware of the following limitations:

  • Users have no email address unless you assigned an Office 365 license to that user.
  • Groups are listed with their GUID in the groups claim. Meaning you have to create your groups in Django using these GUIDs, instead of their name.
  • Usernames are in the form of an email address, hence users created in Django follow this format.
  • You cannot send any custom claims, only those predefined by Azure AD.

Package installation

Python package:

pip install django-auth-adfs

Setting up django

In your project’s settings.py add these settings.

AUTHENTICATION_BACKENDS = (
    ...
    'django_auth_adfs.backend.AdfsAuthCodeBackend',
    ...
)

INSTALLED_APPS = (
    ...
    # Needed for the ADFS redirect URI to function
    'django_auth_adfs',
    ...

# checkout the documentation for more settings
AUTH_ADFS = {
    "SERVER": "adfs.yourcompany.com",
    "CLIENT_ID": "your-configured-client-id",
    "RELYING_PARTY_ID": "your-adfs-RPT-name",
    # Make sure to read the documentation about the AUDIENCE setting
    # when you configured the identifier as a URL!
    "AUDIENCE": "microsoft:identityserver:your-RelyingPartyTrust-identifier",
    "CA_BUNDLE": "/path/to/ca-bundle.pem",
    "CLAIM_MAPPING": {"first_name": "given_name",
                      "last_name": "family_name",
                      "email": "email"},
}

# Configure django to redirect users to the right URL for login
LOGIN_URL = "django_auth_adfs:login"
LOGIN_REDIRECT_URL = "/"

########################
# OPTIONAL SETTINGS
########################

MIDDLEWARE = (
    ...
    # With this you can force a user to login without using
    # the LoginRequiredMixin on every view class
    #
    # You can specify URLs for which login is not enforced by
    # specifying them in the LOGIN_EXEMPT_URLS setting.
    'django_auth_adfs.middleware.LoginRequiredMiddleware',
)

# You can point login failures to a custom Django function based view for customization of the UI
CUSTOM_FAILED_RESPONSE_VIEW = 'dot.path.to.custom.views.login_failed'

In your project’s urls.py add these paths:

urlpatterns = [
    ...
    path('oauth2/', include('django_auth_adfs.urls')),
]

This will add these paths to Django:

  • /oauth2/login where users are redirected to, to initiate the login with ADFS.
  • /oauth2/login_no_sso where users are redirected to, to initiate the login with ADFS but forcing a login screen.
  • /oauth2/callback where ADFS redirects back to after login. So make sure you set the redirect URI on ADFS to this.
  • /oauth2/logout which logs out the user from both Django and ADFS.

You can use them like this in your django templates:

<a href="{% url 'django_auth_adfs:logout' %}">Logout</a>
<a href="{% url 'django_auth_adfs:login' %}">Login</a>
<a href="{% url 'django_auth_adfs:login-no-sso' %}">Login (no SSO)</a>

OAuth2 and ADFS explained

This chapter tries to explain how ADFS implements the OAuth2 and OpenID Connect standard and how we can use this in Django.

OAuth2 vs. OpenID Connect

What’s OAuth2?

The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf.

What’s important is that it’s only an authorization framework. It only tells you what the user is allowed to do but it doesn’t tell you who the user is. At its core, there’s nothing in the protocol that gives you info about the user.

To solve this, there’s the OpenID Connect framework.

OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 [RFC6749] protocol. It enables Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.

So, where the OAuth2 protocol lacks any user identifiable info, OpenID Connect does give you info about who the user is. The access token returned by OpenID Connect is a signed JWT token (JSON Web Token) containing claims about the user.

django-auth-adfs uses this access token to validate the issuer of the token by verifying the signature and also uses it to keep the Django users database up to date and at the same time authenticate users.

Depending on the version of ADFS, there’s support for different pieces of these protocol. The table below tries to list the support in various ADFS versions:

Protocol ADFS 2012 R2 ADFS 2016 Azure AD
OAuth2 Yes Yes Yes
OpenID Connect No** Yes Yes

** ADFS 2012 doesn’t implement OpenID Connect, but it does return the access token as a JWT token, just like OpenID Connect would.

OpenID Connect / OAuth2 Flow support:

Version ADFS 2012 R2 ADFS 2016 Azure AD
Authorization code grant Yes Yes Yes
Implicit grant no Yes Yes
Resource owner password credential no Yes Yes
Client credential grant no Yes Yes

References:

The Authorization Code Grant is what django-auth-adfs uses.

OAuth2 and Django

Let’s step through the process of how django-auth-adfs uses OAuth2 to authenticate and authorize users.

Note

In all the graphs below, remember that the access token is what contains the info about our user in the form of a signed JWT token.

The OAuth2 RFC 6749 specifies the Authorization Code Grant flow as follows:

  +----------+
  | Resource |
  |   Owner  |
  |          |
  +----------+
       ^
       |
      (B)
  +----|-----+          Client Identifier      +---------------+
  |         -+----(A)-- & Redirection URI ---->|               |
  |  User-   |                                 | Authorization |
  |  Agent  -+----(B)-- User authenticates --->|     Server    |
  |          |                                 |               |
  |         -+----(C)-- Authorization Code ---<|               |
  +-|----|---+                                 +---------------+
    |    |                                         ^      v
   (A)  (C)                                        |      |
    |    |                                         |      |
    ^    v                                         |      |
  +---------+                                      |      |
  |         |>---(D)-- Authorization Code ---------'      |
  |  Client |          & Redirection URI                  |
  |         |                                             |
  |         |<---(E)----- Access Token -------------------'
  +---------+       (w/ Optional Refresh Token)

Note: The lines illustrating steps (A), (B), and (C) are broken into
two parts as they pass through the user-agent.

The flow illustrated includes the following steps:

(A)  The client initiates the flow by directing the resource owner's
     user-agent to the authorization endpoint.  The client includes
     its client identifier, requested scope, local state, and a
     redirection URI to which the authorization server will send the
     user-agent back once access is granted (or denied).

(B)  The authorization server authenticates the resource owner (via
     the user-agent) and establishes whether the resource owner
     grants or denies the client's access request.

(C)  Assuming the resource owner grants access, the authorization
     server redirects the user-agent back to the client using the
     redirection URI provided earlier (in the request or during
     client registration).  The redirection URI includes an
     authorization code and any local state provided by the client
     earlier.

(D)  The client requests an access token from the authorization
     server's token endpoint by including the authorization code
     received in the previous step.  When making the request, the
     client authenticates with the authorization server.  The client
     includes the redirection URI used to obtain the authorization
     code for verification.

(E)  The authorization server authenticates the client, validates the
     authorization code, and ensures that the redirection URI
     received matches the URI used to redirect the client in
     step (C).  If valid, the authorization server responds back with
     an access token and, optionally, a refresh token.

One thing missing in the graph from the RFC is the Resource Server. Let’s add it to make things complete:

  +----------+
  | Resource |
  |   Owner  |
  |          |
  +----------+
       ^
       |
      (B)
  +----|-----+          Client Identifier      +---------------+
  |         -+----(A)-- & Redirection URI ---->|               |
  |  User-   |                                 | Authorization |
  |  Agent  -+----(B)-- User authenticates --->|     Server    |
  |          |                                 |               |
  |         -+----(C)-- Authorization Code ---<|               |
  +-|----|---+                                 +---------------+
    |    |                                         ^      v
   (A)  (C)                                        |      |
    |    |                                         |      |
    ^    v                                         |      |
  +---------+                                      |      |
  |         |>---(D)-- Authorization Code ---------'      |
  |  Client |          & Redirection URI                  |
  |         |                                             |
  |         |<---(E)----- Access Token -------------------'
  +---------+       (w/ Optional Refresh Token)
      |  ^
      |  |
     (F) Access Token
      | (G)
      v  |
  +-----------------+
  |                 |
  | Resource Server |
  |                 |
  +-----------------+

Extra steps

(F)  The client makes a protected resource request to the resource
     server by presenting the access token.
(G)  The resource server validates the access token, and if valid,
     serves the request.

Alright, now that we have the entire flow, let’s translate the roles to our components and use a bit more comprehensible terms:

 +----------+
 |          |
 |   User   |
 |          |
 +----------+
      ^
      |
     (B)               Resource
 +----|-----+          & Client Identifier    +---------------+
 |         -+----(A)-- & Redirection URI ---->|               |
 | Web      |                                 |      ADFS     |
 | Browser -+----(B)-- User authenticates --->|     Server    |
 |          |                                 |               |
 |         -+----(C)-- Authorization Code ---<|               |
 +-|---|----+                                 +---------------+
   |   |  ^                                       ^      v
  (A) (C)(G)                                      |      |
   |   |  |                                       |      |
   ^   v  |                                       |      |
 +--------|+                                      |      |
 |         |>---(D)-- Authorization Code ---------'      |
 |  Django |          & Redirection URI                  |
 |  Login  |                                             |
 |         |<---(E)----- Access Token -------------------'
 +---------+       (w/ Optional Refresh Token)
   |    ^
   |    |
  (F) Access Token
   |   (G) Session ID
   v    |
 +-------------------------------+
 |                               |
 | Django Authentication Backend |
 |                               |
 +-------------------------------+

The following things changed:

  • A resource parameter was added to step A. This is an ADFS specific thing used to identify which application.
  • Step G was extended up to the web browser. Resembling the session cookie sent back to the web browser.
  • Resource OwnerUser
  • User-AgentWeb Browser
  • Authorization ServeADFS Server
  • ClientDjango Login
  • Resource ServerDjango Authentication Backend

Notice how 2 roles were replaced by “pieces” of Django. Django effectively takes up 2 roles here.

If you were to split Django in 2 parts, it’s login pages and the authentication backends, then the login pages would map to the Client role. It wants to get a session for the user and give it a session cookie.

The authentication backend maps to the Resource Server role, authenticating/authorizing the user and creating the session. The session you can think of as being the protected resource.

Once the session is created, OAuth2 isn’t used anymore. Django uses its sessions to authenticate and authorize the user on subsequent requests.

On the ADFS side, you need to configure both the Client role part of Django (called a Native Application in ADFS 4.0), as well as the Resource Server part (called a Web Application in ADFS 4.0).

Rest Framework and OAuth2

When activating Django Rest Framework integration to protect an API, the roles shift once more.

The example assumes a situation where you use a script or some other application to make requests to your API. In that case, the OAuth2 flow also changes from the Authorization Code Grant flow to the Resource Owner Password Credentials Grant flow.

Note

If you would call the API from a Single Page Application (SPA), you’ll most likely be using the Implicit Grant flow. We won’t explain this flow here, but the principle is sort of the same.

Here’s the RFC explanation again:

  +----------+
  | Resource |
  |  Owner   |
  |          |
  +----------+
       v
       |    Resource Owner
      (A) Password Credentials
       |
       v
  +---------+                                  +---------------+
  |         |>--(B)---- Resource Owner ------->|               |
  |         |         Password Credentials     | Authorization |
  | Client  |                                  |     Server    |
  |         |<--(C)---- Access Token ---------<|               |
  |         |    (w/ Optional Refresh Token)   |               |
  +---------+                                  +---------------+

The flow illustrated includes the following steps:

(A)  The resource owner provides the client with its username and
     password.

(B)  The client requests an access token from the authorization
     server's token endpoint by including the credentials received
     from the resource owner.  When making the request, the client
     authenticates with the authorization server.

(C)  The authorization server authenticates the client and validates
     the resource owner credentials, and if valid, issues an access
     token.

Again, let’s add the Resource Server role to the picture:

  +----------+
  | Resource |
  |  Owner   |
  |          |
  +----------+
       v
       |    Resource Owner
      (A) Password Credentials
       |
       v
  +---------+                                  +---------------+
  |         |>--(B)---- Resource Owner ------->|               |
  |         |         Password Credentials     | Authorization |
  | Client  |                                  |     Server    |
  |         |<--(C)---- Access Token ---------<|               |
  |         |    (w/ Optional Refresh Token)   |               |
  +---------+                                  +---------------+
     |   ^
     |   |
    (D) Access Token
     |  (E)
     v   |
  +-----------------+
  |                 |
  | Resource Server |
  |                 |
  +-----------------+

Extra steps

(D)  The client makes a protected resource request to the resource
     server by presenting the access token.
(E)  The resource server validates the access token, and if valid,
     serves the request.

And let’s map it to our components:

 +----------+
 |          |
 | User     |
 |          |
 +----------+
      v
      |    Resource Owner
     (A) Password Credentials
      |
      v
 +-------------+                                  +---------------+
 |             |>--(B)---- Resource Owner ------->|               |
 |             |         Password Credentials     |      ADFS     |
 | Application |                                  |     Server    |
 |             |<--(C)---- Access Token ---------<|               |
 |             |    (w/ Optional Refresh Token)   |               |
 +-------------+                                  +---------------+
    |   ^
    |   |
   (D) Access Token
    |  (E)
    v   |
 +-----------------------+
 |                       |
 | Django Rest Framework |
 |          API          |
 |                       |
 +-----------------------+

Let’s go over the changes again:

  • Resource OwnerUser
  • ClientApplication
  • Resource ServerDjango Rest Framework API

In this case, a user inputs his username and password into an application/script. The application fetches an access token on behalf of the user and uses it to make calls to you API.

ADFS and OAuth2 lingo compared

Potayto, potahto…

OAuth2 and ADFS don’t keep the same name for components. Below is an overview of what OAuth2 role maps to which configuration part on ADFS.

OAuth2 Azure AD ADFS 2016 ADFS 2012
Resource Owner User User User
Authorization Server ADFS server ADFS server ADFS server
Client Native Application
  • Native Application
  • Server Application
Client
Resource Server Web app / API Web API Relying Party

Note

For ADFS 2016, we assumed you use application group configuration instead of the “old-fashion” Relying Party Trust config.

For ADFS 2012, the client part is not visible from the GUI and can only be configured via PowerShell commands.

Settings Reference

AUDIENCE

  • Default:
  • Type: string or list

Required

Set this to the value of the aud claim your ADFS server sends back in the JWT token.

You can lookup this value by executing the powershell command Get-AdfsRelyingPartyTrust on the ADFS server and taking the Identifier value. But beware, it doesn’t match exactly if it’s not a URL.

Examples

Relying Party Trust identifier aud claim value
your-RelyingPartyTrust-identifier microsoft:identityserver:your-RelyingPartyTrust-identifier
https://adfs.yourcompany.com/adfs/services/trust https://adfs.yourcompany.com/adfs/services/trust

BLOCK_GUEST_USERS

  • Default: False
  • Type: boolean

Whether guest users of your Azure AD is allowed to log into the site. This is validated by matching the http://schemas.microsoft.com/identity/claims/tenantid-key in the claims towards the configured tenant.

BOOLEAN_CLAIM_MAPPING

  • Default: None
  • Type: dictionary

A dictionary of claim/field mappings that is used to set boolean fields on the user account in Django.

The key represents user model field (e.g. first_name) and the value represents the claim short name (e.g. given_name).

If the value is any of y, yes, t, true, on, 1, the field will be set to True. All other values, or the absence of the claim, will result in a value of False

example

AUTH_ADFS = {
    "BOOLEAN_CLAIM_MAPPING": {"is_staff": "user_is_staff",
                              "is_superuser": "user_is_superuser"},
}

Note

You can find the short name for the claims you configure in the ADFS management console underneath ADFSServiceClaim Descriptions

CA_BUNDLE

  • Default: True
  • Type: boolean or string

The value of this setting is passed to the call to the Requests package when fetching the access token from ADFS. It allows you to control the webserver certificate verification of the ADFS server.

True to use the default CA bundle of the requests package.

/path/to/ca-bundle.pem allows you to specify a path to a CA bundle file. If your ADFS server uses a certificate signed by an enterprise root CA, you will need to specify the path to it’s certificate here.

False disables the certificate check.

Have a look at the Requests documentation for more details.

Warning

Do not set this value to False in a production setup. Because we load certain settings from the ADFS server, this might lead to a security issue. DNS hijacking for example might cause an attacker to inject his own access token signing certificate.

CLAIM_MAPPING

  • Default: None
  • Type: dictionary

A dictionary of claim/field mappings that will be used to populate the user account in Django. The user’s details will be set according to this setting upon each login.

The key represents the user model field (e.g. first_name) and the value represents the claim short name (e.g. given_name).

example

AUTH_ADFS = {
    "CLAIM_MAPPING": {"first_name": "given_name",
                      "last_name": "family_name",
                      "email": "email"},
}

The dictionary can also map extra details to the Django user account using an Extension of the User model Set a dictionary as value in the CLAIM_MAPPING setting with as key the name User model. You will need to make sure the related field exists before the user authenticates. This can be done by creating a receiver on the post_save signal that creates the related instance when the User instance is created.

example

'CLAIM_MAPPING': {'first_name': 'given_name',
                  'last_name': 'family_name',
                  'email': 'upn',
                  'userprofile': {
                      'employee_id': 'employeeid'
                  }}

Note

You can find the short name for the claims you configure in the ADFS management console underneath ADFSServiceClaim Descriptions

CLIENT_ID

  • Default:
  • Type: dictionary

Required

Set this to the value you configured on your ADFS server as ClientId when executing the Add-AdfsClient command.

You can lookup this value by executing the powershell command Get-AdfsClient on the ADFS server and taking the ClientId value.

CLIENT_SECRET

  • Default: None
  • Type: string

A Client secret is generated by ADFS server when executing the Add-AdfsClient command with the -GenerateClientSecret parameter.

You can lookup this value by executing the powershell command Get-AdfsClient on the ADFS server and taking the ClientSecret value.

CONFIG_RELOAD_INTERVAL

  • Default: 24
  • Unit: hours
  • Type: integer

When starting Django, some settings are retrieved from the ADFS metadata file or the OpenID Connect configuration on the ADFS server. Based on this information, certain configuration for this module is calculated.

This setting determines the interval after which the configuration is reloaded. This allows to automatically follow the token signing certificate rollover on ADFS.

CREATE_NEW_USERS

  • Default: True
  • Type: boolean

Determines whether users are created automatically if they do not exist.

If set to False, then you need to create your users before they can log in.

DISABLE_SSO

  • Default: False
  • Type: boolean

Setting this to True will globally disable the seamless single sign-on capability of ADFS. Forcing ADFS to prompt users for a username and password, instead of automatically logging them in with their current user. This allows users to use a different account then the one they are logged in with on their workstation.

You can also selectively enable this setting by using <a href="{% url 'django_auth_adfs:login-no-sso' %}">...</a> in a template instead of the regular <a href="{% url 'django_auth_adfs:login' %}">...</a>

Attention

This does not work with ADFS 3.0 on windows 2012 because this setting requires OpenID Connect which is not supported on ADFS 3.0

JWT_LEEWAY

  • Default: 0
  • Type: str

Allows you to set a leeway of the JWT token. See the official PyJWT docs for more information.

CUSTOM_FAILED_RESPONSE_VIEW

  • Default: lambda
  • Type: str or callable

Allows you to set a custom django function view to handle login failures. Can be a dot path to your Django function based view function or a callable.

Callable must have the following method signature accepting error_message and status arguments:

def failed_response(request, error_message, status):
    # Return an error message
    return render(request, 'myapp/login_failed.html', {
        'error_message': error_message,
    }, status=status)

GROUP_CLAIM

Alias of GROUPS_CLAIM

GROUPS_CLAIM

  • Default: group for ADFS or groups for Azure AD
  • Type: string

Name of the claim in the JWT access token from ADFS that contains the groups the user is member of. If an entry in this claim matches a group configured in Django, the user will join it automatically.

Set this setting to None to disable automatic group handling. The group memberships of the user will not be touched.

Important

If not set to None, a user’s group membership in Django will be reset to math this claim’s value. If there’s no value in the access token, the user will be removed from all groups.

Note

You can find the short name for the claims you configure in the ADFS management console underneath ADFSServiceClaim Descriptions

GROUP_TO_FLAG_MAPPING

  • Default: None
  • Type: dictionary

This settings allows you to set flags on a user based on his group membership in Active Directory.

For example, if a user is a member of the group Django Staff, you can automatically set the is_staff field of the user to True.

The key represents the boolean user model field (e.g. is_staff) and the value, which can either be a single String or an array of Strings, represents the group(s) name (e.g. Django Staff).

example

AUTH_ADFS = {
    "GROUP_TO_FLAG_MAPPING": {"is_staff": ["Django Staff", "Other Django Staff"],
                              "is_superuser": "Django Admins"},
}

Note

The group doesn’t need to exist in Django for this to work. This will work as long as it’s in the groups claim in the access token.

GUEST_USERNAME_CLAIM

  • Default: None
  • Type: string

When these criteria are met:

  1. A guest_username_claim is configured
  2. Token claims do not have the configured settings.USERNAME_CLAIM in it
  3. The settings.BLOCK_GUEST_USERS is set to False
  4. The claims tid does not match settings.TENANT_ID or claims idp does not match iss.

Then, the GUEST_USERNAME_CLAIM can be used to populate a username, when the USERNAME_CLAIM cannot be found in the claims.

This can be useful when you want to use upn as a username claim for your own users, but some guest users (such as normal outlook users) don’t have that claim.

LOGIN_EXEMPT_URLS

  • Default: None
  • Type: list

When you activate the LoginRequiredMiddleware middleware, by default every page will redirect an unauthenticated user to the page configured in the Django setting LOGIN_URL.

If you have pages that should not trigger this redirect, add them to this setting as a list value.

Every item it the list is interpreted as a regular expression.

example

AUTH_ADFS = {
    'LOGIN_EXEMPT_URLS': [
        '^$',
        '^api'
    ],
}

MIRROR_GROUPS

  • Default: False
  • Type: boolean

This parameter will create groups from ADFS in the Django database if they do not exist already.

True will create groups.

False will not create any extra groups.

Important

This parameter only has effect if GROUP_CLAIM is set to something other then None.

RELYING_PARTY_ID

  • Default:
  • Type: string

Required

Set this to the Relying party trust identifier value of the Relying Party Trust (2012) or Web application (2016) you configured in ADFS.

You can lookup this value by executing the powershell command Get-AdfsRelyingPartyTrust (2012) or Get-AdfsWebApiApplication (2016) on the ADFS server and taking the Identifier value.

RESOURCE

Alias for RELYING_PARTY_ID

RETRIES

  • Default: 3
  • Type: integer

The number of time a request to the ADFS server is retried. It allows, in combination with TIMEOUT to fine tune the behaviour of the connection to ADFS.

SCOPES

  • Default: []
  • Type: list

Only used when you have v2 AzureAD config

SERVER

  • Default:
  • Type: string

Required when your identity provider is an on premises ADFS server.

Only one of SERVER or TENANT_ID can be set.

The FQDN of the ADFS server you want users to authenticate against.

SETTINGS_CLASS

  • Default: django_auth_adfs.config.Settings
  • Type: string

By default, django-auth-adfs reads the configuration from the Django setting AUTH_ADFS. You can provide the configuration in a custom implementation and point to it by using the SETTINGS_CLASS setting:

# in myapp.adfs.config

class CustomSettings:

    SERVER = 'bar'
    AUDIENCE = 'foo'
    ...


# in settings.py

AUTH_ADFS = {
    'SETTINGS_CLASS': 'myapp.adfs.config.CustomSettings',
    # other settings are not needed
}

The value must be an importable dotted Python path, and the imported object must be callable with no arguments to initialize.

Use cases are storing configuration in database so an administrator can edit the configuration in an admin interface.

TENANT_ID

  • Default:
  • Type: string

Required when your identity provider is an Azure AD instance.

Only one of TENANT_ID or SERVER can be set.

The FQDN of the ADFS server you want users to authenticate against.

TIMEOUT

  • Default: 5
  • Unit: seconds
  • Type: integer

The timeout in seconds for every request made to the ADFS server. It’s passed on as the timeout parameter to the underlying calls to the requests library.

It allows, in combination with RETRIES to fine tune the behaviour of the connection to ADFS.

USERNAME_CLAIM

  • Default: winaccountname for ADFS or upn for Azure AD.
  • Type: string

Name of the claim sent in the JWT token from ADFS that contains the username. If the user doesn’t exist yet, this field will be used as it’s username.

The value of the claim must be a unique value. No 2 users should ever have the same value.

Warning

You shouldn’t need to set this value for ADFS or Azure AD unless you use custom user models. Because winaccountname maps to the sAMAccountName on Active Directory, which is guaranteed to be unique. The same for Azure AD where upn maps to the UserPrincipleName, which is unique on Azure AD.

Note

You can find the short name for the claims you configure in the ADFS management console underneath ADFSServiceClaim Descriptions

VERSION

  • Default: v1.0
  • Type: string

Version of the Azure Active Directory endpoint version. By default it is set to v1.0. At the time of writing this documentation, it can also be set to v2.0. For new projects, v2.0 is recommended. v1.0 is kept as a default for backwards compatibility.

PROXIES

  • Default: None
  • Type: dict

An optional proxy for all communication with the server. Example: {'http': '10.0.0.1', 'https': '10.0.0.2'} See the requests documentation for more information.

ADFS Config Guides

Windows 2012 R2 - ADFS 3.0

Getting this module to work is sometimes not so straight forward. If your not familiar with JWT tokens or ADFS itself, it might take some tries to get all settings right.

This guide tries to give a basic overview of how to configure ADFS and how to determine the settings for django-auth-adfs. Installing and configuring the basics of ADFS is not explained here.

Step 1 - Configuring a Relying Party Trust

From the AD FS Management screen, go to AD FS ➜ Trust Relationships ➜ Relying Party Trusts and click Add Relying Party Trust…

_images/01_add_relying_party.png

Click Start

_images/02_add_relying_party_wizard_page1.png

Select Enter data about the relying party manually and click Next

_images/03_add_relying_party_wizard_page2.png

Enter a display name for the relying party and click Next.

_images/04_add_relying_party_wizard_page3.png

Select AD FS profile and click Next

_images/05_add_relying_party_wizard_page4.png

Leave everything empty click Next

_images/06_add_relying_party_wizard_page5.png

We don’t need WS-Federation or SAML support so leave everything empty and click Next

_images/07_add_relying_party_wizard_page6.png

Enter a relying party trust identifier and click add. The identifier can be anything but beware, there’s a difference between entering a URL and something else. For more details see the example section of the AUDIENCE setting.

Note

This is the value for the AUDIENCE and the RELYING_PARTY_ID settings.

_images/08_relying_party_id.png

Select I do not want to configure… and click Next.

_images/09_add_relying_party_wizard_page8.png

Select Permit all users to access the relying party and click Next.

_images/10_add_relying_party_wizard_page9.png

Review the settings and click Next to create the relying party.

_images/11_add_relying_party_wizard_review.png

Check Open the Edit Claim Rules dialog… and click Close

_images/12_add_relying_party_wizard_page11.png
Step 2 - Configuring Claims

If you selected Open the Edit Claim Rules dialog… while adding a relying party, this screen will open automatically. Else you can open it by right clicking the relying party in the list and select Edit Claim Rules…

On the Issuance Transform Rules tab, click the Add Rule button

_images/13_configure_claims_page1.png

Select Send LDAP Attributes as Claims and click Next

_images/14_configure_claims_page2.png

Give the rule a name and select Active Directory as the attribute store. Then configure the below claims.

LDAP Attribute Outgoing Claim Type
E-Mail-Addresses E-Mail Address
Given-Name Given Name
Surname Surname
Token-Groups - Unqualified Names Group
SAM-Account-Name Windows Account Name
_images/15_configure_claims_page3.png

Click OK to save the settings

Note

The Outgoing Claim Type is what will be visible in the JWT Access Token. The first 3 claims will go into the CLAIM_MAPPING setting. The 4th is the GROUPS_CLAIM setting. The 5th is the USERNAME_CLAIM setting.

You cannot just copy the outgoing claim type value from this screen and use it in the settings. The name of the claim as it is in the JWT token is the short name which you can find in the AD FS Management screen underneath AD FS ➜ Service ➜ Claim Descriptions


You should now see the rule added. Click OK to save the settings.

_images/16_configure_claims_page4.png
Step 3 - Add an ADFS client

While the previous steps could be done via the GUI, the next step must be performed via PowerShell.

Pick a value for the following fields.

Name Example value
Name Django Application OAuth2 Client
ClientId 487d8ff7-80a8-4f62-b926-c2852ab06e94
RedirectUri http://web.example.com/oauth2/callback

Now execute the following command from a powershell console.

PS C:\Users\Administrator> Add-ADFSClient -Name "Django Application OAuth2 Client" `
                                          -ClientId "487d8ff7-80a8-4f62-b926-c2852ab06e94" `
                                          -RedirectUri "http://web.example.com/oauth2/callback"

The ClientId value will be the CLIENT_ID setting and the RedirectUri value is based on where you added the `django_auth_adfs in your urls.py file.

Step 4 - Determine configuration settings

Once everything is configured, you can use the below PowerShell commands to determine the value for the settings of this package. The <<<<<< in the output indicate which settings should match this value.

PS C:\Users\Administrator> Get-AdfsClient -Name "Django Application OAuth2 Client"

RedirectUri : {http://web.example.com:8000/oauth2/callback}
Name        : Django Application OAuth2 Client
Description :
ClientId    : 487d8ff7-80a8-4f62-b926-c2852ab06e94      <<< CLIENT_ID <<<
BuiltIn     : False
Enabled     : True
ClientType  : Public

PS C:\Users\Administrator> Get-AdfsProperties | select HostName | Format-List

HostName : adfs.example.com      <<< SERVER <<<

PS C:\Users\Administrator> Get-AdfsRelyingPartyTrust -Name "Django Application" | Select Identifier | Format-List

Identifier : {web.example.com}      <<< RELYING_PARTY_ID and AUDIENCE <<<

If you followed this guide, you should end up with a configuration like this.

AUTH_ADFS = {
    "SERVER": "adfs.example.com",
    "CLIENT_ID": "487d8ff7-80a8-4f62-b926-c2852ab06e94 ",
    "RELYING_PARTY_ID": "web.example.com",
    "AUDIENCE": "microsoft:identityserver:web.example.com",
    "CLAIM_MAPPING": {"first_name": "given_name",
                      "last_name": "family_name",
                      "email": "email"},
    "USERNAME_CLAIM": "winaccountname",
    "GROUP_CLAIM": "group"
}
Enabling SSO for other browsers

By default, ADFS only supports seamless single sign-on for Internet Explorer. In other browsers, users will always be prompted for their username and password.

To enable SSO also for other browsers like Chrome and Firefox, execute the following PowerShell command:

[System.Collections.ArrayList]$UserAgents = Get-AdfsProperties | select -ExpandProperty WIASupportedUserAgents
$UserAgents.Add("Mozilla/5.0")
Set-ADFSProperties -WIASupportedUserAgents $UserAgents

After that, restart the ADFS service on every server in the ADFS farm.

For firefox, you’ll also have to change it’s network.automatic-ntlm-auth.trusted-uris setting to include the URI of your ADFS server.

Windows 2016 - ADFS 4.0

Getting this module to work is sometimes not so straight forward. If your not familiar with JWT tokens or ADFS itself, it might take some tries to get all settings right.

This guide tries to give a basic overview of how to configure ADFS and how to determine the settings for django-auth-adfs. Installing and configuring the basics of ADFS is not explained here.

Step 1 - Configuring an Application Group

From the AD FS Management screen, go to AD FS ➜ Application Groups and click Add Application Group…

_images/01_add_app_group.png

Fill in a name for the application group, select Web browser accessing a web application and click Next.

_images/02_add_app_group_wizard_page1.png

Make note of the Client Identifier value. This will be the value for the CLIENT_ID setting.

The Redirect URI value must match with the domain where your Django application is located and the patch where you mapped the django_auth_adfs urls in your urls.py file. If you follow the installation steps from this documentation, this should be something like https://your.domain.com/oauth2/callback.

_images/03_add_native_app.png

Select Permit everyone and click Next.

_images/04_native_app_access_policy.png

Review the settings and click Next

While they both are the same in this screenshot, they can be changed independently from one another afterwards.

_images/05_review_settings.png

Close the wizard by clicking Close. Our django application is now registered in ADFS.

_images/06_wizard_end.png
Step 2 - Configuring Claims

Open the properties for the application group we just created. Select the Web application entry and click Edit

_images/07_app_group_settings.png

On the Issuance Transform Rules tab, click the Add Rule button

_images/08_add_claim_rules.png

Select Send LDAP Attributes as Claims and click Next

_images/08_add_ldap_attributes_part1.png

Give the rule a name and select Active Directory as the attribute store. Then configure the below claims.

LDAP Attribute Outgoing Claim Type
E-Mail-Addresses E-Mail Address
Given-Name Given Name
Surname Surname
Token-Groups - Unqualified Names Group
SAM-Account-Name Windows Account Name
_images/08_add_ldap_attributes_part2.png

Click Finish to save the settings

Note

The Outgoing Claim Type is what will be visible in the JWT Access Token. The first 3 claims will go into the CLAIM_MAPPING setting. The 4th is the GROUPS_CLAIM setting. The 5th is the USERNAME_CLAIM setting.

You cannot just copy the outgoing claim type value from this screen and use it in the settings. The name of the claim as it is in the JWT token is the short name which you can find in the AD FS Management screen underneath AD FS ➜ Service ➜ Claim Descriptions


You should now see the rule added. Click OK a couple of times to save the settings.

Step 3 - Determine configuration settings

Once everything is configured, you can use the below PowerShell commands to determine the value for the settings of this package. The <<<<<< in the output indicate which settings should match this value.

PS C:\Users\Administrator> Get-AdfsNativeClientApplication

Name                       : Django Application - Native application
Identifier                 : 487d8ff7-80a8-4f62-b926-c2852ab06e94      <<< CLIENT_ID <<<
ApplicationGroupIdentifier : Django Application
Description                :
Enabled                    : True
RedirectUri                : {http://web.example.com:8000/oauth2/callback}
LogoutUri                  :

PS C:\Users\Administrator> Get-AdfsProperties | select HostName | Format-List

HostName : adfs.example.com      <<< SERVER <<<

PS C:\Users\Administrator> Get-AdfsWebApiApplication | select Identifier | Format-List

Identifier             : {web.example.com}      <<< RELYING_PARTY_ID and AUDIENCE <<<

If you followed this guide, you should end up with a configuration like this.

AUTH_ADFS = {
    "SERVER": "adfs.example.com",
    "CLIENT_ID": "487d8ff7-80a8-4f62-b926-c2852ab06e94",
    "RELYING_PARTY_ID": "web.example.com",
    "AUDIENCE": "microsoft:identityserver:web.example.com",
    "CLAIM_MAPPING": {"first_name": "given_name",
                      "last_name": "family_name",
                      "email": "email"},
    "USERNAME_CLAIM": "winaccountname",
    "GROUP_CLAIM": "group"
}
Enabling SSO for other browsers

By default, ADFS only supports seamless single sign-on for Internet Explorer. In other browsers, users will always be prompted for their username and password.

To enable SSO also for other browsers like Chrome and Firefox, execute the following PowerShell command:

[System.Collections.ArrayList]$UserAgents = Get-AdfsProperties | select -ExpandProperty WIASupportedUserAgents
$UserAgents.Add("Mozilla/5.0")
Set-ADFSProperties -WIASupportedUserAgents $UserAgents

After that, restart the ADFS service on every server in the ADFS farm.

For firefox, you’ll also have to change it’s network.automatic-ntlm-auth.trusted-uris setting to include the URI of your ADFS server.

Azure AD

Getting this module to work is sometimes not so straightforward. If you’re not familiar with JWT tokens or Azure AD itself, it might take some tries to get all the settings right.

This guide tries to give a basic overview of how to configure Azure AD and how to determine the settings for django-auth-adfs. Installing and configuring the basics of Azure AD is not explained here.

Step 1 - Register a backend application

After signing in to Azure. Open the Azure Active Directory dashboard.

_images/01-azure_active_directory.png

Note down your Tenant_ID as you will need it later.

_images/02-azure_dashboard.png

Navigate to App Registrations, then click New registration in the upper left hand corner.

_images/03-new_registrations.png

Here you register your application.

  1. The display name of your application.
  2. What type of accounts can access your application.
  3. Here you need to add allowed redirect URIs. The Redirect URI value must match with the domain where your Django application is located(eg. http://localhost:8000/oauth2/callback).
_images/04-app_registrations_specs.png

When done registering, you will be redirected to your applications overview. Here you need to note down your Client_ID. This is how your Django project finds the right Azure application.

_images/05-application_overview.png

Next we need to generate a Client_Secret. Your application will use this to prove its identity when requesting a token.

_images/06-add_Secret.png

Give it a short (display) name. This is only used by you, to help keep track of in case you make more client secrets.

_images/07-add_Secret_name.png

Copy your secret (value). It will be become hidden after a short time, so be sure to note this quickly.

_images/08-copy_Secret.png

Step 2 - Configuring settings.py

We need to update the settings.py to accommodate our registered Azure AD application.

Replace your AUTH_ADFS with this.

# Client secret is not public information. Should store it as an environment variable.

client_id = 'Your client id here'
client_secret = 'Your client secret here'
tenant_id = 'Your tenant id here'


AUTH_ADFS = {
    'AUDIENCE': client_id,
    'CLIENT_ID': client_id,
    'CLIENT_SECRET': client_secret,
    'CLAIM_MAPPING': {'first_name': 'given_name',
                      'last_name': 'family_name',
                      'email': 'upn'},
    'GROUPS_CLAIM': 'roles',
    'MIRROR_GROUPS': True,
    'USERNAME_CLAIM': 'upn',
    'TENANT_ID': tenant_id,
    'RELYING_PARTY_ID': client_id,
}

Add this to your AUTHENTICATION_BACKENDS.

AUTHENTICATION_BACKENDS = [
    ...
    'django_auth_adfs.backend.AdfsAccessTokenBackend',
    ...
]

Add this path to your project’s urls.py file.

urlpatterns = [
    ...
    path('oauth2/', include('django_auth_adfs.urls')),
    ...
]
Step 3 - Register and configure an Azure AD frontend application

Just like we did with our backend application in step 1, we have to register a new app for our frontend. In this example we are authenticating a Django Rest Framework token through a single page application(SPA). The redirect URI value must match with the domain where your frontend application is located(eg. http://localhost:3000).

_images/09_register_frontend_app.PNG

Copy your frontend’s client ID, you will need later

_images/10_copy-frontend-client_id.png

Now we need to add a scope of permissions to our API. Navigate back to app registrations and click on your backend application. Go to Expose an API in the sidebar and press add a scope.

_images/11-navigate_to_expose_an_api.PNG

If you have not created an Application ID URI, it will be autogenerated for you. Select it and press save and continue.

_images/13_set_app_id.PNG

Then we will create the actual scope. Call it “read”, and just fill in all the required fields with “read” (maybe write an actual description).

_images/14_add_a_scope.PNG

Now we are going to add our frontend application as a trusted app for our backend. Press add a client application

_images/15_add_authorized_app_1.png

Here you need to paste in your frontend application (client) id.

_images/16_add_authorized_app_2.PNG

Now navigate back to app registrations. Click on your frontend application and navigate to API permissions. Press add a permission.

_images/17_navigate_to_api_permissions.PNG

Then we have to press My API’s and then select the backend application. (This could be different if you don’t have owner rights of the backend application.)

_images/18_add_permission.PNG

Here we can give our frontend the permission scope we created earlier. Press Delegated permissions (should be default) and select the permission you created and press add permission

_images/19_add-permission-2.PNG

Finally, sometimes the plugin will need to obtain the user groups claim from MS Graph (for example when the user has too many groups to fit in the access token), to ensure the plugin can do this successfully add the GroupMember.Read.All permission.

_images/20_add-permission-3.png

Login Middleware

django-auth-adfs ships with a middleware class named LoginRequiredMiddleware. You can use it to force an unauthenticated user to login and be redirected to the URL specified in in Django’s LOGIN_URL setting without having to add code to every view.

By default it’s disabled for the page defined in the LOGIN_URL setting and the redirect page for ADFS. But by setting the LOGIN_EXEMPT_URLS setting, you can exclude other pages from authentication. Have a look at the Settings Reference for more information.

To enable the middleware, add it to MIDDLEWARE in settings.py (or MIDDLEWARE_CLASSES if using Django <1.10. make sure to add it after any other session or authentication middleware to be sure all other methods of identifying the user are tried first.

In your settings.py file, add the following:

MIDDLEWARE = (
    ...
    'django_auth_adfs.middleware.LoginRequiredMiddleware',
)

AUTH_ADFS = {
    ...
    "LOGIN_EXEMPT_URLS": ["api/", "public/"],
    ...
}

Rest Framework integration

Setup

When using Django Rest Framework, you can also use this package to authenticate your REST API clients. For this you need to do some extra configuration.

You also need to install djangorestframework (or add it to your project dependencies):

pip install djangorestframework

The default AdfsBackend backend expects an authorization_code. The backend will take care of obtaining an access_code from the Adfs server.

With the Django Rest Framework integration the client application needs to acquire the access token by itself. See for an example: Requesting an access token. To authenticate against the API you need to enable the AdfsAccessTokenBackend.

Steps to enable the Django Rest Framework integration are as following:

Add an extra authentication class to Django Rest Framework in settings.py:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'django_auth_adfs.rest_framework.AdfsAccessTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    )
}

Enable the AdfsAccessTokenBackend authentication backend in settings.py:

AUTHENTICATION_BACKENDS = (
    ...
    'django_auth_adfs.backend.AdfsAccessTokenBackend',
    ...
)

Prevent your API from triggering a login redirect:

AUTH_ADFS = {
    'LOGIN_EXEMPT_URLS': [
        '^api',  # Assuming you API is available at /api
    ],
}

(Optional) Override the standard Django Rest Framework login pages in your main urls.py:

urlpatterns = [
    ...
    # The default rest framework urls shouldn't be included
    # If we include them, we'll end up with the DRF login page,
    # instead of being redirected to the ADFS login page.
    #
    # path('api-auth/', include('rest_framework.urls')),
    #
    # This overrides the DRF login page
    path('oauth2/', include('django_auth_adfs.drf_urls')),
    ...
]

Requesting an access token

When everything is configured, you can request an access token in your client (script) and access the api like this:

Note

This example is written for ADFS on windows server 2016 but with some changes in the URLs should also work for Azure AD.

import getpass
import requests
from pprint import pprint

# Ask for password
user = getpass.getuser()
password = getpass.getpass("Password for "+user+": ")
user = user + "@example.com"

# Get an access token
payload = {
    "grant_type": "password",
    "resource": "your-relying-party-id",
    "client_id": "your-configured-client-id",
    "username": user,
    "password": password,
}
response = requests.post(
    "https://adfs.example.com/adfs/oauth2/token",
    data=payload,
    verify=False
)
response.raise_for_status()
response_data = response.json()
access_token = response_data['access_token']

# Make a request towards this API
headers = {
    'Accept': 'application/json',
    'Authorization': 'Bearer ' + access_token,
}
response = requests.get(
    'https://web.example.com/api/questions',
    headers=headers,
    verify=False
)
pprint(response.json())

Note

The following example is written for ADFS on windows server 2012 R2 and needs the requests-ntlm module.

This example is here only for legacy reasons. If possible it’s advised to upgrade to 2016. Support for 2012 R2 is about to end.

import getpass
import re
import requests
from requests_ntlm import HttpNtlmAuth
from pprint import pprint

# Ask for password
user = getpass.getuser()
password = getpass.getpass("Password for "+user+": ")
user = "EXAMPLE\\" + user

# Get a authorization code
headers = {"User-Agent": "Mozilla/5.0"}
params = {
    "response_type": "code",
    "resource": "your-relying-party-id",
    "client_id": "your-configured-client-id",
    "redirect_uri": "https://djangoapp.example.com/oauth2/callback"
}
response = requests.get(
    "https://adfs.example.com/adfs/oauth2/authorize/wia",
    auth=HttpNtlmAuth(user, password),
    headers=headers,
    allow_redirects=False,
    params=params,
)
response.raise_for_status()
code = re.search('code=(.*)', response.headers['location']).group(1)

# Get an access token
data = {
    'grant_type': 'authorization_code',
    'client_id': 'your-configured-client-id',
    'redirect_uri': 'https://djangoapp.example.com/oauth2/callback',
    'code': code,
}
response = requests.post(
    "https://adfs.example.com/adfs/oauth2/token",
    data,
)
response.raise_for_status()
response_data = response.json()
access_token = response_data['access_token']

# Make a request towards this API
headers = {
    'Accept': 'application/json',
    'Authorization': 'Bearer %s' % access_token,
}
response = requests.get(
    'https://djangoapp.example.com/v1/pets?name=rudolf',
    headers=headers
)
pprint(response.json())

Demo

A Vagrantfile and example project are available to show what’s needed to convert a Django project from form based authentication to ADFS authentication.

Prerequisites

  • A hypervisor like virtualbox.

  • A working vagrant installation. On Debian 11 (bullseye) if you use the stock vagrant package you need to install these plugins:

    vagrant plugin install winrm
    vagrant plugin install winrm-fs
    vagrant plugin install winrm-elevated
    vagrant plugin install vagrant-reload
    
  • The github repository should be cloned/downloaded in some directory.

This guide assumes you’re using VirtualBox, but another hypervisor should also work. If you choose to use another one, make sure there’s a windows server 2019 vagrant box available for it.

Components

The demo consists of 2 parts:

  • A web server VM.
  • A windows server 2019 VM.

The webserver will run Django and is reachable at http://web.example.com:8000. The windows server will run a domain controller and ADFS service.

Starting the environment

Web server

First we get the web server up and running.

  1. Navigate to the directory where you cloned/downloaded the github repository.

  2. Bring up the web server by running the command:

    vagrant up web
    
  3. Wait as the vagrant box is downloaded and the needed software installed.

  4. Next, SSH into the web server:

    vagrant ssh web
    
  5. Once connected, start the Django project:

    cd /vagrant/demo/adfs
    python3 manage.py runserver 0.0.0.0:8000
    

you should now be able to browse the demo project by opening the page http://localhost:8000 in a browser. Pages requiring authentication wont work, because the ADFS server is not there yet.

Note

There are 2 versions of the web example. One is a forms based authentication example, the other depends on ADFS. If you want to run the forms based example, change the path above to /vagrant/demo/formsbased

ADFS server

The next vagrant box to start is the ADFS server. The scripts used for provisioning the ADFS server can be found in the folder /vagrant inside the repository.

  1. Navigate to the directory where you cloned/downloaded the github repository.

  2. Bring up the ADFS server by running the command:

    vagrant up adfs
    
  3. Wait as the vagrant box is downloaded and the needed software installed. For this windows box, it takes a couple of coffees before it’s done.

  4. Next, open window showing the login screen of the windows server. The login credentials are:

    username: vagrant
    password: vagrant
    
  5. Once logged in, install a browser like Chrome of Firefox.

  6. Next, in that browser on the windows server, verify you can open the page http://web.example.com:8000

In the AD FS management console, you can check how the example project is configured. The config is in the Application Groups folder.

Note

You wont be able to test the demo project from outside the windows machine because port 443 is not forwarded and name resolution of adfs.example.com won’t work. You can workaround this by forwarding that port 443 from the guest to port 443 on your host and manually adding the right IP addresses in you hosts file.

Note

Because windows server virtual boxes are rather rare on the vagrant cloud (they need to be rebuild every 180 days), it might be that the box specified in the Vagrantfile doesn’t work anymore. If you replace it by another one that’s just a vanilla windows server, it should work.

Using the demo

Once everything is up and running, you can click around in the very basic poll app that the demo is.

  • The bottom of the page shows details about the logged in user.

  • There are 2 users already created in the Active Directory domain. Both having the default password Password123

    • bob@example.com which is a Django super user because he’s a member of active directory group django_admins.
    • alice@example.com which is a regular Django user.
  • By default, only the page to vote on a poll requires you to be logged in.

  • There are no questions by default. Create some in the admin section with user bob.

  • Compare the files in /vagrant/demo/formsbased to those in /vagrant/demo/adfs to see what was changed to enable ADFS authentication in a demo project.

Troubleshooting

Turn on Django debug logging

If you run into any problems, set the logging level in Django to DEBUG. You can do this by adding the configuration below to your settings.py

You can see this logging in your console, or in you web server log if you’re using something like Apache with mod_wsgi.

More details about logging in Django can be found in the official Django documentation

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(name)s %(message)s'
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'verbose'
        },
    },
    'loggers': {
        'django_auth_adfs': {
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    },
}

Run Django with warnings enabled

Start the python interpreter that runs you Django with the -Wd parameter. This will show warnings that are otherwise suppressed.

python -Wd manage.py runserver

Have a look at the demo project

There’s an simple demo project available in the /demo folder and in the demo chapter of the documentation.

If you compare the files in the adfs folder with those in the formsbased folder, you’ll see what needs to be changed in a standard Django project to enable ADFS authentication.

Besides that, there are a couple of PowerShell scripts available that are used while provisioning the ADFS server for the demo. you can find them in the /vagrant folder in this repository. They might be useful to figure out what is wrong with the configuration of your ADFS server.

Note that they are only meant for getting a demo running. By no means are they meant to configure your ADFS server.

Frequently Asked Questions

Why am I always redirected to /accounts/profile/ after login?

This is default Django behaviour. You can change it by setting the Django setting named LOGIN_REDIRECT_URL.

How do I store additional info about a user?

django_auth_adfs can only store information in existing fields of the user model. If you want to store extra info, you’ll have to extend the default user model with extra fields and adjust the CLAIM_MAPPING setting accordingly.

You can read about how to extend the user model here

I’m receiving an SSLError: CERTIFICATE_VERIFY_FAILED error.

double check your CA_BUNDLE setting. Most likely your ADFS server is using a certificate signed by an enterprise root CA. you’ll need to put it’s certificate in a file and set CA_BUNDLE to it’s path.

I’m receiving an KeyError: 'upn' error when authenticating against Azure AD.

In some circumstances, Azure AD does not send the upn claim used to determine the username. It’s observed to happen with guest users who’s source in the users overview of Azure AD is Microsoft Account instead of Azure Active Directory.

In such cases, try setting the USERNAME_CLAIM to email instead of the default upn. Or create a new user in your Azure AD directory.

Why am I prompted for a username and password in Chrome/Firefox?

By default, ADFS only triggers seamless single sign-on for Internet Explorer or Edge.

Have a look at the ADFS configuration guides for details about how to got this working for other browsers also.

Why is a user added and removed from the same group on every login?

This can be caused by having a case insensitive database, such as a MySQL database with default settings. You can read more about collation settings in the official documentation.

The redirect_uri starts with HTTP, while my site is HTTPS only.

When you run Django behind a TLS terminating webserver or load balancer, then Django doesn’t know the client arrived over a HTTPS connection. It will only see the plain HTTP traffic. Therefor, the link it generates and sends to ADFS as the redirect_uri query parameter, will start with HTTP, instead of HTTPS.

To tell Django to generate HTTPS links, you need to set it’s SECURE_PROXY_SSL_HEADER setting and inject the correct HTTP header and value on your web server.

For more info, have a look at Django’s docs.

I cannot get it working!

Make sure you follow the instructions in the troubleshooting guide. It will enable debugging and can quickly tell you what is wrong.

Also, walk through the Settings Reference once, you might find one that needs to be adjusted to match your situation.

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

Get Started!

Types of Contributions

You can contribute in many ways:

Report Bugs

Report bugs in the issue section of the repository on GitHub.

If you are reporting a bug, please include:

  • Detailed steps to reproduce the bug.
  • Any details about your local setup that might be helpful in troubleshooting.
Fix Bugs

Look through the issues for bugs. Anything tagged with “bug” is open to whoever wants to implement it.

Implement Features

Look through the issues for features. Anything tagged with “feature” is open to whoever wants to implement it.

Write Documentation

We could always use more documentation, whether as part of the docs or in docstrings in the code.

Submit Feedback

The best way to send feedback is to file an issue on GitHub.

If you are proposing a feature:

  • Explain in detail how it would work.
  • Keep the scope as narrow as possible, to make it easier to implement.
Set up your environment
  1. Fork the upstream django-auth-adfs repository into a personal account.
  2. Install poetry running pip install poetry or curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
  3. Configure poetry to create a virtual environment in your project folder: poetry config virtualenvs.in-project true
  1. Install dependencies by running poetry install
  2. Create a new branch for your changes
  3. Push the topic branch to your personal fork
  4. Create a pull request to the django-auth-adfs repository with a detailed explanation