An example with minimal dependencies is a loopback within an Okta Org.
Say you want to get to the admin interface of an Org. It's at:
/home/admin-entry
URL encoded (what you need for RelayState) that is:
%2Fhome%2Fadmin-entry
You can verify this by looking at the URL of the Admin link on the /app/UserHome page.
You create a Identity Provider called loopback in Okta. My example has an ACS URL of:
https://dev-971545.oktapreview.com/auth/saml20/loopback
You create an App that is linked to that Identity Provider called loopback. It has a Single Sign-on URL (visible after clicking View Setup Instructions on the Sign-on tab) of:
https://dev-971545.oktapreview.com/app/independentconsultantdev927755_loopback_1/exkadbfail8okn4W80h7/sso/saml
There are two layers of RelayState:
- The app you want Okta to direct you to: in this case the loopback app
- The path within that app
Passing the RelayState to an App SSO URL directly as a URL query parameter works according to SAML specification:
https://dev-971545.oktapreview.com/app/independentconsultantdev927755_loopback_1/exkadbfail8okn4W80h7/sso/saml?RelayState=%2Fhome%2Fadmin-entry
The Okta App SSO URL responds with an HTML form with a RelayState value of /home/admin-entry
HTML entity encoded as /home/admin-entry
. This is correct and SAML specification compliant behavior.
Following that URL brings me to the Admin dashboard.
There's no inbound SAML yet. This is all within Okta. Let's add the SAML layer.
The path of that App SSO URL is:
/app/independentconsultantdev927755_loopback_1/exkadbfail8okn4W80h7/sso/saml?RelayState=%2Fhome%2Fadmin-entry
We URL encode that to prepare it to be the value of a query parameter:
%2Fapp%2Findependentconsultantdev927755_loopback_1%2Fexkadbfail8okn4W80h7%2Fsso%2Fsaml%3FRelayState%3D%252Fhome%252Fadmin-entry
You then drop that on the ACS of the loopback Identity Provider as the RelayState query parameter:
https://dev-971545.oktapreview.com/auth/saml20/loopback?RelayState=%2Fapp%2Findependentconsultantdev927755_loopback_1%2Fexkadbfail8okn4W80h7%2Fsso%2Fsaml%3FRelayState%3D%252Fhome%252Fadmin-entry
The Okta ACS URL responds with an HTML form with a RelayState value of ?RelayState=/app/independentconsultantdev927755_loopback_1/exkadbfail8okn4W80h7/sso/saml?RelayState=%2Fhome%2Fadmin-entry
HTML entity encoded as %3FRelayState%3D%2Fapp%2Findependentconsultantdev927755_loopback_1%2Fexkadbfail8okn4W80h7%2Fsso%2Fsaml%3FRelayState%3D%252Fhome%252Fadmin-entry
. This is INCORRECT and violates the SAML 2.0 specification.
An HTTPS request to an Okta ACS URL with a RelayState query parameter of ActualRelayState
yields an HTTPS response with an HTML form with a RelayState value equal to ?RelayState=ActualRelayState
. It literally prepends ?RelayState=
. It should respond with an HTML form with a RelayState value equal to ActualRelayState
. To clarify, this is INCORRECT and violates the SAML 2.0 specification.
In our case, the ACS with RelayState generates a form which POSTS to Okta which logs you in and then redirects to:
?RelayState=/app/independentconsultantdev927755_loopback_1/exkadbfail8okn4W80h7/sso/saml?RelayState=%2Fhome%2Fadmin-entry
That's completely useless and won't work. Ideally the Okta ACS would write the HTML form with the literal RelayState instead.
So instead, we strip off the leading slash (%2F in the encoded version) and use a fromURI query parameter on the Okta ACS instead of using RelayState:
https://dev-971545.oktapreview.com/auth/saml20/loopback?fromURI=app%2Findependentconsultantdev927755_loopback_1%2Fexkadbfail8okn4W80h7%2Fsso%2Fsaml%3FRelayState%3D%252Fhome%252Fadmin-entry
You can convince yourself that worked or you can open the Network tab of your browser's developer tools and trace the communications. The ACS drops a RelayState prepending the slash (that's why we had to remove it), then gets redirected to the IdP (the loopback App), then gets reflected back to the ACS (preserving the RelayState). Login completes and Okta redirects to the loopback App SSO URL with the RelayState in the query parameter. Okta processes that and redirects to itself and then redirects to the embedded RelayState which takes you to the Admin Dashboard.
This is a working example of Deep Linking from SP-initiated flow from Okta with Okta as a SAML intermediary to another app.