Fix: SAML ACS Now Accepts Absolute RelayState URLs
Fix: SAML ACS Now Accepts Absolute RelayState URLs
Version: 1.0.54
Overview
In v1.0.54 we resolved a bug in the SAML Single Sign-On (SSO) callback handler where post-login redirects silently fell back to /dashboard whenever an Identity Provider (IdP) supplied an absolute URL as the RelayState parameter. Users were authenticated successfully, but they were not returned to the page they originally intended to visit.
Background: What Is RelayState?
In a SAML 2.0 authentication flow, RelayState is an opaque string passed through the IdP round-trip. Service Providers (SPs) typically encode the user's original destination URL into RelayState before redirecting to the IdP, then read it back from the ACS (Assertion Consumer Service) callback to redirect the user after a successful login.
The SAML 2.0 specification does not mandate whether RelayState must be a relative or absolute URL — both forms are used in practice, and many enterprise IdPs (including some configurations of Okta, Azure AD, and PingFederate) issue absolute URLs.
What Was Broken
The ACS callback at src/app/api/auth/sso/[orgSlug]/saml/callback/route.ts contained the following guard:
// Before fix (v1.0.53 and earlier)
const callbackUrl =
typeof relayState === 'string' && relayState.startsWith('/')
? relayState
: '/dashboard';
Because 'https://app.example.com/dashboard'.startsWith('/') is false, any absolute RelayState URL — even one pointing to the same application — was discarded. The user would land on /dashboard regardless of where they were trying to go.
The Fix
The guard was updated to additionally accept absolute URLs that match the application's own origin:
// After fix (v1.0.54)
const callbackUrl =
typeof relayState === 'string' &&
(
relayState.startsWith('/') ||
relayState.startsWith(process.env.NEXT_PUBLIC_APP_URL ?? '')
)
? relayState
: '/dashboard';
Why This Approach Is Safe
The fix deliberately avoids accepting any absolute URL. Only URLs whose prefix matches NEXT_PUBLIC_APP_URL (the application's own origin) pass the check. This means:
- Same-origin absolute URLs — accepted, redirect works as intended.
- Relative paths — accepted, unchanged behaviour.
- External/cross-origin URLs — rejected, user is sent to
/dashboard. This prevents open-redirect attacks where a maliciousRelayStatecould forward a freshly authenticated user to a phishing site.
Configuration Requirement
For the fix to take effect you must ensure NEXT_PUBLIC_APP_URL is set in your environment to the fully-qualified origin of your deployment:
# .env (or your hosting environment)
NEXT_PUBLIC_APP_URL=https://app.example.com
If this variable is not set, the fallback is an empty string (''), which means relayState.startsWith('') is always true — accepting any RelayState value including external URLs. Always set NEXT_PUBLIC_APP_URL in production.
Who Is Affected
You are affected by this bug if:
- Your organisation uses SAML SSO to authenticate learners.
- Your SAML IdP is configured to echo back an absolute URL (rather than a relative path) as the
RelayState. - Users reported being dropped on the dashboard after login instead of the course or page they were navigating to.
Upgrading
- Deploy v1.0.54 of the platform.
- Confirm
NEXT_PUBLIC_APP_URLis set correctly in all environments. - Test a SAML login flow with a deep-link URL (e.g. navigate directly to a course page, get redirected to the IdP, complete login, and verify you land on the correct course page).