Skip to content

Commit

Permalink
feat: implement OAuth 2 provider
Browse files Browse the repository at this point in the history
This commit uses the Django OAuth toolkit to add an OAuth provider.
Django REST Framework and Graphene have both been linked up to this
provider.

This lays down groundwork for access to APIs via OAuth2.
  • Loading branch information
ngurenyaga committed Nov 8, 2021
1 parent 7d8d75d commit dc37a57
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 41 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,17 @@ Deployment

This application is deployed via Google Cloud Build ( <https://cloud.google.com/build> ) to Google Cloud Run ( <https://cloud.google.com/run> ).
There's a `cloudbuild.yaml` file in the home folder. Secrets (e.g production settings) are managed with Google Secret Manager ( <https://cloud.google.com/secret-manager> ).

Changing OAuth Token Behavior
------------------------------
The following environment variables are available:

```python
ACCESS_TOKEN_EXPIRE_SECONDS = env.int("ACCESS_TOKEN_EXPIRE_SECONDS", default=3600)
ALLOWED_REDIRECT_URI_SCHEMES = env.list("ALLOWED_REDIRECT_URI_SCHEMES", default=["http", "https"])
AUTHORIZATION_CODE_EXPIRE_SECONDS = env.int("AUTHORIZATION_CODE_EXPIRE_SECONDS", default=600)
REFRESH_TOKEN_EXPIRE_SECONDS = env.int("REFRESH_TOKEN_EXPIRE_SECONDS", default=3600)
REFRESH_TOKEN_GRACE_PERIOD_SECONDS = env.int("REFRESH_TOKEN_GRACE_PERIOD_SECONDS", default=600)
```

The indicated defaults can be overidden during deployment by setting those variables.
24 changes: 24 additions & 0 deletions config/graphql_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import rest_framework
from graphene_django.views import GraphQLView
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.settings import api_settings


class DRFAuthenticatedGraphQLView(GraphQLView):
def parse_body(self, request):
if isinstance(request, rest_framework.request.Request):
return request.data
return super(DRFAuthenticatedGraphQLView, self).parse_body(request)

@classmethod
def as_view(cls, *args, **kwargs):
view = super(DRFAuthenticatedGraphQLView, cls).as_view(*args, **kwargs)
view = authentication_classes(api_settings.DEFAULT_AUTHENTICATION_CLASSES)(view)

# it's not possible to use stricter permissions e.g Django Model permissions
# because the GraphQL view cannot be associatded with one queryset
view = permission_classes((IsAuthenticated,))(view)

view = api_view(["GET", "POST"])(view)
return view
22 changes: 22 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"rest_framework_datatables",
"corsheaders",
"mjml",
"oauth2_provider",
"django.contrib.admin",
"graphene_django",
"wagtail.contrib.forms",
Expand Down Expand Up @@ -284,6 +285,7 @@
# ------------------------------------------------------------------------------
INSTALLED_APPS += ["compressor"]
STATICFILES_FINDERS += ["compressor.finders.CompressorFinder"]

# django-rest-framework
# -------------------------------------------------------------------------------
REST_FRAMEWORK = {
Expand Down Expand Up @@ -314,6 +316,7 @@
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
),
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoModelPermissions",),
"DEFAULT_METADATA_CLASS": "rest_framework.metadata.SimpleMetadata",
Expand All @@ -327,6 +330,25 @@
"TIME_FORMAT": "iso-8601",
}

# OAuth
OAUTH2_PROVIDER = {
# using the default HS256 keys
# see https://django-oauth-toolkit.readthedocs.io/en/1.5.0/oidc.html#using-hs256-keys
"OIDC_ENABLED": True,
# this is the list of available scopes
"SCOPES": {
"read": "Read scope",
"write": "Write scope",
"openid": "OpenID Connect scope",
},
}

ACCESS_TOKEN_EXPIRE_SECONDS = env.int("ACCESS_TOKEN_EXPIRE_SECONDS", default=3600)
ALLOWED_REDIRECT_URI_SCHEMES = env.list("ALLOWED_REDIRECT_URI_SCHEMES", default=["http", "https"])
AUTHORIZATION_CODE_EXPIRE_SECONDS = env.int("AUTHORIZATION_CODE_EXPIRE_SECONDS", default=600)
REFRESH_TOKEN_EXPIRE_SECONDS = env.int("REFRESH_TOKEN_EXPIRE_SECONDS", default=3600)
REFRESH_TOKEN_GRACE_PERIOD_SECONDS = env.int("REFRESH_TOKEN_GRACE_PERIOD_SECONDS", default=600)

CORS_URLS_REGEX = r"^/api/.*$"

# Project specific settings
Expand Down
9 changes: 7 additions & 2 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
from django.views import defaults as default_views
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import RedirectView
from graphene_django.views import GraphQLView
from rest_framework.authtoken.views import obtain_auth_token
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls

