0
votes

I am an advanced beginner and I have been stuck on this issue for a couple of days now. The app I am creating requires access to each user's Salesforce account. In order to make an API connection, we need to store their Salesforce username, password and access token.

The user will provide their Salesforce username and password then click a button to Authorize. This button directs the user to:

https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id=dsf3434&redirect_uri=https%3A%2F%2Fwww.mysite.com/oauth/salesforce/&display=popup

The response from Salesforce Oauth servers should be:

https://www.mysite.com/oauth/salesforce/?display=popup&code=aPrx6RMKPrMpm.SHNCxBmHz3ZiHMc.xZRY9RHLekkzW_G8Uu.KyqmmY0.JGCr5roqPT49vTCbg%3D%3D

Here is the views.py file

from django.views.generic.base import RedirectView, TemplateView, View
from django.http import Http404, HttpResponse
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy, reverse
from guardian.mixins import LoginRequiredMixin
from simple_salesforce import Salesforce
import logging

from campaigns.views import CampaignOwnerPermission
from . import api, utils, settings
from .models import OauthToken


class SalesforceOauthRedirectView(
    LoginRequiredMixin,
    RedirectView
):

    def get_redirect_url(self):
        logger = logging.getLogger(__name__)

        # Extract 'code' parameter from return URL and set to variable
        if (
            not self.request.GET.get(
                'code', None
            ) is None
        ):
            try:
                existing_credentials = OauthToken.objects.get(
                    user=request.user
                    )
                # get stored Salesforce username and password
                username = str(existing_credentials.salesforce_user_id)
                password = str(existing_credentials.password)
                payload = {
                    'grant_type': 'password',
                    'client_id': str(settings.CONSUMER_KEY),
                    'client_secret': str(settings.CONSUMER_SECRET),
                    'username': username,
                    # must concatenate password & code before passing to server
                    'password': password + str(code)
                    }

                try:
                    # Post payload to Salesforce Oauth server and get user
                    # token in response.
                    r = requests.post(
                        "https://login.salesforce.com/services/oauth2/token",
                        headers={
                            "Content-Type":"application/x-www-form-urlencoded"
                        },
                            data=payload
                    )

                    # Decode the JSON response from Salesforce Oauth server
                    decoded = json.loads(r.content)
                    # Store access_token to database
                    existing_credentials.token = decoded['access_token']
                    existing_credentials.active = True
                    existing_credentials.save()

                    messages.add_message(
                        self.request,
                        messages.SUCCESS,
                        _(
                            'Successfully updated Salesforce  \
                            authentication with user credentials: "%s"'
                            %
                            salesforce_user_id
                        )
                    )
                    # Success point
                    return reverse_lazy('providers:provider_list')

                    # except (ValueError, KeyError, TypeError):
                    # logger.error('Could not decode response from Salesforce API')

                    # Not sure how this should be arranged
                except:
                    logger.error(
                        'Could not get Oauth_token from Salesforce API.'
                        )
                    messages.add_message(
                        self.request,
                        messages.WARNING,
                            ('Could not get Oauth_token from Salesforce API.\n\n \
                            Salesforce may be experiencing an outage.  Try again \
                            in a few minutes and contact explorro support if the \
                            problem persists.'
                            )
                    )
                    return reverse_lazy('providers:provider_list')


            except:
                logger.error('Could not get users Salesforce credentials')
                messages.add_message(
                    self.request,
                    messages.WARNING,
                    ('There was a problem authenticating with \
                     Salesforce.  Be sure to enter your Salesforce \
                     username and password before attempting to authorize your\
                     account.  Contact our support team if you need some help.'
                     )
                )
                return reverse_lazy('providers:provider_list')

        else:
            pass
            return reverse_lazy('providers:provider_list')
            messages.add_message(
                self.request,
                messages.WARNING,
                ('Could not retrieve Salesforce Authorization Code\n\n \
                 Contact your Salesforce administrator for assistance.'
                )
            )

