Middleware handler for Mobile and Browser API calls - django

I want to develop a Django based web service where my web application (Platform:Angular JS) and mobile applications (Platform: iOS,android,Windows Phone) will communicate with it.
My django web service app only for handling API calls. For security reasons I chose Oauth toolkit for mobile application,for web Session Authentication?
Settings.py for Authentication,
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'oauth2_provider.ext.rest_framework.OAuth2Authentication',
),
My problem:
I want to handle CSRF based authentication if I received a call from browser side.
Call Oauth authentication if I received call from Native Mobile client .
How to diferentiate the two, and how to handle this authentication technique.
Need your help!!

You can use django-mobile library to detect requests from mobile browsers and then call diferent authentication classes.
django-mobile library defines MobileDetectionMiddleware and SetFlavourMiddleware middleware classes in it.
MobileDetectionMiddleware detects if the requests originated from a mobile or a web browser.
SetFlavourMiddleware class sets the flavour attribute in the request. There are 2 possible values for flavour:
'mobile' # Mobile requests
'full' # Web requests
After the correct flavour is somehow chosen by the middlewares, it's assigned to the request.flavour attribute.
Step-1: Configure django-mobile in your application
Follow the steps given in https://github.com/gregmuellegger/django-mobile#installation to install and configure django-mobile in your application.
After it has been configured, you can just use request.flavour to check if request is from a mobile browser or a web browser.
Step-2: Create a custom WebSessionAuthentication class
Since you want to use OAuth's OAuth2Authenticationfor mobile application and DRF's SessionAuthentication for web, we can create a custom WebSessionAuthentication class which will inherit from the DRF's SessionAuthentication.
This WebSessionAuthentication class will perform no authentication for mobile requests. In case of web requests, it will perform proper session authentication with CSRF checks.
from rest_framework.authentication import SessionAuthentication
class WebSessionAuthentication(SessionAuthentication):
"""
Performs session authentication for web requests
"""
def authenticate(self, request):
"""
Returns a `User` if the request session currently has a logged in user
and request is a web request
Otherwise returns `None`.
"""
underlying_request = request._request # get the underlying HttpRequest object
if underlying_request.flavour == 'mobile': # check if mobile request
return None # No authentication performed for mobile requests
# For web requests perform DRF's original session authentication
return super(WebSessionAuthentication, self).authenticate(request)
Step-3: Create a custom MobileOAuth2Authentication class
MobileOAuth2Authentication class performs authentication for only mobile requests i.e. having .flavour as mobile. No authentication is performed for web requests.
from oauth2_provider.ext.rest_framework import OAuth2Authentication
class MobileOAuth2Authentication(OAuth2Authentication):
"""
Performs outh2 authentication for mobile requests
"""
def authenticate(self, request):
"""
Returns two-tuple of (user, token) if mobile authentication succeeds,
or None otherwise.
"""
underlying_request = request._request # get the underlying HttpRequest object
if underlying_request.flavour == 'full': # check if web request
return None # No authentication performed for web requests
# For mobile requests perform OAuth2's original authentication
return super(MobileOAuth2Authentication, self).authenticate(request)
Step-4: Define your authentication classes in settings
After creating custom WebSessionAuthentication and MobileOuth2Authentication authentication classes, define these authentication class in your project settings.
'DEFAULT_AUTHENTICATION_CLASSES': (
'my_app.authentication.WebSessionAuthentication', # custom session authentication class for web requests
'my_app.authentication.MobileOAuth2Authentication', # custom oauth2 authentication class for mobile requests
),

Related

How to use dj-rest-auth with many clients

