Search code examples
c#authenticationauthorization.net-6.0quickbooks-online

Impossible to reconnect user using OAuth in


I'm trying configure an authentication / Authorization process, using OAuth in .NET 6 for QBO (QuickbookOnline) authority.

User successfully go through the process once and is authenticated - everything works fine, all this using CookieAuthenticationDefault and AddOAuth(). Later, user press "Logoff" where I, in backend, call SignOutAsync. I do have to call using Apk from QBO to revoke it's token (this is a, as per my understandings, a requirement in order to been a trusted app using QBO - user must disconnect). All this, works fine; disconnect user thus having its token get revoked.

But, when user try to log back in once again, process goes to the QBO endpoint as per the first time, but when it comes back to my endpoint, I get a "error":"invalid_client". How can it has been through once and done it correctly and not doing it correctly on the second (later) try?

Here is my configuration:

Startup.cs

services.Configure<CookiePolicyOptions>(options => {
            options.CheckConsentNeeded=ctx => HostEnvironment.IsProduction();
            options.MinimumSameSitePolicy=SameSiteMode.None;
 }).AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
   .AddCookie()
   .AddOAuth("qbo",o => {
          o.AuthorizationEndpoint=Configuration["QBO:AuthorizationEndpoint"];
          o.CallbackPath="/signin-qbo";
          o.ClientId=Configuration["QBO:ClientId"];
          o.ClientSecret=Configuration["QBO:ClientSecret"];
          o.TokenEndpoint=Configuration["QBO:TokenEndpoint"];
          o.UserInformationEndpoint=Configuration["QBO:UserInformationEndpoint"];
          o.ClaimsIssuer=Configuration["QBO:ClaimsIssuer"];
          o.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier,"sub");
          o.ClaimActions.MapJsonKey(ClaimTypes.Name,"givenName");
          o.ClaimActions.MapJsonKey(ClaimTypes.Email,"email");
          o.ClaimActions.MapJsonKey(ClaimTypes.GivenName,"givenName");
          o.ClaimActions.MapJsonKey(ClaimTypes.Surname,"familyName");
          o.ClaimActions.MapJsonKey("emailVerified","emailVerified");
          o.ClaimActions.MapJsonKey("realmid","realmid");
          o.SaveTokens=true;
          o.Scope.Clear();
          o.UsePkce=true;

          new string[] { "com.intuit.quickbooks.accounting","openid","email","profile" }.ToList().ForEach(s => o.Scope.Add(s));

          o.Events.OnCreatingTicket=async ctx => {
              var accessToken = ctx.AccessToken;
              var refreshToken = ctx.RefreshToken;
              var companyId = ctx.HttpContext.Request.Query["realmid"];

              ctx.Backchannel.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
              ctx.Backchannel.DefaultRequestHeaders.Authorization=new AuthenticationHeaderValue("Bearer",accessToken);

              var response = await ctx.Backchannel.GetStringAsync(ctx.Options.UserInformationEndpoint);
              var userInfo = JsonSerializer.Deserialize<JsonElement>(response);
              ctx.RunClaimActions(userInfo);

              var id = userInfo.GetString("sub");
              using var applicationContext = ctx.HttpContext.RequestServices.GetService<ApplicationContext>();
              var applicationUser = applicationContext.ApplicationUsers.SingleOrDefault(u => u.AccountingSystemId==id);

              if(applicationUser==null) {
                 applicationUser=new ApplicationUser {
                    AccountingSystemId=userInfo.GetString("sub"),
                    GivenName=userInfo.GetString("givenName"),
                    Surname=userInfo.GetString("familyName"),
                    Email=userInfo.GetString("email"),
                    EmailVerified=bool.Parse(userInfo.GetString("emailVerified")),
                    CompanyId=companyId,
                    AccessToken=accessToken,
                    RefreshToken=refreshToken
                 };
                 applicationContext.ApplicationUsers.Add(applicationUser);
              } else {
                 applicationUser.AccessToken=accessToken;
                 applicationUser.RefreshToken=refreshToken;
              }
              applicationContext.SaveChanges();
          };
     });

Login/logout

    [AllowAnonymous]
    public async Task SignInQBO(string returnUrl = "/") {
        try {
            await HttpContext.ChallengeAsync("qbo",new AuthenticationProperties { RedirectUri=returnUrl });
        } catch(Exception ex) {
            var s = ex;
        }
    }

    [Authorize]
    public async Task<RedirectToPageResult> SignOutQBO() {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

        var oauthClient = new OAuth2Client(_configuration["QBO:ClientId"],_configuration["QBO:ClientSecret"],new PathString("/"),"sandbox");
        var t = await oauthClient.RevokeTokenAsync(usr.RefreshToken);

        return RedirectToPage("/Index");
    }

Solution

  • Your understanding here is not correct:

    I do have to call using Apk from QBO to revoke it's token (this is a, as 
    per my understandings, a requirement in order to been a trusted app using
    QBO - user must disconnect)
    

    You do not need to disconnect from QBO when the user logs off. In fact, you should not be doing this, and won't pass your app review if you do.

    The only time you should be revoking the token is if the user specifically clicks a button in your app telling you to disconnect your app from QuickBooks. e.g. they are clicking a button labeled [Disconnect from QuickBooks].

    Just logging off does not require a disconnect/token revoke.