The 'code' parameter (example value = randomTextCode) is parsed from the response URL, and passed as part of the 'payload' variable (line 38) to https://login.salesforce.com/services/oauth2/token

The last step should be receiving a JSON response from https://login.salesforce.com/services/oauth2/token which contains the access_token

The error seems to be originating from the first try/except statement, as I am getting the exception error message on lines #103-106.

While running locally, here is the server response (301)

"GET /oauth/salesforce/?display=popup&code=aPrx6RMKPrMpm.SHNCxBmHz3ZgWIqBWQKmln4Q6TfdI8TbmeWuMw5H..Di.342no15VYNvmgzA%3D%3D HTTP/1.1" 301 0

Here is models.py

from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _


class OauthToken(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name='Salesforce User',
        verbose_name=_('Salesforce User'),
        db_index=True,
    )
    salesforce_user_id = models.EmailField(
        verbose_name=_('Salesforce user id'),
        db_index=True, unique=True
    )
    password = models.CharField(    # TODO: This needs to be encrypted!!
        verbose_name=_('Salesforce password'),
        max_length=256,
    )
    token = models.CharField(
        verbose_name=_('Token'),
        max_length=256,
        null=True,
        blank=True,
    )
    security_token = models.CharField(
        verbose_name=_('Security Token'),
        max_length=256
    )
    active = models.BooleanField(
        verbose_name=_('Active'),
        default=False,
        db_index=True
    )

    def __unicode__(self):
        return str(self.salesforce_user_id)

    class Meta:
        verbose_name = _('Oauth token')
        verbose_name_plural = _('Oauth tokens')

Any help troubleshooting this issue is greatly appreciated.

2
Could you paste the actual error? - Henrik Andersson
The error that is displayed to the user is " There was a problem authenticating with Salesforce. Be sure to enter your Salesforce username and password before attempting to authorize your account. Contact our support team if you need some help." This is generated from the "except:" code starting at line 98 through 108 - Joe Fusaro

2 Answers

0
votes

It looks like the problem is going to be either a syntax or data error on your side of the handshake. The except clause generating your error message is actually outside of the requests.post to salesforce--that is, an exception is being generated by one of these lines of code:

existing_credentials = OauthToken.objects.get(
    user=request.user
)
# get stored Salesforce username and password
username = str(existing_credentials.salesforce_user_id)
password = str(existing_credentials.password)
payload = {
    'grant_type': 'password',
    'client_id': str(settings.CONSUMER_KEY),
    'client_secret': str(settings.CONSUMER_SECRET),
    'username': username,
    # must concatenate password & code before passing to server
    'password': password + str(code)
    }

There is probably a much more helpful error message being silenced here by the broad try/except block that can help you move things forward. I'd advise you actually let the exception be raised and see what you find out (could be that the ORM can't actually find the OauthToken you are searching for, for example). You can modify the except clause in place:

# Line 96
except BaseException as e:
    raise e # To print to the console
    # or, to print to the error logs
    logger.error("%s: %s" % (e.__class__, e.message))
    messages.add_message(
        self.request,
        messages.WARNING,
        ('There was a problem authenticating with \
         Salesforce.  Be sure to enter your Salesforce \
         username and password before attempting to authorize your\
         account.  Contact our support team if you need some help.'
         )
    )
    return reverse_lazy('providers:provider_list')

This should be enough to help you discover the actual exception being thrown so you can debug further.

0
votes

There were a few things that I was doing wrong. I decided to write my answer here and will update it later when I make additional improvements. Special thanks to Casey Kinsey for his suggestion on using BaseException for troubleshooting.

First off, I was using the wrong grant_type parameter. I had been using the 'password' grant_type parameter, when the correct parameter is 'authorization_code'.

Second, I was creating an overly difficult testing process. I was making updates, deploying to a staging Heroku environment, and troubleshooting. To increase the speed of troubleshooting, I changed the redirect_uri in (1) the the link that the user clicks to authorize their acccount (this is in another file,) (2) the payload variable that is posted to Salesforce, and (3) the redirect_uri in the Salesforce connected app.

