Set up Django authentication with Kinde

By Raihaan Raman — Published

In this tutorial, we’ll show you how to integrate a Django application with Kinde. This integration will enhance security and the user experience, and by the end, you’ll be able to unlock Kinde’s robust authentication and user management features.

Let’s get started

Link to this section

If you’ve installed Django already, check which version you have by running this command:

$ python -m django --version

If Django isn’t installed, follow the instructions on the official Django website.

We’ll first create a new standard Django project, add a simple HTML page and a css file, import the Kinde Python SDK, wire it all up, and in the end, have a fully-functional Django app protected by Kinde.

Open up a terminal, and let’s get coding!

Some initial setup

Link to this section
  1. Create a new Django project by running

    $ django-admin startproject kinde_django_login
    
  2. Create an app within the project by running:

    $ python manage.py startapp mysuperapp
    

    Note that you can call your app whatever you want. We’ll call this one mysuperapp and will refer to this name for the rest of this tutorial

  3. In the mysuperapp directory, create a directory called templates, and within that templates directory, create a sub-directory called mysuperapp. This lets you have templates that share the same name within multiple applications in a Django project.

  4. Also create a directory called static, where we will store a CSS file. Note that we’re using the django.contrib.staticfiles Django app to serve the CSS file.

    While django.contrib.staticfiles is fine for a development environment, it’s not safe for Production use. The Django docs explain how to deploy static files in Production.

  5. Your directory and files should now look like this:

    kinde_django_login/
      kinde_django_login/
    	  __init__.py
    		asgi.py
    		settings.py
    		urls.py
    		wsgi.py
    	mysuperapp/
        __init__.py
        admin.py
        apps.py
        migrations/
            __init__.py
        models.py
    		static/
        templates/
            mysuperapp/
        tests.py
    	  views.py
    	manage.py
    

Add some simple content

