> **Building with AI coding agents?** If you're using an AI coding agent, install the official Scalekit plugin. It gives your agent full awareness of the Scalekit API — reducing hallucinations and enabling faster, more accurate code generation.
>
> - **Claude Code**: `/plugin marketplace add scalekit-inc/claude-code-authstack` then `/plugin install <auth-type>@scalekit-auth-stack`
> - **GitHub Copilot CLI**: `copilot plugin marketplace add scalekit-inc/github-copilot-authstack` then `copilot plugin install <auth-type>@scalekit-auth-stack`
> - **Codex**: run the bash installer, restart, then open Plugin Directory and enable `<auth-type>`
> - **Skills CLI** (Windsurf, Cline, 40+ agents): `npx skills add scalekit-inc/skills --list` then `--skill <skill-name>`
>
> `<auth-type>` / `<skill-name>`: `agent-auth`, `full-stack-auth`, `mcp-auth`, `modular-sso`, `modular-scim` — [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Preserve target route post-auth

Users may bookmark specific pages of your app, but their session might be expired. They need to be redirected to the page they asked for after authentication. That means your app needs to preserve the user's original destination.

You will capture the user's original destination, carry it through the OAuth flow safely, and redirect back after login. You will prevent open-redirect attacks by validating and signing the return URL.
**Two safe patterns:** Use either `state` embedding (short paths only) or a signed `return_to` cookie. Avoid passing raw URLs in query strings without validation.

1. ## Capture the intended destination

   When an unauthenticated user requests a protected route, capture its path.

   ```javascript title="Express.js"
   app.get('/login', (req, res) => {
     const nextPath = typeof req.query.next === 'string' ? req.query.next : '/'
     // Only allow internal paths
     const safe = nextPath.startsWith('/') && !nextPath.startsWith('//') ? nextPath : '/'
     res.cookie('sk_return_to', safe, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' })
     // build authorization URL next
   })
   ```
   ```python title="Flask"
   @app.route('/login')
   def login():
       next_path = request.args.get('next', '/')
       safe = next_path if next_path.startswith('/') and not next_path.startswith('//') else '/'
       resp = make_response()
       resp.set_cookie('sk_return_to', safe, httponly=True, secure=True, samesite='Lax', path='/')
       return resp
   ```
   ```go title="Gin"
   func login(c *gin.Context) {
     nextPath := c.Query("next")
     if nextPath == "" || !strings.HasPrefix(nextPath, "/") || strings.HasPrefix(nextPath, "//") {
       nextPath = "/"
     }
     cookie := &http.Cookie{Name: "sk_return_to", Value: nextPath, HttpOnly: true, Secure: true, Path: "/"}
     http.SetCookie(c.Writer, cookie)
   }
   ```
   ```java title="Spring"
   @GetMapping("/login")
   public void login(HttpServletRequest request, HttpServletResponse response) {
     String nextPath = Optional.ofNullable(request.getParameter("next")).orElse("/");
     boolean safe = nextPath.startsWith("/") && !nextPath.startsWith("//");
     Cookie cookie = new Cookie("sk_return_to", safe ? nextPath : "/");
     cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setPath("/");
     response.addCookie(cookie);
   }
   ```
**Reading cookies in Express:** If you access `req.cookies` in Node.js, enable cookie parsing middleware (for example, `cookie-parser`) early in your server setup.

2. ## Build the authorization URL

   Generate the authorization URL as in the quickstart. Optionally include a short hint in `state` like `"n=/billing"` after signing or encoding.

   ```javascript title="Express.js" {5-10}
   const redirectUri = 'https://your-app.com/auth/callback'
   const options = { scopes: ['openid','profile','email','offline_access'] }
   const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options)
   res.redirect(authorizationUrl)
   ```
   ```python title="Flask"
   redirect_uri = 'https://your-app.com/auth/callback'
   options = AuthorizationUrlOptions(scopes=['openid','profile','email','offline_access'])
   authorization_url = scalekit_client.get_authorization_url(redirect_uri, options)
   return redirect(authorization_url)
   ```
   ```go title="Gin" {6-10}
   redirectUri := "https://your-app.com/auth/callback"
   options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}}
   authorizationURL, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options)
   c.Redirect(http.StatusFound, authorizationURL.String())
   ```
   ```java title="Spring" {6-9}
   String redirectUri = "https://your-app.com/auth/callback";
   AuthorizationUrlOptions options = new AuthorizationUrlOptions();
   options.setScopes(Arrays.asList("openid","profile","email","offline_access"));
   URL authorizationUrl = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options);
   return new RedirectView(authorizationUrl.toString());
   ```
3. ## After callback, redirect safely

   After exchanging the code and creating a session, read `sk_return_to`. Validate and normalize the path. Default to `/dashboard` or `/`.

   ```javascript title="Express.js" {8-15}
   app.get('/auth/callback', async (req, res) => {
     // ... exchange code ...
     const raw = req.cookies.sk_return_to || '/'
     const safe = raw.startsWith('/') && !raw.startsWith('//') ? raw : '/'
     res.clearCookie('sk_return_to', { path: '/' })
     res.redirect(safe || '/dashboard')
   })
   ```
   ```python title="Flask" {8-13}
   def callback():
       # ... exchange code ...
       raw = request.cookies.get('sk_return_to', '/')
       safe = raw if raw.startswith('/') and not raw.startswith('//') else '/'
       resp = redirect(safe or '/dashboard')
       resp.delete_cookie('sk_return_to', path='/')
       return resp
   ```
   ```go title="Gin" {9-15}
   func callback(c *gin.Context) {
     // ... exchange code ...
     raw, _ := c.Cookie("sk_return_to")
     if raw == "" || !strings.HasPrefix(raw, "/") || strings.HasPrefix(raw, "//") {
       raw = "/"
     }
     http.SetCookie(c.Writer, &http.Cookie{Name: "sk_return_to", Value: "", MaxAge: -1, Path: "/"})
     c.Redirect(http.StatusFound, raw)
   }
   ```
   ```java title="Spring" {9-15}
   public RedirectView callback(HttpServletRequest request, HttpServletResponse response) {
     // ... exchange code ...
     String raw = getCookie(request, "sk_return_to").orElse("/");
     boolean ok = raw.startsWith("/") && !raw.startsWith("//");
     Cookie clear = new Cookie("sk_return_to", ""); clear.setPath("/"); clear.setMaxAge(0);
     response.addCookie(clear);
     return new RedirectView(ok ? raw : "/dashboard");
   }
   ```
4. ## Sign return_to values <Badge type="tip" text="Optional" />

   If you pass `return_to` via query string or store longer values, compute an HMAC and verify it before redirecting. Reject unsigned or invalid pairs.

   ```javascript title="HMAC signing" {5-11}
   import crypto from 'crypto'
   function sign(value, secret) {
     const mac = crypto.createHmac('sha256', secret).update(value).digest('base64url')
     return `${value}|${mac}`
   }
   function verify(signed, secret) {
     const [v, mac] = signed.split('|')
     const good = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(sign(v, secret).split('|')[1]))
     return good ? v : null
   }
   ```
   ```python title="HMAC signing" {5-11}
   import hmac, hashlib, base64
   def sign(value: str, secret: bytes) -> str:
       mac = hmac.new(secret, value.encode(), hashlib.sha256).digest()
       return f"{value}|{base64.urlsafe_b64encode(mac).decode().rstrip('=')}"
   def verify(signed: str, secret: bytes) -> str | None:
       try:
           value, mac = signed.split('|', 1)
           expected = sign(value, secret).split('|', 1)[1]
           if hmac.compare_digest(mac, expected):
               return value
       except Exception:
           pass
       return None
   ```
   ```go title="HMAC signing" {6-14}
   import (
     "crypto/hmac"
     "crypto/sha256"
     "encoding/base64"
   )
   func sign(value string, secret []byte) string {
     mac := hmac.New(sha256.New, secret)
     mac.Write([]byte(value))
     sum := mac.Sum(nil)
     return value + "|" + base64.RawURLEncoding.EncodeToString(sum)
   }
   func verify(signed string, secret []byte) *string {
     parts := strings.SplitN(signed, "|", 2)
     if len(parts) != 2 { return nil }
     expected := strings.SplitN(sign(parts[0], secret), "|", 2)[1]
     if hmac.Equal([]byte(parts[1]), []byte(expected)) {
       return &parts[0]
     }
     return nil
   }
   ```
   ```java title="HMAC signing" {7-16}
   import javax.crypto.Mac;
   import javax.crypto.spec.SecretKeySpec;
   import java.util.Base64;
   String sign(String value, byte[] secret) throws Exception {
     Mac mac = Mac.getInstance("HmacSHA256");
     mac.init(new SecretKeySpec(secret, "HmacSHA256"));
     byte[] raw = mac.doFinal(value.getBytes(StandardCharsets.UTF_8));
     String b64 = Base64.getUrlEncoder().withoutPadding().encodeToString(raw);
     return value + "|" + b64;
   }
   String verify(String signed, byte[] secret) throws Exception {
     String[] parts = signed.split("\\|", 2);
     if (parts.length != 2) return null;
     String expected = sign(parts[0], secret).split("\\|", 2)[1];
     return MessageDigest.isEqual(parts[1].getBytes(StandardCharsets.UTF_8), expected.getBytes(StandardCharsets.UTF_8)) ? parts[0] : null;
   }
   ```
**Limit scope and length:** Allowlist a small set of internal prefixes (for example, `/app`, `/billing`) and cap `return_to` length (for example, 512 chars). Reject anything else.
**Never redirect to external origins:** Allow only same-origin paths (e.g., `/billing`). Do not accept absolute URLs or protocol-relative URLs. This blocks open redirects.

---

## 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 |
