sso: support public MCIAS authorize URL for non-Tailnet browsers

Add [sso].public_url: the browser-facing MCIAS base URL for the SSO
authorize redirect, kept separate from [mcias].server_url (the internal
address used for the server-to-server code exchange). Enables public SSO
without routing internal auth through the edge. Bumps mcdsl to v1.9.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kyle Isom
2026-06-11 11:06:43 -07:00
parent bed563fd20
commit ee31dff01e
7 changed files with 48 additions and 17 deletions

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.25.7
require ( require (
git.wntrmute.dev/kyle/goutils v1.21.0 git.wntrmute.dev/kyle/goutils v1.21.0
git.wntrmute.dev/mc/mcdsl v1.7.0 git.wntrmute.dev/mc/mcdsl v1.9.0
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.5
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0

4
go.sum
View File

@@ -1,7 +1,7 @@
git.wntrmute.dev/kyle/goutils v1.21.0 h1:ZR7ovV400hsF09zc8tkdHs6vyen8TDJ7flong/dnFXM= git.wntrmute.dev/kyle/goutils v1.21.0 h1:ZR7ovV400hsF09zc8tkdHs6vyen8TDJ7flong/dnFXM=
git.wntrmute.dev/kyle/goutils v1.21.0/go.mod h1:JQ8NL5lHSEYl719UMf20p4G1ei70RVGma0hjjNXCR2c= git.wntrmute.dev/kyle/goutils v1.21.0/go.mod h1:JQ8NL5lHSEYl719UMf20p4G1ei70RVGma0hjjNXCR2c=
git.wntrmute.dev/mc/mcdsl v1.7.0 h1:dAh2SGdzjhz0H66i3KAMDm1eRYYgMaxqQ0Pj5NzF7fc= git.wntrmute.dev/mc/mcdsl v1.9.0 h1:TGqVhf9uhhh5jpMhN+8eNtBPSi/wwNXQn/NFDAcU4wg=
git.wntrmute.dev/mc/mcdsl v1.7.0/go.mod h1:MhYahIu7Sg53lE2zpQ20nlrsoNRjQzOJBAlCmom2wJc= git.wntrmute.dev/mc/mcdsl v1.9.0/go.mod h1:MhYahIu7Sg53lE2zpQ20nlrsoNRjQzOJBAlCmom2wJc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=

View File

@@ -37,8 +37,16 @@ type MCIASConfig struct {
// SSOConfig holds SSO redirect settings for the web UI. // SSOConfig holds SSO redirect settings for the web UI.
type SSOConfig struct { type SSOConfig struct {
// RedirectURI is the callback URL that MCIAS redirects to after login. // RedirectURI is the callback URL that MCIAS redirects to after login.
// Must exactly match the redirect_uri registered in MCIAS config. // Must exactly match the redirect_uri registered in MCIAS config. For
// public (non-Tailnet) browser access this must be the public hostname.
RedirectURI string `toml:"redirect_uri"` RedirectURI string `toml:"redirect_uri"`
// PublicURL is the browser-facing MCIAS base URL used to build the SSO
// authorize redirect (e.g. "https://mcias.metacircular.net"). When empty,
// the backend [mcias].server_url is used for the redirect too. Set this
// when browsers cannot resolve the internal MCIAS name; the
// server-to-server code exchange still uses [mcias].server_url.
PublicURL string `toml:"public_url"`
} }
// WebConfig holds settings for the standalone web UI server (metacrypt-web). // WebConfig holds settings for the standalone web UI server (metacrypt-web).

View File

@@ -175,6 +175,7 @@ func New(cfg *config.Config, logger *slog.Logger) (*WebServer, error) {
if cfg.SSO.RedirectURI != "" { if cfg.SSO.RedirectURI != "" {
ssoClient, ssoErr := mcdsso.New(mcdsso.Config{ ssoClient, ssoErr := mcdsso.New(mcdsso.Config{
MciasURL: cfg.MCIAS.ServerURL, MciasURL: cfg.MCIAS.ServerURL,
PublicURL: cfg.SSO.PublicURL,
ClientID: "metacrypt", ClientID: "metacrypt",
RedirectURI: cfg.SSO.RedirectURI, RedirectURI: cfg.SSO.RedirectURI,
CACert: cfg.MCIAS.CACert, CACert: cfg.MCIAS.CACert,
@@ -183,7 +184,12 @@ func New(cfg *config.Config, logger *slog.Logger) (*WebServer, error) {
return nil, fmt.Errorf("webserver: create SSO client: %w", ssoErr) return nil, fmt.Errorf("webserver: create SSO client: %w", ssoErr)
} }
ws.ssoClient = ssoClient ws.ssoClient = ssoClient
logger.Info("SSO enabled: redirecting to MCIAS for login", "mcias_url", cfg.MCIAS.ServerURL) authorizeURL := cfg.SSO.PublicURL
if authorizeURL == "" {
authorizeURL = cfg.MCIAS.ServerURL
}
logger.Info("SSO enabled: redirecting to MCIAS for login",
"authorize_url", authorizeURL, "exchange_url", cfg.MCIAS.ServerURL)
} }
if tok := cfg.MCIAS.ServiceToken; tok != "" { if tok := cfg.MCIAS.ServiceToken; tok != "" {

View File

@@ -65,11 +65,11 @@ func Open(path string) (*sql.DB, error) {
// connection to serialize all access and eliminate busy errors. // connection to serialize all access and eliminate busy errors.
database.SetMaxOpenConns(1) database.SetMaxOpenConns(1)
// Ensure permissions are correct even if the file already existed. // Best-effort permissions tightening. This may fail inside rootless
if err := os.Chmod(path, 0600); err != nil { // podman containers where fchmod is denied in the user namespace.
_ = database.Close() // The database still functions correctly without it.
return nil, fmt.Errorf("db: chmod %s: %w", path, err) // See: log/2026-04-03-uid-incident.md
} _ = os.Chmod(path, 0600)
return database, nil return database, nil
} }
@@ -168,9 +168,7 @@ func Snapshot(database *sql.DB, destPath string) error {
return fmt.Errorf("db: snapshot: %w", err) return fmt.Errorf("db: snapshot: %w", err)
} }
if err := os.Chmod(destPath, 0600); err != nil { _ = os.Chmod(destPath, 0600) // best-effort; may fail in rootless containers
return fmt.Errorf("db: chmod snapshot %s: %w", destPath, err)
}
return nil return nil
} }

View File

@@ -39,9 +39,18 @@ const (
// Config holds the SSO client configuration. The values must match the // Config holds the SSO client configuration. The values must match the
// SSO client registration in MCIAS config. // SSO client registration in MCIAS config.
type Config struct { type Config struct {
// MciasURL is the base URL of the MCIAS server. // MciasURL is the base URL of the MCIAS server used for the
// server-to-server authorization-code exchange. This is typically the
// internal/Tailnet address so the exchange does not depend on the public
// edge.
MciasURL string MciasURL string
// PublicURL, when set, is the browser-facing MCIAS base URL used to build
// the authorize redirect. It must be resolvable and reachable by end-user
// browsers (e.g. the public hostname). When empty, MciasURL is used for
// both, which only works when MciasURL is itself browser-reachable.
PublicURL string
// ClientID is the registered SSO client identifier. // ClientID is the registered SSO client identifier.
ClientID string ClientID string
@@ -104,9 +113,19 @@ func New(cfg Config) (*Client, error) {
}, nil }, nil
} }
// AuthorizeURL returns the MCIAS authorize URL with the given state parameter. // authorizeBase returns the browser-facing MCIAS base URL: PublicURL when
// configured, otherwise MciasURL.
func (c *Client) authorizeBase() string {
if c.cfg.PublicURL != "" {
return c.cfg.PublicURL
}
return c.cfg.MciasURL
}
// AuthorizeURL returns the MCIAS authorize URL (browser-facing) with the
// given state parameter.
func (c *Client) AuthorizeURL(state string) string { func (c *Client) AuthorizeURL(state string) string {
base := strings.TrimRight(c.cfg.MciasURL, "/") base := strings.TrimRight(c.authorizeBase(), "/")
return base + "/sso/authorize?" + url.Values{ return base + "/sso/authorize?" + url.Values{
"client_id": {c.cfg.ClientID}, "client_id": {c.cfg.ClientID},
"redirect_uri": {c.cfg.RedirectURI}, "redirect_uri": {c.cfg.RedirectURI},

2
vendor/modules.txt vendored
View File

@@ -2,7 +2,7 @@
## explicit; go 1.24.0 ## explicit; go 1.24.0
git.wntrmute.dev/kyle/goutils/certlib/certgen git.wntrmute.dev/kyle/goutils/certlib/certgen
git.wntrmute.dev/kyle/goutils/lib git.wntrmute.dev/kyle/goutils/lib
# git.wntrmute.dev/mc/mcdsl v1.7.0 # git.wntrmute.dev/mc/mcdsl v1.9.0
## explicit; go 1.25.7 ## explicit; go 1.25.7
git.wntrmute.dev/mc/mcdsl/auth git.wntrmute.dev/mc/mcdsl/auth
git.wntrmute.dev/mc/mcdsl/config git.wntrmute.dev/mc/mcdsl/config