djangogoogle-signindjango-allauthdj-rest-auth

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.


Solution

  • 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:

    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.