--- 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).