refactor(oidc): drop single-subject allowlist, authenticate-only (ADR-012)
ADR-011's single-user authz (ID-token subject must equal AllowedSubject, else 403) is replaced by ADR-012's model: Dex authentication is the only gate — any Dex-authenticated subject may establish a session. Whether that subject has a tapir user, and routing to registration if not, is decided downstream in internal/web (next commit). Removals (noted): oidc.Config.AllowedSubject + its required-field check + the callback 403 branch; config.Config.AllowedSubject + TAPIR_ALLOWED_SUBJECT env wiring; the AllowedSubject arg in cmdServe. ui-spec.md updated to reflect the supersession. Sessions, cookie signing, login/callback/logout unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
const (
|
||||
testClientID = "tapir-web"
|
||||
allowedSub = "allowed-subject-123"
|
||||
testSubject = "dex-subject-123"
|
||||
)
|
||||
|
||||
// fakeIssuer is an httptest-backed OIDC provider: it serves a discovery
|
||||
@@ -115,12 +115,11 @@ func writeJSON(t *testing.T, w http.ResponseWriter, v any) {
|
||||
func newAuth(t *testing.T, f *fakeIssuer) *oidc.DexAuth {
|
||||
t.Helper()
|
||||
auth, err := oidc.New(context.Background(), oidc.Config{
|
||||
Issuer: f.server.URL,
|
||||
ClientID: testClientID,
|
||||
ClientSecret: "test-client-secret",
|
||||
RedirectURL: "http://tapir.test/auth/callback",
|
||||
SessionSecret: "test-session-secret-please-change",
|
||||
AllowedSubject: allowedSub,
|
||||
Issuer: f.server.URL,
|
||||
ClientID: testClientID,
|
||||
ClientSecret: "test-client-secret",
|
||||
RedirectURL: "http://tapir.test/auth/callback",
|
||||
SessionSecret: "test-session-secret-please-change",
|
||||
}, oidc.WithInsecureCookies())
|
||||
require.NoError(t, err)
|
||||
return auth
|
||||
@@ -140,12 +139,12 @@ func login(t *testing.T, auth *oidc.DexAuth) (state, nonce string) {
|
||||
return q.Get("state"), q.Get("nonce")
|
||||
}
|
||||
|
||||
// authenticate completes a full login+callback for the allowlisted subject and
|
||||
// returns the resulting session cookie.
|
||||
// authenticate completes a full login+callback for the test subject and returns
|
||||
// the resulting session cookie.
|
||||
func authenticate(t *testing.T, auth *oidc.DexAuth, f *fakeIssuer) *http.Cookie {
|
||||
t.Helper()
|
||||
state, nonce := login(t, auth)
|
||||
f.sub, f.email, f.nonce = allowedSub, "maintainer@d-ma.be", nonce
|
||||
f.sub, f.email, f.nonce = testSubject, "maintainer@d-ma.be", nonce
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
auth.Routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet,
|
||||
@@ -189,7 +188,7 @@ func TestLoginRedirectsToAuthorize(t *testing.T) {
|
||||
require.Contains(t, q.Get("scope"), "openid")
|
||||
}
|
||||
|
||||
func TestCallbackAllowedSubjectSetsSession(t *testing.T) {
|
||||
func TestCallbackSetsSession(t *testing.T) {
|
||||
f := newFakeIssuer(t)
|
||||
auth := newAuth(t, f)
|
||||
|
||||
@@ -199,26 +198,35 @@ func TestCallbackAllowedSubjectSetsSession(t *testing.T) {
|
||||
req.AddCookie(cookie)
|
||||
user, ok := auth.CurrentUser(req)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, allowedSub, user.Subject)
|
||||
require.Equal(t, testSubject, user.Subject)
|
||||
require.Equal(t, "maintainer@d-ma.be", user.Email)
|
||||
|
||||
require.True(t, cookie.HttpOnly)
|
||||
require.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
||||
}
|
||||
|
||||
func TestCallbackNonAllowedSubjectForbidden(t *testing.T) {
|
||||
// TestCallbackAnySubjectAuthenticates proves the single-subject allowlist is gone
|
||||
// (ADR-012): a subject other than any prior allowlist still gets a session.
|
||||
func TestCallbackAnySubjectAuthenticates(t *testing.T) {
|
||||
f := newFakeIssuer(t)
|
||||
auth := newAuth(t, f)
|
||||
|
||||
state, nonce := login(t, auth)
|
||||
f.sub, f.email, f.nonce = "intruder-999", "intruder@elsewhere.test", nonce
|
||||
f.sub, f.email, f.nonce = "some-other-subject-999", "other@elsewhere.test", nonce
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
auth.Routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet,
|
||||
"/auth/callback?code=valid-code&state="+state, nil))
|
||||
|
||||
require.Equal(t, http.StatusForbidden, rec.Code)
|
||||
require.Empty(t, rec.Result().Cookies(), "no session for a rejected subject")
|
||||
require.Equal(t, http.StatusFound, rec.Code)
|
||||
require.Equal(t, "/", rec.Header().Get("Location"))
|
||||
|
||||
cookie := sessionCookie(t, rec.Result())
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.AddCookie(cookie)
|
||||
user, ok := auth.CurrentUser(req)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "some-other-subject-999", user.Subject)
|
||||
}
|
||||
|
||||
func TestCallbackUnknownStateRejected(t *testing.T) {
|
||||
@@ -304,12 +312,11 @@ func TestExpiredSessionRejected(t *testing.T) {
|
||||
f := newFakeIssuer(t)
|
||||
clock := time.Now()
|
||||
auth, err := oidc.New(context.Background(), oidc.Config{
|
||||
Issuer: f.server.URL,
|
||||
ClientID: testClientID,
|
||||
ClientSecret: "test-client-secret",
|
||||
RedirectURL: "http://tapir.test/auth/callback",
|
||||
SessionSecret: "test-session-secret-please-change",
|
||||
AllowedSubject: allowedSub,
|
||||
Issuer: f.server.URL,
|
||||
ClientID: testClientID,
|
||||
ClientSecret: "test-client-secret",
|
||||
RedirectURL: "http://tapir.test/auth/callback",
|
||||
SessionSecret: "test-session-secret-please-change",
|
||||
}, oidc.WithInsecureCookies(),
|
||||
oidc.WithSessionTTL(time.Minute),
|
||||
oidc.WithClock(func() time.Time { return clock }))
|
||||
|
||||
Reference in New Issue
Block a user