BFF v4 - Remote API call being forwarded to the frontend itself #322
-
Hi, On v4.0.0-rc.1 I have a setup where a frontend runs at a different host than the BFF host and an external API is being mapped. I am following the 'Getting Started - Multiple Frontends' over https://docs.duendesoftware.com/bff/getting-started/multi-frontend/ and regarding the '5 - Remote API Proxying' I followed the 'Direct proxying per frontend' approach and in my case, it is therefore: .AddFrontends(
new BffFrontend(BffFrontendName.Parse("frontend1"))
.MappedToOrigin(Origin.Parse("https://localhost:5013"))
.MappedToPath(LocalPath.Parse("/frontend1"))
.WithRemoteApis(
new RemoteApi(LocalPath.Parse("/api1"), new Uri("https://localhost:7260/Api1"))
.WithAccessToken(RequiredTokenType.User)
)
); I reduced my projet to the minimal which still exposes my issue: using Duende.Bff;
using Duende.Bff.AccessTokenManagement;
using Duende.Bff.DynamicFrontends;
using Duende.Bff.Endpoints;
using Duende.Bff.Yarp;
using Sirius.Bff;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddCors(opt =>
{
opt.AddDefaultPolicy(policy =>
{
policy
.WithOrigins("https://localhost:5013")
.WithHeaders("x-csrf", "content-type")
.WithMethods("GET")
.AllowCredentials();
});
});
builder.Services
.AddBff()
.ConfigureOpenIdConnect(options =>
{
options.Authority = "https://idp";
options.ClientId = "clientId";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.ResponseMode = "query";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.MapInboundClaims = false;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("offline_access");
options.Scope.Add("https://something.onmicrosoft.com/scope1");
})
.ConfigureCookies(options =>
{
options.Cookie.SameSite = SameSiteMode.Lax;
})
.AddFrontends(
new BffFrontend(BffFrontendName.Parse("frontend1"))
.MappedToOrigin(Origin.Parse("https://localhost:5013"))
.MappedToPath(LocalPath.Parse("/frontend1"))
.WithRemoteApis(
new RemoteApi(LocalPath.Parse("/api1"), new Uri("https://localhost:7260/Api1"))
.WithAccessToken(RequiredTokenType.User)
)
);
builder.Services.AddTransient<IReturnUrlValidator, FrontendHostReturnUrlValidator>();
var app = builder.Build();
app.UseCors();
app.UseAuthentication();
app.UseRouting();
app.UseBff();
app.UseAuthorization();
app.MapControllers();
app.Run(); The frontend is running at the https://localhost:5013 and the external API at https://localhost:7260/Api1. The Bff is at https://localhost:7082. I am able to login at the IdP but when the frontend attempts to call the API, it results on a GET 404. Examining the called URL, it points to the frontend itself (GET https://localhost:5013/api1) Am I doing the WithRemoteApis mapping correctly? ![]() |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments
-
The scenario you're describing fits the "Host the UI Separately" section in our docs, where you wish to host the UI on a separate site from the BFF. This sample contains code to demonstrate that scenario. In your code, you're most likely calling You may also need to update any Fetch API or XmlHttpRequest calls (or the equivalent in your SPA framework) to include credentials, ensuring that all cookies are sent back to the BFF. |
Beta Was this translation helpful? Give feedback.
-
Hi @wcabus It was also missing the including of credentials on the API fetch I was doing, so I needed to do a async function callApi1() {
var req = new Request(api1Url, {
headers: new Headers({
'X-CSRF': '1'
}),
credentials: "include"
})
var resp = await fetch(req);
log("API1 Result: " + resp.status);
if (resp.ok) {
showApi(await resp.json());
}
} This time the BFF at the However I am still having a 404 as result of the GET the frontend does... By the way, I see if I do builder.Services.AddBff().AddRemoteApis();
app.MapRemoteBffApiEndpoint("/api1", new Uri("https://localhost:7260/Api1"))
.WithAccessToken(RequiredTokenType.User); then the API is reached correctly, with a 200 as a result of the GET. app.MapRemoteBffApiEndpoint("/api/users", new Uri("https://remoteHost/users"))
.WithAccessToken(RequiredTokenType.User); Do you have an idea of what can still be causing the 404? |
Beta Was this translation helpful? Give feedback.
-
Hi Pedro, Thanks for your question—you've raised a very good point. We know that the current API and documentation for handling multiple frontends can be a bit confusing. Your feedback is valuable, and we're planning to resolve these ambiguities in the next Release Candidate (RC). Let me clarify a couple of things based on your code. Understanding
|
Beta Was this translation helpful? Give feedback.
-
Thank you for your reply, it was very clarifying! In fact, it was a total surprise for me that the .MappedToOrigin was referring in fact to the BFF and not the origin of the frontend I was 'adding'... This made me re-read the documentation with this information in mind. As I understand now, specially by reading again https://docs.duendesoftware.com/bff/getting-started/multi-frontend/ "This is useful for scenarios where you want to serve several SPAs or frontend apps from the same backend, each with their own authentication and API proxying configuration" I see that the added value of the multi-frontend support is to allow each frontend to have mapped different APIs and/or different OIDC configurations and/or cookie settings. In principle, my scenario is where 2 frontends will share the same APIs and very likely the same OIDC configurations + cookie settings. .AddFrontends(
new BffFrontend(BffFrontendName.Parse("frontend1"))
.MappedToOrigin(Origin.Parse("https://localhost:7082"))
.WithRemoteApis(
new RemoteApi(LocalPath.Parse("/api1"), new Uri("https://localhost:7260/Api1"))
.WithAccessToken(RequiredTokenType.User)
),
new BffFrontend(BffFrontendName.Parse("frontend2"))
.MappedToOrigin(Origin.Parse("https://localhost:7082"))
.WithRemoteApis(
new RemoteApi(LocalPath.Parse("/api1"), new Uri("https://localhost:7260/Api1"))
.WithAccessToken(RequiredTokenType.User)
)
) So can I say that a single frontend configuration will suffice? In case that each frontend will need different OIDC configurations (still to be confirmed on my side) then I see multiple frontends will be required. Sorry for all the questions branching from here, but the filtering on the origin was something I didn't got the first time I read the documentation. And sorry, but again even with your suggestion which makes sense new BffFrontend(BffFrontendName.Parse("frontend1"))
// This should be the actual origin of your BFF server.
.MappedToOrigin(Origin.Parse("https://localhost:7082"))
.WithRemoteApis(
new RemoteApi(LocalPath.Parse("/api1"), new Uri("https://localhost:7260/Api1"))
.WithAccessToken(RequiredTokenType.User)
) I am still getting a 404 on API call GET https://localhost:7082/api1 ... What could be wrong? const loginUrl = "https://localhost:7082/bff/login?returnUrl=https://localhost:5013";
const silentLoginUrl = "https://localhost:7082/bff/silent-login";
let logoutUrl = "https://localhost:7082/bff/logout?returnUrl=https://localhost:5013";
const userUrl = "https://localhost:7082/bff/user";
const api1Url = "https://localhost:7082/api1";
const api2Url = "https://localhost:7082/api2/test";
async function onLoad() {
var req = new Request(userUrl, {
headers: new Headers({
'X-CSRF': '1'
}),
credentials: "include",
})
try {
var resp = await fetch(req);
if (resp.ok) {
let claims = await resp.json();
showUser(claims);
if (claims) {
log("user logged in");
let logoutUrlClaim = claims.find(claim => claim.type === 'bff:logout_url');
if (logoutUrlClaim) {
logoutUrl = logoutUrlClaim.value;
}
}
else {
log("user not logged in");
}
} else if (resp.status === 401) {
log("user not logged in");
// if we've detected that the user is no already logged in, we can attempt a silent login
// this will trigger a normal OIDC request in an iframe using prompt=none.
// if the user is already logged into IdentityServer, then the result will establish a session in the BFF.
// this whole process avoids redirecting the top window without knowing if the user is logged in or not.
var silentLoginResult = await silentLogin();
// the result is a boolean letting us know if the user has been logged in silently
log("silent login result: " + silentLoginResult);
if (silentLoginResult) {
// if we now have a user logged in silently, then reload this window
window.location.reload();
}
}
}
catch (e) {
log("error checking user status");
}
}
onLoad();
function login() {
window.location = loginUrl;
}
function logout() {
window.location = logoutUrl;
}
async function callApi1() {
var req = new Request(api1Url, {
headers: new Headers({
'X-CSRF': '1'
}),
credentials: "include"
})
var resp = await fetch(req);
log("API1 Result: " + resp.status);
if (resp.ok) {
showApi(await resp.json());
}
}
document.querySelector(".login").addEventListener("click", login, false);
document.querySelector(".call_api1").addEventListener("click", callApi1, false);
document.querySelector(".logout").addEventListener("click", logout, false);
function showApi() {
document.getElementById('api-result').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
} else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('api-result').innerText += msg + '\r\n';
});
}
function showUser() {
document.getElementById('user').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
} else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('user').innerText += msg + '\r\n';
});
}
function log() {
document.getElementById('response').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
} else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('response').innerText += msg + '\r\n';
});
}
// this will trigger the silent login and return a promise that resolves to true or false.
function silentLogin(iframeSelector) {
iframeSelector = iframeSelector || "#bff-silent-login";
const timeout = 5000;
return new Promise((resolve, reject) => {
function onMessage(e) {
// look for messages sent from the BFF iframe
if (e.data && e.data['source'] === 'bff-silent-login') {
window.removeEventListener("message", onMessage);
// send along the boolean result
resolve(e.data.isLoggedIn);
}
};
// listen for the iframe response to notify its parent (i.e. this window).
window.addEventListener("message", onMessage);
// we're setting up a time to handle scenarios when the iframe doesn't return immediaetly (despite prompt=none).
// this likely means the iframe is showing the error page at IdentityServer (typically due to client misconfiguration).
window.setTimeout(() => {
window.removeEventListener("message", onMessage);
// we can either just treat this like a "not logged in"
resolve(false);
// or we can trigger an error, so someone can look into the reason why
// reject(new Error("timed_out"));
}, timeout);
// send the iframe to the silent login endpoint to kick off the workflow
document.querySelector(iframeSelector).src = silentLoginUrl;
});
} |
Beta Was this translation helpful? Give feedback.
-
Having multiple frontends with the same selection criteria doesn't work. In the pipeline, I select the first frontend that matches the criteria. I think it's probably a good idea if the system warns if there are multiple frontends that have the same selection criteria. However, to your scenario: I would suggest not using the multi-frontend support for this and just creating a single frontend. Since your api surface and openid connect configuration remains the same, you can just omit the calls to .AddFrontends(). Then mapping the remote api can just be done on the WebApplicationBuilder. As to the 404. I don't see a clear reason why the system returns a 404. Can it be that the api itself returns a 404 for this call? I've been working on a multi-frontend sample. It's a bit different than your situation, and still a work in progress, but here I'm rougly applying the same code and it does work: |
Beta Was this translation helpful? Give feedback.
Having multiple frontends with the same selection criteria doesn't work. In the pipeline, I select the first frontend that matches the criteria. I think it's probably a good idea if the system warns if there are multiple frontends that have the same selection criteria.
However, to your scenario: I would suggest not using the multi-frontend support for this and just creating a single frontend. Since your api surface and openid connect configuration remains the same, you can just omit the calls to .AddFrontends().
Then mapping the remote api can just be done on the WebApplicationBuilder.
As to the 404. I don't see a clear reason why the system returns a 404. Can it be that the api itself re…