From 6256c340c9e5545b18c310d1068035c67ee84319 Mon Sep 17 00:00:00 2001 From: mahdavi Date: Thu, 8 Sep 2022 15:22:50 +0430 Subject: [PATCH] bugfix in oauth toolkit --- accounts/settings.py | 1 + apps/gooyal_oauth2/validators.py | 51 +++++++ apps/users/constans.py | 10 -- apps/users/models.py | 2 +- apps/wallet/constans.py | 11 ++ apps/wallet/models.py | 2 +- apps/wallet/views.py | 25 +++- client.py | 190 +++++++++++++++++++++++++ run.sh | 12 +- templates/wallet/transaction_list.html | 6 +- 10 files changed, 288 insertions(+), 22 deletions(-) create mode 100644 apps/wallet/constans.py create mode 100644 client.py diff --git a/accounts/settings.py b/accounts/settings.py index 71793a4..016ef68 100644 --- a/accounts/settings.py +++ b/accounts/settings.py @@ -195,6 +195,7 @@ STATIC_URL = '/static/' # CLIENT_SECRET = config('CLIENT_SECRET') CELERY_BROKER_URL = config('CELERY_BROKER_URL') + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') USE_X_FORWARDED_HOST = True USE_X_FORWARDED_PORT = True \ No newline at end of file diff --git a/apps/gooyal_oauth2/validators.py b/apps/gooyal_oauth2/validators.py index b50511d..3897d48 100755 --- a/apps/gooyal_oauth2/validators.py +++ b/apps/gooyal_oauth2/validators.py @@ -1,6 +1,8 @@ import base64 +import binascii import logging from datetime import datetime, timedelta +from urllib.parse import unquote_plus import requests # import service_clients @@ -9,6 +11,7 @@ from django.utils.timezone import make_aware from oauth2_provider.models import get_access_token_model from oauth2_provider.oauth2_validators import OAuth2Validator as BaseOAuth2Validator from .settings import oauth2_settings +from django.conf import settings log = logging.getLogger("oauth2_provider") @@ -208,3 +211,51 @@ class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223 # else: # self._set_oauth2_error_on_request(request, access_token, scopes) # return False + + def _authenticate_basic_auth(self, request): + """ + Authenticates with HTTP Basic Auth. + + Note: as stated in rfc:`2.3.1`, client_id and client_secret must be encoded with + "application/x-www-form-urlencoded" encoding algorithm. + """ + auth_string = self._extract_basic_auth(request) + if not auth_string: + return False + + try: + encoding = request.encoding or settings.DEFAULT_CHARSET or "utf-8" + except AttributeError: + encoding = "utf-8" + + try: + b64_decoded = base64.b64decode(auth_string) + except (TypeError, binascii.Error): + log.debug("Failed basic auth: %r can't be decoded as base64", auth_string) + return False + + try: + auth_string_decoded = b64_decoded.decode(encoding) + except UnicodeDecodeError: + log.debug("Failed basic auth: %r can't be decoded as unicode by %r", auth_string, encoding) + return False + + try: + client_id, client_secret = map(unquote_plus, auth_string_decoded.split(":", 1)) + except ValueError: + log.debug("Failed basic auth, Invalid base64 encoding.") + return False + + if self._load_application(client_id, request) is None: + log.debug("Failed basic auth: Application %s does not exist" % client_id) + return False + elif request.client.client_id != client_id: + log.debug("Failed basic auth: wrong client id %s" % client_id) + return False + + # TODO: check why not work + elif not client_secret == request.client.client_secret: + log.debug("Failed basic auth: wrong client secret %s" % client_secret) + return False + else: + return True diff --git a/apps/users/constans.py b/apps/users/constans.py index 4e13669..dce005e 100644 --- a/apps/users/constans.py +++ b/apps/users/constans.py @@ -2,13 +2,3 @@ from model_utils.choices import Choices MAX_OTP_TRY = 3 DEVELOPMENT_PHONE_NUMBERS = ['+989999999999', '+989999999998'] - -STATE_CHOICES = Choices( - (1, 'created', 'created'), - (2, 'delayed', 'delayed'), # delayed as user wish - (3, 'pending', 'pending'), # wait for external service response - (4, 'incomplete', 'incomplete'), # started and wait for internal progress to complete - (5, 'success', 'success'), - (6, 'failed', 'failed'), - (7, 'expected_failure', 'expected_failure'), # no exact data available but guessed to be failed - ) \ No newline at end of file diff --git a/apps/users/models.py b/apps/users/models.py index ea49b61..78cb7e0 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -190,7 +190,7 @@ class User(AbstractUser): ) def __str__(self): - return f"{self.pk} - {self.username}" + return self.name or self.get_full_name() or self.username or self.phone_number or self.email or _('no name') def create_user_in_introspection(token, content): diff --git a/apps/wallet/constans.py b/apps/wallet/constans.py new file mode 100644 index 0000000..23ea495 --- /dev/null +++ b/apps/wallet/constans.py @@ -0,0 +1,11 @@ +from model_utils.choices import Choices + +STATE_CHOICES = Choices( + (1, 'created', 'created'), + (2, 'delayed', 'delayed'), # delayed as user wish + (3, 'pending', 'pending'), # wait for external service response + (4, 'incomplete', 'incomplete'), # started and wait for internal progress to complete + (5, 'success', 'success'), + (6, 'failed', 'failed'), + (7, 'expected_failure', 'expected_failure'), # no exact data available but guessed to be failed + ) \ No newline at end of file diff --git a/apps/wallet/models.py b/apps/wallet/models.py index 66e2bc2..98cd25e 100755 --- a/apps/wallet/models.py +++ b/apps/wallet/models.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from model_utils.choices import Choices from rest_framework.exceptions import APIException, ValidationError -from apps.users.constans import STATE_CHOICES +from .constans import STATE_CHOICES from apps.users.models import User diff --git a/apps/wallet/views.py b/apps/wallet/views.py index 18caa70..0021160 100755 --- a/apps/wallet/views.py +++ b/apps/wallet/views.py @@ -10,7 +10,7 @@ from rest_framework.response import Response from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements from apps.wallet.models import Transaction from apps.wallet.serializers import TransactionSerializer, DepositSerializer, WithdrawSerializer -from apps.users.constans import STATE_CHOICES +from .constans import STATE_CHOICES def get_application_client_id(request): @@ -24,7 +24,25 @@ def get_application_client_id(request): application_client_id = None return application_client_id +# TODO: this is user invoice create view +# class TransactionList(generics.ListCreateAPIView): +# permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] +# serializer_class = TransactionSerializer +# required_alternate_scopes = { +# "GET": [['wallet.transaction:list']], +# "POST": [['wallet.invoice:create']], +# } +# +# def get_queryset(self): +# user = self.request.user +# return Transaction.objects.filter(Q(payee=user) | Q(payer=user)).all() +# +# def perform_create(self, serializer): +# user = self.request.user +# serializer.save(payee=user, application_client_id=get_application_client_id(self.request)) + +# this is application create invoice view class TransactionList(generics.ListCreateAPIView): permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] serializer_class = TransactionSerializer @@ -38,8 +56,9 @@ class TransactionList(generics.ListCreateAPIView): return Transaction.objects.filter(Q(payee=user) | Q(payer=user)).all() def perform_create(self, serializer): - user = self.request.user - serializer.save(payee=user, application_client_id=get_application_client_id(self.request)) + payer = self.request.user + payee = self.request.auth.application.user + serializer.save(payer=payer, payee=payee, application_client_id=get_application_client_id(self.request)) class TransactionDetail(generics.RetrieveAPIView): diff --git a/client.py b/client.py new file mode 100644 index 0000000..242b65f --- /dev/null +++ b/client.py @@ -0,0 +1,190 @@ +import json +import requests + +OAUTH_CLIENT_ID = 'qHhNuALPINvJ8UHAcBGsX1uVRWu3AmRMwRL5kVbi' +# OAUTH_CLIENT_SECRET = 'pbkdf2_sha256$390000$A8ru6iwhcjU1wEgPL6n6b8$GOhCcrfqw4Xkw6USpuEL6esjmNgqJe1A8q1Ec2dLxhw=' +OAUTH_CLIENT_SECRET = 'pbkdf2_sha256$390000$A8ru6iwhcjU1wEgPL6n6b8$GOhCcrfqw4Xkw6USpuEL6esjmNgqJe1A8q1Ec2dLxhw=' +API_URI = 'http://127.0.0.1:8000' +# API_URI = 'https://accounts.gooyal.com' +# NOTIFICATION_URI = 'https://notifications.gooyal.com' +# NOTIFICATION_URI = 'http://127.0.0.1:8001' + + +class ApiClient(): + def __init__(self, phone_number, auth_data=None): + self.auth_data = {} + if auth_data: + self.auth_data = auth_data + + self.phone_number = phone_number + + def login(self, password): + phone_number = self.phone_number + + data = { + "grant_type": "password", + "username": phone_number, + "password": password, + # "scope": 'introspection', + "scope": 'introspection accounts.profile:list accounts.profile:retrieve accounts.account:retrieve accounts.account:update accounts.profile:inquiry accounts.account:change_password wallet.transaction:list wallet.invoice:create wallet.transaction:retrieve wallet.invoice:pay wallet.invoice:receipt wallet.deposit:submit wallet.deposit:verify wallet.withdraw:submit wallet.withdraw:verify ', + "auth_fields": 'phone_number:otp' + } + auth = (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET) + + response = requests.post(f'{API_URI}/oauth2/token/', + data=data, + auth=auth) + + auth_data = response.json() + if 'access_token' in auth_data: + self.auth_data = auth_data + + return auth_data + + def _request(self, path, data=None, files=None, method='get', with_auth=True, encode=True): + if files: + header = {} + else: + header = { + "Content-Type": "application/json", + "charset": "utf-8" + } + if with_auth: + access_token = self.auth_data.get('access_token', '') + header.setdefault("Authorization", f"Bearer {access_token}", ) + + if data and encode: + data = json.dumps(data) + + response = requests.request(method, f'{API_URI}/{path}', headers=header, data=data, files=files) + try: + return response.json() + except: + return response.content.decode() + + def request_otp(self): + phone_number = self.phone_number + + path = 'users/api/request_otp/' + method = 'post' + data = { + 'phone_number': phone_number + } + result = self._request(path=path, data=data, method=method, with_auth=False) + return result + + def get_account(self): + path = 'users/api/account/' + result = self._request(path=path) + return result + + def update_account(self, **kwargs): + path = 'users/api/account/' + data = {} + fields = ( + 'username', + "first_name", + "last_name", + 'name', + 'iban', + 'iban_card_number', + 'iban_account_number', + ) + for key, value in kwargs.items(): + if key in fields: + data.setdefault(key, value) + + avatar = kwargs.get('avatar') + if avatar: + files = {'avatar': avatar} + encode = False + else: + files = None + encode = True + + result = self._request(path=path, data=data, method='put', files=files, encode=encode) + return result + + def change_password(self, old_password, new_password, old_password_gateway='otp'): + path = 'users/api/change_password/' + data = {'old_password': old_password, + 'new_password': new_password, + 'old_password_gateway': old_password_gateway + } + + result = self._request(path=path, data=data, method='put') + return result + + def get_user_profile(self, code): + path = f'users/api/users/{code}/' + result = self._request(path=path) + return result + + def get_transactions(self, page=None): + path = 'wallet/api/transactions/' + if page: + page = int(page) + path = f'{path}?page={page}' + + return self._request(path=path) + + def get_transaction(self, code): + path = f'transactions/{code}' + return self._request(path=path) + + # create user invoice + # def create_invoice(self, amount, delay, payer=None): + # data = { + # 'delay': delay, + # 'amount': amount + # } + # if payer: + # data.setdefault('payer', {'code': payer}) + # response = self._request('transactions/', method='post', data=data) + # return response and 'code' in response, response + + # create application invoice + def create_invoice(self, amount, delay): + data = { + 'delay': delay, + 'amount': amount + } + response = self._request('transactions/', method='post', data=data) + return response and 'code' in response, response + + def pay_transaction(self, code): + path = f'transactions/{code}/pay' + return self._request(path=path) + + def receipt_transaction(self, code): + path = f'transactions/{code}/receipt' + return self._request(path=path) + + def get_introspection(self): + path = 'oauth2/introspect/' + access_token = self.auth_data.get('access_token', '') + data = { + 'token': access_token, + } + return self._request(path=path, data=data) + + def notify(self): + data = { + 'phone_number': "+989106853582", + 'body': 'this is a test' + } + # data = json.dumps(data) + header = { + # "Content-Type": "application/json", + "charset": "utf-8" + } + access_token = self.auth_data.get('access_token', '') + # access_token = 'Px7WLUKoynua6qJq5IkIG8Fn5dPLXq' + header.setdefault("Authorization", f"Bearer {access_token}", ) + + response = requests.request('post', f'{NOTIFICATION_URI}/notifications/', headers=header, json=data) + try: + return response.json() + except: + return response.text + diff --git a/run.sh b/run.sh index 4682313..d2dcafd 100755 --- a/run.sh +++ b/run.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -while ! nc -z $DB_HOST 3306 ; do - echo "Waiting for the MySQL Server" - sleep 3 -done +#while ! nc -z $DB_HOST 3306 ; do +# echo "Waiting for the MySQL Server" +# sleep 3 +#done -python3 manage.py collectstatic -python3 manage.py migrate +#python3 manage.py collectstatic +#python3 manage.py migrate gunicorn accounts.wsgi:application --bind 0.0.0.0:8000 -w 4 \ No newline at end of file diff --git a/templates/wallet/transaction_list.html b/templates/wallet/transaction_list.html index 1c73cc9..4d0c458 100644 --- a/templates/wallet/transaction_list.html +++ b/templates/wallet/transaction_list.html @@ -2,7 +2,11 @@ {% block content %} {% for transaction in object_list %} - {{ transaction }} + {% if transaction.payee == request.user %} + دریافت مبلغ {{ transaction.amount }}از: {{ transaction.payer }} در وضعیت {{ transaction.get_state_display }} + {% else %} + پرداخت مبلغ {{ transaction.amount }} به: {{ transaction.payee }} + {% endif %} {% endfor %} {% endblock %} \ No newline at end of file