Getting a Error 400: redirect_uri_mismatch when trying to use OAuth2 with Google Sheets from a Django view

Question:

I am trying to connect to Google Sheets’ API from a Django view. The bulk of the code I have taken from this link:
https://developers.google.com/sheets/api/quickstart/python

Anyway, here are the codes:

sheets.py (Copy pasted from the link above, function renamed)

from __future__ import print_function
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']

# The ID and range of a sample spreadsheet.
SAMPLE_SPREADSHEET_ID = '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'
SAMPLE_RANGE_NAME = 'Class Data!A2:E'

def test():
    """Shows basic usage of the Sheets API.
    Prints values from a sample spreadsheet.
    """
    creds = None
    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)

    service = build('sheets', 'v4', credentials=creds)

    # Call the Sheets API
    sheet = service.spreadsheets()
    result = sheet.values().get(spreadsheetId=SAMPLE_SPREADSHEET_ID,
                                range=SAMPLE_RANGE_NAME).execute()
    values = result.get('values', [])

    if not values:
        print('No data found.')
    else:
        print('Name, Major:')
        for row in values:
            # Print columns A and E, which correspond to indices 0 and 4.
            print('%s, %s' % (row[0], row[4]))

urls.py

urlpatterns = [
    path('', views.index, name='index')
]

views.py

from django.http import HttpResponse
from django.shortcuts import render

from .sheets import test

# Views

def index(request):
    test()
    return HttpResponse('Hello world')

All the view function does is just call the test() method from the sheets.py module. Anyway, when I run my server and go the URL, another tab opens up for the Google oAuth2, which means that the credentials file is detected and everything. However, in this tab, the following error message is displayed from Google:

Error 400: redirect_uri_mismatch The redirect URI in the request, http://localhost:65262/, does not match the ones authorized for the OAuth client.

In my API console, I have the callback URL set exactly to 127.0.0.1:8000 to match my Django’s view URL. I don’t even know where the http://localhost:65262/ URL comes from. Any help in fixing this? And can someone explain to me why this is happening? Thanks in advance.

EDIT
I tried to remove the port=0 in the flow method as mentioned in the comment, then the URL mismatch occurs with http://localhost:8080/, which is again pretty weird because my Django app is running in the 8000 port.

Asked By: darkhorse

||

Answers:

The redirect URI tells Google the location you would like the authorization to be returned to. This must be set up properly in google developer console to avoid anyone hijacking your client. It must match exactly.

To to Google developer console. Edit the client you are currently using and add the following as a redirect uri

http://localhost:65262/

enter image description here

Tip click the little pencil icon to edit a client 🙂

TBH while in development its easier to just add the port that google says you are calling from then fiddle with the settings in your application.

You shouldn’t be using Flow.run_local_server() unless you don’t have the intention of deploying the code. This is because run_local_server launches a browser on the server to complete the flow.

This works just fine if you’re developing the project locally for yourself.

If you’re intent on using the local server to negotiate the OAuth flow. The Redirect URI configured in your secrets must match that, the local server default for the host is localhost and port is 8080.

If you’re looking to deploy the code, you must perform the flow via an exchange between the user’s browser, your server and Google.

Since you have a Django server already running, you can use that to negotiate the flow.

For example,

Say there is a tweets app in a Django project with urls.py module as follows.

from django.urls import path, include

from . import views

urlpatterns = [
    path('google_oauth', views.google_oath, name='google_oauth'),
    path('hello', views.say_hello, name='hello'),
]

urls = include(urlpatterns)

You could implement a guard for views that require credentials as follow.

import functools
import json
import urllib

from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

from django.shortcuts import redirect
from django.http import HttpResponse

SCOPES = ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', 'openid']

def provides_credentials(func):
    @functools.wraps(func)
    def wraps(request):
        # If OAuth redirect response, get credentials
        flow = InstalledAppFlow.from_client_secrets_file(
            'credentials.json', SCOPES,
            redirect_uri="http://localhost:8000/tweet/hello")

        existing_state = request.GET.get('state', None)
        current_path = request.path
        if existing_state:
            secure_uri = request.build_absolute_uri(
                ).replace('http', 'https')
            location_path = urllib.parse.urlparse(existing_state).path 
            flow.fetch_token(
                authorization_response=secure_uri,
                state=existing_state
            )
            request.session['credentials'] = flow.credentials.to_json()
            if location_path == current_path:
                return func(request, flow.credentials)
            # Head back to location stored in state when
            # it is different from the configured redirect uri
            return redirect(existing_state)


        # Otherwise, retrieve credential from request session.
        stored_credentials = request.session.get('credentials', None)
        if not stored_credentials:
            # It's strongly recommended to encrypt state.
            # location is needed in state to remember it.
            location = request.build_absolute_uri() 
            # Commence OAuth dance.
            auth_url, _ = flow.authorization_url(state=location)
            return redirect(auth_url)

        # Hydrate stored credentials.
        credentials = Credentials(**json.loads(stored_credentials))

        # If credential is expired, refresh it.
        if credentials.expired and creds.refresh_token:
            creds.refresh(Request())

        # Store JSON representation of credentials in session.
        request.session['credentials'] = credentials.to_json()

        return func(request, credentials=credentials)
    return wraps


@provides_credentials
def google_oauth(request, credentials):
    return HttpResponse('Google OAUTH <a href="/tweet/hello">Say Hello</a>')

@provides_credentials
def say_hello(request, credentials):
    # Use credentials for whatever
    return HttpResponse('Hello')

Note that this is only an example. If you decide to go this route, I recommend looking into extracting the OAuth flow to its very own Django App.

Answered By: Oluwafemi Sule

I had the same problem with the redirect_uri error and it turned out (as implied above) that I created my credentials in the google console as type "Web server" instead of "desktop app". I created new creds as "desktop app", downloaded the JSON and it worked.

Ultimately, I want to use the GMAIL API for a web server, but that is a different flow than the sample.

Answered By: Doug Nintzel

I had the same problem.

  1. Just added the redirect url which is mentioned in error (such that in your case its – http://localhost:65262/) to Authorized redirect URIs on Google cloud console.
  2. I was using jupyter notebook to generate the token for gsheet api, so it was waiting for me to authenticate (S/A Please visit this URL to authorize this application: .
  3. After doing the first step, clicked on link in step 2. Authorization went seamlessly fine.
    FYI, I was using a web application based OAuth.
Answered By: Abhijay