I'd like to have many different clients be able to access my django website (more specifically its API) but I'm not sure how to do this with django-allauth, dj-rest-auth and simplejwt.
My current client app is using the built in django template engine and is set up with django-allauth for social authentication (Google etc). It's working using the documented installation recommendations.
I would now like to create different types of clients that aren't using the django template engine (e.g. Angular, Vue, flutter mobile etc) but I'm confused how dj-rest-auth is used so that it scales to support any number of client types.
Using Google social sign in as an example, when I create a new client, I have to register a new redirect_uri specific to that client.
To test this all out, I created a simple flask app with a single link so that I can retrieve a "code/access_token" before sending it to my Django app. The link is created using the following...
var codeRequestUrl =
`https://accounts.google.com/o/oauth2/v2/auth?\
scope=email&\
access_type=offline&\
include_granted_scopes=true&\
response_type=code&\
state=state_parameter_passthrough_value&\
redirect_uri=http%3A//127.0.0.1:5000/callback&\
client_id=${clientId}`;
...and the code is retrieved at the '/callback' endpoint in flask...
#app.route("/callback", methods=['GET'])
def redirect():
code = request.args.get('code', '')
req = requests.post('http://127.0.0.1:8000/api/dj-rest-auth/google/', data={'code':code})
return "done..."
...from where I send an x-www-form-urlencoded POST request back to a dj-rest-auth endpoint that is set up as per its documentation...
class GoogleLogin(SocialLoginView):
callback_url = 'http://127.0.0.1:5000/callback'
adapter_class = GoogleOAuth2Adapter
client_class = OAuth2Client
...
urlpatterns += [
...
path('dj-rest-auth/google/', GoogleLogin.as_view(), name='google_login'),
....
]
Django then successfully returns an access_token, refresh_token and some info about the logged in user.
But this isn't something that scales well. If I were to also create an Angular client, I'd need to register a different callback (because the Angular client would be running on a different port and/or address, and I'd also need another path set up in urls.py and associate it with a new SocialLoginView subclass that can handle the different callback_url (redirect_uri).
And with all this in mind, I have no idea how to do all of this with a flutter mobile app, which as far as I'm aware, has no concept of a callback_url, so I'm not sure how making a POST request to .../dj-rest-auth/google/ would even work given that I'd instantly get a redirect_uri_mismatch error.
Have I got it backwards and the client registered at Google is the Angular, Vue, Flash etc app? That would mean that each client would have to handle its own client_id and client_secret, which then seems to bypass django-allauth's and dj-rest-auth's functionality.
I feel like I'm misinterpreting this, so I would really appreciate some suggestions.
I feel confident enough to answer my own question.
In short, yes, multiple clients (including thirdparty) is a reasonably straight forward process. Unfortunately a lot of the blog posts and tutorials that exist take the perspective of a 'second party' client, which really confuses things. The result is a lot of error messages relating to the redirect_uri.
To their credit, the Google docs for their example Flask app was exactly what I needed, but there are a couple of observations that are really important, and what caused so much confusion for me.
First, and most important, the callback (redirect_uri) is not needed in Django at all. In Django, something like this is all that is required.
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
class GoogleLogin(SocialLoginView):
adapter_class = GoogleOAuth2Adapter
client_class = OAuth2Client
urlpatterns += [
...
path('auth/google/', GoogleLogin.as_view(), name='google_login'),
...
]
So no callback attribute is required. The reason for this is that the Flask (or thirdparty app) handles all of the Google side authentication.
The second observation was that the redirect_uri in the Flask app seemed have have to be the same for both the "code" request step, and the "access_token" step.
You can see it in the linked example where the oauth2callback function (which handles the redirect_uri), but I've modified for use with dj-rest-auth
#app.route('/')
def index():
if 'credentials' not in flask.session:
return flask.redirect(flask.url_for('oauth2callback'))
credentials = json.loads(flask.session['credentials'])
if credentials['expires_in'] <= 0:
return flask.redirect(flask.url_for('oauth2callback'))
else:
data = {'access_token': credentials['access_token']}
headers = headers = {'Content-Type': 'application/x-www-form-urlencoded'}
r = requests.post(f'{URL_ROOT}/api/auth/google/', data=data, headers=headers)
response_json = json.loads(r.text)
access_token = response_json['access_token'] # JWT Access Token
refresh_token = response_json['refresh_token']
# Make a query to your Django website
headers = headers = {'Authorization': f'Bearer {access_token}'}
r = requests.post(f'{URL_ROOT}/api/object/{OBJECT_ID}/action/', data=data, headers=headers)
# do stuff with r
#app.route('/oauth2callback')
def oauth2callback():
if 'code' not in flask.request.args:
auth_uri = ('https://accounts.google.com/o/oauth2/v2/auth?response_type=code'
'&client_id={}&redirect_uri={}&scope={}').format(CLIENT_ID, REDIRECT_URI, SCOPE)
return flask.redirect(auth_uri)
else:
auth_code = flask.request.args.get('code')
data = {'code': auth_code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': REDIRECT_URI,
'grant_type': 'authorization_code'}
r = requests.post('https://oauth2.googleapis.com/token', data=data)
flask.session['credentials'] = r.text # This has the access_token
return flask.redirect(flask.url_for('index'))
So in summary, it's a bit like this:
On the index/home page, the user presses a "Login" html anchor that points to /login.
Flask doesnt have any credentials, so redirects to /oauth2callback to begin authentication.
First, the "code" is retrieved using Googles' GET /auth endpoint, and by using your app's client id.
The redirect_uri ensures the code flow goes back to itself, but this time, with the "code" now know, do a POST request to Google's /token endpoint using your apps's client id and client secret. Again, the redirect_uri is the same (/oauth2callback).
Now that Googles's access_token is known, the Flask app redirects back to /index (although it could be anywhere at this point)
Back in /index, the Flask app now has Google's "access_token". Use that to log into Django's dj-rest-auth endpoint that you created.
Django will then return its own access_token and refresh_token, so continue to use those as needed.
I hope this helps.
Note that your flask app will need to be registered as a new Web App with Google's OAuth2 console (so it has it's own client id and client secret). In other words, don't reuse what you may have already created with an existing Django allauth implementation (which was my scenario). Each thirdparty app maker will handle their own OAuth2 credentials.

