Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add aws_identity grant_type for getting access tokens #12

Merged
merged 7 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.12"
# You can also specify other tool versions:
# nodejs: "20"
# rust: "1.70"
# golang: "1.20"

# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
# fail_on_warning: true

# Optionally build your docs in additional formats such as PDF and ePub
# formats:
# - pdf
# - epub

# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
# python:
# install:
# - requirements: docs/requirements.txt
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
ARG PYVERSION=3.9.19-bullseye

FROM python:${PYVERSION} AS dev

WORKDIR /app

COPY requirements.txt /app/

RUN apt-get update \
&& apt-get install -q -y \
jq \
&& apt-get clean

RUN pip install -r requirements.txt

FROM dev as prod

COPY ./ /app/


10 changes: 5 additions & 5 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ pipeline {
}
stage('Test'){
parallel {
stage('Unit Test Django 3.0'){
steps {
sh 'toxtest/bin/tox -e py3.8-django{3.0}'
}
}
stage('Unit Test Django 3.1'){
steps {
sh 'toxtest/bin/tox -e py3.8-django{3.1}'
Expand All @@ -36,6 +31,11 @@ pipeline {
sh 'toxtest/bin/tox -e py3.8-django{4.1}'
}
}
stage('Unit Test Django 4.2'){
steps {
sh 'toxtest/bin/tox -e py3.8-django{4.2}'
}
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,10 @@ License
=======

*django-oauth2* is a fork of *django-oauth2-provider* which is released under the MIT License. Please see the LICENSE file for details.


Packaging
=========

$ python -m build

68 changes: 68 additions & 0 deletions aws_identity_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import os
import sys
import json

from datetime import datetime
from urllib import request, error
import requests

import boto3
# aws-v4-signature==2.0
from awsv4sign import generate_http11_header

service = 'sts'
region = 'us-west-2'

session = boto3.Session()
creds = session.get_credentials()
access_key = creds.access_key
secret_key = creds.secret_key
session_token = creds.token

print(f"access_key: {access_key[:10]}<redacted...>")
print(f"secret_key: {secret_key[:10]}<redacted...>")
print(f"session_token: {session_token[:20]}<redacted...>")
print(f"profile: {os.environ.get('AWS_PROFILE')}")

url = 'https://sts.{region}.amazonaws.com/'.format(region=region)
httpMethod = 'post'
canonicalHeaders = {
'host': f'sts.{region}.amazonaws.com',
'x-amz-date': datetime.utcnow().strftime('%Y%m%dT%H%M%SZ'),
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
}
if session_token:
canonicalHeaders['x-amz-security-token'] = session_token

payload_str = "Action=GetCallerIdentity&Version=2011-06-15"

headers = generate_http11_header(
service, region, access_key, secret_key,
url, 'post', canonicalHeaders, {},
'', payload_str
)

token_request_args = {
"grant_type": "aws_identity",
"region": region,
"post_body": payload_str,
"headers_json": json.dumps(headers),
}
print(payload_str)
print(json.dumps(headers, indent=4))

req = request.Request("https://sts.us-west-2.amazonaws.com/", data=payload_str.encode('utf-8'), headers=headers, method='POST')
try:
response = request.urlopen(req)
print(f"Local request test result: {response.read()}")
except error.HTTPError as e:
print(f"HTTPError: {e}: {e.fp.read()}")
sys.exit(1)

print("Attempting access_token grant request with same signed request:\n")

token_response = requests.post("http://localhost:8000/oauth2/access_token",
data=token_request_args)
token_info = token_response.json()

print(token_info)
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

services:
test:
build:
context: .
target: dev
user: ${UID}
volumes:
- ${WORKSPACE:-.}:/app
environment:
- DJANGO_SETTINGS_MODULE=tests.settings

web:
build:
context: .
target: dev
user: ${UID}
volumes:
- ${WORKSPACE:-.}:/app
ports:
- "8000:8000"
environment:
- DJANGO_SETTINGS_MODULE=tests.settings
# entrypoint: [ "python3", "manage.py", "runserver" ]
9 changes: 9 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
v 4.1
-----
* Add aws_identity grant_type
* Update for Django 3.1-4.2

v 4.0
-----
* Update for Django 3.0-4.1

v 2.4
-----
* Add HTTP Authorization Bearer token support to Oauth2UserMiddleware
Expand Down
23 changes: 22 additions & 1 deletion docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Add :attr:`provider.oauth2.urls` to your root ``urls.py`` file.

::

url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')),
path('oauth2/', include(('provider.oauth2.urls', 'oauth2'))),


.. note:: The namespace argument is required.
Expand Down Expand Up @@ -92,6 +92,27 @@ in :rfc:`4`.
.. note:: Remember that you should always use HTTPS for all your OAuth
2 requests otherwise you won't be secured.

Request an Access Token using AWS credentials
---------------------------------------------

The new aws_identity grant_type uses the parameters for a signed GetCallerIdentity
request to prove the caller's identity.

Your client needs to submit a :attr:`POST` request to
:attr:`/oauth2/access_token` including the following parameters:

* ``region`` - AWS Region
* ``post_body`` - The post body used for signing the request. Usually ``Action=GetCallerIdentity&Version=2011-06-15``
* ``headers_json`` - The headers produced by the AWSv4 signing process

The region value is used to produce the standard https://sts.(region).amazonaws.com/ url used to
make the GetCallerIdentity request. The URL is generated server side to reduce the risk of an
attack based on sending an improperly crafted full URL.

The aws-v4-signature library implements awsv4sign.generate_http11_header(). An example is
presented in the root of the repository in aws_identity_examply.py.


Integrate with Django Authentication
####################################

Expand Down
2 changes: 1 addition & 1 deletion provider/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = "4.0"
__version__ = "4.2"
# The major version is expected to follow the current django major version:q
6 changes: 6 additions & 0 deletions provider/oauth2/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ class AuthorizedClientAdmin(admin.ModelAdmin):
raw_id_fields = ('user',)


class AwsAccountAdmin(admin.ModelAdmin):
list_display = ('arn', 'client', 'max_token_lifetime')
raw_id_fields = ('acting_user',)


admin.site.register(models.AccessToken, AccessTokenAdmin)
admin.site.register(models.Grant, GrantAdmin)
admin.site.register(models.Client, ClientAdmin)
admin.site.register(models.AuthorizedClient, AuthorizedClientAdmin)
admin.site.register(models.AwsAccount, AwsAccountAdmin)
admin.site.register(models.RefreshToken)
admin.site.register(models.Scope)
3 changes: 3 additions & 0 deletions provider/oauth2/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ class Oauth2(AppConfig):
name = 'provider.oauth2'
label = 'oauth2'
verbose_name = "Provider Oauth2"

def ready(self):
import provider.oauth2.signals
32 changes: 32 additions & 0 deletions provider/oauth2/fixtures/test_oauth2.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,24 @@
"model": "auth.user",
"pk": 2
},
{
"fields": {
"date_joined": "2012-01-23 05:53:31",
"email": "",
"first_name": "",
"groups": [],
"is_active": true,
"is_staff": false,
"is_superuser": false,
"last_login": "2012-01-23 05:53:31",
"last_name": "",
"password": "sha1$0cf1b$d66589690edd96b410170fcae5cc2bdfb68821e7",
"user_permissions": [],
"username": "test-user-aws"
},
"model": "auth.user",
"pk": 3
},
{
"fields": {
"name": "basic",
Expand All @@ -88,5 +106,19 @@
},
"model": "oauth2.scope",
"pk": 2
},
{
"fields": {
"arn": "arn:aws:iam::123456789012:role/testrole",
"account_id": "123456789012",
"name": "testrole",
"general_type": "role",
"client": 2,
"autoprovision_user": false,
"acting_user": 3,
"scope": ["basic", "advanced"]
},
"model": "oauth2.awsaccount",
"pk": 1
}
]
52 changes: 49 additions & 3 deletions provider/oauth2/forms.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
from six import string_types
import logging
from io import StringIO
from urllib import request
from urllib.error import HTTPError
from xml.etree import ElementTree

from django import forms
from django.contrib.auth import authenticate
from django.conf import settings
from django.utils.translation import gettext as _
from django.utils import timezone
from provider.constants import RESPONSE_TYPE_CHOICES, SCOPES, PUBLIC
from provider.forms import OAuthForm, OAuthValidationError
from provider.utils import now
from provider.utils import now, ArnHelper
from provider.oauth2.models import Client, Grant, RefreshToken, Scope

log = logging.getLogger('provider.oauth2')

DEFAULT_SCOPE = getattr(settings, 'OAUTH2_DEFAULT_SCOPE', 'read')

Expand Down Expand Up @@ -53,7 +59,7 @@ class ScopeModelChoiceField(forms.ModelMultipleChoiceField):
# widget = forms.TextInput

def to_python(self, value):
if isinstance(value, string_types):
if isinstance(value, str):
return [s for s in value.split(' ') if s != '']
elif isinstance(value, list):
value_list = list()
Expand Down Expand Up @@ -311,6 +317,46 @@ def clean(self):
return data


class AwsGrantForm(OAuthForm):
grant_type = forms.CharField(required=True)
region = forms.CharField(required=True)
post_body = forms.CharField(required=True)
headers_json = forms.JSONField(required=True)

def clean_grant_type(self):
grant_type = self.cleaned_data.get('grant_type')

if grant_type != 'aws_identity':
raise OAuthValidationError({'error': 'invalid_grant'})

return grant_type

def clean(self):
region = self.cleaned_data['region']

sts_url = f"https://sts.{region}.amazonaws.com/"

post_body = self.cleaned_data['post_body']
headers_json = self.cleaned_data['headers_json']

req = request.Request(sts_url, data=post_body.encode('utf-8'), headers=headers_json, method='POST')
try:
response = request.urlopen(req)
except HTTPError as e:
log.info("Error calling GetCallerIdentity for aws_identity grant: %s", e)
raise OAuthValidationError({'error': 'invalid_grant'})

xmldata = response.read()

et = ElementTree.parse(StringIO(xmldata.decode('utf-8')))
root = et.getroot()
result = root.find('{https://sts.amazonaws.com/doc/2011-06-15/}GetCallerIdentityResult')
caller_arn = result.find('{https://sts.amazonaws.com/doc/2011-06-15/}Arn').text
self.cleaned_data['arn_string'] = caller_arn
self.cleaned_data['arn'] = ArnHelper(caller_arn)
return self.cleaned_data


class PublicClientForm(OAuthForm):
client_id = forms.CharField(required=True)
grant_type = forms.CharField(required=True)
Expand Down
Loading