Commit b27894f

mo khan <mo@mokhan.ca>
2025-03-15 21:20:53
refactor: authorize unsigned JWT in requests
1 parent 80f1b83
bin/api
@@ -55,6 +55,10 @@ class Entity
   def to_h
     @attributes
   end
+
+  def to_gid
+    ::GlobalID.create(self, app: "example")
+  end
 end
 
 class Organization < Entity
@@ -73,11 +77,11 @@ module HTTPHelpers
     authorization = Rack::Auth::AbstractRequest.new(request.env)
     return false unless authorization.provided?
 
-    response = rpc.allowed(
+    response = rpc.allowed({
       subject: authorization.params,
       permission: permission,
-      resource: ::GlobalID.create(resource, app: "example").to_s
-    )
+      resource: resource.to_gid.to_s,
+    }, headers: { 'Authorization' => "Bearer #{authorization.params}"})
     response.error.nil? && response.data.result
   end
 
@@ -93,11 +97,11 @@ module HTTPHelpers
     http_response(code: 201, body: JSON.pretty_generate(body.to_h))
   end
 
-  def json_unauthorized(permission)
+  def json_unauthorized(permission, resource)
     http_response(code: 401, body: JSON.pretty_generate({
       error: {
         code: 401,
-        message: "`#{permission}` is required",
+        message: "`#{permission}` is required on `#{resource.to_gid}`",
       }
     }))
   end
@@ -128,15 +132,21 @@ class API
       when "/organizations", "/organizations.json"
         return json_ok(Organization.all.map(&:to_h))
       when "/projects", "/projects.json"
-        return json_ok(Project.all.map(&:to_h))
+        resource = Organization.default
+        if authorized?(request, :read_project, resource)
+          return json_ok(Project.all.map(&:to_h))
+        else
+          return json_unauthorized(:read_project, resource)
+        end
       end
     when Rack::POST
       case request.path
       when "/projects", "/projects.json"
-        if authorized?(request, :create_project, Organization.default)
+        resource = Organization.default
+        if authorized?(request, :create_project, resource)
           return json_created(Project.create!(JSON.parse(request.body.read, symbolize_names: true)))
         else
-          return json_unauthorized(:create_project)
+          return json_unauthorized(:create_project, resource)
         end
       end
     end
bin/idp
@@ -322,6 +322,7 @@ module Authz
   class OrganizationPolicy < DeclarativePolicy::Base
     condition(:owner) { true }
 
+    rule { owner }.enable :read_project
     rule { owner }.enable :create_project
   end
 
@@ -334,9 +335,9 @@ module Authz
 
     def to_jwt
       [
-        Base64.strict_encode64(JSON.generate({alg: "RS256", typ: "JWT"})),
+        Base64.strict_encode64(JSON.generate(alg: "none")),
         Base64.strict_encode64(JSON.generate(claims)),
-        Base64.strict_encode64(JSON.generate({})),
+        ""
       ].join(".")
     end
   end
@@ -354,8 +355,13 @@ module Authz
       def can?(request)
         subject = subject_of(request.subject)
         resource = resource_from(request.resource)
+        permission = request.permission.to_sym
+
         policy = DeclarativePolicy.policy_for(subject, resource)
-        policy.can?(request.permission.to_sym)
+        policy.can?(permission)
+      rescue StandardError => error
+        puts error.inspect
+        false
       end
 
       def subject_of(token)
@@ -371,7 +377,7 @@ module Authz
       def from_jwt(token)
         token
           .split('.', 3)
-          .map { |x| JSON.parse(Base64.strict_decode64(x), symbolize_names: true) }
+          .map { |x| JSON.parse(Base64.strict_decode64(x), symbolize_names: true) rescue {} }
       end
     end
   end
bin/ui
@@ -108,13 +108,7 @@ class UI
         }
       )
     end
-    if response.code.to_i == 200
-      [200, { "Content-Type" => "application/json" }, [JSON.pretty_generate(
-        request.params.merge(JSON.parse(response.body))
-      )]]
-    else
-      [response.code, response.header, [response.body]]
-    end
+    [response.code, response.header, [response.body]]
   end
 
   def saml_post_to_idp(request)
