bootrapping support
This commit is contained in:
parent
393abf3e0e
commit
7e22fcf3c7
9 changed files with 715 additions and 23 deletions
|
|
@ -45,14 +45,20 @@ type mockMFAProvider struct {
|
|||
required bool
|
||||
requiredErr error
|
||||
|
||||
validateErr error
|
||||
validateErr error
|
||||
validateCalls int
|
||||
validatedUser string
|
||||
validatedToken string
|
||||
}
|
||||
|
||||
func (m *mockMFAProvider) CheckMFARequired(_ context.Context, _ string) (bool, error) {
|
||||
return m.required, m.requiredErr
|
||||
}
|
||||
|
||||
func (m *mockMFAProvider) ValidateMFAToken(_ context.Context, _, _ string) error {
|
||||
func (m *mockMFAProvider) ValidateMFAToken(_ context.Context, user, token string) error {
|
||||
m.validateCalls++
|
||||
m.validatedUser = user
|
||||
m.validatedToken = token
|
||||
return m.validateErr
|
||||
}
|
||||
|
||||
|
|
@ -80,10 +86,21 @@ func testClient() map[string]*domain.Client {
|
|||
return map[string]*domain.Client{
|
||||
"test-client": {
|
||||
ClientID: "test-client",
|
||||
DisplayName: "Test Client",
|
||||
RedirectURIs: []string{"https://app.example.com/callback"},
|
||||
AllowedScopes: []string{"openid", "profile", "email"},
|
||||
ClientType: "public",
|
||||
},
|
||||
"netkingdom-bootstrap-console": {
|
||||
ClientID: "netkingdom-bootstrap-console",
|
||||
DisplayName: "NetKingdom Bootstrap Console",
|
||||
RedirectURIs: []string{
|
||||
"http://127.0.0.1:8876/oidc/callback",
|
||||
"http://localhost:8876/oidc/callback",
|
||||
},
|
||||
AllowedScopes: []string{"openid", "profile", "email", "groups"},
|
||||
ClientType: "public",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +163,28 @@ func TestAuthorizeHandler_ValidRequest_RedirectsToAuthelia(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeHandler_BootstrapConsoleRedirectURI_RedirectsToAuthelia(t *testing.T) {
|
||||
auth := &mockAuthProvider{authorizeURL: "https://authelia.example.com/auth?state=bootstrap"}
|
||||
mfa := &mockMFAProvider{}
|
||||
emitter := &captureEmitter{}
|
||||
|
||||
h := newAuthorizeHandler(auth, mfa, emitter)
|
||||
params := validAuthorizeParams()
|
||||
params.Set("client_id", "netkingdom-bootstrap-console")
|
||||
params.Set("redirect_uri", "http://127.0.0.1:8876/oidc/callback")
|
||||
|
||||
req := authorizeRequest(params)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusFound {
|
||||
t.Errorf("expected 302 redirect, got %d (body: %s)", w.Code, w.Body.String())
|
||||
}
|
||||
if loc := w.Header().Get("Location"); loc != "https://authelia.example.com/auth?state=bootstrap" {
|
||||
t.Errorf("expected Authelia redirect, got %q", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeHandler_EmitsAuthStart(t *testing.T) {
|
||||
auth := &mockAuthProvider{authorizeURL: "https://authelia.example.com/auth"}
|
||||
mfa := &mockMFAProvider{}
|
||||
|
|
@ -449,6 +488,164 @@ func TestAuthorizeCallback_MFAFailed_AuthFailure(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeCallback_MFARequired_RendersChallengeWithoutToken(t *testing.T) {
|
||||
auth := &mockAuthProvider{
|
||||
callbackResult: &domain.AuthResult{Username: "alice"},
|
||||
}
|
||||
mfa := &mockMFAProvider{required: true}
|
||||
emitter := &captureEmitter{}
|
||||
|
||||
sessions := oidc.NewSessionStore()
|
||||
h := &oidc.AuthorizeHandler{
|
||||
ClientConfig: testClient(),
|
||||
Auth: auth,
|
||||
MFA: mfa,
|
||||
Sessions: sessions,
|
||||
Emitter: emitter,
|
||||
}
|
||||
|
||||
h.PendingStates().Store("random-state", &oidc.PendingState{
|
||||
ClientID: "test-client",
|
||||
RedirectURI: "https://app.example.com/callback",
|
||||
PKCEChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||
PKCEChallengeMethod: "S256",
|
||||
State: "random-state",
|
||||
Scopes: []string{"openid"},
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet,
|
||||
"/authorize/callback?code=authelia-code&state=random-state", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTPCallback(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 challenge page, got %d (body: %s)", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
for _, want := range []string{"Verify sign-in", "alice", "Test Client", `name="mfa_token"`} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("challenge page missing %q in body: %s", want, body)
|
||||
}
|
||||
}
|
||||
if mfa.validateCalls != 0 {
|
||||
t.Errorf("MFA token should not be validated until form submission, got %d calls", mfa.validateCalls)
|
||||
}
|
||||
ps, ok := h.PendingStates().Load("random-state")
|
||||
if !ok {
|
||||
t.Fatal("expected pending state to remain for MFA form submission")
|
||||
}
|
||||
if ps.AuthenticatedUser != "alice" {
|
||||
t.Errorf("AuthenticatedUser: want alice, got %q", ps.AuthenticatedUser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeCallback_MFASubmission_ValidToken_RedirectsWithCode(t *testing.T) {
|
||||
auth := &mockAuthProvider{}
|
||||
mfa := &mockMFAProvider{required: true}
|
||||
emitter := &captureEmitter{}
|
||||
|
||||
sessions := oidc.NewSessionStore()
|
||||
h := &oidc.AuthorizeHandler{
|
||||
ClientConfig: testClient(),
|
||||
Auth: auth,
|
||||
MFA: mfa,
|
||||
Sessions: sessions,
|
||||
Emitter: emitter,
|
||||
}
|
||||
|
||||
h.PendingStates().Store("random-state", &oidc.PendingState{
|
||||
ClientID: "test-client",
|
||||
RedirectURI: "https://app.example.com/callback?from=bootstrap",
|
||||
PKCEChallenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
|
||||
PKCEChallengeMethod: "S256",
|
||||
State: "random-state",
|
||||
Scopes: []string{"openid"},
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
AuthenticatedUser: "alice",
|
||||
})
|
||||
|
||||
form := url.Values{"state": {"random-state"}, "mfa_token": {"123456"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/authorize/callback", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTPCallback(w, req)
|
||||
|
||||
if w.Code != http.StatusFound {
|
||||
t.Errorf("expected 302 redirect, got %d (body: %s)", w.Code, w.Body.String())
|
||||
}
|
||||
if mfa.validatedUser != "alice" || mfa.validatedToken != "123456" {
|
||||
t.Errorf("validated MFA: want alice/123456, got %q/%q", mfa.validatedUser, mfa.validatedToken)
|
||||
}
|
||||
loc := w.Header().Get("Location")
|
||||
parsed, err := url.Parse(loc)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid Location header: %v", err)
|
||||
}
|
||||
if parsed.Query().Get("from") != "bootstrap" {
|
||||
t.Errorf("expected original redirect query to be preserved, got %q", loc)
|
||||
}
|
||||
if parsed.Query().Get("code") == "" {
|
||||
t.Error("expected code param in redirect, got empty")
|
||||
}
|
||||
if parsed.Query().Get("state") != "random-state" {
|
||||
t.Errorf("expected state=random-state, got %q", parsed.Query().Get("state"))
|
||||
}
|
||||
if _, ok := h.PendingStates().Load("random-state"); ok {
|
||||
t.Error("expected pending MFA state to be deleted after successful submission")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeCallback_MFASubmission_InvalidToken_AuthFailure(t *testing.T) {
|
||||
auth := &mockAuthProvider{}
|
||||
mfa := &mockMFAProvider{
|
||||
required: true,
|
||||
validateErr: domain.ErrMFAFailed,
|
||||
}
|
||||
emitter := &captureEmitter{}
|
||||
|
||||
h := &oidc.AuthorizeHandler{
|
||||
ClientConfig: testClient(),
|
||||
Auth: auth,
|
||||
MFA: mfa,
|
||||
Sessions: oidc.NewSessionStore(),
|
||||
Emitter: emitter,
|
||||
}
|
||||
h.PendingStates().Store("random-state", &oidc.PendingState{
|
||||
ClientID: "test-client",
|
||||
RedirectURI: "https://app.example.com/callback",
|
||||
PKCEChallenge: "abc",
|
||||
PKCEChallengeMethod: "S256",
|
||||
State: "random-state",
|
||||
Scopes: []string{"openid"},
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
AuthenticatedUser: "alice",
|
||||
})
|
||||
|
||||
form := url.Values{"state": {"random-state"}, "mfa_token": {"wrong"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/authorize/callback", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTPCallback(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
if _, ok := h.PendingStates().Load("random-state"); ok {
|
||||
t.Error("expected pending MFA state to be deleted after invalid submission")
|
||||
}
|
||||
found := false
|
||||
for _, ev := range emitter.events {
|
||||
if ev.EventType == telemetry.EventAuthFailure && ev.ErrorType == "mfa_failed" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected mfa_failed auth_failure telemetry event")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeCallback_AuthProviderFailed_AuthFailure(t *testing.T) {
|
||||
auth := &mockAuthProvider{
|
||||
callbackErr: domain.ErrAuthFailed,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue