Handle identity provider initiated SSO
Learn how to securely implement IdP-initiated Single Sign-On for your application
This guide shows you how to securely implement Identity Provider (IdP)-initiated Single Sign-On for your application. When users log into your application directly from their identity provider’s portal, Scalekit converts the IdP-initiated request to a Service Provider (SP)-initiated flow for enhanced security.
Review the authentication sequence
The workflow converts the traditional IdP-initiated flow to a secure SP-initiated flow by:
- The user logs into their identity provider portal and selects your application
- The identity provider sends user details as assertions to Scalekit
- Scalekit redirects to your initiate login endpoint with a JWT token
- Your application validates the JWT and generates a new SP-initiated authorization URL
To securely implement IdP-initiated SSO, follow these steps to convert incoming IdP-initiated requests to SP-initiated flows:
- Set up an initiate login endpoint and register it in Dashboard > Developers > Redirect URLs > Initiate Login URL
- Extract information from the JWT token containing organization, connection, and user details
- Convert to SP-initiated flow using the extracted parameters to generate a new authorization URL
- Handle errors with proper callback processing and error handling best practices
Implementation examples
Section titled “Implementation examples”Use the extracted parameters to initiate a new SSO request. This converts the IdP-initiated flow to a secure SP-initiated flow. Here are implementation examples:
5 collapsed lines
// Security: ALWAYS verify requests are from Scalekit before processing// This prevents unauthorized parties from triggering your interceptor logic
// Use case: Handle IdP-initiated SSO requests from enterprise customer portals// Examples: Okta dashboard, Azure AD portal, Google Workspace apps
const express = require('express');const app = express();
app.get('/login', async (req, res) => { try { // Your Initiate Login Endpoint receives a JWT const { error_description, idp_initiated_login } = req.query;
if (error_description) { return res.redirect('/login?error=auth_failed'); }
// Decode the JWT and extract claims if (idp_initiated_login) { const { connection_id, organization_id, login_hint, relay_state } = await scalekit.getIdpInitiatedLoginClaims(idp_initiated_login);
// Use ONE of the following properties for authorization const options = {}; if (connection_id) options.connectionId = connection_id; if (organization_id) options.organizationId = organization_id; if (login_hint) options.loginHint = login_hint; if (relay_state) options.state = relay_state;
// Generate Authorization URL for SP-initiated flow const url = scalekit.getAuthorizationUrl( process.env.REDIRECT_URI, options );
return res.redirect(url); }
// Handle regular login flow here res.redirect('/login'); } catch (error) { console.error('IdP-initiated login error:', error); res.redirect('/login?error=auth_failed'); }});5 collapsed lines
# Security: ALWAYS verify requests are from Scalekit before processing# This prevents unauthorized parties from triggering your interceptor logic
# Use case: Handle IdP-initiated SSO requests from enterprise customer portals# Examples: Okta dashboard, Azure AD portal, Google Workspace apps
from flask import Flask, request, redirect, url_forimport os
app = Flask(__name__)
@app.route('/login')def login(): try: # Your Initiate Login Endpoint receives a JWT error_description = request.args.get('error_description') idp_initiated_login = request.args.get('idp_initiated_login')
if error_description: return redirect(url_for('login', error='auth_failed'))
# Decode the JWT and extract claims if idp_initiated_login: claims = await scalekit_client.get_idp_initiated_login_claims(idp_initiated_login)
# Extract claims with fallbacks connection_id = claims.get('connection_id') organization_id = claims.get('organization_id') login_hint = claims.get('login_hint') relay_state = claims.get('relay_state')
# Create authorization options options = AuthorizationUrlOptions() if connection_id: options.connection_id = connection_id if organization_id: options.organization_id = organization_id if login_hint: options.login_hint = login_hint if relay_state: options.state = relay_state
# Generate Authorization URL for SP-initiated flow authorization_url = scalekit_client.get_authorization_url( redirect_uri=os.getenv('REDIRECT_URI'), options=options )
return redirect(authorization_url)
# Handle regular login flow here return redirect(url_for('login')) except Exception as error: print(f"IdP-initiated login error: {error}") return redirect(url_for('login', error='auth_failed'))5 collapsed lines
// Security: ALWAYS verify requests are from Scalekit before processing// This prevents unauthorized parties from triggering your interceptor logic
// Use case: Handle IdP-initiated SSO requests from enterprise customer portals// Examples: Okta dashboard, Azure AD portal, Google Workspace apps
package main
import ( "net/http" "github.com/gin-gonic/gin")
func (a *App) handleLogin(c *gin.Context) { // Your Initiate Login Endpoint receives a JWT errDescription := c.Query("error_description") idpInitiatedLogin := c.Query("idp_initiated_login")
if errDescription != "" { c.Redirect(http.StatusFound, "/login?error=auth_failed") return }
// Decode the JWT and extract claims if idpInitiatedLogin != "" { claims, err := scalekitClient.GetIdpInitiatedLoginClaims(c.Request.Context(), idpInitiatedLogin) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return }
// Create authorization options with ONE of the following properties options := scalekit.AuthorizationUrlOptions{} if claims.ConnectionID != "" { options.ConnectionId = claims.ConnectionID } if claims.OrganizationID != "" { options.OrganizationId = claims.OrganizationID } if claims.LoginHint != "" { options.LoginHint = claims.LoginHint } if claims.RelayState != "" { options.State = claims.RelayState }
// Generate Authorization URL for SP-initiated flow authUrl, err := scalekitClient.GetAuthorizationUrl(redirectUrl, options) if err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) return }
c.Redirect(http.StatusFound, authUrl.String()) return }
// Handle regular login flow here c.Redirect(http.StatusFound, "/login")}5 collapsed lines
// Security: ALWAYS verify requests are from Scalekit before processing// This prevents unauthorized parties from triggering your interceptor logic
// Use case: Handle IdP-initiated SSO requests from enterprise customer portals// Examples: Okta dashboard, Azure AD portal, Google Workspace apps
import org.springframework.web.bind.annotation.*;import org.springframework.web.servlet.view.RedirectView;import javax.servlet.http.HttpServletResponse;
@RestControllerpublic class AuthController {
@GetMapping("/login") public RedirectView handleLogin( @RequestParam(required = false, name = "error_description") String errorDescription, @RequestParam(required = false, name = "idp_initiated_login") String idpInitiatedLoginToken, HttpServletResponse response) throws IOException {
if (errorDescription != null) { return new RedirectView("/login?error=auth_failed"); }
// Decode the JWT and extract claims if (idpInitiatedLoginToken != null) { IdpInitiatedLoginClaims claims = scalekitClient.authentication() .getIdpInitiatedLoginClaims(idpInitiatedLoginToken);
if (claims == null) { response.sendError(HttpStatus.BAD_REQUEST.value(), "Invalid idp_initiated_login token"); return null; }
// Create authorization options with ONE of the following AuthorizationUrlOptions options = new AuthorizationUrlOptions(); if (claims.getConnectionID() != null) { options.setConnectionId(claims.getConnectionID()); } if (claims.getOrganizationID() != null) { options.setOrganizationId(claims.getOrganizationID()); } if (claims.getLoginHint() != null) { options.setLoginHint(claims.getLoginHint()); } if (claims.getRelayState() != null) { options.setState(claims.getRelayState()); }
// Generate Authorization URL for SP-initiated flow String url = scalekitClient.authentication() .getAuthorizationUrl(redirectUrl, options) .toString();
response.sendRedirect(url); return null; }
// Handle regular login flow here return new RedirectView("/login"); }}Implementation details
Section titled “Implementation details”Endpoint setup
Section titled “Endpoint setup”Implement this as a backend route, not a frontend page. Scalekit redirects the browser to the Initiate Login URL with a GET request; your server reads the idp_initiated_login parameter, validates it, and issues its own redirect to continue the flow — the route never renders a page.
Your initiate login endpoint will receive requests with the following format:
https://yourapp.com/login?idp_initiated_login=<encoded_jwt_token>JWT token structure
Section titled “JWT token structure”The idp_initiated_login parameter contains a signed JWT with organization, connection, and user details.
View JWT structure
{ "organization_id": "org_225336910XXXX588", "connection_id": "conn_22533XXXXX575236", "login_hint": "name@example.com", "exp": 1723042087, "nbf": 1723041787, "iat": 1723041787, "iss": "https://b2b-app.com"}Error callback format
Section titled “Error callback format”If errors occur, the redirect URI will receive a callback with this format:
https://{your-subdomain}.scalekit.dev/callback ?error="<error_category>" &error_description="<details>"After completing the SP-initiated flow, users are redirected back to your callback URL where you can complete the authentication process. Next, let’s look at how to test your IdP-initiated SSO implementation.
Integrating with a downstream auth provider
Section titled “Integrating with a downstream auth provider”If your application uses a third-party service like Firebase Authentication to manage user sessions, you must initiate its sign-in flow after completing Step 3.
This process has two stages: first, the IdP redirects the user to your app via Scalekit, and second, your app triggers a new sign-in flow with Firebase using the Authorization URL you just generated.
Review the downstream auth flow
The example below shows how to pass the Authorization URL to the Firebase Web SDK.
import { getAuth, OAuthProvider, signInWithRedirect } from "firebase/auth";
// Security: Configure OIDC provider properly to prevent token injectionconst auth = getAuth();
// "scalekit" is the OIDC provider you configured in Firebaseconst scalekitProvider = new OAuthProvider("scalekit");
// Use the authorizationUrl generated in Step 3scalekitProvider.setCustomParameters({ connection_id: "<connection_id>", // Enables Firebase to forward the connection ID to Scalekit});
// Initiate Firebase sign-in with Scalekit providersignInWithRedirect(auth, scalekitProvider);Security considerations
Section titled “Security considerations”While IdP-initiated SSO offers convenience, it comes with significant security risks. Scalekit’s approach converts the flow to SP-initiated to mitigate these vulnerabilities.
Traditional IdP-initiated SSO security risks
Section titled “Traditional IdP-initiated SSO security risks”Stolen SAML assertions: Attackers can steal SAML assertions and use them to gain unauthorized access. If an attacker manages to steal these assertions, they can:
- Inject them into another service provider, gaining access to that user’s account
- Inject them back into your application with altered assertions, potentially elevating their privileges
With a stolen SAML assertion, an attacker can gain access to your application as the compromised user, bypassing the usual authentication process.
How attackers steal SAML assertions
Section titled “How attackers steal SAML assertions”Attackers can steal SAML assertions through various methods:
- Man-in-the-middle (MITM) attacks: Intercepting and replacing the SAML response during transmission
- Open redirect attacks: Exploiting improper endpoint validation to redirect the SAML response to a malicious server
- Leaky logs and headers: Sensitive information, including SAML assertions, can be leaked through logs or headers
- Browser-based attacks: Exploiting browser vulnerabilities to steal SAML assertions
The challenge for service providers
Section titled “The challenge for service providers”The chief problem with stolen assertions is that everything appears legitimate to the service provider (your application). The message and assertion are valid, issued by the expected identity provider, and signed with the expected key. However, the service provider cannot verify whether the assertions are stolen or not.
If you encounter issues implementing IdP-initiated SSO:
- Verify configuration: Ensure your redirect URI is properly configured in Dashboard > Developers > Redirect URLs
- Check JWT processing: Verify you’re correctly processing the JWT token from the
idp_initiated_loginparameter - Validate error handling: Ensure your error handling properly captures and processes any error messages
- Test connections: Confirm the organization and connection IDs in the JWT are valid and active
- Review logs: Check both your application logs and Scalekit dashboard logs for debugging information