Link to this section
  1. In the static directory, add a new CSS file called styles.css, with the following contents:

    :root {
        --g-color-black: #000;
        --g-color-white: #fff;
    
        --g-color-grey-50: #f6f6f6;
        --g-color-grey-600: #636363;
        --g-color-grey-700: #4d4d4d;
        --g-color-grey-900: #0f0f0f;
    
        --g-box-shadow: 0px 6px 12px rgba(18, 20, 23, 0.06), 0px 15px 24px rgba(18, 20, 23, 0.07),
            0px -4px 12px rgba(18, 20, 23, 0.05);
    
        --g-font-family: Helvetica, sans-serif;
    
        --g-font-size-x-small: 0.75rem; /* 12px */
        --g-font-size-small: 0.875rem; /* 14px */
        --g-font-size-base: 1rem; /* 16px */
        --g-font-size-large: 1.25rem; /* 20x */
        --g-font-size-x-large: 1.5rem; /* 24px */
        --g-font-size-2x-large: 2rem; /* 32px */
        --g-font-size-3x-large: 2.5rem; /* 40px */
        --g-font-size-4x-large: 4rem; /* 64px */
    
        --g-font-weight-base: 400;
        --g-font-weight-semi-bold: 500;
        --g-font-weight-bold: 600;
        --g-font-weight-black: 700;
    
        --g-border-radius-small: 0.5rem;
        --g-border-radius-base: 1rem;
        --g-border-radius-large: 1.5rem;
    
        --g-spacing-small: 0.5rem; /* 8px */
        --g-spacing-base: 1rem; /* 16px */
        --g-spacing-large: 1.5rem; /* 24px */
        --g-spacing-x-large: 2rem; /* 32px */
        --g-spacing-2x-large: 2.5rem; /* 40px */
        --g-spacing-3x-large: 3rem; /* 48px */
        --g-spacing-6x-large: 6rem; /* 96px */
    }
    
    * {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
    }
    
    html,
    body {
        font-family: var(--g-font-family);
    }
    
    a {
        color: inherit;
        text-decoration: none;
    }
    
    button {
        appearance: none;
        background-color: transparent;
        border: none;
    }
    
    .text-subtle {
        color: var(--g-color-grey-600);
        font-size: var(--g-font-size-x-small);
        font-weight: var(--g-font-weight-base);
    }
    
    .text-body-1 {
        font-size: var(--g-font-size-2x-large);
        font-weight: var(--g-font-weight-base);
    }
    
    .text-body-2 {
        font-size: var(--g-font-size-x-large);
        font-weight: var(--g-font-weight-base);
    }
    
    .text-body-3 {
        color: var(--g-color-grey-900);
        font-size: var(--g-font-size-small);
        font-weight: var(--g-font-weight-base);
    }
    
    .text-display-1 {
        font-size: var(--g-font-size-4x-large);
        font-weight: var(--g-font-weight-black);
        line-height: 1.2;
    }
    
    .text-display-2 {
        font-size: var(--g-font-size-3x-large);
        font-weight: var(--g-font-weight-black);
        line-height: 1.4;
    }
    
    .text-display-3 {
        font-size: var(--g-font-size-x-large);
        font-weight: var(--g-font-weight-black);
    }
    
    .text-heading-1 {
        font-size: var(--g-font-size-large);
        font-weight: var(--g-font-weight-semi-bold);
    }
    
    .text-heading-2 {
        font-size: var(--g-font-size-base);
        font-weight: var(--g-font-weight-semi-bold);
    }
    
    .container {
        padding: 0 var(--g-spacing-6x-large);
        margin: auto;
    }
    
    .nav {
        align-items: center;
        display: flex;
        justify-content: space-between;
        padding-bottom: var(--g-spacing-x-large);
        padding-top: var(--g-spacing-x-large);
        width: 100%;
    }
    
    .sign-in-btn {
        margin-right: var(--g-spacing-small);
    }
    
    .btn {
        border-radius: var(--g-border-radius-small);
        display: inline-block;
        font-weight: var(--g-font-weight-bold);
        padding: var(--g-spacing-base);
    }
    
    .btn-ghost {
        color: var(--g-color-grey-700);
    }
    
    .btn-dark {
        background-color: var(--g-color-black);
        color: var(--g-color-white);
    }
    
    .btn-light {
        background: var(--g-color-white);
        color: var(--g-color-black);
        font-weight: 600;
    }
    
    .btn-big {
        font-size: var(--g-font-size-large);
        padding: var(--g-font-size-large) var(--g-font-size-x-large);
    }
    
    .hero {
        align-items: center;
        display: flex;
        flex-direction: column;
        height: 45rem;
        justify-content: center;
        text-align: center;
    }
    
    .hero-title {
        margin-bottom: var(--g-spacing-x-large);
    }
    
    .hero-tagline {
        margin-bottom: var(--g-spacing-3x-large);
    }
    
    .card {
        background: var(--g-color-black);
        border-radius: var(--g-border-radius-large);
        box-shadow: var(--g-box-shadow);
        color: var(--g-color-white);
    }
    
    .link {
        text-decoration: underline;
        text-underline-offset: 0.2rem;
    }
    
    .link:hover,
    .link:focus {
        background: #f1f2f4;
    }
    
    .footer {
        padding-bottom: var(--g-spacing-x-large);
        padding-top: var(--g-spacing-x-large);
    }
    
    .footer-tagline {
        margin-bottom: var(--g-font-size-x-small);
        margin-top: var(--g-font-size-x-small);
    }
    
    .start-hero {
        padding: var(--g-spacing-2x-large);
        text-align: center;
    }
    
    .start-hero-intro {
        margin-bottom: var(--g-spacing-base);
    }
    
    .avatar {
        align-items: center;
        background-color: var(--g-color-grey-50);
        border-radius: var(--g-border-radius-large);
        display: flex;
        height: var(--g-spacing-3x-large);
        justify-content: center;
        text-align: center;
        width: var(--g-spacing-3x-large);
    }
    
    .profile-blob {
        align-items: center;
        display: grid;
        gap: var(--g-spacing-base);
        grid-template-columns: auto 1fr;
    }
    
    .next-steps-section {
        margin-top: var(--g-spacing-2x-large);
    }
    
  2. In the templates/mysuperapp directory, create a new file called index.html with the following contents:

    <!doctype html>
    <html lang="en">
        <head>
            <meta charset="UTF-8" />
            <meta http-equiv="X-UA-Compatible" content="IE=edge" />
            <meta name="viewport" content="width=device-width, initial-scale=1.0" />
            <title>Kinde - Django example</title>
            <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
            {% load static %}
            <link rel="stylesheet" href="{% static 'styles.css' %}" />
        </head>
        <body>
            <header>
                <nav class="nav container">
                    <h1 class="text-display-3">Kinde Django!</h1>
                    {% if not authenticated %}
                    <div class="js-logged-out-view">
                        <a id="login" class="btn btn-ghost sign-in-btn" href="/mysuperapp/login">
                            Sign in
                        </a>
                        <a id="register" class="btn btn-dark" href="/mysuperapp/register">
                            Sign up
                        </a>
                    </div>
                    {% else %}
                    <div class="js-logged-in-view">
                        <div class="profile-blob">
                            <div class="js-user-avatar avatar">
                                {% if authenticated %}{{ user_initials }}{% endif %}
                            </div>
                            <div>
                                <p class="js-user-name text-heading-2">
                                    {% if authenticated %}{{ user_full_name }}{% endif %}
                                </p>
                                <a id="logout" class="text-subtle" href="/mysuperapp/logout">
                                    Sign out
                                </a>
                            </div>
                        </div>
                    </div>
                    {% endif %}
                </nav>
            </header>
    
            <main>
                <div class="container">
                    {% if not authenticated %}
                    <div class="js-logged-out-view">
                        <div class="card hero">
                            <p class="text-display-1 hero-title">
                                Let's start authenticating <br />
                                with Kinde
                            </p>
                            <p class="text-body-1 hero-tagline">Configure your app</p>
    
                            <a
                                href="https://kinde.com/docs/developer-tools/python-sdk/"
                                target="_blank"
                                rel="noreferrer"
                                class="btn btn-light btn-big"
                            >
                                Go to docs
                            </a>
                        </div>
                    </div>
                    {% else %}
                    <div class="js-logged-in-view">
                        <div class="card start-hero">
                            <p class="text-body-2 start-hero-intro">Woohoo!</p>
                            <p class="text-display-2">
                                Your authentication is all sorted.
                                <br />
                                Build the important stuff.
                            </p>
                        </div>
                        <section class="next-steps-section">
                            <h2 class="text-heading-1">Next steps for you</h2>
                        </section>
                    </div>
                    {% endif %}
                </div>
            </main>
    
            <footer class="footer">
                <div class="container">
                    <strong class="text-heading-2">Kinde</strong>
                    <p class="footer-tagline text-body-3">
                        Visit our
                        <a class="link" href="https://kinde.com/docs"> help center </a>
                    </p>
    
                    <small class="text-subtle"> © 2023 Kinde Django! </small>
                </div>
            </footer>
        </body>
    </html>
    

