0

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.

gmcc051
  • 390
  • 3
  • 17

1 Answers1

1

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.

gmcc051
  • 390
  • 3
  • 17