Search code examples
asp.netoauthopenidquickbooks-online

QBO API Connect and Authorize in the same step


I am using the QBO API SDK (IppDotNetSdkQuickBooksApiV3) and can't figure out how to allow a user to Connect to QuickBooks and authorize my app. It is currently happening in two (2) steps:

  1. A user clicks "Connect To Intuit" and is sent to Intuit to login
  2. They are redirected back to my app and then have to connect their actual file again

I am clearly missing something but don't know what it is. I am using the ipp:connecttointuit functionality which is built into the app so I don't know how to customize it for the result I am looking for.

My app works with the two steps above however I can't have my app listed in the apps.com site using the process detailed above. They (apps.com) want the user to login using their QBO credentials, authorize the app and then automatically redirect the user back to my site with the app working. They don't want the duplicate authorization (I can't blame them).

Totally stuck. I am an ok programmer but have no experience with OpenId or OAuth.


protected void Page_Load(object sender, EventArgs e)
        {
            var openIdRelyingParty = new OpenIdRelyingParty();
            var openid_identifier = ConfigurationManager.AppSettings["openid_identifier"];
            var returnUrl = "~/OpenID/Connect";
            var response = openIdRelyingParty.GetResponse();
            if (response == null)
            {
                // Stage 2: user submitting Identifier
                Identifier id;
                if (Identifier.TryParse(openid_identifier, out id))
                {
                    IAuthenticationRequest request = openIdRelyingParty.CreateRequest(openid_identifier);
                    FetchRequest fetch = new FetchRequest();
                    fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email));
                    fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Name.FullName));
                    fetch.Attributes.Add(new AttributeRequest("http://axschema.org/intuit/realmId"));
                    request.AddExtension(fetch);
                    request.RedirectToProvider();
                }
            }
            else
            {
                if (response.FriendlyIdentifierForDisplay == null)
                {
                    Response.Redirect("~/OpenID/Connect");
                }

                // Stage 3: OpenID Provider sending assertion response
                //Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;
                FetchResponse fetch = response.GetExtension<FetchResponse>();
                if (fetch != null)
                {
                    var openIdEmail = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);
                    var openIdFullName = fetch.GetAttributeValue(WellKnownAttributes.Name.FullName);
                    var openIdRealmId = fetch.GetAttributeValue("http://axschema.org/intuit/realmId");

                    string userName = Membership.GetUserNameByEmail(openIdEmail);

                    //If username is null---------------------------------------------------
                    if (userName == null)
                    {

                        //DG added this---------------------------
                        String NewPassword = Membership.GeneratePassword(6, 1);
                        Membership.CreateUser(openIdEmail, NewPassword, openIdEmail);
                        //DG added this----------------------------

                        //Membership.CreateUser(openIdEmail, Guid.NewGuid().ToString(), openIdEmail);

                        FormsAuthentication.SetAuthCookie(openIdEmail, true);
                        //if (Request.QueryString["Subscribe"] != null)
                        //{
                        String csname = "DirectConnectScript";
                        Type cstype = this.GetType();
                        ClientScriptManager csm = Page.ClientScript;

                        // Check to see if the startup script is already registered.
                        if (!csm.IsStartupScriptRegistered(cstype, csname))
                        {
                            StringBuilder cstext = new StringBuilder();
                            cstext.AppendLine("<script>");
                            cstext.AppendLine("$(document).ready(function () {");
                            cstext.AppendLine("intuit.ipp.anywhere.directConnectToIntuit();");
                            cstext.AppendLine("});");
                            cstext.AppendLine("</script>");
                            csm.RegisterStartupScript(cstype, csname, cstext.ToString());
                            //}
                        }

                    }
                    else if (Request.QueryString["Disconnect"] != null)
                    {
                        RestHelper.clearProfile(RestProfile.GetRestProfile());
                        Response.Redirect("~/ManageConnection");
                    }

                    //If username is not null---------------------------------------------------
                    else if (userName != null)
                    {
                        FormsAuthentication.SetAuthCookie(userName, true);

                        if (!string.IsNullOrEmpty(returnUrl))
                        {
                            Response.Redirect("~/ManageConnection");
                        }
                    }
                }

            }
        }

