Search code examples
python-3.xdjangooauth-2.0

Creating a draft in Gmail from Django admin action with Google OAuth


I'm trying to create a Django admin action that will create a draft email in my Gmail account, addressed to selected contacts. I'm getting stuck with the Google OAuth flow.

admin.py:

...

DEBUG = os.getenv('DEBUG', 'False') == 'True'
if DEBUG:
    os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

SCOPES = ['https://www.googleapis.com/auth/gmail.compose']

def email_contacts(modeladmin, request, queryset):
    flow = Flow.from_client_secrets_file(
        'contacts/client_secret.json',
        scopes=SCOPES)
    flow.redirect_uri = "http://localhost:8000/callback"
    authorization_url, state = flow.authorization_url(
        access_type='offline',
        include_granted_scopes='true')
    return HttpResponseRedirect(authorization_url)

def auth_callback(request):
    code = request.GET.get('code')
    flow = Flow.from_client_secrets_file(
        'contacts/client_secret.json',
        scopes=SCOPES)
    flow.redirect_uri = "http://localhost:8000"
    flow.fetch_token(code=code)
    creds = flow.credentials
    send_email(creds)

def send_email(creds):
    message_body = "Test content"
    message = MIMEMultipart()
    message['to'] = '[email protected]'
    message.attach(MIMEText(message_body, "plain"))
    try:
        service = build('gmail', 'v1', credentials=creds)
        message = {'message': {'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}}
        service.users().drafts().create(userId='me', body=message).execute()
    except HttpError as err:
        print(err)

...

class ContactAdmin(admin.ModelAdmin):
    actions = [emails_contacts]

(Just trying to draft a test email so far; not yet trying to populate the email with data from the queryset)

urls.py:

... 

from contacts.admin import auth_callback

urlpatterns = [
    path('callback/', auth_callback, name='oauth_callback'),
    path('admin/', admin.site.urls),
...

client_secret.json:

{"web":{"client_id":"....apps.googleusercontent.com","project_id":"...","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","...":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"...","redirect_uris":["http://localhost:8000/callback","http://localhost:8000/callback/","http://localhost/callback","http://localhost/callback/","http://localhost:8000/","http://localhost:8000","http://localhost","http://localhost/"]}}

(Listing lots of redirect_uris to be safe)

The error:

CustomOAuth2Error at /callback/

(redirect_uri_mismatch) Bad Request

Request Method: GET Request URL: http://localhost:8000/callback/?state=...&code=...&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.compose Django Version: 4.2.1 Exception Type: CustomOAuth2Error Exception Value:

(redirect_uri_mismatch) Bad Request

Exception Location: /home/me/.local/share/virtualenvs/contacts/lib/python3.9/site-packages/oauthlib/oauth2/rfc6749/errors.py, line 400, in raise_from_error Raised during: contacts.admin.auth_callback Python Executable: /home/me/.local/share/virtualenvs/contacts/bin/python Python Version: 3.9.5

...

The error is triggering on this line of code: flow.fetch_token(code=code)


Solution

  • I got it working. I don't know exactly what the problem was but here are the relevant parts of the working code.

    admin.py:

    ...
    
    DEBUG = os.getenv('DEBUG', 'False') == 'True'
    if DEBUG:
        redirect_uri='https://127.0.0.1:8000/oauth/'
    else:
        redirect_uri='https://my-url.com/oauth/'
    
    SCOPES = ['https://www.googleapis.com/auth/gmail.compose']
    
    def authenticate(request):
        flow = Flow.from_client_secrets_file('contacts/client_secret.json',
            scopes=SCOPES,
            redirect_uri=redirect_uri,
        )
        authorization_url, state = flow.authorization_url(
            access_type='offline',
            include_granted_scopes='true'
        )
        request.session['oauth2_state'] = state
        return redirect(authorization_url)
    
    def oauth2callback(request):
        state = request.session.get('oauth2_state', '')
        flow = Flow.from_client_secrets_file('contacts/client_secret.json',
            scopes=SCOPES,
            state=state,
            redirect_uri=request.build_absolute_uri('/oauth/')
        )
        flow.fetch_token(authorization_response=request.build_absolute_uri())
        credentials_json = flow.credentials.to_json()
        request.session['gmail_credentials'] = credentials_json
        messages.success(request, 'OAuth authentication successful but no emails drafted. Please perform action again.')
        return redirect(request.build_absolute_uri('/admin/contacts/contact/'))
    
    def get_gmail_credentials(request):
        credentials_json = request.session.get('gmail_credentials')
        if not credentials_json:
            return None
        credentials_info = json.loads(credentials_json)
        info = {
            "client_id": credentials_info['client_id'],
            "client_secret": credentials_info['client_secret'],
            "token": credentials_info['token'],
            "refresh_token": credentials_info['refresh_token'],
            "token_uri": credentials_info['token_uri'],
            "scopes": SCOPES,
        }
        return credentials.Credentials.from_authorized_user_info(info)
    
    ...
    
    class ContactAdmin(admin.ModelAdmin):
        actions = [emails_contacts]
    

    urls.py:

    ...
    urlpatterns = [
        path('authenticate/', authenticate, name='authenticate'),
        path('oauth/', oauth2callback, name='oauth'),
        path('compose-email/', compose_emails, name='compose_email'),
    ...
    

    client_secret.json:

    {"web":{"client_id":"MASKED.apps.googleusercontent.com","project_id":"MASKED","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"MASKED","redirect_uris":["https://127.0.0.1:8000/oauth/","https://my-url.com/oauth/"]}}