Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7e5372d
callback url bug fixed
adityaauthnull250401 May 12, 2026
4ab71c3
Merge pull request #22 from authsec-ai/callbackurl-bug-fix
adityaauthnull250401 May 12, 2026
3cd8f96
authsec.url issue fixed
adityaauthnull250401 May 12, 2026
0a56a6d
temp
ritam77 May 12, 2026
16862c9
Merge pull request #23 from authsec-ai/main
ritamAN77 May 12, 2026
ae60e4e
feat: backport standards-compliant MCP OAuth flow to prod
ritam77 Jun 2, 2026
be1c455
feat: consume auth_request_context on /token, fail-closed validation
ritam77 Jun 2, 2026
2d8dc0f
feat: add /authsec/applications/:id/rotate-introspection-secret
ritam77 Jun 2, 2026
42307b9
feat: port /applications validate, test, launch, access-policy to bac…
ritam77 Jun 2, 2026
86d237c
feat: add SDK-facing /sdk-policy + /sdk-manifest with Basic auth
ritam77 Jun 2, 2026
35768db
docs: add full e2e runbook for the demo flow
ritam77 Jun 2, 2026
8c05c0e
docs: add Windows + cloudflared local-run guide for the demo
ritam77 Jun 2, 2026
4f56e48
docs: full port plan + curl reference for backport endpoints
ritam77 Jun 2, 2026
dc03f92
feat: Phase 1+2+3 — admin reads, activation, connection prereg/revoke
ritam77 Jun 2, 2026
891647b
feat: Phase 4+7 — drift events + consent grants
ritam77 Jun 2, 2026
322042b
feat: Phase 5+6 — scope CRUD + tool-scope mapping
ritam77 Jun 2, 2026
5482ceb
feat: Phase 8 part 1 — Application-scoped roles + scope grants
ritam77 Jun 2, 2026
eeb5de8
feat: Phase 8 part 2 — bindings + user access reads
ritam77 Jun 2, 2026
aa37e81
feat: Phase 9 — governance views (port plan complete)
ritam77 Jun 2, 2026
2d9f8ae
feat: deep RBAC scope filtering on /oauth/v2/introspect
ritam77 Jun 2, 2026
9abd751
fix: scan text[] columns into pq.StringArray, not []string
ritam77 Jun 2, 2026
d25c884
fix: add explicit gorm:column tags to Raw().Scan() row structs
ritam77 Jun 2, 2026
27b7b9d
fix: add AUTHSEC_OAUTH_BASE_URL, stop redirecting v2 traffic to Hydra
ritam77 Jun 3, 2026
4578981
feat: login port session 1 — schema + HydraLoginService + /login/page…
ritam77 Jun 3, 2026
631564b
feat: login port session 2 — custom-login completion + reject
ritam77 Jun 3, 2026
4fd2c35
feat: login port session 3 — consent handler with 3-way scope interse…
ritam77 Jun 3, 2026
c380769
feat: login port sessions 4+5+6 — OIDC federated + SAML stubs + provi…
ritam77 Jun 3, 2026
8895025
feat: tenant.oidc_providers stores client_secret inline (matches tena…
ritam77 Jun 3, 2026
8edaae5
fix: drop redirect_uri column from oidc_providers SELECT
ritam77 Jun 3, 2026
bdc426e
feat: federated JIT user creation, per-MCP scoping via resource_serve…
ritam77 Jun 3, 2026
946b454
fix: federated JIT anchors to any active clients row when legacy_clie…
ritam77 Jun 3, 2026
1fcb0f2
fix: set oidc_user_identities.profile_data to '{}' on insert (jsonb r…
ritam77 Jun 3, 2026
c6533ff
feat: SAML federated login via legacy OAuthLoginService (no new XML lib)
ritam77 Jun 3, 2026
06cb553
feat: expose OAuth discovery metadata at bare /.well-known/* (RFC 8414)
ritam77 Jun 3, 2026
000befe
fix: /login/page-data falls back to resource query param when client …
ritam77 Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,13 @@ func main() {
// Phase 4: background workers
// ─────────────────────────────────────────────────────────

// mcp_oauth_clients ↔ Hydra reconciler (v2 OAuth flow).
// Disabled by default for the first rollout; flip the env var off after
// the standards-compliant DCR/token path is verified end-to-end.
if os.Getenv("AUTHSEC_DISABLE_HYDRA_RECONCILER_V2") != "true" {
services.StartHydraReconcilerV2(context.Background())
}

// Audit log cleanup (runs daily, removes events older than 90 days)
go func() {
ticker := time.NewTicker(24 * time.Hour)
Expand Down
20 changes: 20 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,21 @@ type Config struct {
ReactAppURL string // Frontend app URL for redirects (e.g., https://app.authsec.dev)
IdentityProviderURL string // Identity provider base URL for OIDC callbacks

// OAuthBaseURL is the canonical public URL of the AuthSec backend that
// serves the /authsec/oauth/v2/* surface. Used by CanonicalIssuerOnly
// middleware to redirect non-canonical-host traffic AND by the well-known
// metadata builders to emit the correct `issuer` and endpoint URLs.
//
// Must point at the host where THIS backend serves the v2 OAuth handlers,
// NOT at Hydra. On single-host deployments this is the same as the admin
// API host. On multi-host deployments (e.g. prod.api.authsec.ai for admin
// + auth.prod.authsec.ai for v2 OAuth) this is the OAuth host.
//
// If empty, CanonicalIssuerOnly is a no-op (no redirects) and the
// well-knowns fall back to the request host. Set via env
// AUTHSEC_OAUTH_BASE_URL.
OAuthBaseURL string

// SDK-Manager migration (all optional)
OAuthAuthURL string // OAuth authorization endpoint
OAuthTokenURL string // OAuth token exchange endpoint
Expand Down Expand Up @@ -235,6 +250,10 @@ func LoadConfig() *Config {
hydraPublicURL := getEnv("HYDRA_PUBLIC_URL", "http://localhost:4444")
reactAppURL := getEnv("REACT_APP_URL", "https://app.authsec.dev")
identityProviderURL := getEnv("IDENTITY_PROVIDER_URL", "https://app.authsec.dev")
// AUTHSEC_OAUTH_BASE_URL — public URL of this backend's /authsec/oauth/v2
// surface. Empty = no canonical-issuer enforcement. See OAuthBaseURL
// field doc on Config struct.
oAuthBaseURL := getEnv("AUTHSEC_OAUTH_BASE_URL", "")

// SDK-Manager migration config (all optional)
oauthAuthURL := getEnv("OAUTH_AUTH_URL", "")
Expand Down Expand Up @@ -322,6 +341,7 @@ func LoadConfig() *Config {
HydraPublicURL: hydraPublicURL,
ReactAppURL: reactAppURL,
IdentityProviderURL: identityProviderURL,
OAuthBaseURL: oAuthBaseURL,

// SDK-Manager migration
OAuthAuthURL: oauthAuthURL,
Expand Down
262 changes: 262 additions & 0 deletions controllers/admin/identity_providers_v2_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package admin

import (
"encoding/json"
"errors"
"net/http"

"github.com/authsec-ai/authsec/controllers/shared"
"github.com/authsec-ai/authsec/middlewares"
"github.com/authsec-ai/authsec/models"
"github.com/authsec-ai/authsec/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
)

// IdentityProvidersV2Controller serves the tenant-scoped IDP registry on the
// prod backport.
//
// Routes:
//
// POST /authsec/identity-providers
// GET /authsec/identity-providers
// GET /authsec/identity-providers/:id
// PUT /authsec/identity-providers/:id/status
// DELETE /authsec/identity-providers/:id
type IdentityProvidersV2Controller struct {
service *services.IdentityProviderV2Service
}

func NewIdentityProvidersV2Controller() *IdentityProvidersV2Controller {
return &IdentityProvidersV2Controller{service: services.NewIdentityProviderV2Service()}
}

type createIDPRequest struct {
ProviderType string `json:"provider_type" binding:"required"`
DisplayName string `json:"display_name" binding:"required"`
Config json.RawMessage `json:"config" binding:"required"`
}

type oidcCreateConfig struct {
ProviderName string `json:"provider_name" binding:"required"`
ConfigRef string `json:"config_ref" binding:"required"`
}

func (ctrl *IdentityProvidersV2Controller) Create(c *gin.Context) {
tenantID, err := shared.ResolveTenantIDString(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "tenant_id required in JWT"})
return
}
userIDStr, _ := middlewares.ResolveUserID(c)
userID, _ := uuid.Parse(userIDStr)

var req createIDPRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body: " + err.Error()})
return
}

switch req.ProviderType {
case models.IdentityProviderOIDC:
var cfg oidcCreateConfig
if err := json.Unmarshal(req.Config, &cfg); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid oidc config: " + err.Error()})
return
}
idp, err := ctrl.service.CreateOIDC(services.CreateOIDCIDPRequest{
TenantID: tenantID,
CreatedByUserID: userID,
DisplayName: req.DisplayName,
ProviderName: cfg.ProviderName,
ConfigRef: cfg.ConfigRef,
})
if err != nil {
if errors.Is(err, services.ErrIdentityProviderAlreadyExists) {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, idp)
default:
c.JSON(http.StatusBadRequest, gin.H{
"error": "unsupported provider_type; only 'oidc' implemented in Phase 4",
})
}
}

func (ctrl *IdentityProvidersV2Controller) List(c *gin.Context) {
tenantID, err := shared.ResolveTenantIDString(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "tenant_id required in JWT"})
return
}
rows, err := ctrl.service.List(tenantID, c.Query("provider_type"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, rows)
}

func (ctrl *IdentityProvidersV2Controller) Get(c *gin.Context) {
tenantID, err := shared.ResolveTenantIDString(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "tenant_id required in JWT"})
return
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid identity provider id"})
return
}
idp, err := ctrl.service.GetByID(tenantID, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "identity provider not found"})
return
}
c.JSON(http.StatusOK, idp)
}

func (ctrl *IdentityProvidersV2Controller) UpdateStatus(c *gin.Context) {
tenantID, err := shared.ResolveTenantIDString(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "tenant_id required in JWT"})
return
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid identity provider id"})
return
}
var body struct {
Status string `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if err := ctrl.service.UpdateStatus(tenantID, id, body.Status); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "identity provider not found"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": body.Status})
}

func (ctrl *IdentityProvidersV2Controller) Delete(c *gin.Context) {
tenantID, err := shared.ResolveTenantIDString(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "tenant_id required in JWT"})
return
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid identity provider id"})
return
}
if err := ctrl.service.Delete(tenantID, id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "identity provider not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}

// ──────────────────────────────────────────────────────────────────────────
// Application ↔ IDP policy endpoints
// ──────────────────────────────────────────────────────────────────────────

// PinIDP handles POST /authsec/applications/:id/identity-providers
func (ctrl *IdentityProvidersV2Controller) PinIDP(c *gin.Context) {
tenantID, err := shared.ResolveTenantIDString(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "tenant_id required in JWT"})
return
}
applicationID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application id"})
return
}
var body struct {
IdentityProviderID string `json:"identity_provider_id" binding:"required"`
Enabled *bool `json:"enabled,omitempty"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
idpID, err := uuid.Parse(body.IdentityProviderID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid identity_provider_id"})
return
}
enabled := true
if body.Enabled != nil {
enabled = *body.Enabled
}
row, err := ctrl.service.PinIDPToApplication(tenantID, applicationID, idpID, enabled)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, row)
}

// UnpinIDP handles DELETE /authsec/applications/:id/identity-providers/:idp_id
func (ctrl *IdentityProvidersV2Controller) UnpinIDP(c *gin.Context) {
tenantID, err := shared.ResolveTenantIDString(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "tenant_id required in JWT"})
return
}
applicationID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application id"})
return
}
idpID, err := uuid.Parse(c.Param("idp_id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid idp_id"})
return
}
if err := ctrl.service.UnpinIDPFromApplication(tenantID, applicationID, idpID); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "policy not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
}

// ListApplicationPolicies handles GET /authsec/applications/:id/identity-providers
func (ctrl *IdentityProvidersV2Controller) ListApplicationPolicies(c *gin.Context) {
tenantID, err := shared.ResolveTenantIDString(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "tenant_id required in JWT"})
return
}
applicationID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application id"})
return
}
rows, err := ctrl.service.ListApplicationPolicies(tenantID, applicationID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, rows)
}
Loading
Loading