Forward fresh-login requirements to the authentication provider
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 44s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 44s
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
139994cfac
commit
8d4336e944
7 changed files with 103 additions and 3 deletions
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -54,6 +55,12 @@ func (a *AutheliaAdapter) AuthorizeURL(_ context.Context, req domain.AuthRequest
|
||||||
q.Set("response_type", "code")
|
q.Set("response_type", "code")
|
||||||
q.Set("state", req.State)
|
q.Set("state", req.State)
|
||||||
q.Set("scope", "openid profile email groups")
|
q.Set("scope", "openid profile email groups")
|
||||||
|
if req.PromptLogin {
|
||||||
|
q.Set("prompt", "login")
|
||||||
|
}
|
||||||
|
if req.MaxAge != nil {
|
||||||
|
q.Set("max_age", strconv.FormatInt(int64(req.MaxAge.Seconds()), 10))
|
||||||
|
}
|
||||||
|
|
||||||
return base + "?" + q.Encode(), nil
|
return base + "?" + q.Encode(), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -452,3 +452,23 @@ func TestHandleCallback_AuthResultContainsNoRawTokens(t *testing.T) {
|
||||||
t.Error("AuthResult.Claims must not expose raw access_token — security boundary violation")
|
t.Error("AuthResult.Claims must not expose raw access_token — security boundary violation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthorizeURLForwardsFreshLoginRequirements(t *testing.T) {
|
||||||
|
adapter := authelia.New(testConfig(), &mockHTTPClient{})
|
||||||
|
for _, seconds := range []int{0, 60} {
|
||||||
|
age := time.Duration(seconds) * time.Second
|
||||||
|
target, err := adapter.AuthorizeURL(context.Background(), domain.AuthRequest{PromptLogin: true, MaxAge: &age})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
parsed, _ := url.Parse(target)
|
||||||
|
if parsed.Query().Get("prompt") != "login" || parsed.Query().Get("max_age") != fmt.Sprint(seconds) {
|
||||||
|
t.Fatalf("freshness requirements missing from provider URL: %s", target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target, _ := adapter.AuthorizeURL(context.Background(), domain.AuthRequest{})
|
||||||
|
parsed, _ := url.Parse(target)
|
||||||
|
if parsed.Query().Has("prompt") || parsed.Query().Has("max_age") {
|
||||||
|
t.Fatal("ordinary SSO was changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -386,8 +386,8 @@ func TestServiceClientExampleContracts(t *testing.T) {
|
||||||
if errs := config.ValidateConfig(cfg); len(errs) != 0 {
|
if errs := config.ValidateConfig(cfg); len(errs) != 0 {
|
||||||
t.Fatalf("service client examples must validate: %v", errs)
|
t.Fatalf("service client examples must validate: %v", errs)
|
||||||
}
|
}
|
||||||
if len(cfg.Clients) != 4 {
|
if len(cfg.Clients) != 5 {
|
||||||
t.Fatalf("service client examples: want 4, got %d", len(cfg.Clients))
|
t.Fatalf("client examples: want 4 service clients and 1 human client, got %d", len(cfg.Clients))
|
||||||
}
|
}
|
||||||
|
|
||||||
codingAgent := cfg.Clients[0]
|
codingAgent := cfg.Clients[0]
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package domain
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthProvider handles login UI delegation and session management.
|
// AuthProvider handles login UI delegation and session management.
|
||||||
|
|
@ -18,6 +19,9 @@ type AuthProvider interface {
|
||||||
|
|
||||||
// AuthRequest contains the parameters for initiating an auth flow.
|
// AuthRequest contains the parameters for initiating an auth flow.
|
||||||
type AuthRequest struct {
|
type AuthRequest struct {
|
||||||
|
// Forward interactive freshness requirements to the actual login provider.
|
||||||
|
PromptLogin bool
|
||||||
|
MaxAge *time.Duration
|
||||||
ClientID string
|
ClientID string
|
||||||
RedirectURI string
|
RedirectURI string
|
||||||
State string
|
State string
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,8 @@ func (h *AuthorizeHandler) serveAuthorize(w http.ResponseWriter, r *http.Request
|
||||||
|
|
||||||
// Delegate to Auth provider.
|
// Delegate to Auth provider.
|
||||||
authURL, err := h.Auth.AuthorizeURL(ctx, domain.AuthRequest{
|
authURL, err := h.Auth.AuthorizeURL(ctx, domain.AuthRequest{
|
||||||
|
PromptLogin: promptLogin,
|
||||||
|
MaxAge: maxAge,
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
RedirectURI: redirectURI,
|
RedirectURI: redirectURI,
|
||||||
State: state,
|
State: state,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
|
|
||||||
// mockAuthProvider implements domain.AuthProvider.
|
// mockAuthProvider implements domain.AuthProvider.
|
||||||
type mockAuthProvider struct {
|
type mockAuthProvider struct {
|
||||||
|
request domain.AuthRequest
|
||||||
authorizeURL string
|
authorizeURL string
|
||||||
authorizeErr error
|
authorizeErr error
|
||||||
|
|
||||||
|
|
@ -29,7 +30,8 @@ type mockAuthProvider struct {
|
||||||
callbackErr error
|
callbackErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockAuthProvider) AuthorizeURL(_ context.Context, _ domain.AuthRequest) (string, error) {
|
func (m *mockAuthProvider) AuthorizeURL(_ context.Context, req domain.AuthRequest) (string, error) {
|
||||||
|
m.request = req
|
||||||
if m.authorizeErr != nil {
|
if m.authorizeErr != nil {
|
||||||
return "", m.authorizeErr
|
return "", m.authorizeErr
|
||||||
}
|
}
|
||||||
|
|
@ -871,3 +873,22 @@ func TestAuthorizeHandler_ServeHTTP_DispatchesToCallback(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFreshLoginRequirementsReachProvider(t *testing.T) {
|
||||||
|
for _, age := range []string{"0", "60"} {
|
||||||
|
auth := &mockAuthProvider{authorizeURL: "https://auth.example/login"}
|
||||||
|
handler := newAuthorizeHandler(auth, &mockMFAProvider{}, &captureEmitter{})
|
||||||
|
params := validAuthorizeParams()
|
||||||
|
params.Set("prompt", "login")
|
||||||
|
params.Set("max_age", age)
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(response, authorizeRequest(params))
|
||||||
|
if response.Code != http.StatusFound {
|
||||||
|
t.Fatalf("authorize failed: %d", response.Code)
|
||||||
|
}
|
||||||
|
expected, _ := time.ParseDuration(age + "s")
|
||||||
|
if !auth.request.PromptLogin || auth.request.MaxAge == nil || *auth.request.MaxAge != expected {
|
||||||
|
t.Fatalf("fresh login requirements lost before provider: %+v", auth.request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
46
workplans/KEY-WP-0033-vergabe-fresh-login.md
Normal file
46
workplans/KEY-WP-0033-vergabe-fresh-login.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
---
|
||||||
|
id: KEY-WP-0033
|
||||||
|
type: workplan
|
||||||
|
title: "Preserve fresh-user authentication for the Vergabe company handoff"
|
||||||
|
domain: infotech
|
||||||
|
repo: key-cape
|
||||||
|
status: active
|
||||||
|
owner: codex
|
||||||
|
topic_slug: netkingdom
|
||||||
|
created: "2026-09-12"
|
||||||
|
updated: "2026-09-12"
|
||||||
|
related: [VERGABE-WP-0019, NK-WP-0037, USER-WP-0025]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Forward login freshness to the actual authentication provider
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: KEY-WP-0033-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
KeyCape parsed prompt=login and max_age but dropped them before Authelia.
|
||||||
|
Forward these through the provider-neutral AuthRequest and the Authelia adapter.
|
||||||
|
Absent values retain ordinary SSO behavior. Handler and adapter regressions
|
||||||
|
cover forced login and zero/nonzero maximum age. Full Go suite passes after
|
||||||
|
correcting the existing example-count regression: the example file already
|
||||||
|
contains four service clients and the admitted human approver client.
|
||||||
|
|
||||||
|
## Publish, preflight and prove the fresh recipient boundary
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: KEY-WP-0033-T02
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Publish the exact source and validate current live config before replacing the
|
||||||
|
single issuer instance. The live image is dcebd46/digest 7ff54c54; current main
|
||||||
|
also contains startup validation and tenant provenance changes documented in
|
||||||
|
docs/operations.md. Preserve all unrelated client, credential and MFA policy
|
||||||
|
configuration. NK-WP-0037 adds the exact public Vergabe callback with no tenant
|
||||||
|
assertion or MFA downgrade. Confirm prompt=login reaches Authelia through the
|
||||||
|
live redirect, wrong callbacks and missing PKCE fail, and the actual recipient
|
||||||
|
uses their own identity. Provider restart invalidates pending in-memory logins;
|
||||||
|
USER-WP-0025-T03 still owns full provider sign-out coordination.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue