BFF v4 - Logout request is mapped to the frontend itself? #329
-
|
Hello, My setup is the same as mentioned at https://github.com/orgs/DuendeSoftware/discussions/322, so I am using v4.0.0-rc.1 and where a frontend runs at a different host than the BFF host and an external API is being mapped but simplified now to use the default frontend: 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;
})
.AddRemoteApis();
builder.Services.AddTransient<IReturnUrlValidator, FrontendHostReturnUrlValidator>();
var app = builder.Build();
app.UseCors();
app.UseAuthentication();
app.UseRouting();
app.UseBff();
app.UseAuthorization();
app.MapControllers();
app.MapRemoteBffApiEndpoint("/api1", new Uri("https://localhost:7260/Api1"))
.WithAccessToken(RequiredTokenType.User)
.WithAccessTokenRetriever<MultiApiAccessTokenRetriever>();
app.Run();The frontend is based from the one we get from one of the the Duende BFF templates: index.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>app.js: 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";
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;
});
}It happens that when I do the logout the frontend (hosted at https://localhost:5013) does a GET to https://localhost:5013/bff/logout . Apparently it doesn't reach the BFF and for sure the IdP doesn't receive any logout request as the user is still logged in:
I compared to the SplitHost sample for v3 https://github.com/DuendeSoftware/samples/blob/main/BFF/v3/SplitHosts/FrontendHost/wwwroot/index.html and besides some different html and javascript the logout URL follows the pattern I am using: Am I misconfiguring something? |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
|
In your app.js file, you have the following code when a user logs in: let logoutUrlClaim = claims.find(claim => claim.type === 'bff:logout_url');
if (logoutUrlClaim) {
logoutUrl = logoutUrlClaim.value;
}It's possible that the |
Beta Was this translation helpful? Give feedback.

In your app.js file, you have the following code when a user logs in:
It's possible that the
bff:logout_urlclaim contains a relative URL/bff/logout, which causes the URL to not work since there is no such endpoint on the frontend app side. If you keep the constant value as defined at the top of the file, logging out should work.