让 Xero .Net Core OAuth2 示例工作

Getting Xero .Net Core OAuth2 sample to work

大家好,

我们需要通过 XERO Api 最新的 OAuth2 标准与(上面提到的)VS 中最新的 .NET CORE 3.1 建立集成。

过去 2 天我已经遍历了 GitHub 中的现有样本库,甚至没有达到任何身份验证点。 这是我目前遇到的问题:只是让我的应用程序进行身份验证。

我直接从 GitHub 下载上面的示例并输入(至少从我所见)完成这项工作所需的唯一 2 个变量:ClientIDClientSecret(变成 appsettings.json)。该应用程序还使用正确的 ClientID 和 ClientSecret 在 Xero 的 MyApps 下注册。

我的环境非常简单,正如他们在示例应用程序中假设的那样:运行 来自 localhost:5000,并在 Xero 的 MyApps 下注册相同的内容。 除了,他们说,将您的 OAuth2 重定向 URLS 注册为

http://localhost:5000/signup-oidc

.NET CORE 似乎不喜欢那样,所以我将它们设为

http://localhost:5000/signup_oidc

因此,当我 运行 执行此操作时,我会看到已在视图中声明的标准 2 个 Xero 按钮(注册和登录)。

Click SignIn Xero button, which should fire:

    [HttpGet]
    [Route("signin")]
    [Authorize(AuthenticationSchemes = "XeroSignIn")]
    public IActionResult SignIn()
    {
        return RedirectToAction("OutstandingInvoices");
    }

但没有,(正确地)因为我的用户身份尚未通过身份验证。这(根据 Xero 的身份验证方案)将我带到 Xero 的身份端点。 (通过 POSTMAN 检查)https://login.xero.com/identity/connect/authorize 包含我的 ClientID、推荐 URL 和范围作为参数)

问题是,然后我不断得到这个:

我拥有的东西 checked/tried:

  1. 已检查是否检测到我的 appsettings.json 并且 ClientID/Secret 在 Startup.cs
  2. 中请求时正确加载
  3. 已将 Startup.cs 中的回调路径更新为“/signin_oidc”
  4. 改变范围
  5. 在不同的点将我的 clientID 和 Secret 注入到 XeroClient 中以确保它被持久化。
  6. 在 S.O 上每隔一个 [Xero-Api] 标记为 post。
  7. 阅读 Xero Sample Project README 几遍。

在这个阶段,我应该会看到 Xero 的登录页面,要求我登录我的 Xero 帐户,然后要求我授权我的应用程序申请的范围,然后重定向回我的应用程序。 (至少是第一次)。

现阶段我有点不知所措。

见Startup.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();

        services.TryAddSingleton(new XeroConfiguration
        {
            ClientId = Configuration["Xero:ClientId"],
            ClientSecret = Configuration["Xero:ClientSecret"]
        });

        services.TryAddSingleton<IXeroClient, XeroClient>();
        services.TryAddSingleton<IAccountingApi, AccountingApi>();
        services.TryAddSingleton<MemoryTokenStore>();

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = "XeroSignIn";
        })
        .AddCookie(options =>
        {
            options.Cookie.Name = "XeroIdentity";

            // Clean up cookies that don't match in local MemoryTokenStore.
            // In reality you wouldn't need this, as you'd be storing tokens in a real data store somewhere peripheral, so they won't go missing between restarts
            options.Events = new CookieAuthenticationEvents
            {
                OnValidatePrincipal = async context =>
                {
                    var tokenStore = context.HttpContext.RequestServices.GetService<MemoryTokenStore>();
                    var token = await tokenStore.GetAccessTokenAsync(context.Principal.XeroUserId());

                    if (token == null)
                    {
                        context.RejectPrincipal(); 
                    }
                }
            };
        })
        .AddOpenIdConnect("XeroSignIn", options =>
        {
            options.Authority = "https://identity.xero.com";

            options.ClientId = Configuration["Xero:ClientId"];
            options.ClientSecret = Configuration["Xero:ClientSecret"];

            options.ResponseType = "code";

            options.Scope.Clear();
            options.Scope.Add("openid");
            options.Scope.Add("profile");
            options.Scope.Add("email");

            options.CallbackPath = "/signin_oidc";

            options.Events = new OpenIdConnectEvents
            {
                OnTokenValidated = OnTokenValidated()
            };
        })
        .AddOpenIdConnect("XeroSignUp", options =>
        {
            options.Authority = "https://identity.xero.com";

            options.ClientId = Configuration["Xero:ClientId"];
            options.ClientSecret = Configuration["Xero:ClientSecret"];

            options.ResponseType = "code";

            options.Scope.Clear();
            options.Scope.Add("offline_access");
            options.Scope.Add("openid");
            options.Scope.Add("profile");
            options.Scope.Add("email");
            options.Scope.Add("accounting.settings");
            options.Scope.Add("accounting.transactions");

            options.CallbackPath = "/signin_oidc";

            options.Events = new OpenIdConnectEvents
            {
                OnTokenValidated = OnTokenValidated()
            };
        });

        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    private static Func<TokenValidatedContext, Task> OnTokenValidated()
    {
        return context =>
        {
            var tokenStore = context.HttpContext.RequestServices.GetService<MemoryTokenStore>();

            var token = new XeroOAuth2Token
            {
                AccessToken = context.TokenEndpointResponse.AccessToken,
                RefreshToken = context.TokenEndpointResponse.RefreshToken,
                ExpiresAtUtc = DateTime.UtcNow.AddSeconds(Convert.ToDouble(context.TokenEndpointResponse.ExpiresIn))
            };

            tokenStore.SetToken(context.Principal.XeroUserId(), token);

            return Task.CompletedTask;
        };
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();
        app.UseCookiePolicy();

        app.UseAuthentication();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

} 并且 HomeController.cs

