108 lines
4.3 KiB
Go
108 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
func TestStateBoundToCookieSingleUseAndExpiry(t *testing.T) {
|
|
a := &app{pending: map[string]pending{"valid": {Challenge: "challenge", Expires: time.Now().Add(time.Minute)}, "expired": {Expires: time.Now().Add(-time.Minute)}}}
|
|
request := func(state, cookie string) *http.Request {
|
|
r := httptest.NewRequest("GET", "https://login.example/callback?state="+state, nil)
|
|
if cookie != "" {
|
|
r.AddCookie(&http.Cookie{Name: cookieName, Value: cookie})
|
|
}
|
|
return r
|
|
}
|
|
for _, r := range []*http.Request{request("valid", ""), request("valid", "other"), request("expired", "expired")} {
|
|
if _, err := a.take(r); err == nil {
|
|
t.Fatal("invalid state accepted")
|
|
}
|
|
}
|
|
if p, err := a.take(request("valid", "valid")); err != nil || p.Challenge != "challenge" {
|
|
t.Fatal("valid state rejected")
|
|
}
|
|
if _, err := a.take(request("valid", "valid")); err == nil {
|
|
t.Fatal("replayed state accepted")
|
|
}
|
|
}
|
|
func TestConsentRejectsPrivilegeExpansionAndFiltersClaims(t *testing.T) {
|
|
f := flowRequest{Subject: "human:known", Scopes: []string{"openid", "email"}, Context: claims{Username: "alice", Email: "[email protected]", EmailVerified: true, Groups: []string{"operators"}}}
|
|
s, err := consentSession(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := s["groups"]; ok {
|
|
t.Fatal("groups leaked without scope")
|
|
}
|
|
if _, ok := s["preferred_username"]; ok {
|
|
t.Fatal("profile leaked without scope")
|
|
}
|
|
for _, scope := range []string{"admin", "offline_access", "unknown"} {
|
|
bad := f
|
|
bad.Scopes = append([]string{"openid"}, scope)
|
|
if _, err := consentSession(bad); err == nil {
|
|
t.Fatalf("accepted %s", scope)
|
|
}
|
|
}
|
|
f.Audience = []string{"other-service"}
|
|
if _, err := consentSession(f); err == nil {
|
|
t.Fatal("unexpected audience accepted")
|
|
}
|
|
f.Audience = nil
|
|
f.Context.EmailVerified = false
|
|
if _, err := consentSession(f); err == nil {
|
|
t.Fatal("unverified email accepted")
|
|
}
|
|
}
|
|
func TestLoginValidatesClientAndUsesPKCEAndNonce(t *testing.T) {
|
|
admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(map[string]any{"client": map[string]string{"client_id": r.URL.Query().Get("login_challenge")}})
|
|
}))
|
|
defer admin.Close()
|
|
a := &app{admin: admin.URL, client: admin.Client(), allowed: map[string]bool{"gitea": true}, pending: map[string]pending{}, oauth: oauth2.Config{ClientID: "hydra-login", RedirectURL: "https://login.example/callback", Endpoint: oauth2.Endpoint{AuthURL: "https://upstream.example/authorize"}}}
|
|
w := httptest.NewRecorder()
|
|
a.handler().ServeHTTP(w, httptest.NewRequest("GET", "https://login.example/login?login_challenge=rogue", nil))
|
|
if w.Code != 403 {
|
|
t.Fatal("unknown client accepted")
|
|
}
|
|
w = httptest.NewRecorder()
|
|
a.handler().ServeHTTP(w, httptest.NewRequest("GET", "https://login.example/login?login_challenge=gitea", nil))
|
|
if w.Code != 303 {
|
|
t.Fatalf("status %d", w.Code)
|
|
}
|
|
u, _ := url.Parse(w.Header().Get("Location"))
|
|
q := u.Query()
|
|
if q.Get("code_challenge_method") != "S256" || q.Get("code_challenge") == "" || q.Get("nonce") == "" || q.Get("state") == "" {
|
|
t.Fatal("missing protocol binding")
|
|
}
|
|
cookies := w.Result().Cookies()
|
|
if len(cookies) != 1 || !cookies[0].Secure || !cookies[0].HttpOnly || cookies[0].SameSite != http.SameSiteLaxMode || cookies[0].Value != q.Get("state") {
|
|
t.Fatal("unsafe cookie")
|
|
}
|
|
if w.Header().Get("Cache-Control") != "no-store" {
|
|
t.Fatal("missing cache protection")
|
|
}
|
|
}
|
|
func TestHydraRedirectCannotLeaveTrustedOrigin(t *testing.T) {
|
|
for _, target := range []string{"https://evil.example/oauth2/auth", "https://[email protected]/oauth2/auth", "https://hydra.example/other"} {
|
|
admin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(map[string]string{"redirect_to": target})
|
|
}))
|
|
a := &app{admin: admin.URL, public: "https://hydra.example", client: admin.Client()}
|
|
w := httptest.NewRecorder()
|
|
a.accept(w, httptest.NewRequest("GET", "https://login.example/login", nil), "login", "challenge", map[string]string{"subject": "human:test"})
|
|
if w.Code != 502 || strings.Contains(w.Header().Get("Location"), "evil") {
|
|
t.Fatal("untrusted redirect accepted")
|
|
}
|
|
admin.Close()
|
|
}
|
|
}
|