Files
kandev/apps/backend/internal/auth/httpapi/cookie.go
T
João Salavisa b20047b3eb feat(plugins): add auth capability for plugin-provided OIDC/SAML login (#1964)
* feat(plugins): add auth capability for plugin-provided OIDC/SAML login

Plugins can now authenticate a visitor against an external IdP (OIDC/SAML)
and contribute a login button to the pre-auth screen:

- new `auth` manifest capability + a capability-gated X-Kandev-Auth-Login
  webhook-response directive; the host mints the kandev_session (ADR 0050),
  the plugin never receives the raw token. Plugin-set Set-Cookie is dropped.
- auth.Service.AuthenticateExternal: existing-identity / link-by-email / JIT
  member provisioning; refuses to auto-link to an admin; verified-email is
  the plugin's contract.
- auth_providers manifest block + plugins.SSOProviders() populate the
  anonymous boot payload (auth.ssoProviders); the login page renders a
  "Continue with <provider>" button per provider.
- ADR 0050 + plugin-manifest / plugin-authoring / auth spec docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: exclude publish_review_findings_kandev from public-docs coverage

The native code-review agent (#1957) added the publish_review_findings_kandev
MCP tool but did not update docs/public/coverage.json; that PR did not touch
docs/public so the path-filtered validator never ran. It's an internal review
reporting tool, not a user-facing feature, so record it as an exclusion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugins,auth): address review on SSO directive and JIT provisioning

- takeAuthLoginDirective strips every case variant of X-Kandev-Auth-Login and
  rejects (403) when a plugin sends more than one, so no variant leaks to the
  browser and login never uses a nondeterministically-picked assertion.
- provisionExternalUser rolls back the just-created user when the identity
  insert fails (transient error or a lost UNIQUE(provider,subject) race) and
  resolves to the winning identity, so no account is left without a login and
  no email is reserved by an unusable row. Adds userstore.DeleteUser.
- auth spec: mark the login-page SSO buttons + provider discovery as landed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: drop redundant coverage exclusion; main now covers publish_review_findings_kandev

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 18:20:39 +01:00

41 lines
1.4 KiB
Go

package httpapi
import (
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// setSessionCookie writes the kandev session cookie: HttpOnly (no JS access),
// SameSite=Lax (sent on same-site navigations and the same-origin WS upgrade,
// blocked on cross-site POSTs), Secure when the request arrived over TLS
// (directly or via a reverse proxy that sets X-Forwarded-Proto).
func setSessionCookie(c *gin.Context, name, token string, ttl time.Duration) {
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(name, token, int(ttl.Seconds()), "/", "", requestIsTLS(c), true)
}
// SetSessionCookie writes the kandev session cookie with the same flags as the
// login/setup handlers. Exported for callers outside this package that mint a
// session and must set the cookie themselves — the plugin SSO login bridge
// (internal/backendapp) after an auth-capable plugin validates an external
// identity. Keeping this the single cookie-writing path means TLS/SameSite/
// HttpOnly flags never drift between the local and SSO login flows.
func SetSessionCookie(c *gin.Context, name, token string, ttl time.Duration) {
setSessionCookie(c, name, token, ttl)
}
func clearSessionCookie(c *gin.Context, name string) {
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie(name, "", -1, "/", "", requestIsTLS(c), true)
}
func requestIsTLS(c *gin.Context) bool {
if c.Request.TLS != nil {
return true
}
return strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https")
}