Self-serve portal link without a Kinde SDK
Can someone help me understand where I am going wrong with generating a self-serve portal link? I use Netlify and have paths to call my Express app. Authentication all works, but I can't seem to generate a link for the self-serve portal.
Netlify function error
Aug 21, 01:40:03 PM: 918de926 Duration: 4.61 ms Memory Usage: 146 MB
Aug 21, 01:40:13 PM: e57788d1 INFO GET /portal - User: anonymous
Aug 21, 01:40:13 PM: e57788d1 INFO Got the keys
Aug 21, 01:40:13 PM: e57788d1 INFO Token verification successful
Aug 21, 01:40:14 PM: e57788d1 INFO Access token type: string
Aug 21, 01:40:14 PM: e57788d1 INFO Access token (first 50 chars): eyJhbGciOiJSUzI1NiIsImtpZCI6IjlmOjYzOjVmOjg4OjczOm
Aug 21, 01:40:14 PM: e57788d1 ERROR Portal API error: {"errors":[{"code":"INVALID_CREDENTIALS","message":"Invalid credentials used to access API"},{"code":"MISSING_AUDIENCE","message":"Missing or incorrect Management API audience"}]}
Aug 21, 01:40:14 PM: e57788d1 Duration: 675.77 ms Memory Usage: 146 MB
Aug 21, 01:40:14 PM: 4a9bb456 INFO GET /access/ - User: anonymous
Aug 21, 01:40:14 PM: 4a9bb456 Duration: 6.58 ms Memory Usage: 146 MBAug 21, 01:40:03 PM: 918de926 Duration: 4.61 ms Memory Usage: 146 MB
Aug 21, 01:40:13 PM: e57788d1 INFO GET /portal - User: anonymous
Aug 21, 01:40:13 PM: e57788d1 INFO Got the keys
Aug 21, 01:40:13 PM: e57788d1 INFO Token verification successful
Aug 21, 01:40:14 PM: e57788d1 INFO Access token type: string
Aug 21, 01:40:14 PM: e57788d1 INFO Access token (first 50 chars): eyJhbGciOiJSUzI1NiIsImtpZCI6IjlmOjYzOjVmOjg4OjczOm
Aug 21, 01:40:14 PM: e57788d1 ERROR Portal API error: {"errors":[{"code":"INVALID_CREDENTIALS","message":"Invalid credentials used to access API"},{"code":"MISSING_AUDIENCE","message":"Missing or incorrect Management API audience"}]}
Aug 21, 01:40:14 PM: e57788d1 Duration: 675.77 ms Memory Usage: 146 MB
Aug 21, 01:40:14 PM: 4a9bb456 INFO GET /access/ - User: anonymous
Aug 21, 01:40:14 PM: 4a9bb456 Duration: 6.58 ms Memory Usage: 146 MBCode
/**
* API endpoint: Redirect to self-serve portal
*
* Generates a one-time portal link for the authenticated user and redirects them.
* Uses the user's session to get their access token and call Kinde's Account API.
*
* @route GET /portal
* @middleware protectRoute - Ensures user is authenticated
* @middleware getUser - Adds user object to request
*/
app.get("/portal", protectRoute, getUser, async (req, res) => {
try {
// Get access token from the user's session via Kinde client
const accessToken = await kindeClient.getToken(req);
console.log("Access token type:", typeof accessToken);
console.log("Access token (first 50 chars):", accessToken ? accessToken.substring(0, 50) : "null");
if (!accessToken) {
return res.redirect(`${baseUrl}/access/?error=no-token`);
}
// Generate portal link using Kinde Account API
const portalResponse = await fetch(
`${kindeConfig.issuerBaseUrl}/api/v1/account_api/portal_link`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
return_url: `${baseUrl}/access/`,
}),
},
);
if (!portalResponse.ok) {
console.error("Portal API error:", await portalResponse.text());
return res.redirect(`${baseUrl}/access/?error=portal-failed`);
}
const portalData = await portalResponse.json();
// Redirect user to the portal
res.redirect(portalData.url);
} catch (error) {
console.error("Portal generation error:", error);
res.redirect(`${baseUrl}/access/?error=portal-error`);
}
});/**
* API endpoint: Redirect to self-serve portal
*
* Generates a one-time portal link for the authenticated user and redirects them.
* Uses the user's session to get their access token and call Kinde's Account API.
*
* @route GET /portal
* @middleware protectRoute - Ensures user is authenticated
* @middleware getUser - Adds user object to request
*/
app.get("/portal", protectRoute, getUser, async (req, res) => {
try {
// Get access token from the user's session via Kinde client
const accessToken = await kindeClient.getToken(req);
console.log("Access token type:", typeof accessToken);
console.log("Access token (first 50 chars):", accessToken ? accessToken.substring(0, 50) : "null");
if (!accessToken) {
return res.redirect(`${baseUrl}/access/?error=no-token`);
}
// Generate portal link using Kinde Account API
const portalResponse = await fetch(
`${kindeConfig.issuerBaseUrl}/api/v1/account_api/portal_link`,
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
return_url: `${baseUrl}/access/`,
}),
},
);
if (!portalResponse.ok) {
console.error("Portal API error:", await portalResponse.text());
return res.redirect(`${baseUrl}/access/?error=portal-failed`);
}
const portalData = await portalResponse.json();
// Redirect user to the portal
res.redirect(portalData.url);
} catch (error) {
console.error("Portal generation error:", error);
res.redirect(`${baseUrl}/access/?error=portal-error`);
}
});