---
name: mas_oidc_loopback
kind: function
lang: go
domain: infra
version: "0.1.0"
purity: impure
signature: "func MasOidcLoopback(cfg MasOidcLoopbackConfig) (*MasOidcLoopbackResult, error)"
description: "Ejecuta el flujo OAuth2 Authorization Code + PKCE contra Matrix Authentication Service (MAS) usando un servidor HTTP loopback en localhost para recibir el callback. Abre el browser del SO (o imprime la URL si OpenBrowser=false), espera el codigo de autorizacion, lo intercambia por tokens y devuelve AccessToken listo para usar como Bearer contra Synapse."
tags: ["matrix", "mas", "oidc", "oauth2", "pkce", "loopback", "client", "infra", "matrix-mas", "auth"]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports:
  - "context"
  - "crypto/rand"
  - "crypto/sha256"
  - "encoding/base64"
  - "encoding/json"
  - "fmt"
  - "io"
  - "net"
  - "net/http"
  - "net/url"
  - "os/exec"
  - "runtime"
  - "strings"
  - "time"
tested: true
tests:
  - "state mismatch devuelve error"
  - "token endpoint 400 devuelve error con body"
  - "timeout sin callback devuelve error"
  - "validacion - Issuer vacio"
  - "validacion - Issuer sin slash final"
  - "validacion - ClientID vacio"
  - "validacion - LoopbackPort negativo"
  - "scopes nil usa defaults - error es de discovery no de scopes"
test_file_path: "functions/infra/mas_oidc_loopback_test.go"
file_path: "functions/infra/mas_oidc_loopback.go"
params:
  - name: Issuer
    desc: "URL base del MAS. Debe terminar en '/'. La funcion hace GET a {Issuer}.well-known/openid-configuration para descubrir authorization_endpoint y token_endpoint."
  - name: ClientID
    desc: "ULID del client registrado en MAS. Para clients publicos (client_auth_method: none) no se necesita client_secret."
  - name: Scopes
    desc: "Lista de scopes OAuth2 a solicitar. Si nil o vacio, usa defaults: [openid, urn:matrix:org.matrix.msc2967.client:api:*]. Para acceso completo Matrix."
  - name: LoopbackPort
    desc: "Puerto local para el servidor de callback (debe coincidir con redirect_uri registrado en MAS: http://127.0.0.1:{port}/callback). Si 0, elige un puerto libre dinamicamente."
  - name: OpenBrowser
    desc: "Si true, abre el browser del SO automaticamente (xdg-open/open/rundll32). Si false, imprime la URL a stdout para apertura manual."
  - name: TimeoutSeconds
    desc: "Tiempo maximo en segundos esperando el callback del browser. Default 300s si <= 0."
output: "MasOidcLoopbackResult con AccessToken (Bearer para Synapse), RefreshToken, ExpiresIn, TokenType, Scope e IDToken."
---

## Ejemplo

```go
import "fn-registry/functions/infra"

cfg := infra.MasOidcLoopbackConfig{
    Issuer:         "https://auth-af2f3d.organic-machine.com/",
    ClientID:       "VDC4XQ2ZKN2TJ0BYVJ54FK7M6Y",  // matrix_client_pc client en MAS
    Scopes:         []string{"openid", "urn:matrix:org.matrix.msc2967.client:api:*"},
    LoopbackPort:   8765,
    OpenBrowser:    true,
    TimeoutSeconds: 300,
}
res, err := infra.MasOidcLoopback(cfg)
if err != nil {
    log.Fatalf("login failed: %v", err)
}
// res.AccessToken -> Bearer token para requests Synapse
// res.RefreshToken -> guardar para renovacion posterior
fmt.Printf("Logged in. Token expires in %d seconds.\n", res.ExpiresIn)
```

Con OpenBrowser=false (servidor headless o CLI):

```go
cfg := infra.MasOidcLoopbackConfig{
    Issuer:         "https://auth-af2f3d.organic-machine.com/",
    ClientID:       "VDC4XQ2ZKN2TJ0BYVJ54FK7M6Y",
    LoopbackPort:   8765,
    OpenBrowser:    false,  // imprime la URL a stdout
    TimeoutSeconds: 120,
}
res, err := infra.MasOidcLoopback(cfg)
// El usuario copia la URL del stdout y la abre en su browser
```

## Cuando usarla

Cuando una app desktop (Wails, Tauri, CLI Go) necesite autenticar al usuario contra MAS
sin un browser embebido: la funcion gestiona todo el flujo PKCE, arranca el servidor
loopback, espera el callback y devuelve los tokens listos para usar.
Usar antes de cualquier llamada autenticada a la Matrix Client-Server API via Synapse.

## Gotchas

- **LoopbackPort debe coincidir con el redirect_uri registrado en MAS.**
  Si el client en MAS tiene `redirect_uris: [http://127.0.0.1:8765/callback]`, el
  `LoopbackPort` debe ser `8765`. MAS rechaza con 400 si el redirect_uri no coincide.
  Con `LoopbackPort: 0` la funcion elige puerto libre, pero el client en MAS necesitaria
  soportar wildcard `http://127.0.0.1:*/callback` (verificar la config del client en MAS).

- **El client en MAS debe ser publico (client_auth_method: none).**
  Esta funcion implementa PKCE sin client_secret (RFC 7636). Si el client tiene
  `client_secret_basic` o `client_secret_post`, el token endpoint rechazara el
  intercambio porque falta el secret. Para clients confidenciales, usar otra funcion
  con autenticacion del client.

- **OpenBrowser en servidores headless:**
  `xdg-open` en Linux requiere entorno de escritorio. En servidores SSH sin DISPLAY,
  usar `OpenBrowser: false` e imprimir la URL para que el operador la abra en su PC.

- **El loopback server muere tras recibir el primer callback.**
  No es apto para flujos multi-sesion ni refresh. Para renovar tokens usar el
  `RefreshToken` con un helper de token refresh (oauth2_refresh_go_infra).

- **State mismatch indica ataque CSRF o multi-tab.**
  Si el callback llega con un state distinto al generado, la funcion aborta con error.
  El browser puede mostrar un error si el usuario abre varias pestanas del authorize.

- **Timeout:** si el usuario no completa el login antes de `TimeoutSeconds`, la funcion
  devuelve error y el loopback server se cierra. El proceso del browser queda abierto
  (el OS no lo mata automaticamente).
