fix(api): relay Mojang logins when no auth source is configured
felis api wired the hasJoined multiplexer only when felis.toml had at least one [[auth_source]]. With none, the source list stayed nil and every hasJoined answer was a 204. That was harmless while nothing pointed at the route, but the installer now starts Velocity with -Dmojang.sessionserver aimed at felis-api unconditionally, and the generated felis.toml tells the operator to delete the LittleSkin block for a Mojang-only server. Doing exactly that turned every login away, premium accounts included, and felis-api logged nothing about it. Always build the list through authSourcesFromConfig, which prepends Mojang in code, so an empty config is a Mojang-only relay. felis nano already behaves this way with the same file. The new test pins authSourcesFromConfig itself: Mojang first, the only Identity source, and still present when nothing is configured. Marking a configured source Identity makes it fail. The call site in cmdAPI is now a single unconditional assignment and has no unit test of its own.
This commit is contained in:
3 files changed
+50
-12
No files matched your search
+8
-9
@@ -286,15 +286,14 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
|
||||
}
|
||||
fmt.Fprintln(stderr, "felis api: external face fails closed (Access JWKS key function not configured)")
|
||||
|
||||
// Felis-nano: wire the multi-source hasJoined multiplexer only when third-party auth
|
||||
// sources are configured. Mojang leads as the code-owned identity anchor (正版优先);
|
||||
// config can only append namespace-rewritten third-party sources, never a trusted one,
|
||||
// so a misconfig cannot reopen the impersonation hole. No sources = a.AuthSources stays
|
||||
// nil = the endpoint 204s every login (ships off).
|
||||
if len(cfg.AuthSources) > 0 {
|
||||
a.AuthSources = authSourcesFromConfig(cfg.AuthSources)
|
||||
fmt.Fprintf(stderr, "felis api: hasJoined multiplexer active — Mojang + %d third-party source(s)\n", len(cfg.AuthSources))
|
||||
}
|
||||
// Felis-nano: the multi-source hasJoined multiplexer. Mojang leads as the code-owned
|
||||
// identity anchor (正版优先); config can only append namespace-rewritten third-party
|
||||
// sources, never a trusted one, so a misconfig cannot reopen the impersonation hole.
|
||||
// Wired unconditionally: the installer points Velocity at this route whether or not any
|
||||
// [[auth_source]] is configured, so an empty list has to mean a Mojang-only relay, the
|
||||
// same as under `felis nano`. A nil list would 204 every login, premium ones included.
|
||||
a.AuthSources = authSourcesFromConfig(cfg.AuthSources)
|
||||
fmt.Fprintf(stderr, "felis api: hasJoined multiplexer active — Mojang + %d third-party source(s)\n", len(cfg.AuthSources))
|
||||
|
||||
// Passkey (WebAuthn) verifier (spec §14, Phase 6). One relying party spans BOTH
|
||||
// web faces: the RP id is the panel hostname (console.<root>), and because that is
|
||||
|
||||
@@ -3,8 +3,47 @@ package main
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"felis.lolicon.best/internal/config"
|
||||
)
|
||||
|
||||
// TestAuthSourcesFromConfig pins the one place the hasJoined identity anchor is decided:
|
||||
// Mojang is prepended in code, first, and is the only source whose UUIDs are trusted as-is.
|
||||
// The empty case matters on its own — both `felis api` and `felis nano` call this with a
|
||||
// config that has no [[auth_source]] at all, and that has to be a Mojang-only relay rather
|
||||
// than an empty list that rejects every login.
|
||||
func TestAuthSourcesFromConfig(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
configured []config.AuthSourceConfig
|
||||
}{
|
||||
{"no configured sources", nil},
|
||||
{"configured sources", []config.AuthSourceConfig{
|
||||
{Tag: "littleskin", Prefix: "LS", URL: "https://littleskin.example/hasJoined"},
|
||||
{Tag: "guild", Prefix: "GD", URL: "https://guild.example/hasJoined"},
|
||||
}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := authSourcesFromConfig(tc.configured)
|
||||
if len(got) != len(tc.configured)+1 {
|
||||
t.Fatalf("got %d sources, want Mojang + %d configured", len(got), len(tc.configured))
|
||||
}
|
||||
if got[0].Tag != "mojang" || got[0].URL != mojangSessionServer || !got[0].Identity {
|
||||
t.Errorf("first source = %+v, want the Mojang identity anchor", got[0])
|
||||
}
|
||||
for i, c := range tc.configured {
|
||||
s := got[i+1]
|
||||
if s.Identity {
|
||||
t.Errorf("configured source %q is marked Identity; only Mojang may be", c.Tag)
|
||||
}
|
||||
if s.Tag != c.Tag || s.Prefix != c.Prefix || s.URL != c.URL {
|
||||
t.Errorf("source %d = %+v, want %+v in config order", i+1, s, c)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewAPIServerSetsHardenedTimeouts pins the gosec-G112 hardening on every
|
||||
// felis-api listener: the shared factory must bound the header and idle phases
|
||||
// (Slowloris + idle-connection exhaustion) while leaving WriteTimeout UNSET, because
|
||||
|
||||
+3
-3
@@ -139,9 +139,9 @@ type API struct {
|
||||
|
||||
// AuthSources is the Felis-nano multi-source hasJoined multiplexer's upstream
|
||||
// Yggdrasil list, in priority order (config order; the Mojang Identity source
|
||||
// first for 正版优先). Nil — the default — makes the session verifier reject every
|
||||
// login (204), so the endpoint ships inert until cmd/felis wires configured
|
||||
// sources. Consumed by handleHasJoined (handlers_hasjoined.go).
|
||||
// first for 正版优先). Nil makes the session verifier reject every login (204);
|
||||
// cmd/felis always wires at least the Mojang source through authSourcesFromConfig.
|
||||
// Consumed by handleHasJoined (handlers_hasjoined.go).
|
||||
AuthSources []AuthSource
|
||||
|
||||
// Now is the clock, injectable for tests. Defaults to time.Now.
|
||||
|
||||
Reference in new issue
Block a user