1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
|
Getting started
===============
Build a OAuth2 provider using Django, Django OAuth Toolkit, and OAuthLib.
What we will build?
-------------------
The plan is to build an OAuth2 provider from ground up.
On this getting started we will:
* Create the Django project.
* Install and configure Django OAuth Toolkit.
* Create two OAuth2 applications.
* Use Authorization code grant flow.
* Use Client Credential grant flow.
What is OAuth?
----------------
OAuth is an open standard for access delegation, commonly used as a way for Internet users to grant websites or applications access to their information on other websites but without giving them the passwords.
-- `Whitson Gordon`_
Django
------
Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. Built by experienced developers, it takes care of much of the hassle of Web development, so you can focus on writing your app without needing to reinvent the wheel.
-- `Django website`_
Let's get start by creating a virtual environment::
mkproject iam
This will create, activate and change directory to the new Python virtual environment.
Install Django::
pip install Django
Create a Django project::
django-admin startproject iam
This will create a mysite directory in your current directory. With the following structure::
.
└── iam
├── iam
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
Create a Django application::
cd iam/
python manage.py startapp users
That’ll create a directory :file:`users`, which is laid out like this::
.
├── iam
│ ├── asgi.py
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── users
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
If you’re starting a new project, it’s highly recommended to set up a custom user model, even if the default `User`_ model is sufficient for you. This model behaves identically to the default user model, but you’ll be able to customize it in the future if the need arises.
-- `Django documentation`_
Edit :file:`users/models.py` adding the code below:
.. code-block:: python
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
Change :file:`iam/settings.py` to add ``users`` application to ``INSTALLED_APPS``:
.. code-block:: python
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'users',
]
Configure ``users.User`` to be the model used for the ``auth`` application by adding ``AUTH_USER_MODEL`` to :file:`iam/settings.py`:
.. code-block:: python
AUTH_USER_MODEL = 'users.User'
Create initial migration for ``users`` application ``User`` model::
python manage.py makemigrations
The command above will create the migration::
Migrations for 'users':
users/migrations/0001_initial.py
- Create model User
Finally execute the migration::
python manage.py migrate
The ``migrate`` output::
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, users
Running migrations:
Applying contenttypes.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0001_initial... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying users.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying sessions.0001_initial... OK
Django OAuth Toolkit
--------------------
Django OAuth Toolkit can help you by providing, out of the box, all the endpoints, data, and logic needed to add OAuth2 capabilities to your Django projects.
Install Django OAuth Toolkit::
pip install django-oauth-toolkit
Add ``oauth2_provider`` to ``INSTALLED_APPS`` in :file:`iam/settings.py`:
.. code-block:: python
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'users',
'oauth2_provider',
]
Execute the migration::
python manage.py migrate
The ``migrate`` command output::
Operations to perform:
Apply all migrations: admin, auth, contenttypes, oauth2_provider, sessions, users
Running migrations:
Applying oauth2_provider.0001_initial... OK
Applying oauth2_provider.0002_auto_20190406_1805... OK
Include ``oauth2_provider.urls`` to :file:`iam/urls.py` as follows:
.. code-block:: python
from django.contrib import admin
from django.urls import include, path
from oauth2_provider import urls as oauth2_urls
urlpatterns = [
path('admin/', admin.site.urls),
path('o/', include(oauth2_urls)),
]
This will make available endpoints to authorize, generate token and create OAuth applications.
Last change, add ``LOGIN_URL`` to :file:`iam/settings.py`:
.. code-block:: python
LOGIN_URL = '/admin/login/'
We will use Django Admin login to make our life easy.
Create a user::
python manage.py createsuperuser
Username: wiliam
Email address: me@wiliam.dev
Password:
Password (again):
Superuser created successfully.
OAuth2 Authorization Grants
---------------------------
An authorization grant is a credential representing the resource owner's authorization (to access its protected resources) used by the client to obtain an access token.
-- `RFC6749`_
The OAuth framework specifies several grant types for different use cases.
-- `Grant types`_
We will start by given a try to the grant types listed below:
* Authorization code
* Client credential
These two grant types cover the most initially used use cases.
Authorization Code
------------------
The Authorization Code flow is best used in web and mobile apps. This is the flow used for third party integration, the user authorizes your partner to access its products in your APIs.
Start the development server::
python manage.py runserver
Point your browser to http://127.0.0.1:8000/o/applications/register/ lets create an application.
Fill the form as show in the screenshot below and before save take note of ``Client id`` and ``Client secret``, we will use it in a minute.
If you want to use this application with OIDC and ``HS256`` (see :doc:`OpenID Connect <oidc>`), uncheck ``Hash client secret`` to allow verifying tokens using JWT signatures. This means your client secret will be stored in cleartext but is the only way to successfully use signed JWT's with ``HS256``.
.. note::
``RS256`` is the more secure algorithm for signing your JWTs. Only use ``HS256`` if you must.
Using ``RS256`` will allow you to keep your ``client_secret`` hashed.
.. image:: _images/application-register-auth-code.png
:alt: Authorization code application registration
Export ``Client id`` and ``Client secret`` values as environment variable:
.. sourcecode:: sh
export ID=vW1RcAl7Mb0d5gyHNQIAcH110lWoOW2BmWJIero8
export SECRET=DZFpuNjRdt5xUEzxXovAp40bU3lQvoMvF3awEStn61RXWE0Ses4RgzHWKJKTvUCHfRkhcBi3ebsEfSjfEO96vo2Sh6pZlxJ6f7KcUbhvqMMPoVxRwv4vfdWEoWMGPeIO
Now let's generate an authentication code grant with PKCE (Proof Key for Code Exchange), useful to prevent authorization code injection. To do so, you must first generate a ``code_verifier`` random string between 43 and 128 characters, which is then encoded to produce a ``code_challenge``:
.. sourcecode:: python
import random
import string
import base64
import hashlib
code_verifier = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(random.randint(43, 128)))
code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest()
code_challenge = base64.urlsafe_b64encode(code_challenge).decode('utf-8').replace('=', '')
Take note of ``code_challenge`` since we will include it in the code flow URL. It should look something like ``XRi41b-5yHtTojvCpXFpsLUnmGFz6xR15c3vpPANAvM``.
Export ``code_verifier`` value as environment variable, it should be something like:
.. sourcecode:: sh
export CODE_VERIFIER=N0hHRVk2WDNCUUFPQTIwVDNZWEpFSjI4UElNV1pSTlpRUFBXNTEzU0QzRTMzRE85WDFWTzU2WU9ESw==
To start the Authorization code flow go to this `URL`_ which is the same as shown below::
http://127.0.0.1:8000/o/authorize/?response_type=code&code_challenge=XRi41b-5yHtTojvCpXFpsLUnmGFz6xR15c3vpPANAvM&code_challenge_method=S256&client_id=vW1RcAl7Mb0d5gyHNQIAcH110lWoOW2BmWJIero8&redirect_uri=http://127.0.0.1:8000/noexist/callback
Note the parameters we pass:
* **response_type**: ``code``
* **code_challenge**: ``XRi41b-5yHtTojvCpXFpsLUnmGFz6xR15c3vpPANAvM``
* **code_challenge_method**: ``S256``
* **client_id**: ``vW1RcAl7Mb0d5gyHNQIAcH110lWoOW2BmWJIero8``
* **redirect_uri**: ``http://127.0.0.1:8000/noexist/callback``
This identifies your application, the user is asked to authorize your application to access its resources.
Go ahead and authorize the ``web-app``
.. image:: _images/application-authorize-web-app.png
:alt: Authorization code authorize web-app
Remember we used ``http://127.0.0.1:8000/noexist/callback`` as ``redirect_uri`` you will get a **Page not found (404)** but it worked if you get a url like::
http://127.0.0.1:8000/noexist/callback?code=uVqLxiHDKIirldDZQfSnDsmYW1Abj2
This is the OAuth2 provider trying to give you a ``code``. in this case ``uVqLxiHDKIirldDZQfSnDsmYW1Abj2``.
Export it as an environment variable:
.. code-block:: sh
export CODE=uVqLxiHDKIirldDZQfSnDsmYW1Abj2
Now that you have the user authorization is time to get an access token::
curl -X POST -H "Cache-Control: no-cache" -H "Content-Type: application/x-www-form-urlencoded" "http://127.0.0.1:8000/o/token/" -d "client_id=${ID}" -d "client_secret=${SECRET}" -d "code=${CODE}" -d "code_verifier=${CODE_VERIFIER}" -d "redirect_uri=http://127.0.0.1:8000/noexist/callback" -d "grant_type=authorization_code"
To be more easy to visualize::
curl -X POST \
-H "Cache-Control: no-cache" \
-H "Content-Type: application/x-www-form-urlencoded" \
"http://127.0.0.1:8000/o/token/" \
-d "client_id=${ID}" \
-d "client_secret=${SECRET}" \
-d "code=${CODE}" \
-d "code_verifier=${CODE_VERIFIER}" \
-d "redirect_uri=http://127.0.0.1:8000/noexist/callback" \
-d "grant_type=authorization_code"
The OAuth2 provider will return the follow response:
.. code-block:: json
{
"access_token": "jooqrnOrNa0BrNWlg68u9sl6SkdFZg",
"expires_in": 36000,
"token_type": "Bearer",
"scope": "read write",
"refresh_token": "HNvDQjjsnvDySaK0miwG4lttJEl9yD"
}
To access the user resources we just use the ``access_token``::
curl \
-H "Authorization: Bearer jooqrnOrNa0BrNWlg68u9sl6SkdFZg" \
-X GET http://localhost:8000/resource
Client Credential
-----------------
The Client Credential grant is suitable for machine-to-machine authentication. You authorize your own service or worker to change a bank account transaction status to accepted.
Point your browser to http://127.0.0.1:8000/o/applications/register/ lets create an application.
Fill the form as show in the screenshot below, and before saving take note of ``Client id`` and ``Client secret`` we will use it in a minute.
.. image:: _images/application-register-client-credential.png
:alt: Client credential application registration
Export ``Client id`` and ``Client secret`` values as environment variable:
.. code-block:: sh
export ID=axXSSBVuvOyGVzh4PurvKaq5MHXMm7FtrHgDMi4u
export SECRET=1fuv5WVfR7A5BlF0o155H7s5bLgXlwWLhi3Y7pdJ9aJuCdl0XV5Cxgd0tri7nSzC80qyrovh8qFXFHgFAAc0ldPNn5ZYLanxSm1SI1rxlRrWUP591wpHDGa3pSpB6dCZ
The Client Credential flow is simpler than the Authorization Code flow.
We need to encode ``client_id`` and ``client_secret`` as HTTP base authentication encoded in ``base64`` I use the following code to do that.
.. code-block:: python
>>> import base64
>>> client_id = "axXSSBVuvOyGVzh4PurvKaq5MHXMm7FtrHgDMi4u"
>>> secret = "1fuv5WVfR7A5BlF0o155H7s5bLgXlwWLhi3Y7pdJ9aJuCdl0XV5Cxgd0tri7nSzC80qyrovh8qFXFHgFAAc0ldPNn5ZYLanxSm1SI1rxlRrWUP591wpHDGa3pSpB6dCZ"
>>> credential = "{0}:{1}".format(client_id, secret)
>>> base64.b64encode(credential.encode("utf-8"))
b'YXhYU1NCVnV2T3lHVnpoNFB1cnZLYXE1TUhYTW03RnRySGdETWk0dToxZnV2NVdWZlI3QTVCbEYwbzE1NUg3czViTGdYbHdXTGhpM1k3cGRKOWFKdUNkbDBYVjVDeGdkMHRyaTduU3pDODBxeXJvdmg4cUZYRkhnRkFBYzBsZFBObjVaWUxhbnhTbTFTSTFyeGxScldVUDU5MXdwSERHYTNwU3BCNmRDWg=='
>>>
Export the credential as an environment variable
.. code-block:: sh
export CREDENTIAL=YXhYU1NCVnV2T3lHVnpoNFB1cnZLYXE1TUhYTW03RnRySGdETWk0dToxZnV2NVdWZlI3QTVCbEYwbzE1NUg3czViTGdYbHdXTGhpM1k3cGRKOWFKdUNkbDBYVjVDeGdkMHRyaTduU3pDODBxeXJvdmg4cUZYRkhnRkFBYzBsZFBObjVaWUxhbnhTbTFTSTFyeGxScldVUDU5MXdwSERHYTNwU3BCNmRDWg==
To start the Client Credential flow you call ``/token/`` endpoint directly::
curl -X POST -H "Authorization: Basic ${CREDENTIAL}" -H "Cache-Control: no-cache" -H "Content-Type: application/x-www-form-urlencoded" "http://127.0.0.1:8000/o/token/" -d "grant_type=client_credentials"
To be easier to visualize::
curl -X POST \
-H "Authorization: Basic ${CREDENTIAL}" \
-H "Cache-Control: no-cache" \
-H "Content-Type: application/x-www-form-urlencoded" \
"http://127.0.0.1:8000/o/token/" \
-d "grant_type=client_credentials"
The OAuth2 provider will return the following response:
.. code-block:: json
{
"access_token": "PaZDOD5UwzbGOFsQr34LQ7JUYOj3yK",
"expires_in": 36000,
"token_type": "Bearer",
"scope": "read write"
}
Next step is :doc:`first tutorial <tutorial/tutorial_01>`.
.. _Django website: https://www.djangoproject.com/
.. _Whitson Gordon: https://en.wikipedia.org/wiki/OAuth#cite_note-1
.. _User: https://docs.djangoproject.com/en/3.0/ref/contrib/auth/#django.contrib.auth.models.User
.. _Django documentation: https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project
.. _RFC6749: https://rfc-editor.org/rfc/rfc6749.html#section-1.3
.. _Grant Types: https://oauth.net/2/grant-types/
.. _URL: http://127.0.0.1:8000/o/authorize/?response_type=code&client_id=vW1RcAl7Mb0d5gyHNQIAcH110lWoOW2BmWJIero8&redirect_uri=http://127.0.0.1:8000/noexist/callback
|