Let’s get Django with it

Link to this section
  1. Open the views.py file and replace it with the following contents:

    from django.shortcuts import render
    
    # Create your views here.
    def index(request):
        return render(request, "mysuperapp/index.html", {})
    
  2. In the mysuperapp directory, create an empty file called urls.py, and add the following contents to it

    from django.urls import path
    
    from . import views
    
    urlpatterns = [
        path("", views.index, name="index"),
    ]
    
  3. Point the root URLConf to the mysuperapp module. To do this, navigate to the project level url file located in the kinde_django_login/urls.py and make the following changes:

    1. Import the include function from django.urls
    2. Add a urlpattern to point to the root of the mysuperapp application.
    from django.contrib import admin
    from django.urls import include, path # Import the include function
    
    urlpatterns = [
        #Add a urlpattern to the superapp application
        path("mysuperapp/", include("mysuperapp.urls")),
        path('admin/', admin.site.urls),
    ]
    
  4. While we’re dealing with project level files, let’s add a reference to mysuperapp in the INSTALLED_APPS array in the Django settings. Open up the kinde_django_login/settings.py file, find the INSTALLED_APPS array, and add a new entry called mysupperapp.

    # Application definition
    
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'mysuperapp' #Add this entry!
    ]
    
  5. Still in the settings.py file, ensure session cookies are not accessible by Javascript, like you would in a production environment. But since we’re running the Django app locally without SSL, we’ll leave the SESSION_COOKIE_SECURE set to False.

    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SECURE = False # !! Important: Set to True for Production
    
  6. From the kinde_django_login directory, run:

    python manage.py migrate
    

    This will use the default Sqlite database and create a few tables based on the installed apps defined in settings.py . We need some of the tables Django creates for session management later down the track.

  7. At this point, if you run python manage.py runserver from the kinde_django_login directory, and navigate to http://127.0.0.1:8000/mysuperapp/ , you should see a web page with a Sign In and Sign Up button. These buttons won’t work yet, until the next set of steps are complete, that is.

