BFF v4 - External API not being mapped #336
-
|
Hello, This is a follow-up of the thread https://github.com/orgs/DuendeSoftware/discussions/322 where at the end I couldn't still find the cause of getting a 404 on a GET to an external mapped API. In the meantime I started using two separate frontend and no default frontend anymore (the project needs distinct clientIds when connecting to the IdP, so therefore the need for two frontend arose). I double checked my setup and all seems correct, including the API mapping. Then attempted to debug the BFF itself, by removing the Duende.BFF.Yarp nuget and adding a project reference to your products\bff\src\Bff.Yarp\Bff.Yarp.csproj. My setup is the following:
For simplification, I am logging in to the IdP and calling the API just from the mobile frontend (https://localhost:5014)/ The problem is that I can able to do login, call the user endpoint, all of that, except that I get a 404 as result of calling the API with a GET By debugging the BFF, I see this
The request is a 'GET https://localhost:7082/mobile/api1' which seems correct to me. The 'trie' dictionary has both the frontends. But this method is trying to find a match of '/api' as the key, that is, trying to find a frontend named /api which I find odd... Naturally 2 levels up at the callstack, we see that no frontend will be selected, resorting to the default frontend which is null, because I didn't set any:
Is this normal to happen? Below is a simplified version of my source code: My BFF host: var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddCors(opt =>
{
opt.AddDefaultPolicy(policy =>
{
policy
.WithOrigins("https://localhost:5013", "https://localhost:5014")
.WithHeaders("x-csrf", "content-type")
.AllowCredentials();
});
});
builder.Services
.AddBff(options =>
{
options.SessionCleanupInterval = TimeSpan.FromMinutes(config.SessionCleanupIntervalInMinutes);
options.RevokeRefreshTokenOnLogout = false;
options.AntiForgeryHeaderName = config.AntiForgeryHeaderName;
options.AntiForgeryHeaderValue = config.AntiForgeryHeaderValue;
options.SetAnonymousSessionResponse(config);
options.RequireLogoutSessionId = config.RequireLogoutSessionId;
options.LicenseKey = config.LicenseKey;
})
.AddFrontends(
new BffFrontend(BffFrontendName.Parse("web"))
.WithOpenIdConnectOptions(op =>
{
op.Authority = "https://idp";
op.ClientId = "clientId1";
op.ClientSecret = "secret1";
op.ResponseType = "code";
op.ResponseMode = "query";
op.GetClaimsFromUserInfoEndpoint = true;
op.MapInboundClaims = false;
op.SaveTokens = true;
op.SetScope(config);
op.TokenValidationParameters = new()
{
NameClaimType = "name",
RoleClaimType = "role"
};
})
.WithCookieOptions(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Name = config.CookieName;
options.SetCookieSameSite(config);
options.SlidingExpiration = config.CookieSlidingIsEnabled;
options.ExpireTimeSpan = TimeSpan.FromMinutes(config.CookieExpireTimeInMinutes);
})
.MappedToPath(LocalPath.Parse("/web"))
.WithRemoteApis(
new RemoteApi(LocalPath.Parse("/api1"), new Uri("https://localhost:7260/Api1"))
.WithAccessToken(RequiredTokenType.User)
),
new BffFrontend(BffFrontendName.Parse("mobile"))
.WithOpenIdConnectOptions(op =>
{
op.Authority = "https://idp";
op.ClientId = "clientId2";
op.ClientSecret = "secret2";
op.ResponseType = "code";
op.ResponseMode = "query";
op.GetClaimsFromUserInfoEndpoint = true;
op.MapInboundClaims = false;
op.SaveTokens = true;
op.SetScope(config);
op.TokenValidationParameters = new()
{
NameClaimType = "name",
RoleClaimType = "role"
};
})
.WithCookieOptions(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.Name = config.CookieName;
options.SetCookieSameSite(config);
options.SlidingExpiration = config.CookieSlidingIsEnabled;
options.ExpireTimeSpan = TimeSpan.FromMinutes(config.CookieExpireTimeInMinutes);
})
.MappedToPath(LocalPath.Parse("/mobile"))
.WithRemoteApis(
new RemoteApi(LocalPath.Parse("/api1"), new Uri("https://localhost:7260/Api1"))
.WithAccessToken(RequiredTokenType.User)
)
)
.AddServerSideSessions<RedisUserSessionStore>();
// Custom services
builder.Services.AddTransient<IUserSessionStoreCleanup, RedisUserSessionStore>();
builder.Services.AddTransient<IReturnUrlValidator, FrontendHostReturnUrlValidator>();
var app = builder.Build();
app.UseBffPreProcessing();
app.UseCors();
app.UseRouting();
app.UseAuthentication();
app.UseBff();
app.UseAuthorization();
app.MapControllers();
app.Run();Frontend Mobile: const loginUrl = "https://localhost:7082/mobile/bff/login?returnUrl=https://localhost:5014";
const silentLoginUrl = "https://localhost:7082/mobile/bff/silent-login";
let logoutUrl = "https://localhost:7082/mobile/bff/logout?returnUrl=https://localhost:5014";
const userUrl = "https://localhost:7082/mobile/bff/user";
const api1Url = "https://localhost:7082/mobile/api1";
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");
}
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;
});
}HTML: <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title></title>
<link href="libs/bootstrap.min.css" rel="stylesheet"/>
<link href="StyleSheet.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<header class="page-header">
<h1>Hello BFF</h1>
</header>
<div class="row">
<ul class="list-unstyled list-inline">
<li><a class="btn btn-primary" href="index.html">Home</a></li>
<li>
<button class="btn btn-default login">Login</button>
</li>
<li>
<button class="btn btn-primary call_api1">Call API1</button>
</li>
<li>
<button class="btn btn-info logout">Logout</button>
</li>
</ul>
</div>
<div class="row">
<ul class="list-unstyled list-inline">
</ul>
</div>
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">Message</div>
<div class="panel-body">
<pre id="response"></pre>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">Current User</div>
<div class="panel-body">
<pre id="user"></pre>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">API Result</div>
<div class="panel-body">
<pre id="api-result"></pre>
</div>
</div>
</div>
</div>
</div>
<script src="app.js"></script>
<iframe id="bff-silent-login"></iframe>
</body>
</html> |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
|
Seems the cause for getting the 404 when calling the API was that I was missing the I did not include the |
Beta Was this translation helpful? Give feedback.


Seems the cause for getting the 404 when calling the API was that I was missing the
.AddRemoteApis()at theAddBffcall. Also I need to remove theapp.UseBffPreProcessing()since that is automatically done by default.I did not include the
.AddRemoteApis()in the first place since I can see it only be mentioned at the single frontend (https://docs.duendesoftware.com/bff/getting-started/single-frontend/) but not at the multi-frontend (https://docs.duendesoftware.com/bff/getting-started/multi-frontend/) Getting Started pages. Perhaps it make sense to be mentioned at the documentation there?