ej2 / python-quickbooks

A Python library for accessing the Quickbooks API.
MIT License
407 stars 195 forks source link

Issue on getting State Token to Callback view #367

Closed LeavesEatingCow closed 1 month ago

LeavesEatingCow commented 1 month ago

I am going through an official example of how to implement Oauth2 for using the quickbooks API. I am having trouble getting the state_token from the session variables within the callback view. Whenever I ise the oauth view it sends an authorization request to the quickbooks server. The request gives me a state token and a url to use to get an authorization code to use other APIs in quickbooks. I was able to print the state token in the oauth view by just printing "request.session['state']". This lets me now that it is being saved in the session. So when I redirect to the given url, Quickbooks sends me a code with the same state_token it gave me using the callback view in this example. The callback endpoint is requested by the Quickbooks server for validation purposes to make sure the state_token given back and the state_token in the session are the same, otherwise it'll return an error. This callback endpoint was given by me to Quickbooks website. But whenever I try and retrieve the state_token within the callback view, it is always null/None. So whenever the callback view checks if the state tokens are the same in

state_tok != auth_client.state_token:
        return HttpResponse('unauthorized', status=401)

it always returns unauthorized. I've debugged a lot and found out that the session id when the session is stored in oauth() and when I try fetching it in callback() are different. I've looked in the issues on GitHub and it seems no one is having an issue like me. Is there a step I am missing or could the issue be out-dated dependencies?

Here are the docs I am following: https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/faq

Here is a part from the docs where I am referring to:

Use the ‘state’ parameter in your authorization request to add your unique session token. Your app will match the unique session token with the one returned in the Intuit OAuth 2.0 server response. This verifies the user, not a malicious attacker or bot, started the authorization process.

from intuitlib.client import AuthClient
from intuitlib.migration import migrate
from intuitlib.enums import Scopes
from intuitlib.exceptions import AuthClientError

from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseServerError
from django.conf import settings
from django.contrib.sessions.models import Session

from django.core import serializers

from app.services import qbo_api_call

# Create your views here.
def index(request):
    return render(request, 'index.html')

def oauth(request):
    auth_client = AuthClient(
        settings.CLIENT_ID, 
        settings.CLIENT_SECRET, 
        settings.REDIRECT_URI, 
        settings.ENVIRONMENT,
    )

    url = auth_client.get_authorization_url([Scopes.ACCOUNTING])
    request.session['state'] = auth_client.state_token

    return redirect(url)

def callback(request):
    auth_client = AuthClient(
        settings.CLIENT_ID,
        settings.CLIENT_SECRET, 
        settings.REDIRECT_URI, 
        settings.ENVIRONMENT, 
        state_token=request.session.get('state', None),
    )

    state_tok = request.GET.get('state', None)
    error = request.GET.get('error', None)

    if error == 'access_denied':
        return redirect('app:index')

    if state_tok is None:
        return HttpResponseBadRequest()
    elif state_tok != auth_client.state_token:
        return HttpResponse('unauthorized', status=401)

    auth_code = request.GET.get('code', None)
    realm_id = request.GET.get('realmId', None)
    request.session['realm_id'] = realm_id

    if auth_code is None:
        return HttpResponseBadRequest()

    try:
        auth_client.get_bearer_token(auth_code, realm_id=realm_id)
        request.session['access_token'] = auth_client.access_token
        request.session['refresh_token'] = auth_client.refresh_token
        request.session['id_token'] = auth_client.id_token
    except AuthClientError as e:
        # just printing status_code here but it can be used for retry workflows, etc
        print(e.status_code)
        print(e.content)
        print(e.intuit_tid)
    except Exception as e:
        print(e)
    return redirect('app:connected')

def connected(request):
    auth_client = AuthClient(
        settings.CLIENT_ID, 
        settings.CLIENT_SECRET, 
        settings.REDIRECT_URI, 
        settings.ENVIRONMENT, 
        access_token=request.session.get('access_token', None), 
        refresh_token=request.session.get('refresh_token', None), 
        id_token=request.session.get('id_token', None),
    )

    if auth_client.id_token is not None:
        return render(request, 'connected.html', context={'openid': True})
    else:
        return render(request, 'connected.html', context={'openid': False})

I've used print methods to print the session id and to check if the sessions were the same between oauth and callback.