from mycarehub.common.views import AboutView, HomeView

from .graphql_auth import DRFAuthenticatedGraphQLView

urlpatterns = [
path("", HomeView.as_view(), name="home"),
path(
Expand All @@ -33,7 +34,9 @@
r"^favicon\.ico$",
RedirectView.as_view(url=settings.STATIC_URL + "favicon.ico", permanent=True),
),
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True)), name="graphql"),
path(
"graphql", csrf_exempt(DRFAuthenticatedGraphQLView.as_view(graphiql=True)), name="graphql"
),
# content management
path("admin/", include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_urls)),
Expand All @@ -53,6 +56,8 @@
path("api/", include("config.api_router")),
# DRF auth token
path("auth-token/", obtain_auth_token),
# OAuth 2
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
]

if settings.DEBUG:
Expand Down
41 changes: 41 additions & 0 deletions mycarehub/templates/fragments/atoms/oauth_menu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% load static i18n compress%}

<li class="nav-item {% if active == " oauth-nav" %}active{% endif %}" id="oauth-nav">
<a class="nav-link collapsed" id="oauth-nav-link" href="#" data-toggle="collapse" data-target="#oauth-menu"
aria-expanded="true" aria-controls="oauth-menu">
<i class="fas fa-fw fa-lock"></i>
<span>OAuth</span>
</a>
<div id="oauth-menu" class="collapse {% if active == " oauth-nav" %}show{% endif %}"
data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
{% if request.user.is_superuser %}
<a class="collapse-item {% if selected == " applications" %}active {% endif %}" id="applications"
href="{% url 'oauth2_provider:list' %}">
Applications
</a>
{% endif %}

<a class="collapse-item {% if selected == " authorized-tokens" %}active {% endif %}"
id="authorized-tokens" href="{% url 'oauth2_provider:authorized-token-list' %}">
My Tokens
</a>
</div>
</div>
</li>

<script>
function collapseMenu() {
const facilities_nav_link = document.getElementById("oauth-nav-link");
facilities_nav_link.className = "nav-link collapsed";
const facilities_menu = document.getElementById("oauth-menu");
facilities_menu.className = "collapse";
}

const items = document.querySelectorAll('.collapse-item');
items.forEach(input => input.addEventListener('click', collapseMenu()));

document.addEventListener("DOMContentLoaded", () => {
collapseMenu()
});
</script>
3 changes: 3 additions & 0 deletions mycarehub/templates/fragments/head.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/themes/default/style.min.css" />
{% endblock css %}

{% block extrastyles %}
{% endblock extrastyles %}

{% block javascript %}
{% compress js %}
<script defer src="{% static 'js/vendors.min.js' %}"></script>
Expand Down
1 change: 1 addition & 0 deletions mycarehub/templates/fragments/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<hr class="sidebar-divider">
<div class="sidebar-heading">Operations</div>
{% include "fragments/atoms/facilities_menu.html" %}
{% include "fragments/atoms/oauth_menu.html" %}
<hr class="sidebar-divider">
<hr class="sidebar-divider">
{% include "fragments//atoms/sidebar_toggle.html" %}
Expand Down
29 changes: 29 additions & 0 deletions mycarehub/templates/oauth2_provider/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{% extends "base.html" %}

{% block extrastyles %}
<style>
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}

.block-center {
max-width: 500px;
padding: 19px 29px 29px;
margin: 0 auto 20px;
background-color: #fff;
border: 1px solid #e5e5e5;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
}

.block-center .block-center-heading {
margin-bottom: 10px;
}
</style>
{% endblock extrastyles %}
39 changes: 0 additions & 39 deletions mycarehub/templates/pages/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,6 @@ <h1 class="h3 mb-0 text-gray-800">Welcome, {% firstof user.name user.username us

<!-- Content Row -->
<div class="row">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Open Tickets
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{open_ticket_count}}
</div>
</div>
<div class="col-auto">
<i class="fas fa-clipboard-list fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>

<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
Expand All @@ -48,26 +29,6 @@ <h1 class="h3 mb-0 text-gray-800">Welcome, {% firstof user.name user.username us
</div>
</div>

<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Appointments, Month-To-Date
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{appointments_mtd}}
</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar-check fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>

<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
Expand Down
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ django-mjml[requests]~=0.11.0
django-taggit~=1.5.1
django-modelcluster~=5.2
django-model-utils~=4.2.0 # https://github.com/jazzband/django-model-utils
django-oauth-toolkit~=1.5.0
django-phonenumber-field~=5.2.0
django-storages[google]~=1.12.3 # https://github.com/jschneier/django-storages
djangorestframework-datatables~=0.6.0
Expand Down

0 comments on commit dc37a57

Please sign in to comment.