Добрый день, эксперты,
Нам необходимо создать интеграцию с XERO Api через их новейшие стандарты OAuth2 с последним (вышеупомянутым). NET CORE 3.1 в VS.
Я поливал существующую базу образцов в GitHub за последние 2 дня, даже не достигнув каких-либо точек аутентификации. Вот где я сейчас застрял: просто заставляю мое приложение проходить аутентификацию.
Я прибег к загрузке приведенного выше примера непосредственно из GitHub и вводу (по крайней мере из того, что я вижу). ) только две переменные, необходимые для этой работы: ClientID и ClientSecret (в настройки приложений. json). Приложение также зарегистрировано под MyApps в Xero с правильными ClientID и ClientSecret.
Моя среда довольно проста, как они предполагают в примере приложения: Запуск этого с localhost: 5000, и зарегистрируйте то же самое под своими MyApps в ксеро. За исключением, говорят, зарегистрируйте свои URL-адреса перенаправления OAuth2 как
http://localhost: 5000 / signup-oid c
. NET Мне кажется, что CORE это не нравится, поэтому я имею их как
http://localhost: 5000 / signup_oid c
Поэтому, когда я запускаю это, мне представляются стандартные 2 кнопки Xero (SignUp & SignIn), которые уже были объявлены в представлении.
Нажмите кнопку Xero SignIn, которая должна выстрелить:
[HttpGet]
[Route("signin")]
[Authorize(AuthenticationSchemes = "XeroSignIn")]
public IActionResult SignIn()
{
return RedirectToAction("OutstandingInvoices");
}
Но не (правильно), так как мой личность пользователя еще не аутентифицирована. Это (в соответствии со схемой аутентификации Xero) приводит меня к конечной точке идентификации Xero. (как проверено через POSTMAN) (https://login.xero.com/identity/connect/authorize, содержащий мой ClientID, URL реферала и области в качестве параметров)
Проблема в том, что я продолжаю получать это :
Вещи, которые я проверял / пробовал:
- Проверено, что мои appsettings. json обнаружены и ClientID / Secret корректно загружается при запросе в Startup.cs
- Обновлен CallbackPath в Startup.cs до "/ signin_oid c"
- Изменение областей действия
- Добавление моего clientID & Secret в XeroClient в разных точках, чтобы убедиться, что оно сохраняется.
- буквально просматривая все помеченные сообщения [Xero-Api] на SO
- чтение примера проекта Xero README несколько раз over.
На этом этапе я должен быть представлен на странице входа в 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");
}
}
Любой совет будет с благодарностью!