Kinde is in the building

Link to this section
  1. In the kinde_django_login directory, create a new file called requirements.txt and add the following contents to it:

    kinde-python-sdk==1.2.1
    python-dotenv==1.0.0
    
  2. Install the packages in your Python environment by running:

    python -m pip install -r requirements.txt
    
  3. Still in the kinde_django_login directory, create a new file called .env where we will store the required variables to hook up to Kinde.

  4. If you do not have a Kinde business already, create a new Kinde business.

  5. Navigate to Settings > Applications and select View Details on the default Back-end app.

  6. Copy the App keys.

  1. Add http://127.0.0.1:8000/mysuperapp/callback as a callback URL in the back-end app details.
  1. Add http://127.0.0.1:8000/mysuperapp/ as the logout URL. This is the URL that Kinde redirects to after a successful logout. For the purpose of this tutorial, we’ll redirect the user back to app’s main page.
  1. In the .env file, add the following details, replacing the <CLIENT_ID>, <CLIENT_SECRET> and the <your_subdomain> with the details you copied from the Back-end app.

    KINDE_CLIENT_ID=<CLIENT_ID>
    KINDE_CLIENT_SECRET=<CLIENT_SECRET>
    KINDE_ISSUER_URL=https://<your_subdomain>.kinde.com
    KINDE_CALLBACK_URL=http://127.0.0.1:8000/mysuperapp/callback
    KINDE_LOGOUT_URL=http://127.0.0.1:8000/mysuperapp/
    

Wiring it all up

