让 Xero .Net Core OAuth2 示例工作
Getting Xero .Net Core OAuth2 sample to work
大家好,
我们需要通过 XERO Api 最新的 OAuth2 标准与(上面提到的)VS 中最新的 .NET CORE 3.1 建立集成。
过去 2 天我已经遍历了 GitHub 中的现有样本库,甚至没有达到任何身份验证点。 这是我目前遇到的问题:只是让我的应用程序进行身份验证。
我直接从 GitHub 下载上面的示例并输入(至少从我所见)完成这项工作所需的唯一 2 个变量:ClientID 和 ClientSecret(变成 appsettings.json)。该应用程序还使用正确的 ClientID 和 ClientSecret 在 Xero 的 MyApps 下注册。
我的环境非常简单,正如他们在示例应用程序中假设的那样:运行 来自 localhost:5000,并在 Xero 的 MyApps 下注册相同的内容。 除了,他们说,将您的 OAuth2 重定向 URLS 注册为
.NET CORE 似乎不喜欢那样,所以我将它们设为
因此,当我 运行 执行此操作时,我会看到已在视图中声明的标准 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:
- 已检查是否检测到我的 appsettings.json 并且 ClientID/Secret 在 Startup.cs
中请求时正确加载
- 已将 Startup.cs 中的回调路径更新为“/signin_oidc”
- 改变范围
- 在不同的点将我的 clientID 和 Secret 注入到 XeroClient 中以确保它被持久化。
- 在 S.O 上每隔一个 [Xero-Api] 标记为 post。
- 阅读 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 之间的端口相同。
大家好,
我们需要通过 XERO Api 最新的 OAuth2 标准与(上面提到的)VS 中最新的 .NET CORE 3.1 建立集成。
过去 2 天我已经遍历了 GitHub 中的现有样本库,甚至没有达到任何身份验证点。 这是我目前遇到的问题:只是让我的应用程序进行身份验证。
我直接从 GitHub 下载上面的示例并输入(至少从我所见)完成这项工作所需的唯一 2 个变量:ClientID 和 ClientSecret(变成 appsettings.json)。该应用程序还使用正确的 ClientID 和 ClientSecret 在 Xero 的 MyApps 下注册。
我的环境非常简单,正如他们在示例应用程序中假设的那样:运行 来自 localhost:5000,并在 Xero 的 MyApps 下注册相同的内容。 除了,他们说,将您的 OAuth2 重定向 URLS 注册为
.NET CORE 似乎不喜欢那样,所以我将它们设为
因此,当我 运行 执行此操作时,我会看到已在视图中声明的标准 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:
- 已检查是否检测到我的 appsettings.json 并且 ClientID/Secret 在 Startup.cs 中请求时正确加载
- 已将 Startup.cs 中的回调路径更新为“/signin_oidc”
- 改变范围
- 在不同的点将我的 clientID 和 Secret 注入到 XeroClient 中以确保它被持久化。
- 在 S.O 上每隔一个 [Xero-Api] 标记为 post。
- 阅读 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 之间的端口相同。