cmd/gtwy/main.go
@@ -1,57 +1,12 @@
 package main
 
 import (
-	"fmt"
 	"log"
-	"net"
-	"net/http"
 
-	"github.com/casbin/casbin/v2"
 	"github.com/xlgmokha/x/pkg/env"
-	"github.com/xlgmokha/x/pkg/x"
-	"gitlab.com/mokhax/spike/pkg/authz"
-	"gitlab.com/mokhax/spike/pkg/cfg"
-	"gitlab.com/mokhax/spike/pkg/prxy"
-	"gitlab.com/mokhax/spike/pkg/srv"
+	"gitlab.com/mokhax/spike/pkg/app"
 )
 
-func WithCasbin() authz.Authorizer {
-	enforcer := x.Must(casbin.NewEnforcer("model.conf", "policy.csv"))
-
-	return authz.AuthorizerFunc(func(r *http.Request) bool {
-		host, _, err := net.SplitHostPort(r.Host)
-		if err != nil {
-			return false
-		}
-
-		subject := "71cbc18e-bd41-4229-9ad2-749546a2a4a7" // TODO:: unpack sub claim in JWT
-		ok, err := enforcer.Enforce(subject, host, r.Method, r.URL.Path)
-		if err != nil {
-			fmt.Printf("%v\n", err)
-			return false
-		}
-
-		fmt.Printf("%v: %v %v%v\n", ok, r.Method, host, r.URL.Path)
-		return ok
-	})
-}
-
-func WithRoutes() cfg.Option {
-	return func(c *cfg.Config) {
-		mux := http.NewServeMux()
-		mux.Handle("/", authz.HTTP(WithCasbin(), prxy.New(map[string]string{
-			"idp.example.com": "http://localhost:8282",
-			"ui.example.com":  "http://localhost:8283",
-			"api.example.com": "http://localhost:8284",
-		})))
-
-		cfg.WithMux(mux)(c)
-	}
-}
-
 func main() {
-	log.Fatal(srv.Run(cfg.New(
-		env.Fetch("BIND_ADDR", ":8080"),
-		WithRoutes(),
-	)))
+	log.Fatal(app.Start(env.Fetch("BIND_ADDR", ":8080")))
 }
pkg/app/app.go
@@ -0,0 +1,44 @@
+package app
+
+import (
+	"fmt"
+	"net"
+	"net/http"
+
+	"github.com/casbin/casbin/v3"
+	"github.com/xlgmokha/x/pkg/x"
+	"gitlab.com/mokhax/spike/pkg/authz"
+	"gitlab.com/mokhax/spike/pkg/cfg"
+	"gitlab.com/mokhax/spike/pkg/srv"
+)
+
+func WithCasbin() authz.Authorizer {
+	enforcer := x.Must(casbin.NewEnforcer("model.conf", "policy.csv"))
+
+	return authz.AuthorizerFunc(func(r *http.Request) bool {
+		host, _, err := net.SplitHostPort(r.Host)
+		if err != nil {
+			return false
+		}
+
+		subject, found := authz.TokenFrom(r).Subject()
+		if !found {
+			subject = "*"
+		}
+		ok, err := enforcer.Enforce(subject, host, r.Method, r.URL.Path)
+		if err != nil {
+			fmt.Printf("%v\n", err)
+			return false
+		}
+
+		fmt.Printf("%v: %v -> %v %v%v\n", ok, subject, r.Method, host, r.URL.Path)
+		return ok
+	})
+}
+
+func Start(bindAddr string) error {
+	return srv.Run(cfg.New(
+		bindAddr,
+		cfg.WithMux(authz.HTTP(WithCasbin(), Routes())),
+	))
+}
pkg/app/routes.go
@@ -0,0 +1,17 @@
+package app
+
+import (
+	"net/http"
+
+	"gitlab.com/mokhax/spike/pkg/prxy"
+)
+
+func Routes() http.Handler {
+	mux := http.NewServeMux()
+	mux.Handle("/", prxy.New(map[string]string{
+		"idp.example.com": "http://localhost:8282",
+		"ui.example.com":  "http://localhost:8283",
+		"api.example.com": "http://localhost:8284",
+	}))
+	return mux
+}
pkg/authz/token.go
@@ -0,0 +1,30 @@
+package authz
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/lestrrat-go/jwx/v3/jwt"
+)
+
+func TokenFrom(r *http.Request) jwt.Token {
+	authorization := r.Header.Get("Authorization")
+	if authorization == "" || !strings.Contains(authorization, "Bearer") {
+		return jwt.New()
+	}
+
+	token, err := jwt.ParseRequest(r,
+		jwt.WithContext(r.Context()),
+		jwt.WithHeaderKey("Authorization"),
+		jwt.WithValidate(false), // TODO:: Connect this to a JSON Web Key Set
+		jwt.WithVerify(false),   // TODO:: Connect this to a JSON Web Key Set
+	)
+
+	if err != nil {
+		fmt.Printf("error: %v\n", err)
+		return jwt.New()
+	}
+
+	return token
+}
test/e2e_test.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/playwright-community/playwright-go"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"github.com/xlgmokha/x/pkg/env"
 	"github.com/xlgmokha/x/pkg/serde"
 	"github.com/xlgmokha/x/pkg/x"