OIDC with Keycloak and Django - Missing States

I'm attempting to set up OIDC with Keycloak as my IdP and Django (using Django Rest Framework and mozilla-django-oidc) as my client server. I have got keycloak installed and a rough Django application stood up that successfully redirects to keycloak where I can then successfully authenticate (to keycloak), but when I'm redirected back to django I'm missing information, specifically oidc_states.
The redirect to django triggers this log entry:
[12/Oct/2021 08:28:06] "GET /api/oidc/callback/?state=QGsO26esqdtHZcsfRfYoXvUy0QWcGdZv&session_state=493887a4-600e-4dd2-aaaf-4134ea671c9a&code=dfe1573e-cf8e-4829-8874-a3500ba63712.493887a4-600e-4dd2-aaaf-4134ea671c9a.c1dfdb46-140c-4ccd-8308-6db3b468346a HTTP/1.1" 302 0
This contains three keys: state, session_state, and code.
The default callback view provided by mozilla-django-oidc contains this:
def get(self, request):
"""Callback handler for OIDC authorization code flow"""
if request.GET.get('error'):
if request.user.is_authenticated:
auth.logout(request)
assert not request.user.is_authenticated
elif 'code' in request.GET and 'state' in request.GET:
if 'oidc_states' not in request.session:
return self.login_failure()
# ...
Because keycloak isn't making the redirect with oidc_states added, this is immediately failing and I haven't been able to figure out why. I'm guessing that the problem is with my keycloak client configuration?
To any wayward travelers, I wish you luck. Ultimately, I solved my problem by re-configuring my mozilla-django-oidc settings. Specifically, I was missing:
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend", # default
"mozilla_django_oidc.auth.OIDCAuthenticationBackend",
)
This allowed for my application to authenticate both with the existing flow and using the new OIDC flow. The call to authenticate in the OIDC callback view was failing because I didn't have this authentication backend specified, so it was trying to just use the default.

Djoser password reset implementation

