pythondjangogoogle-oauthdjango-allauthdjango-oauth

How to use django-allauth for Google API?


How is django-allauth implemented to obtain authorization using Oauth2 for a Google API (in my case the Gmail API)? Additionally, I am looking to implement this separately from using django-allauth to have users log in with Google, so I would need to store it separately, and also call it in a view.

Thanks!


Solution

  • I'm assuming you have already followed this document and this, and made all the necessary primary settings.

    Then you can use django-allauth to set up authentication via Google, with tokens saved. Specify the required parameters for gmail scope. Then use these tokens to work with GmailAPI.

    First you need to set some settings, this is described in this document (SOCIALACCOUNT_STORE_TOKENS constant). Something like this:

    #  settings.py
    SOCIALACCOUNT_STORE_TOKENS = True
    SOCIALACCOUNT_EMAIL_AUTHENTICATION = True
    SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True
    

    Also configure “access_type”: “offline”, in the google provider settings, as described here:

    You must set AUTH_PARAMS['access_type'] to offline in order to receive a refresh token on first login and on reauthentication requests (which is needed to refresh authentication tokens in the background, without involving the user’s browser). When unspecified, Google defaults to online.

    Now, after the user logs in to your system through google account, a database entry will be created with his access_token and refresh_token (token_secret in db_table). You can check this in the admin interface in this table:

    admin

    To use GmailAPI, you need the google user ID and its access_token, this can be found like this:

    def get_user_google_tokens(request: HttpRequest, limit: int = 1) -> list[dict]:
        from allauth.socialaccount.models import SocialToken
        from django.db import models
    
        user_google_tokens = list(
            SocialToken.objects
            .filter(account__provider='google', account__user_id=request.user.pk)
            .annotate(google_user_id=models.F('account__uid'))
            .order_by('-expires_at')
            .values('token', 'token_secret', 'expires_at', 'google_user_id')[:limit]
        )
        print(user_google_tokens)
        #  [
        #   {'token': '...', 
        #    'token_secret': '...', 
        #    'expires_at': datetime.datetime(2025, 2, 7, 16, 38, 16, 611588, tzinfo=datetime.timezone.utc), 
        #    'google_user_id': '...'}
        #  ]
        return user_google_tokens
    

    Note that access_token has a short validity of one hour, by default. You can read more about it here. When your access_token expires in time, you will need to refresh it using refresh_token (token_secret), and write new access_token to the database by modifying an existing entry. Here's an example of how to refresh. Also note that sometimes you will not be able to refresh tokens using refresh_token.

    Here is an example of a request to update a token using Postman. But you will probably use some library for http requests for python. Or some off-the-shelf client, for that matter. django-allauth doesn't seem to be able to update tokens, but I don't know for sure, I haven't checked that.

    postman

    UPDATED

    The answer to your question from the comments. Yes, you can use two google social applications, with different client_id, and different settings, but you would then need to use a custom DefaultSocialAccountAdapter, and your own views to specify which config should be used. There may be a simpler option, but this is the first one I tried and it works.

    There is also a much simpler option, you can have one configuration for the google provider, but you can set dynamically the parameters to be used, for example scope and access_type.

    Here's an example, you have a google login endpoint, let's say it's the default google_login endpoint in django-allauth. So you also have one config set up for the google provider. By default, you don't need to ask the user for permission to use their gmail, in which case they just log in. Then, the user wants to use your functions that work with GmailAPI, you ask them to authenticate again and provide additional permissions. In django-allauth, these dynamic parameters can be passed using GET parameters. Here's an example of how to do this using HTML exclusively. You'll probably have this in different files, but here is the gist of how it's done:

    {#Google Gmail Login Form#}  
    <form method="POST" action="{% url 'google_login' %}?scope=https://mail.google.com/&auth_params=access_type=offline">  
      {% csrf_token %}  
      <button type="submit">GOOGLE GMAIL</button>  
    </form>  
      
    {#Google Base Login Form#}  
    <form method="POST" action="{% url 'google_login' %}">  
      {% csrf_token %}  
      <button type="submit">GOOGLE</button>  
    </form>
    

    Here's my basic google config in settings:

    SOCIALACCOUNT_PROVIDERS = {
        'google': {
            'APP': {
                'client_id': '.....',
                'secret': '.....',
                'key': '.....',
                'settings': {
                    'scope': [
                        'profile',
                        'email',
                    ],
                    'auth_params': {
                        'access_type': 'online',
                        # it is important to leave it so that the `google`  
                        # confirmation window is always displayed.                        
                        'prompt': 'consent',
                    },
                    'oauth_pkce_enabled': True,
                },
            },
            'FETCH_USER_INFO': True,
        },
    }
    

    The point is that when a user logs in via google, the refresh_token is not saved, but when logging in to google, with additional parameters - ?scope=https://mail.google.com/&auth_params=access_type=offline it will be saved in the database. In this way it will be possible to understand if the user has permissions to use GmailAPI:

    user_google_tokens = list(  
        SocialToken.objects  
        .filter(account__provider='google', account__user_id=some_user.id) 
        .exclude(token_secret='')  
        .annotate(google_user_id=models.F('account__uid'))  
        .order_by('-expires_at')  
        .values('token', 'token_secret', 'expires_at', 'google_user_id')[:1]  
    )
    

    Here is an example method to handle dynamically passed auth_params, from GET request parameters. You can also check out the other methods to learn more.