@@ -76,20 +77,34 @@ func TestAuthx(t *testing.T) {
 			assert.Contains(t, page.URL(), "http://ui.example.com:8080/oauth/callback")
 			content := x.Must(page.Locator("pre").First().InnerText())
 			item := x.Must(serde.FromJSON[oauth2.Token](strings.NewReader(content)))
-			assert.NotEmpty(t, item.AccessToken)
-			assert.Equal(t, "Bearer", item.TokenType)
-			assert.NotEmpty(t, item.RefreshToken)
+			require.NotEmpty(t, item.AccessToken)
+			require.Equal(t, "Bearer", item.TokenType)
+			require.NotEmpty(t, item.RefreshToken)
 
-			t.Run("lists all the organizations", func(t *testing.T) {
+			t.Run("GET http://api.example.com:8080/organizations.json", func(t *testing.T) {
 				response := x.Must(http.Get("http://api.example.com:8080/organizations.json"))
-				assert.Equal(t, http.StatusOK, response.StatusCode)
+				assert.Equal(t, http.StatusForbidden, response.StatusCode)
+			})
+
+			t.Run("GET http://api.example.com:8080/organizations.json with Authorization", func(t *testing.T) {
+				request := x.Must(http.NewRequestWithContext(t.Context(), "GET", "http://api.example.com:8080/organizations.json", nil))
+				request.Header.Add("Authorization", "Bearer "+item.AccessToken)
+				response := x.Must(client.Do(request))
+				require.Equal(t, http.StatusOK, response.StatusCode)
 				organizations := x.Must(serde.FromJSON[[]map[string]string](response.Body))
 				assert.NotNil(t, organizations)
 			})
 
-			t.Run("lists all the projects", func(t *testing.T) {
+			t.Run("GET http://api.example.com:8080/projects.json", func(t *testing.T) {
 				response := x.Must(http.Get("http://api.example.com:8080/projects.json"))
-				assert.Equal(t, http.StatusOK, response.StatusCode)
+				assert.Equal(t, http.StatusForbidden, response.StatusCode)
+			})
+
+			t.Run("GET http://api.example.com:8080/projects.json with Authorization", func(t *testing.T) {
+				request := x.Must(http.NewRequestWithContext(t.Context(), "GET", "http://api.example.com:8080/projects.json", nil))
+				request.Header.Add("Authorization", "Bearer "+item.AccessToken)
+				response := x.Must(client.Do(request))
+				require.Equal(t, http.StatusOK, response.StatusCode)
 				projects := x.Must(serde.FromJSON[[]map[string]string](response.Body))
 				assert.NotNil(t, projects)
 			})
@@ -100,18 +115,18 @@ func TestAuthx(t *testing.T) {
 				request := x.Must(http.NewRequestWithContext(t.Context(), "POST", "http://api.example.com:8080/projects", io))
 				request.Header.Add("Authorization", "Bearer "+item.AccessToken)
 				response := x.Must(client.Do(request))
-				assert.Equal(t, http.StatusCreated, response.StatusCode)
+				require.Equal(t, http.StatusCreated, response.StatusCode)
 				project := x.Must(serde.FromJSON[map[string]string](response.Body))
 				assert.Equal(t, "example", project["name"])
 			})
 
-			t.Run("creates another projects", func(t *testing.T) {
+			t.Run("creates another project", func(t *testing.T) {
 				io := bytes.NewBuffer(nil)
 				assert.NoError(t, serde.ToJSON(io, map[string]string{"name": "example2"}))
 				request := x.Must(http.NewRequestWithContext(t.Context(), "POST", "http://api.example.com:8080/projects.json", io))
 				request.Header.Add("Authorization", "Bearer "+item.AccessToken)
 				response := x.Must(client.Do(request))
-				assert.Equal(t, http.StatusCreated, response.StatusCode)
+				require.Equal(t, http.StatusCreated, response.StatusCode)
 				project := x.Must(serde.FromJSON[map[string]string](response.Body))
 				assert.Equal(t, "example2", project["name"])
 			})
@@ -121,7 +136,7 @@ func TestAuthx(t *testing.T) {
 	t.Run("OAuth", func(t *testing.T) {
 		t.Run("GET /.well-known/oauth-authorization-server", func(t *testing.T) {
 			response := x.Must(client.Get("http://idp.example.com:8080/.well-known/oauth-authorization-server"))
-			assert.Equal(t, http.StatusOK, response.StatusCode)
+			require.Equal(t, http.StatusOK, response.StatusCode)
 			metadata := x.Must(serde.FromJSON[map[string]interface{}](response.Body))
 			assert.Equal(t, "http://idp.example.com:8080/.well-known/oauth-authorization-server", metadata["issuer"])
 			assert.Equal(t, "http://idp.example.com:8080/oauth/authorize", metadata["authorization_endpoint"])
@@ -149,7 +164,7 @@ func TestAuthx(t *testing.T) {
 
 		t.Run("GET /.well-known/openid-configuration", func(t *testing.T) {
 			response := x.Must(client.Get("http://idp.example.com:8080/.well-known/openid-configuration"))
-			assert.Equal(t, http.StatusOK, response.StatusCode)
+			require.Equal(t, http.StatusOK, response.StatusCode)
 			metadata := x.Must(serde.FromJSON[map[string]interface{}](response.Body))
 			assert.Equal(t, "http://idp.example.com:8080/.well-known/oauth-authorization-server", metadata["issuer"])
 			assert.Equal(t, "http://idp.example.com:8080/oauth/authorize", metadata["authorization_endpoint"])
@@ -230,14 +245,14 @@ func TestAuthx(t *testing.T) {
 			t.Run("token is usable against REST API", func(t *testing.T) {
 				client := conf.Client(ctx, credentials)
 				response := x.Must(client.Get("http://api.example.com:8080/projects.json"))
-				assert.Equal(t, http.StatusOK, response.StatusCode)
+				require.Equal(t, http.StatusOK, response.StatusCode)
 				projects := x.Must(serde.FromJSON[[]map[string]string](response.Body))
 				assert.NotNil(t, projects)
 
 				io := bytes.NewBuffer(nil)
 				assert.NoError(t, serde.ToJSON(io, map[string]string{"name": "foo"}))
 				response = x.Must(client.Post("http://api.example.com:8080/projects", "application/json", io))
-				assert.Equal(t, http.StatusCreated, response.StatusCode)
+				require.Equal(t, http.StatusCreated, response.StatusCode)
 				project := x.Must(serde.FromJSON[map[string]string](response.Body))
 				assert.Equal(t, "foo", project["name"])
 			})
go.mod
@@ -3,27 +3,36 @@ module gitlab.com/mokhax/spike
 go 1.24.0
 
 require (
-	github.com/casbin/casbin/v2 v2.103.0
+	github.com/casbin/casbin/v3 v3.0.0-beta.7
+	github.com/lestrrat-go/jwx/v3 v3.0.0-alpha3
 	github.com/magefile/mage v1.15.0
 	github.com/playwright-community/playwright-go v0.5001.0
-	github.com/stretchr/testify v1.8.4
+	github.com/stretchr/testify v1.10.0
 	github.com/xlgmokha/x v0.0.0-20240605230110-5cbcac4d8ff8
 	golang.org/x/oauth2 v0.28.0
 )
 
 require (
+	github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
 	github.com/arthurnn/twirp-ruby v1.13.0 // indirect
-	github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
-	github.com/casbin/govaluate v1.3.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/deckarep/golang-set/v2 v2.7.0 // indirect
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.4 // indirect
 	github.com/go-stack/stack v1.8.1 // indirect
+	github.com/goccy/go-json v0.10.3 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/jsonapi v1.0.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
+	github.com/lestrrat-go/blackmagic v1.0.2 // indirect
+	github.com/lestrrat-go/httpcc v1.0.1 // indirect
+	github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 // indirect
+	github.com/lestrrat-go/option v1.0.1 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/segmentio/asm v1.2.0 // indirect
+	golang.org/x/crypto v0.36.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
go.sum
@@ -1,23 +1,23 @@
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/arthurnn/twirp-ruby v1.13.0 h1:j0T7I5oxe2niKFdfjiiCmkiydwYeegrbwVMs+Gajm6M=
 github.com/arthurnn/twirp-ruby v1.13.0/go.mod h1:1fVOQuSLzwXoPi9/ejlDYG3roilJIPAZN2sw+A3o48o=
-github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
-github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
-github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic=
-github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
-github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
-github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
+github.com/casbin/casbin/v3 v3.0.0-beta.7 h1:siS3e6cRtuyFlshUgJfw0wnWuK3z3U/ald0C8Jtof24=
+github.com/casbin/casbin/v3 v3.0.0-beta.7/go.mod h1:69HoI+h4yMUTydUMxT7VQh7FgGpoJsB/ZskkVGcvasQ=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k=
 github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
 github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
 github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
 github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
-github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -30,6 +30,16 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
+github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 h1:pzDjP9dSONCFQC/AE3mWUnHILGiYPiMKzQIS+weKJXA=
+github.com/lestrrat-go/httprc/v3 v3.0.0-beta1/go.mod h1:wdsgouffPvWPEYh8t7PRH/PidR5sfVqt0na4Nhj60Ms=
+github.com/lestrrat-go/jwx/v3 v3.0.0-alpha3 h1:HHT8iW+UcPBgBr5A3soZQQsL5cBor/u6BkLB+wzY/R0=
+github.com/lestrrat-go/jwx/v3 v3.0.0-alpha3/go.mod h1:ak32WoNtHE0aLowVWBcCvXngcAnW4tuC0YhFwOr/kwc=
+github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
+github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
 github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
 github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
 github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
@@ -42,19 +52,24 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
 github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
+github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/xlgmokha/x v0.0.0-20240605230110-5cbcac4d8ff8 h1:Hmyf8pgNUs3l8TW0YdUarBVAU+hWX87efBukspg4nWc=
 github.com/xlgmokha/x v0.0.0-20240605230110-5cbcac4d8ff8/go.mod h1:C9MUZ3A7PTPbrLNTvu2lKhpM0dFpPHt5yH8YGuYzmKQ=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -73,6 +88,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -85,7 +102,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
policy.csv
@@ -1,8 +1,11 @@
-p, "\A[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\z", api.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /*
+p, "\Agid:\/\/[a-z]+\/[A-Za-z:]+\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\z", api.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /*.json
 p, *, *, (GET)|(HEAD), /health
 p, *, *, GET, /.well-known/*
-p, *, idp.example.com, (GET)|(POST), /oauth/*
-p, *, idp.example.com, (GET)|(POST), /saml/*
-p, *, ui.example.com, (GET)|(POST), /oauth/*
-p, *, ui.example.com, (GET)|(POST), /saml/*
-p, 71cbc18e-bd41-4229-9ad2-749546a2a4a7, *, *, /*
+p, *, *, GET, /favicon.ico
+p, "\Agid:\/\/[a-z]+\/[A-Za-z:]+\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\z", idp.example.com, (GET)|(POST)|(PATCH)|(PUT)|(DELETE)|(HEAD), /twirp/authx.rpc.*
+p, *, idp.example.com, (GET)|(POST), /oauth*
+p, *, idp.example.com, (GET)|(POST), /saml*
+p, *, idp.example.com, (GET)|(POST), /sessions*
+p, *, ui.example.com, (GET)|(POST), /oauth*
+p, *, ui.example.com, (GET)|(POST), /oidc*
+p, *, ui.example.com, (GET)|(POST), /saml*