public class HomeController : Controller
{
    private readonly MemoryTokenStore _tokenStore;
    private readonly IXeroClient _xeroClient;
    private readonly IAccountingApi _accountingApi;

    public HomeController(MemoryTokenStore tokenStore, IXeroClient xeroClient, IAccountingApi accountingApi)
    {
        _tokenStore = tokenStore;
        _xeroClient = xeroClient;
        _accountingApi = accountingApi;
    }

    [HttpGet]
    public IActionResult Index()
    {
        if (User.Identity.IsAuthenticated)
        {
            return RedirectToAction("OutstandingInvoices");
        }

        return View();
    }

    [HttpGet]
    [Authorize]
    public async Task<IActionResult> OutstandingInvoices()
    {
        var token = await _tokenStore.GetAccessTokenAsync(User.XeroUserId());

        var connections = await _xeroClient.GetConnectionsAsync(token);

        if (!connections.Any())
        {
            return RedirectToAction("NoTenants");
        }

        var data = new Dictionary<string, int>();

        foreach (var connection in connections)
        {
            var accessToken = token.AccessToken;
            var tenantId = connection.TenantId.ToString();

            var organisations = await _accountingApi.GetOrganisationsAsync(accessToken, tenantId);
            var organisationName = organisations._Organisations[0].Name;

            var outstandingInvoices = await _accountingApi.GetInvoicesAsync(accessToken, tenantId, statuses: new List<string>{"AUTHORISED"}, where: "Type == \"ACCREC\"");

            data[organisationName] = outstandingInvoices._Invoices.Count;
        }

        var model = new OutstandingInvoicesViewModel
        {
            Name = $"{User.FindFirstValue(ClaimTypes.GivenName)} {User.FindFirstValue(ClaimTypes.Surname)}",
            Data = data
        };

        return View(model);
    }

    [HttpGet]
    [Authorize]
    public IActionResult NoTenants()
    {
        return View();
    }

    [HttpGet]
    public async Task<IActionResult> AddConnection()
    {
        // Signing out of this client app allows the user to be taken through the Xero Identity connection flow again, allowing more organisations to be connected
        // The user will not need to log in again because they're only signed out of our app, not Xero.
        await HttpContext.SignOutAsync(); 

        return RedirectToAction("SignUp");
    }

    [HttpGet]
    [Route("signup")]
    [Authorize(AuthenticationSchemes = "XeroSignUp")]
    public IActionResult SignUp()
    {
        return RedirectToAction("OutstandingInvoices");
    }

    [HttpGet]
    [Route("signin")]
    [Authorize(AuthenticationSchemes = "XeroSignIn")]
    public IActionResult SignIn()
    {
        return RedirectToAction("OutstandingInvoices");
    }

    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error()
    {
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
    }

    [HttpPost]
    [Route("signin_oidc")]
    public IActionResult signin_oidc()
    {
        return RedirectToAction("OutstandingInvoices");
    }
}

如有任何建议,我们将不胜感激!

在示例中,有两个不同的身份验证方案使用示例中的两个不同的回调 url;一种以 signin-oidc 结尾,一种以 sign up-oidc 结尾。

您需要确保注册两个回调 URL 以使示例完整运行,正如您发现的那样,开发人员门户和代码中的需求完全相同,需要额外的费用注意确保 运行 示例和您注册的回调 url 之间的端口相同。