I am using djosers for my authentication on django backend which eventually i'll be connecting to flutter frontend and i am having trouble implementing the password reset functionality...
from what i have understood, first i need to hit the /users/reset_password/ with email body which will eventually give me the token of authentication which will be used further on confirm reset but the first thing i dont understand is PASSWORD_RESET_CONFIRM_URL field in the settings, like it needs a front end link with uid and token placeholders but what is this token field and what is this PASSWORD_RESET_CONFIRM_URL but i managed to look over a stack overflow question and filled it but now when i hit /users/reset_password/ i get this error:
[WinError 10013] An attempt was made to access a socket in a way forbidden by its access permissions
settings:
DJOSER = {
'PASSWORD_RESET_CONFIRM_URL':'reset/password/reset/confirm/{uid}/{token}',
'LOGIN_FIELD' : 'email',
'USER_CREATE_PASSWORD_RETYPE' : True,
'SERIALIZERS': {
'user_create': 'auth_app.serializers.UseriCreateSerializer',
'user': 'auth_app.serializers.UserCreateSerializer',
}
}
urls.py:
urlpatterns = [
path('',home,name='home'),
path('addInForum/',addInForum,name='addInForum'),
path('addInDiscussion/',addInDiscussion,name='addInDiscussion'),
path('<str:forum_id>/getDiscussion/',getDiscussion,name='getDiscussion'),
path('getDate/',getDate,name='getDate'),
path('reset/password/reset/confirm/<str:uid>/<str:token>/',PasswordResetView,name='PasswordResetView'),
# url(r'^reset/password/reset/confirm/(?P<uid>[\w-]+)/(?P<token>[\w-]+)/$', PasswordResetView.as_view(),),
]
views.py
#api_view(['GET'])
def PasswordResetView(request,uid,token):
post_data = {'uid': uid, 'token': token}
return Response(post_data)
Please remember that djoser should be part of your REST API based on Django REST Framework. You also need to think differently about the url routing in regard of your frontend app..
Usually urls in the form mydomain.com/some_url/whatever are considered "frontend urls" and parsed by routing of your frontend app. On the other hand urls in the form mydomain.com/api/something are considered API urls that are routed via Django's urls.py. I will refer to them as Fronted_URL and API_URL respectively.
So: resetting password works like this. The user that forgot their password and wants to reset it, surely needs to fill some king of form. This form needs to be sent to APIURL returned by resolve('user-reset-password') (by default this returns something like /users/reset_password/)
Here comes PASSWORD_RESET_CONFIRM_URL setting. Because after the body is accepted by the APIURL mentioned above, a mail will be sent to the user with a link that will point to URL entered in that setting. And it has to be FrontendURL! It should be routed by your frontend APP and preferably display some screen. But in the background your frontend app should send the values of uid and token fields to APIURL returned by resolve("user-reset-password-confirm").
This flow allows your frontend app to properly handle the response and display appropriate message to the user and then maybe redirect them to some other screen.
If you don't have a routed frontend app (probably written using REACT, ANGULAR or VUE) then you probably don't need a REST API and should just stick to django-allauth.

Django DRF Token Authentication