from django.views.generic.base import RedirectView, TemplateView, View
from django.http import Http404, HttpResponse
from django.conf import settings
from django.conf.urls import patterns, url, include
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy, reverse
from guardian.mixins import LoginRequiredMixin
from simple_salesforce import Salesforce
import logging, requests, json

from campaigns.views import CampaignOwnerPermission
from . import api, utils, settings
from .models import OauthToken


class SalesforceOauthRedirectView(
    LoginRequiredMixin,
    RedirectView
):
# permanent = False
# query_string = False

    def get_redirect_url(self):
        logger = logging.getLogger(__name__)

        try:
            payload = {
                'grant_type': 'authorization_code',
                'client_id': settings.CONSUMER_KEY,
                'client_secret': settings.CONSUMER_SECRET,
                'code': self.request.GET.get('code'), # get code param from response URL
                # TODO: redirect_uri should NOT be hardcoded
                'redirect_uri': 'https://127.0.0.1:8000/oauth/salesforce/'
                }

            try:
                # Post payload to Salesforce Oauth server and get user
                # token in response.
                r = requests.post(
                    "https://login.salesforce.com/services/oauth2/token",
                    headers={
                        "Content-Type":"application/x-www-form-urlencoded"
                    },
                    data=payload
                )

                try:
                    # Decode the JSON response from Salesforce Oauth server
                    decoded = json.loads(r.content)
                    # Store tokens & Salesforce user info to database
                    creds = OauthToken.objects.get(user=self.request.user)
                    # TODO: Store salesforce_user_id, Thumbnail, etc.
                    creds.access_token = decoded['access_token']
                    # creds.salesforce_organization_id = decoded['refresh_token']
                    # creds.refresh_token = creds['refresh_token']
                    # creds.id_token = creds['id_token']
                    # creds.instance_url = decoded['instance_url']
                    creds.active = True
                    creds.save()

                    messages.add_message(
                        self.request,
                        messages.SUCCESS,
                        _(
                            'Successfully updated Salesforce  \
                            authentication with user credentials: "%s"'
                            %
                            creds.salesforce_user_id
                        )
                    )

                except:
                    logger.error("%s: %s" % (e.__class__, e.args))
                    messages.add_message(
                    self.request,
                    messages.WARNING,
                        ('Error connecting with Salesforce.  \
                        Contact explorro support. [Error 003]')
                        )

                    return reverse_lazy('providers:provider_list')

            except BaseException as e:
                #raise e # Print error to the console
                # or, to print to the error logs
                logger.error("%s: %s" % (e.__class__, e.args))
                messages.add_message(
                    self.request,
                    messages.WARNING,
                    ('Could not get Oauth_token from Salesforce API.\n\n \
                    Salesforce may be experiencing an outage.  Try again \
                    in a few minutes and contact explorro support if the \
                    problem persists. [Error 002]'
                    )
                )
                return reverse_lazy('providers:provider_list')

        except BaseException as e:
            raise e # Print error to console
            logger.error("%s: %s" % (e.__class__, e.args))
            messages.add_message(
                self.request,
                messages.WARNING,
                ('There was a problem authenticating with \
                 Salesforce.  Be sure to enter your Salesforce \
                 username and password before attempting to authorize your\
                 account.  Contact our support team if you need some help. \
                 [Error 003]'
                )
            )
            return reverse_lazy('providers:provider_list')

This solution is not 100% complete; here are the next action items:

(1) there are additional parameters that need to be stored to the database, and the password and all tokens will need to be encrypted in the database (this is what I will work on next.)

(2) When this is pushed to production, the redirect_uri will need to be updated everywhere (in the Salesforce app, in the user authorization link, and in the payload variable)

(3) We need to use the Salesforce ID parameters that are available to us (includes Salesforce username, password, profile picture, etc.) and store those to the database so that this is truly 1-click authentication.

Please feel free to contact me if I can help in any way, I would be happy to help in any way that I can.