> **Building with AI coding agents?** Install the authstack plugin with one command. This equips your agent with accurate Scalekit implementation patterns.
>
> **Recommended** (the `-y` flags skip prompts, so the command runs without an interactive terminal):
> ```bash
> npx -y @scalekit-inc/cli setup -y
> ```
>
> Global:
> ```bash
> npm install -g @scalekit-inc/cli
> scalekit setup -y
> ```
>
> Supports Claude Code, Cursor, GitHub Copilot, Codex + skills for other Agent Skills-compatible agents in `~/.agents/skills`.
> Skills: integrate-agentkit, implement-saaskit, add-mcp-oauth, implement-sso, implement-scim.
> [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Handle the callback and find the user

Build the Modular SSO callback: check the state and errors, exchange the code, find or create the user in your database and start your app's session.
At the end of an SSO login, Scalekit redirects the user to your callback URL. This page builds that endpoint for production. By the end, your app:

- starts each login with a `state` value and rejects responses it didn't ask for
- turns failed logins into a clear message
- finds or creates the user in your database, keyed on a stable ID rather than an email address
- starts your own session

## Before you start

- Complete the [quickstart](/authenticate/sso/add-modular-sso/): your environment is in **Modular Auth** mode, and your callback URL is registered under **Authentication** > **Redirect URLs** > **Allowed callback URLs**.
- Add these to your `.env` file. `REDIRECT_URI` is your registered callback URL. `SESSION_SECRET` signs your app's session cookie; use a long random value.

  ```sh title=".env"
  SCALEKIT_ENVIRONMENT_URL=<your-environment-url>
  SCALEKIT_CLIENT_ID=<your-client-id>
  SCALEKIT_CLIENT_SECRET=<your-client-secret>
  REDIRECT_URI=http://localhost:3000/auth/callback
  SESSION_SECRET=<a-long-random-value>
  ```

## Build the endpoints

1. ## Install the SDK and a web framework

   **Node.js**

   ```sh
   npm install @scalekit-sdk/node express express-session dotenv
   ```

   **Python**

   ```sh
   pip install scalekit-sdk-python flask python-dotenv
   ```

   **Go**

   ```sh
   go get -u github.com/scalekit-inc/scalekit-sdk-go/v2
   ```

   **Java**

   Add `com.scalekit:scalekit-sdk-java` to a Spring Boot web app. See [Install the SDK](/authenticate/sso/add-modular-sso/#set-up-scalekit) for the Maven and Gradle lines.

2. ## Add the login and callback endpoints

   The example has two endpoints:

   - **`/auth/sso`** starts a login. It creates a random `state` value, stores it in the user's browser session and passes it in the authorization URL. It takes the customer's organization ID; in your app, look it up from your records, for example by the email domain the user entered.
   - **`/auth/callback`** finishes it. It checks the response, exchanges the code for the user's profile, finds or creates the user and starts your session.

   **Node.js**

   ```javascript title="server.mjs"
   import crypto from 'node:crypto';
   import 'dotenv/config';
   import express from 'express';
   import session from 'express-session';
   import { ScalekitClient } from '@scalekit-sdk/node';

   const scalekit = new ScalekitClient(
     process.env.SCALEKIT_ENVIRONMENT_URL,
     process.env.SCALEKIT_CLIENT_ID,
     process.env.SCALEKIT_CLIENT_SECRET,
   );
   const redirectUri = process.env.REDIRECT_URI;

   const app = express();
   app.use(
     session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }),
   );

   // Your users table. Replace with your database.
   const users = new Map();

   function findOrCreateUser({ organizationId, idpUserId, email, name }) {
     const key = `${organizationId}:${idpUserId}`;
     let user = users.get(key);
     if (!user) {
       user = { id: crypto.randomUUID(), organizationId, idpUserId };
       users.set(key, user);
     }
     // Keep the profile current: email and name can change at the IdP
     Object.assign(user, { email, name });
     return user;
   }

   // Start an SSO login for one of your customers
   app.get('/auth/sso', (req, res) => {
     const state = crypto.randomBytes(16).toString('hex');
     req.session.ssoState = state;
     const url = scalekit.getAuthorizationUrl(redirectUri, {
       organizationId: req.query.organization_id,
       state,
     });
     res.redirect(url);
   });

   app.get('/auth/callback', async (req, res, next) => {
     const { code, state, error, error_description } = req.query;
     const expectedState = req.session.ssoState;
     delete req.session.ssoState;

     if (error) {
       console.warn(`SSO login failed: ${error}: ${error_description}`);
       return res.status(401).send('Sign-in failed');
     }
     if (!state || state !== expectedState) {
       return res.status(400).send('Sign-in failed: unexpected state');
     }

     let result, claims;
     try {
       result = await scalekit.authenticateWithCode(code, redirectUri);
       // The ID token's oid claim is the user's organization
       claims = await scalekit.validateToken(result.idToken);
     } catch (err) {
       console.warn('Code exchange failed:', err.message);
       return res.status(401).send('Sign-in failed');
     }

     const profile = result.user;
     const user = findOrCreateUser({
       organizationId: claims.oid,
       idpUserId: profile.id.slice(profile.id.indexOf(';') + 1),
       email: profile.email,
       name: [profile.givenName, profile.familyName].filter(Boolean).join(' '),
     });

     // Start a fresh session for the user, so a session ID set before login can't be reused
     req.session.regenerate((err) => {
       if (err) return next(err);
       req.session.userId = user.id;
       res.redirect('/');
     });
   });

   app.listen(3000, () => console.log('Listening on http://localhost:3000'));
   ```

   Run it with `node server.mjs`.

   **Python**

   ```python title="app.py"
   import os
   import secrets

   from dotenv import load_dotenv
   from flask import Flask, redirect, request, session
   from scalekit import AuthorizationUrlOptions, CodeAuthenticationOptions, ScalekitClient

   load_dotenv()

   scalekit_client = ScalekitClient(
       env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
       client_id=os.environ["SCALEKIT_CLIENT_ID"],
       client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
   )
   redirect_uri = os.environ["REDIRECT_URI"]

   app = Flask(__name__)
   app.secret_key = os.environ["SESSION_SECRET"]

   # Your users table. Replace with your database.
   users = {}


   def find_or_create_user(organization_id, idp_user_id, email, name):
       key = (organization_id, idp_user_id)
       user = users.get(key)
       if user is None:
           user = {"id": secrets.token_hex(16), "organization_id": organization_id, "idp_user_id": idp_user_id}
           users[key] = user
       # Keep the profile current: email and name can change at the IdP
       user.update(email=email, name=name)
       return user


   # Start an SSO login for one of your customers
   @app.route("/auth/sso")
   def start_sso():
       state = secrets.token_urlsafe(16)
       session["sso_state"] = state
       options = AuthorizationUrlOptions()
       options.organization_id = request.args.get("organization_id")
       options.state = state
       return redirect(scalekit_client.get_authorization_url(redirect_uri=redirect_uri, options=options))


   @app.route("/auth/callback")
   def callback():
       expected_state = session.pop("sso_state", None)
       error = request.args.get("error")
       if error:
           app.logger.warning("SSO login failed: %s: %s", error, request.args.get("error_description"))
           return "Sign-in failed", 401
       state = request.args.get("state")
       if not state or state != expected_state:
           return "Sign-in failed: unexpected state", 400

       try:
           result = scalekit_client.authenticate_with_code(
               request.args.get("code"), redirect_uri, CodeAuthenticationOptions()
           )
       except Exception as e:
           app.logger.warning("Code exchange failed: %s", e)
           return "Sign-in failed", 401

       profile = result["user"]
       user = find_or_create_user(
           # From the ID token's oid claim: the user's organization
           organization_id=result["organization_id"],
           idp_user_id=profile["id"].split(";", 1)[1],
           email=profile.get("email"),
           name=" ".join(filter(None, [profile.get("givenName"), profile.get("familyName")])),
       )

       # Start a fresh session for the user
       session.clear()
       session["user_id"] = user["id"]
       return redirect("/")


   if __name__ == "__main__":
       app.run(port=3000)
   ```

   Run it with `python app.py`.

   **Go**

   ```go title="main.go"
   package main

   import (
   	"crypto/rand"
   	"encoding/hex"
   	"log"
   	"net/http"
   	"os"
   	"strings"
   	"sync"

   	scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2"
   )

   var (
   	scalekitClient = scalekit.NewScalekitClient(
   		os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
   		os.Getenv("SCALEKIT_CLIENT_ID"),
   		os.Getenv("SCALEKIT_CLIENT_SECRET"),
   	)
   	redirectURI = os.Getenv("REDIRECT_URI")

   	// Your users table. Replace with your database.
   	usersMu sync.Mutex
   	users   = map[string]*AppUser{}
   )

   type AppUser struct {
   	ID, OrganizationID, IdpUserID, Email, Name string
   }

   func findOrCreateUser(organizationID, idpUserID, email, name string) *AppUser {
   	usersMu.Lock()
   	defer usersMu.Unlock()
   	key := organizationID + ":" + idpUserID
   	user, ok := users[key]
   	if !ok {
   		user = &AppUser{ID: randomHex(), OrganizationID: organizationID, IdpUserID: idpUserID}
   		users[key] = user
   	}
   	// Keep the profile current: email and name can change at the IdP
   	user.Email, user.Name = email, name
   	return user
   }

   func randomHex() string {
   	b := make([]byte, 16)
   	if _, err := rand.Read(b); err != nil {
   		panic(err)
   	}
   	return hex.EncodeToString(b)
   }

   // Start an SSO login for one of your customers
   func startSSO(w http.ResponseWriter, r *http.Request) {
   	state := randomHex()
   	http.SetCookie(w, &http.Cookie{
   		Name: "sso_state", Value: state, Path: "/auth", MaxAge: 600,
   		HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode,
   	})
   	authURL, err := scalekitClient.GetAuthorizationUrl(redirectURI, scalekit.AuthorizationUrlOptions{
   		OrganizationId: r.URL.Query().Get("organization_id"),
   		State:          state,
   	})
   	if err != nil {
   		http.Error(w, "Could not start sign-in", http.StatusInternalServerError)
   		return
   	}
   	http.Redirect(w, r, authURL.String(), http.StatusFound)
   }

   func callback(w http.ResponseWriter, r *http.Request) {
   	q := r.URL.Query()
   	stateCookie, cookieErr := r.Cookie("sso_state")
   	http.SetCookie(w, &http.Cookie{Name: "sso_state", Path: "/auth", MaxAge: -1})

   	if errCode := q.Get("error"); errCode != "" {
   		log.Printf("SSO login failed: %s: %s", errCode, q.Get("error_description"))
   		http.Error(w, "Sign-in failed", http.StatusUnauthorized)
   		return
   	}
   	if cookieErr != nil || q.Get("state") == "" || q.Get("state") != stateCookie.Value {
   		http.Error(w, "Sign-in failed: unexpected state", http.StatusBadRequest)
   		return
   	}

   	result, err := scalekitClient.AuthenticateWithCode(
   		r.Context(), q.Get("code"), redirectURI, scalekit.AuthenticationOptions{},
   	)
   	if err != nil {
   		log.Printf("Code exchange failed: %v", err)
   		http.Error(w, "Sign-in failed", http.StatusUnauthorized)
   		return
   	}
   	// The ID token's oid claim is the user's organization
   	claims, err := scalekitClient.ValidateToken(r.Context(), result.IdToken)
   	organizationID, _ := claims["oid"].(string)
   	if err != nil || organizationID == "" {
   		log.Printf("ID token check failed: %v", err)
   		http.Error(w, "Sign-in failed", http.StatusUnauthorized)
   		return
   	}

   	profile := result.User
   	_, idpUserID, _ := strings.Cut(profile.Id, ";")
   	name := strings.TrimSpace(profile.GivenName + " " + profile.FamilyName)
   	user := findOrCreateUser(organizationID, idpUserID, profile.Email, name)

   	// Start your app's session for user.ID here, the way your app does after any login
   	log.Printf("Signed in user %s", user.ID)
   	http.Redirect(w, r, "/", http.StatusFound)
   }

   func main() {
   	http.HandleFunc("/auth/sso", startSSO)
   	http.HandleFunc("/auth/callback", callback)
   	log.Fatal(http.ListenAndServe(":3000", nil))
   }
   ```

   Go doesn't load `.env` files on its own: export the variables in your shell, then run `go run main.go`.

   **Java**

   ```java title="SsoController.java"
   package com.example.sso;

   import com.scalekit.ScalekitClient;
   import com.scalekit.internal.http.AuthenticationOptions;
   import com.scalekit.internal.http.AuthenticationResponse;
   import com.scalekit.internal.http.AuthorizationUrlOptions;
   import com.scalekit.internal.http.IdTokenClaims;
   import jakarta.servlet.http.HttpServletRequest;
   import jakarta.servlet.http.HttpSession;
   import java.security.SecureRandom;
   import java.util.HexFormat;
   import java.util.Map;
   import java.util.UUID;
   import java.util.concurrent.ConcurrentHashMap;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;
   import org.springframework.http.ResponseEntity;
   import org.springframework.web.bind.annotation.GetMapping;
   import org.springframework.web.bind.annotation.RequestParam;
   import org.springframework.web.bind.annotation.RestController;

   @RestController
   public class SsoController {

       private static final Logger log = LoggerFactory.getLogger(SsoController.class);

       private final ScalekitClient scalekitClient = new ScalekitClient(
           System.getenv("SCALEKIT_ENVIRONMENT_URL"),
           System.getenv("SCALEKIT_CLIENT_ID"),
           System.getenv("SCALEKIT_CLIENT_SECRET")
       );
       private final String redirectUri = System.getenv("REDIRECT_URI");
       private final SecureRandom random = new SecureRandom();

       // Your users table. Replace with your database.
       public record AppUser(String id, String organizationId, String idpUserId, String email, String name) {}
       private final Map<String, AppUser> users = new ConcurrentHashMap<>();

       private AppUser findOrCreateUser(String organizationId, String idpUserId, String email, String name) {
           // Keep the profile current: email and name can change at the IdP
           return users.compute(organizationId + ":" + idpUserId, (key, existing) -> new AppUser(
               existing != null ? existing.id() : UUID.randomUUID().toString(),
               organizationId, idpUserId, email, name));
       }

       // Start an SSO login for one of your customers
       @GetMapping("/auth/sso")
       public ResponseEntity<Void> startSso(
               @RequestParam("organization_id") String organizationId, HttpSession session) {
           byte[] bytes = new byte[16];
           random.nextBytes(bytes);
           String state = HexFormat.of().formatHex(bytes);
           session.setAttribute("ssoState", state);

           AuthorizationUrlOptions options = new AuthorizationUrlOptions();
           options.setOrganizationId(organizationId);
           options.setState(state);
           String url = scalekitClient.authentication()
               .getAuthorizationUrl(redirectUri, options)
               .toString();
           return ResponseEntity.status(302).header("Location", url).build();
       }

       @GetMapping("/auth/callback")
       public ResponseEntity<String> callback(
               @RequestParam Map<String, String> params, HttpServletRequest request) {
           HttpSession session = request.getSession();
           String expectedState = (String) session.getAttribute("ssoState");
           session.removeAttribute("ssoState");

           String error = params.get("error");
           if (error != null) {
               log.warn("SSO login failed: {}: {}", error, params.get("error_description"));
               return ResponseEntity.status(401).body("Sign-in failed");
           }
           String state = params.get("state");
           if (state == null || !state.equals(expectedState)) {
               return ResponseEntity.status(400).body("Sign-in failed: unexpected state");
           }

           AuthenticationResponse result;
           Map<String, Object> claims;
           try {
               result = scalekitClient.authentication()
                   .authenticateWithCode(params.get("code"), redirectUri, new AuthenticationOptions());
               // Validates the ID token's signature; its oid claim is the user's organization
               claims = scalekitClient.authentication()
                   .validateAccessTokenAndGetClaims(result.getIdToken());
           } catch (Exception e) {
               log.warn("Code exchange failed: {}", e.getMessage());
               return ResponseEntity.status(401).body("Sign-in failed");
           }

           IdTokenClaims profile = result.getIdTokenClaims();
           String sub = profile.getId();
           String name = String.join(" ",
               java.util.stream.Stream.of(profile.getGivenName(), profile.getFamilyName())
                   .filter(n -> n != null && !n.isBlank())
                   .toList());
           AppUser user = findOrCreateUser(
               (String) claims.get("oid"),
               sub.substring(sub.indexOf(';') + 1),
               profile.getEmail(),
               name);

           // Start a fresh session for the user, so a session ID set before login can't be reused
           session.invalidate();
           request.getSession(true).setAttribute("userId", user.id());
           return ResponseEntity.status(302).header("Location", "/").build();
       }
   }
   ```

3. ## Check the response before you trust it

   The callback handles three outcomes, in this order:

   - **An `error` parameter.** The login failed at Scalekit or at the identity provider (IdP). Log `error` and `error_description`, and show the user a short message rather than the raw text. The codes and their fixes are in the [SSO error reference](/sso/reference/sso-integration-errors/).
   - **A `state` that doesn't match.** The response isn't for a login this browser started, for example a forged link. Reject it. The stored value is removed on each callback, so the same response can't be used twice.
   - **A `code`.** Exchange it once, with the same redirect URI you passed to the authorization URL. The SDK checks the ID token's signature and returns the user's profile.

   > note
   >
   > Your [initiate login endpoint](/sso/guides/idp-init-sso/) also builds an authorization URL, for users who start from their IdP's dashboard. Store a `state` value there the same way, or those logins fail the check.

4. ## Find or create the user

   Key each user on two values, not on their email:

   - **The organization ID**, which says which customer the user belongs to.
   - **The user's ID at the IdP**: the part of the profile's `id` (the ID token's `sub`) after the `;`. The part before it is the connection ID, which changes if your customer deletes their SSO connection and creates a new one.

   Email addresses change when a user is renamed, and two customers' IdPs can each assert any address, so an email alone can't safely identify a user. Store the email and name, and update them at each login. Some IdPs send a changeable value, such as an email address, as the user's ID; see [Choose a stable user identifier](/sso/guides/user-profile-details/#choose-a-stable-user-identifier), which covers Microsoft Entra ID.

   If your app already has users who signed in another way, link an SSO user to an existing account only after a check you trust, such as the email domain being one of the organization's verified domains.

5. ## Start your session

   Scalekit doesn't return a refresh token: after the callback, your app runs the session for as long as your app decides. Start it the way your app does after any login. When the session ends, send the user through SSO again; if they're still signed in at their IdP, they come straight back.

   To log a user out, end your app's session.

6. ## Check it worked

   1. Start your app.
   2. Find your test organization's ID under **Organizations** in the dashboard of your development environment.
   3. Open `http://localhost:3000/auth/sso?organization_id=<test-organization-id>`. The SSO simulator opens.
   4. Leave **User Login via SSO** selected, enter a user name such as `jane` (the domain is `example.com`) and a first and last name, and select **Simulate Login**.

   The callback redirects you to `/`, which this example doesn't serve, so a "not found" page is expected. Your users table has a user with the test organization's ID, `jane@example.com` and the name you entered. Open `http://localhost:3000/auth/callback?code=x&state=forged`: the response is `400 Sign-in failed: unexpected state`.

## Next

- [Onboard an enterprise customer](/sso/guides/onboard-enterprise-customers/): create their organization and give their IT admin the admin portal.
- [Map custom user attributes](/sso/guides/sso-user-attributes/): send roles or groups from the IdP and use them in your app.


---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