I'm having issues with DRF's token based authentication. Following is my landing page code (after login):
#api_view(['GET','POST'],)
def landing(request):
this_tenant=request.user.tenant
end=date_first.date.today()
start=end-date_first.timedelta(days=30)
sales_daily=sales_day_wise(start, end, this_tenant)
invoice_value=sales_raised_value(start, end, this_tenant)
payment_value=sales_collected_value(start, end, this_tenant)
return render(request,'landing.html', {'sales_daily':json.dumps(sales_daily, cls=DjangoJSONEncoder),\
'invoice_value':json.dumps(invoice_value, cls=DjangoJSONEncoder), \
'payment_value':json.dumps(payment_value, cls=DjangoJSONEncoder)})
I was using Django's built-in login view to authenticate and log in a user, then I revised to consider putting token in the header. But that is also not working
This is my login code:
#Redirect authenticated users to landing page
def custom_login(request):
if request.user.is_authenticated():
token, created = Token.objects.get_or_create(user=request.user)
request.session['AUTHORIZATION'] = "TOKEN "+token.key
return redirect(landing)
else:
return login(request)
Following is my DRF settings:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
# 'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
Issue is, when I'm logging in and going to the landing page via browser, DRF is not working and I'm getting the following error:
{"detail":"Authentication credentials were not provided."}
The reason is the custom DRF header (AUTHENTICATION = TOEKN XXXXXXXXXX) is not present in the request.
However, if I use Postman and put in the custom header (AUTHENTICATION = TOKEN XXXXXXXXXXXX), then it works.
How do I solve it?
Would this mean I would need a custom header for every view?
And on using DRF Token, does it open up CSRF vulnerability (this question: Django DRF - How to do CSRF validation with token authentication )?
Thanks a lot!!
How do I solve it? Would this mean I would need a custom header for every view?
TokenAuthentication is used for Single Page App and the token in Request Header needs to be provided by API client(Postman, Javascript or any other client) on each request. In your case, if you want to use Django views you should activate SessionAuthentication. TokenAuthentication and SessionAuthentication can co-exist.
One way is to save the token in the cookie in your in custom login view and read it by javascript client.
On using DRF Token, does it open up CSRF vulnerability (this question: Django DRF - How to do CSRF validation with token authentication )?
Yes. But there are ways to secure the requests.
According to DRF documentation
"If you use token authentication in production you must ensure that your API is only available over https".
Also, make sure set ALLOWED_HOSTS in settings to the domains that you want Django to respond to so the server does not respond to requests with other origins. There other more secure Auths can be used beside TokenAuthentication like JWT that has been mentioned above.
You need to learn the basics first. What is HTTP, what is HTTP headers, what is Django session (it's not an HTTP header and contents of the session doesn't affect the headers), read Django REST Framework documentation on token authentication.
If you want to test your views in browsers, then explicitly allow Django Session authentication in the DRF DEFAULT_AUTHENTICATION_CLASSES configuration variable. It can coexist with token authentication.
You can't make plain web browser append token to the HTTP request unless you're using some plugin like RESTClient, or DHC or REST Easy.
You're adding token to the Django session but you have disabled session authentication in DRF and even if you'd enable it, DRF doesn't read token from Django session because API client has no way to add Token to the Django session. Even if DRF would read Token from Django sessions it would be totally pointless as the client has no control over contents of the session. Session variables are set on the server, not on the client.
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication', # enables simple command line authentication
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
)
}
Adding 'rest_framework.authentication.SessionAuthentication' has solved this issue most of the time.
Alternative
You can use:
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
But now, in order to access protected api urls you must include the Authorization: JWT header.
If it works with curl or postman, this indicates it is not an issue with the backend. This is certainly an issue with the client side code. Have you had a look at your request to your REST api? I would recommend that and ensure the token is being passed in the headers and is formatted correctly.

Facebook OAuth login for mobile app with python-social-auth

I'm currently writing a mobile application in Xamarin and Django with Python Social Auth, my application requires personalisation so I decided to use Facebook authentication instead of allowing users to store a username/password combination due to the security implications.
This works fine when using a normal web browser due to it remembering states, but the tricky part comes when trying to use Xamarin. I tried using Xamarin.Auth, but this caused an error due to the fact that it expected an access_token in the response [1], which I'm unable to get from the Python Social Auth library as it expects a hard-coded redirect after completion [2], which in turn causes a conflict between both libraries.
I tried looking at the following example code, but this expects the mobile app to ALREADY have an access token (taken from a similar question here):
from django.contrib.auth import login
from social.apps.django_app.utils import psa
# Define an URL entry to point to this view, call it passing the
# access_token parameter like ?access_token=<token>. The URL entry must
# contain the backend, like this:
#
# url(r'^register-by-token/(?P<backend>[^/]+)/$',
# 'register_by_access_token')
#psa('social:complete')
def register_by_access_token(request, backend):
# This view expects an access_token GET parameter, if it's needed,
# request.backend and request.strategy will be loaded with the current
# backend and strategy.
token = request.GET.get('access_token')
user = request.backend.do_auth(request.GET.get('access_token'))
if user:
login(request, user)
return 'OK'
else:
return 'ERROR'
If anyone has any experience with using Xamarin and Python Social Auth, it would be really appreciated if you could point me into the right direction with this.