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

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.

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.

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-rest-auth google social auth by access token

I'm using django-rest-auth for social auth via API.
I've configured Facebook and it works perfectly but I've got some issues with Google social auth.
I've added to INSTALLED_APPS:
allauth.socialaccount.providers.google',
Created views:
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
class CustomGoogleOAuth2Adapter(GoogleOAuth2Adapter):
basic_auth = False
class GoogleLogin(SocialLoginView):
adapter_class = CustomGoogleOAuth2Adapter
client_class = OAuth2Client
Created app by admin panel
I've got access_token from https://developers.google.com/oauthplayground/
When I tried to log in by endpoint I got an error:
Reverse for 'redirect' not found. 'redirect' is not a valid view function or pattern name.
It's hard to pin point the error since you did not include the rest of your codes. The error simply means it cannot find the url with name 'redirect'. So there are 2 things you can check:
Have you put in correct urls settings for the end points as per django-rest-auth document ?
urlpatterns = [
...,
url(r'^rest-auth/', include('rest_auth.urls'))
]
Or maybe you forgot the redirect url when you specified the Google social app, as per django-all-auth document ?
Under “APIs & auth” go to “Credentials” and create a new Client ID.
Probably you will want a “Web application” Client ID. Provide your
domain name or test domain name in “Authorized JavaScript origins”.
Finally fill in http://127.0.0.1:8000/accounts/google/login/callback/
in the “Authorized redirect URI” field. You can fill multiple URLs,
one for each test domain. After creating the Client ID you will find
all details for the Django configuration on this page.

Django Admin use JWT

Using:
Django 1.11
Python 3.6
DRF with JWT in FE
I understand that the Django admin uses a session, and basic authentication.
What I did so far: Replaced the Django Admin authentication signin page with AWS-Cognito:
The user goes to domain/admin/*, redirected to signin in AWS
On successful signin the user is redirected to the redirect_uri, leads to a Django View
In the view I replace the code with tokens
I can't navigate to any Admin page - I am trying to redirect, but that doesn't work since I didn't login() the User
Stuck - I would like to associate the User with the fetched tokens and authenticate with every Admin page request, and when the user logs out delete the tokens
What to do next?
When I use JWT with the Front End application, every request.META has HTTP_AUTHORIZATION, and uses a suitable backend.
I know how to add backends, and potentially leverage the user.backend (I also use Cognito-JWT for other FE portions, so already wrote BE for that)
I need to find a way to replace the Django Admin sessions authentication with the fetched token
Thank you!
EDIT:
If I login() the user, and set it to a model backend that I have already I can navigate to any admin page - but using the session that I created when I logged the user in.
I would like to have the user be set to a new model backend, with authentication that uses a token (from Django backend docs):
class MyBackend:
def authenticate(self, request, token=None):
# Check the token and return a user.
...
How do I make the different Admin pages requests pass the token to the authentication?
Where do I store the token? (I could make a NewUserModel that is 1-1 with the Django User model, and place a token field there)
I am thinking of writing a middleware to capture all requests, and looking into the target URL - if Admin url, add the token to the HTTP_AUTHORIZATION once I fetch the user mentioned in #2 (the user is in every request due to DRF)
EDIT 2
My solution is getting more and more like this stack solution, I would have liked to know if there are any other options, but here is what I did so far:
I made a model that has a 1-1 user field, and a tokens field
As I am fetching/creating the user, I am also saving the tokens on the user's related model from #1 above
I created a middleware that is capturing any request in process_request, and has access to the user. I can see the tokens there as I access the user's related model from #1 above.
I am trying to set the HTTP_AUTHORIZATION header on the request, but cannot do that yet (currently stuck here)
In my backend, I am looking at the incoming request, and trying to fetch the HTTP_AUTHORIZATION - not there yet.
EDIT 3
I ended up just using the Django session as is - once the user authenticates with AWS-Cognito once, it is safe to assume that it is a legitimate User.
Then I just dump the Cognito-JWT, and login() the User.
Note: I am still interested in a solution that would drop the Django session for using the Cognito-JWT, and would love to hear suggestions.

Python Social auth authentication via access-token fails

I am currently developing a serverbackand with Django (1.7) that should handle authentication via social Networks by using python-social-auth.
I followed the Tutorial on this site, which describes the process for a simple Webapp.
This worked perfectly for Google and Twitter login.
Since the Server should be just a REST-FULL Backend I decided to get the Access-Token on the client side and send it to the server.
The server than will authenticate with it. This process should be no problem and is even given as an example in the docs of python-social-auth.
However if I do set everything up I will receive an error that says: "Backend not Found 404".
Here a minimal part of the project:
settings.py: (I also included API_KEY and SECRET)
AUTHENTICATION_BACKENDS = (
#'social.backends.facebook.FacebookOAuth2',
'social.backends.google.GoogleOAuth2',
'social.backends.twitter.TwitterOAuth',
'django.contrib.auth.backends.ModelBackend',
)
views.py (for the authentication view)
from django.contrib.auth import login
from social.apps.django_app.utils import psa
#psa('social:complete')
def register_by_access_token(request, backend):
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'
This i copied strait from the docs and only changed backend.do_auth to request.backend.do_auth. This seems to be an error in the docs.
urls.py:
...
url(r'^register-by-token/(?P<backend>[^/]+)/$', 'register_by_access_token')
...
Also as suggested in the docs.
I just tried to get this working just for google-oauth because there is a simple js-lib that gives you the access-token.
This also worked quite nice and I send a request to
GET http://localhost:8000/register-by-token/google-oauth2/<access-token>/
As described above the return was a 404 Backend not found.
I did a little bit of debugging and found out that the error is raised in the login function not the do_auth() function of the backend.
Therefor the actual authentication process works. I also tried using a random generated string as a token and got an according error, that the user cannot be authenticated.
The funny thing is that the user even has a property backend which holds 'social.backends.google.GoogleOAuth2' as it should.
Thank you if you stayed with me for the long post, and I hope someone has an idea what could be wrong :).
Looking forward to your answers.
In you register_by_access_token view, you are getting access_token in GET params
user = request.backend.do_auth(request.GET.get('access_token'))
and url you defiend is:
url(r'^register-by-token/(?P<backend>[^/]+)/$', 'register_by_access_token')
So you need to request something like:
GET http://localhost:8000/register-by-token/google-oauth2/?access_token=<access_token>
whereas, you are doing:
GET http://localhost:8000/register-by-token/google-oauth2/<access-token>/
You are passing access_token in url params, which is wrong.