Link to this section
  1. In the mysuperapp directory, edit the views.py file, and add the following contents:

    from django.shortcuts import render, redirect, HttpResponse
    
    # Import the Kinde SDK, dotenv and a few std lib modules
    import os, base64
    from kinde_sdk.kinde_api_client import GrantType, KindeApiClient
    from dotenv import load_dotenv
    
    # Call load_dotenv so we can access values from the .env file
    load_dotenv()
    
    # For now, we'll just use a dictionary to hold a bunch of clients for each
    # user in memory
    user_clients = {}
    
    # A few helper functions to manage
    # the data coming back from Kinde
    def __get_empty_context():
        return {
            "authenticated": False,
            "user_first_name": "",
            "user_last_name": "",
        }
    
    def __get_user_context(user_id):
        context = __get_empty_context()
    
        user_client = user_clients.get(user_id)
    
        if user_client is not None:
            is_user_authenticated = user_client.get('kinde_client').is_authenticated()
            user_last_name = user_client.get('user_last_name')
            user_first_name = user_client.get('user_first_name')
    
            context = {
                "authenticated": is_user_authenticated,
                "user_first_name": user_first_name,
                "user_last_name": user_last_name,
                "user_full_name": user_first_name + ' ' + user_last_name,
                "user_initials": user_first_name[0] + f"{user_last_name[0] if user_last_name is not None else user_first_name[1]}"
            }
    
        return context
    
    def __get_new_kinde_client():
        return KindeApiClient(
            domain=os.getenv("KINDE_ISSUER_URL"),
            callback_url=os.getenv("KINDE_CALLBACK_URL"),
            client_id=os.getenv("KINDE_CLIENT_ID"),
            client_secret=os.getenv("KINDE_CLIENT_SECRET"),
            grant_type=GrantType.AUTHORIZATION_CODE,
        )
    
    # The main page!
    def index(request):
        context = __get_empty_context()
    
        if request.session.get('user_id') is not None:
            user_id = request.session.get('user_id')
            context = __get_user_context(user_id)
    
        return render(request, "mysuperapp/index.html", context)
    
    # What gets run when you sign in
    def login(request):
        context = __get_empty_context()
    
        # Check if there's a session for this user
        if request.session.get('user_id') is None:
            kinde_client = __get_new_kinde_client()
            return redirect(kinde_client.get_login_url())
        else:
            user_id = request.session.get('user_id')
            context = __get_user_context(user_id)
            return render(request, "mysuperapp/index.html", context)
    
    # What gets run when you register
    def register(request):
        # Check if there's a session for this user
        if request.session.get('user_id') is None:
            kinde_client = __get_new_kinde_client()
            return redirect(kinde_client.get_register_url())
        else:
            user_id = request.session.get('user_id')
            context = __get_user_context(user_id)
            return render(request, "mysuperapp/index.html", context)
    
    # When your user is done authenticating in Kinde
    # Kinde calls this route back
    def callback(request):
        context = __get_empty_context()
    
        if request.session.get('user_id') is None:
    
            kinde_client = __get_new_kinde_client()
    
            kinde_client.fetch_token(authorization_response=request.build_absolute_uri())
            user_details = kinde_client.get_user_details()
            user_id = user_details['id']
            request.session['user_id'] = user_id
            user_clients[user_id] = {
                "kinde_client": kinde_client,
                "authenticated": True,
                "user_first_name": user_details['given_name'],
                "user_last_name": user_details['family_name'],
            }
    
            return redirect("index")
    
        else:
            user_id = request.session.get('user_id')
            context = __get_user_context(user_id)
    
        return render(request, "mysuperapp/index.html", context)
    
    # What gets run when you logout
    def logout(request):
        index_path = request.build_absolute_uri('/mysuperapp/')
        user_id = request.session.get('user_id')
        kinde_client = user_clients.get(user_id).get('kinde_client')
        request.session.clear()
        user_clients[user_id] = None
    
        return redirect(kinde_client.logout(redirect_to=index_path))
    

    It looks like there’s quite a lot going on there, but it’s not that scary. We’ve defined the views for 5 routes:

    1. index - the main page
    2. login - a route that redirects the user to Kinde’s hosted login page
    3. register - a route that redirects the user to Kinde’s hosted register page
    4. callback - a route that Kinde calls back to when a user completes login or registration
    5. logout - a route that redirects the user to Kinde’s logout url
    6. In the callback route logic, we create an instance of KindeApiClient and call the fetch_token function that will retrieve the user’s id_token, access_token and refresh_token. The get_user_details function is then called to retrieve the user’s first name and last name from the id_token
    7. The callback route logic also saves this instance of the KindeApiClient in memory, so that it can reused for that particular user’s session.
  2. In the mysuperapp directory, edit the urls.py file to route to our new views.

    from django.urls import path
    
    from . import views
    
    urlpatterns = [
        path("", views.index, name="index"),
        path("login", views.login, name="login"),
        path("callback", views.callback, name="callback"),
        path("logout", views.logout, name="logout"),
        path("register", views.register, name="register"),
    ]
    
  3. If Django is not already running, from the kinde_django_login directory, run:

    python manage.py runserver
    

    You should see the app that you can use to register a user in your kinde business, and later on, sign in as that user.

There you have it! A Django application with routes to login, register and logout from. Now that you have authentication set up, explore all the other features that Kinde has to offer.