Solution

  • I feel for you, it took me a while to get this working myself.

    Here is my MVC version of it. I hope it helps.

    It starts with QuickBooks/Index. Uses the QB pop up to get the permissions to my app, and then just goes from there. As a bonus, if the user is already logged in to QB they are automatically logged in to the app if they have given permission in the past. This is because I persist the tokens encrypted in the database. (Ignore most of the Session stuff, I just never removed it from the sample that I created the code from).

    Anyway here goes, this a lot of a code to post. Feel free to asks questions in comments if you need any clarification

    I'm assuming that you have something like this that receives the OAuthResponse and redirects back to /QuickBooks/Index (in your case the page that you have posted in the question)

    public class OauthResponseController : Controller
    {
        /// <summary>
        /// OAuthVerifyer, RealmId, DataSource
        /// </summary>
        private String _oauthVerifyer, _realmid, _dataSource;
    
        /// <summary>
        /// Action Results for Index, OAuthToken, OAuthVerifyer and RealmID is recieved as part of Response
        /// and are stored inside Session object for future references
        /// NOTE: Session storage is only used for demonstration purpose only.
        /// </summary>
        /// <returns>View Result.</returns>
        public ViewResult Index()
        {
            if (Request.QueryString.HasKeys())
            {
                // This value is used to Get Access Token.
                _oauthVerifyer = Request.QueryString.GetValues("oauth_verifier").FirstOrDefault().ToString();
                if (_oauthVerifyer.Length == 1)
                {
                    _oauthVerifyer = Request.QueryString["oauth_verifier"].ToString();
                }
                _realmid = Request.QueryString.GetValues("realmId").FirstOrDefault().ToString();
                if (_realmid.Length == 1)
                {
                    _realmid = Request.QueryString["realmId"].ToString();
                }
                Session["Realm"] = _realmid;
    
                //If dataSource is QBO call QuickBooks Online Services, else call QuickBooks Desktop Services
                _dataSource = Request.QueryString.GetValues("dataSource").FirstOrDefault().ToString();
                if (_dataSource.Length == 1)
                {
                    _dataSource = Request.QueryString["dataSource"].ToString();
                }
                Session["DataSource"] = _dataSource;
    
                getAccessToken();
    
    
                //Production applications should securely store the Access Token.
                //In this template, encrypted Oauth access token is persisted in OauthAccessTokenStorage.xml
                OauthAccessTokenStorageHelper.StoreOauthAccessToken();
    
                // This value is used to redirect to Default.aspx from Cleanup page when user clicks on ConnectToInuit widget.
                Session["RedirectToDefault"] = true;
            }
            else
            {
                Response.Write("No oauth token was received");
            }
    
            return View(); // This will redirect to /QuickBooks/OpenIdIndex which is almost the same as the code that you have posted
        }
    
        /// <summary>
        /// Gets the OAuth Token
        /// </summary>
        private void getAccessToken()
        {
            IOAuthSession clientSession = CreateSession();
            try
            {
                IToken accessToken = clientSession.ExchangeRequestTokenForAccessToken((IToken)Session["requestToken"], _oauthVerifyer);
                Session["AccessToken"] = accessToken.Token;
    
                // Add flag to session which tells that accessToken is in session
                Session["Flag"] = true;
    
                // Remove the Invalid Access token since we got the new access token
                Session.Remove("InvalidAccessToken");
                Session["AccessTokenSecret"] = accessToken.TokenSecret;
            }
            catch (Exception ex)
            {
                //Handle Exception if token is rejected or exchange of Request Token for Access Token failed.
                throw ex;
            }
    
        }
    
        /// <summary>
        /// Creates User Session
        /// </summary>
        /// <returns>OAuth Session.</returns>
        private IOAuthSession CreateSession()
        {
            OAuthConsumerContext consumerContext = new OAuthConsumerContext
            {
                ConsumerKey = ConfigurationManager.AppSettings["consumerKey"].ToString(),
                ConsumerSecret = ConfigurationManager.AppSettings["consumerSecret"].ToString(),
                SignatureMethod = SignatureMethod.HmacSha1
            };
    
            return new OAuthSession(consumerContext,
                                            Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlRequestToken,
                                            Constants.OauthEndPoints.IdFedOAuthBaseUrl,
                                             Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlAccessToken);
        }
    }
    

    This is my equivalent page to yours.

    public class QuickBooksController : Controller
    {
        private readonly IQueryChannel _queryChannel;
        private readonly ICommandChannel _commandChannel;
        public QuickBooksController(IQueryChannel queryChannel, ICommandChannel commandChannel)
        {
            _queryChannel = queryChannel;
            _commandChannel = commandChannel;
        }
        /// <summary>
        /// OpenId Relying Party
        /// </summary>
        private static OpenIdRelyingParty openid = new OpenIdRelyingParty();
    
        public ActionResult Index(string returnurl)
        {
            QuickBooksAuthStore.Load();
            if (QuickBooksContext.AccessToken != null)
            {
                if (!new Customers().CheckConnection())
                {
                    QuickBooksContext.FriendlyName = null;
                    OauthAccessTokenStorageHelper.RemoveInvalidOauthAccessToken(QuickBooksContext.FriendlyEmail);
                    QuickBooksContext.AccessToken = null;
                }
            }
            if (returnurl != null || QuickBooksContext.QuickReturnUrl != null)
            {
                if (returnurl != null)
                {
                   QuickBooksContext.QuickReturnUrl = returnurl; 
                }
                if (QuickBooksContext.AccessToken != null)
                {
                    var connected = new Customers().CheckConnection();
                    if (connected)
                    {
                        returnurl = QuickBooksContext.QuickReturnUrl;
                        QuickBooksContext.QuickReturnUrl = null;
                        return Redirect(returnurl);
                    }
                }  
            }
            return View();
        }
    
        public RedirectResult OpenIdIndex()
        {
            var openid_identifier = ConfigurationManager.AppSettings["openid_identifier"] + SessionContext.CurrentUser.MasterCompanyId;
            var response = openid.GetResponse();
            if (response == null)
            {
                // Stage 2: user submitting Identifier
                Identifier id;
                if (Identifier.TryParse(openid_identifier, out id))
                {
                    try
                    {
                        IAuthenticationRequest request = openid.CreateRequest(openid_identifier);
                        FetchRequest fetch = new FetchRequest();
                        fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email));
                        fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Name.FullName));
                        request.AddExtension(fetch);
                        request.RedirectToProvider();
                    }
                    catch (ProtocolException ex)
                    {
                        throw ex;
                    }
                }
            }
            else
            {
                if (response.FriendlyIdentifierForDisplay == null)
                {
                    Response.Redirect("/OpenId");
                }
    
                // Stage 3: OpenID Provider sending assertion response, storing the response in Session object is only for demonstration purpose
                Session["FriendlyIdentifier"] = response.FriendlyIdentifierForDisplay;
                FetchResponse fetch = response.GetExtension<FetchResponse>();
                if (fetch != null)
                {
                    Session["OpenIdResponse"] = "True";
                    Session["FriendlyEmail"] = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);// emailAddresses.Count > 0 ? emailAddresses[0] : null;
                    Session["FriendlyName"] = fetch.GetAttributeValue(WellKnownAttributes.Name.FullName);//fullNames.Count > 0 ? fullNames[0] : null;
    
                    OauthAccessTokenStorageHelper.GetOauthAccessTokenForUser(Session["FriendlyEmail"].ToString());
                    QuickBooksAuthStore.UpdateFriendlyId(Session["FriendlyIdentifier"].ToString(), Session["FriendlyName"].ToString());
                }
            }
    
            string query = Request.Url.Query;
            if (!string.IsNullOrWhiteSpace(query) && query.ToLower().Contains("disconnect=true"))
            {
                Session["AccessToken"] = "dummyAccessToken";
                Session["AccessTokenSecret"] = "dummyAccessTokenSecret";
                Session["Flag"] = true;
                return Redirect("QuickBooks/CleanupOnDisconnect");
            }
            return Redirect("/QuickBooks/Index");
        }
    
        /// <summary>
        /// Service response.
        /// </summary>
        private String txtServiceResponse = "";
    
        /// <summary>
        /// Disconnect Flag.
        /// </summary>
        protected String DisconnectFlg = "";
        public ActionResult Disconnect()
        {
            OAuthConsumerContext consumerContext = new OAuthConsumerContext
            {
                ConsumerKey = ConfigurationManager.AppSettings["consumerKey"].ToString(),
                SignatureMethod = SignatureMethod.HmacSha1,
                ConsumerSecret = ConfigurationManager.AppSettings["consumerSecret"].ToString()
            };
    
            OAuthSession oSession = new OAuthSession(consumerContext, Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlRequestToken,
                                  Constants.OauthEndPoints.AuthorizeUrl,
                                  Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlAccessToken);
    
            oSession.ConsumerContext.UseHeaderForOAuthParameters = true;
            if ((Session["AccessToken"] + "").Length > 0)
            {
                Session["FriendlyName"] = null;
                oSession.AccessToken = new TokenBase
                {
                    Token = Session["AccessToken"].ToString(),
                    ConsumerKey = ConfigurationManager.AppSettings["consumerKey"].ToString(),
                    TokenSecret = Session["AccessTokenSecret"].ToString()
                };
    
                IConsumerRequest conReq = oSession.Request();
                conReq = conReq.Get();
                conReq = conReq.ForUrl(Constants.IaEndPoints.DisconnectUrl);
                try
                {
                    conReq = conReq.SignWithToken();
                }
                catch (Exception ex)
                {
                    throw ex;
                }
    
                //Used just see the what header contains
                string header = conReq.Context.GenerateOAuthParametersForHeader();
    
                //This method will clean up the OAuth Token
                txtServiceResponse = conReq.ReadBody();
    
                //Reset All the Session Variables
                Session.Remove("oauthToken");
    
                // Dont remove the access token since this is required for Reconnect btn in the Blue dot menu
                // Session.Remove("accessToken");
    
                // Add the invalid access token into session for the display of the Disconnect btn
                Session["InvalidAccessToken"] = Session["AccessToken"];
    
                // Dont Remove flag since we need to display the blue dot menu for Reconnect btn in the Blue dot menu
                // Session.Remove("Flag");
    
                ViewBag.DisconnectFlg = "User is Disconnected from QuickBooks!";
    
                //Remove the Oauth access token from the OauthAccessTokenStorage.xml
                OauthAccessTokenStorageHelper.RemoveInvalidOauthAccessToken(Session["FriendlyEmail"].ToString());
            }
    
            return RedirectToAction("Index", "QuickBooks");
        }
    
        public ActionResult CleanUpOnDisconnect()
        {
            //perform the clean up here 
    
            // Redirect to Home when user clicks on ConenctToIntuit widget.
            object value = Session["RedirectToDefault"];
            if (value != null)
            {
                bool isTrue = (bool)value;
                if (isTrue)
                {
                    Session.Remove("InvalidAccessToken");
                    Session.Remove("RedirectToDefault");
                    return Redirect("/QuickBooks/index");
                }
            }
    
            return RedirectToAction("Index", "QuickBooks");
        }
    
        private String consumerSecret, consumerKey, oauthLink, RequestToken, TokenSecret, oauth_callback_url;
    
        public RedirectResult OAuthGrant()
        {
            oauth_callback_url = Request.Url.GetLeftPart(UriPartial.Authority) + ConfigurationManager.AppSettings["oauth_callback_url"];
            consumerKey = ConfigurationManager.AppSettings["consumerKey"];
            consumerSecret = ConfigurationManager.AppSettings["consumerSecret"];
            oauthLink = Constants.OauthEndPoints.IdFedOAuthBaseUrl;
            IToken token = (IToken)Session["requestToken"];
            IOAuthSession session = CreateSession();
            IToken requestToken = session.GetRequestToken();
            Session["requestToken"] = requestToken;
            RequestToken = requestToken.Token;
            TokenSecret = requestToken.TokenSecret;
    
            oauthLink = Constants.OauthEndPoints.AuthorizeUrl + "?oauth_token=" + RequestToken + "&oauth_callback=" + UriUtility.UrlEncode(oauth_callback_url);
            return Redirect(oauthLink);
        }
    
        /// <summary>
        /// Gets the Access Token
        /// </summary>
        /// <returns>Returns OAuth Session</returns>
        protected IOAuthSession CreateSession()
        {
            OAuthConsumerContext consumerContext = new OAuthConsumerContext
            {
                ConsumerKey = consumerKey,
                ConsumerSecret = consumerSecret,
                SignatureMethod = SignatureMethod.HmacSha1
            };
    
            return new OAuthSession(consumerContext,
                                            Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlRequestToken,
                                            oauthLink,
                                            Constants.OauthEndPoints.IdFedOAuthBaseUrl + Constants.OauthEndPoints.UrlAccessToken);
        }
    }
    

    ..and here is the cshtml page for QuickBooks/Index

    @using System.Configuration
    @using PubManager.Domain.WebSession
    @{
        string MenuProxy = Request.Url.GetLeftPart(UriPartial.Authority) + "/" + System.Configuration.ConfigurationManager.AppSettings["menuProxy"];
        string OauthGrant = Request.Url.GetLeftPart(UriPartial.Authority) + "/" + System.Configuration.ConfigurationManager.AppSettings["grantUrl"];
    
        ViewBag.Title = "MagManager - Intuit Anywhere";
        string FriendlyName = (string)HttpContext.Current.Session["FriendlyName"] + "";
        string FriendlyEmail = (string)HttpContext.Current.Session["FriendlyEmail"];
        string FriendlyIdentifier = (string)HttpContext.Current.Session["FriendlyIdentifier"];
    
        string realm = (string)Session["Realm"];
        string dataSource = (string)Session["DataSource"];
        string accessToken = (string)Session["AccessToken"] + "";
        string accessTokenSecret = (string)Session["AccessTokenSecret"];
        string invalidAccessToken = (string)Session["InvalidAccessToken"];
    }
    
    <h1>Intuit - QuickBooks Online</h1>
    
    <div class="well">
        @if (FriendlyName.Length == 0)
        {
            <script>
                window.location.href = "/QuickBooks/OpenIdIndex";
            </script>
        }
        else
        {
            <div id="IntuitInfo">
                <strong>Open Id Information:</strong><br />
                Welcome: @FriendlyName<br />
                E-mail Address: @FriendlyEmail<br />
                <br />
                @if (accessToken.Length > 0 && invalidAccessToken == null)
                {
                    <div id="oAuthinfo">
                        <a onclick="reconcileInvoices()" id="RecInvoices" class="btn btn-primary">Set Invoices Paid</a><br />
                        <br/>
                        <a onclick="recTax()" id="RecTax" class="btn btn-primary">Sync Tax Rates</a><br />
                        <br/>
                        @if (SessionContext.UseAccountCodes)
                        {
                            <a onclick="recProducts()" id="RecProducts" class="btn btn-primary">Sync Products</a><br />
                            <br />
                        }
                        <a href="/QuickBooks/ReconcileCustomers" id="Customers" class="btn btn-primary">Reconcile Customers</a><br/>
                        <br/>
                        <a href="/QuickBooks/Disconnect" id="Disconnect" class="btn btn-primary">
                            Disconnect from
                            QuickBooks
                        </a>
                        <br/><br/>
                        @*<a href="/QuickBooks/Customers" id="QBCustomers" class="btn btn-primary">Get QuickBooks Customer List</a><br/>
            <br/>*@
    
                        @*<br />
            <a href="/QuickBooks/Invoices" id="QBInvoices" class="btn btn-primary">Get QuickBooks Invoice List</a><br />
            <br />*@
                        <br />
                    </div>
                }
                else
                {
                    <br />
                        <ipp:connecttointuit></ipp:connecttointuit>
                }
            </div>
        }
    </div>
    <script type="text/javascript" src="https://js.appcenter.intuit.com/Content/IA/intuit.ipp.anywhere-1.3.5.js"></script>
    <script type="text/javascript">
        intuit.ipp.anywhere.setup({
            menuProxy: '@MenuProxy',
            grantUrl: '@OauthGrant'
        });
    </script>