Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cmd/extensions/oidc-policy/api/v1alpha1/oidcpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,23 @@ func (p *OIDCPolicy) redirectURL(igwURL *url.URL) (*url.URL, error) {
return redirectURL, nil
}

// GetBaseURL returns the base URL (scheme + host + port) for post-authentication redirects.
// It derives this from spec.provider.redirectURI if set, otherwise from igwURL.
func (p *OIDCPolicy) GetBaseURL(igwURL *url.URL) (*url.URL, error) {
redirectURL, err := p.redirectURL(igwURL)
if err != nil {
return nil, err
}

// Extract base URL (scheme + host, no path or query)
baseURL := &url.URL{
Scheme: redirectURL.Scheme,
Host: redirectURL.Host,
}

return baseURL, nil
}

// +kubebuilder:object:root=true

// OIDCPolicyList contains a list of OIDCPolicy
Expand Down
97 changes: 97 additions & 0 deletions cmd/extensions/oidc-policy/api/v1alpha1/oidcpolicy_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,103 @@ func TestOIDCPolicyStatus_Equals(t *testing.T) {
}
}

func TestGetBaseURL(t *testing.T) {
tests := []struct {
name string
redirectURI string
igwURL string
expectedBase string
}{
{
name: "No custom redirectURI - uses igwURL",
redirectURI: "",
igwURL: "http://gateway.example.com:8001",
expectedBase: "http://gateway.example.com:8001",
},
{
name: "Custom redirectURI with non-standard port",
redirectURI: "https://public.example.com:8443/auth/callback",
igwURL: "http://gateway.example.com:8001",
expectedBase: "https://public.example.com:8443",
},
{
name: "Custom redirectURI with standard port",
redirectURI: "https://public.example.com/auth/callback",
igwURL: "http://gateway.example.com:8001",
expectedBase: "https://public.example.com",
},
{
name: "Custom redirectURI with different scheme",
redirectURI: "http://external.example.com:9000/custom/callback",
igwURL: "https://gateway.example.com",
expectedBase: "http://external.example.com:9000",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
policy := &OIDCPolicy{
Spec: OIDCPolicySpec{
OIDCPolicySpecProper: OIDCPolicySpecProper{
Provider: &Provider{
IssuerURL: "https://issuer.com",
ClientID: "client123",
RedirectURI: tt.redirectURI,
},
},
},
}

igwURL, err := url.Parse(tt.igwURL)
if err != nil {
t.Fatal(err)
}

baseURL, err := policy.GetBaseURL(igwURL)
if err != nil {
t.Fatalf("GetBaseURL() error = %v", err)
}

if baseURL.String() != tt.expectedBase {
t.Errorf("GetBaseURL() = %v, want %v", baseURL.String(), tt.expectedBase)
}
})
}
}

func TestGetBaseURL_ExtractsBaseFromRedirectURI(t *testing.T) {
policy := &OIDCPolicy{
Spec: OIDCPolicySpec{
OIDCPolicySpecProper: OIDCPolicySpecProper{
Provider: &Provider{
IssuerURL: "https://issuer.com",
ClientID: "client123",
RedirectURI: "https://public.example.com:8443/auth/callback?foo=bar",
},
},
},
}

igwURL, _ := url.Parse("http://gateway.example.com:8001")
baseURL, err := policy.GetBaseURL(igwURL)
if err != nil {
t.Fatalf("GetBaseURL() error = %v", err)
}

// Base URL should only have scheme and host, no path or query
if baseURL.String() != "https://public.example.com:8443" {
t.Errorf("GetBaseURL() = %v, want https://public.example.com:8443", baseURL.String())
}

if baseURL.Path != "" {
t.Errorf("GetBaseURL() path should be empty, got %v", baseURL.Path)
}

if baseURL.RawQuery != "" {
t.Errorf("GetBaseURL() query should be empty, got %v", baseURL.RawQuery)
}
}

func mockMinimalOIDCPolicy() *OIDCPolicy {
return &OIDCPolicy{
TypeMeta: metav1.TypeMeta{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,21 @@ type ingressGatewayInfo struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Protocol gatewayapiv1.ProtocolType `json:"protocol"`
Port int32 `json:"port"`
url *url.URL
}

func (g *ingressGatewayInfo) GetURL() *url.URL {
if g.url == nil {
host := g.Hostname
// Include port if it's not the standard port for the protocol
if (g.Protocol == gatewayapiv1.HTTPProtocolType && g.Port != 80) ||
(g.Protocol == gatewayapiv1.HTTPSProtocolType && g.Port != 443) {
host = fmt.Sprintf("%s:%d", g.Hostname, g.Port)
}
g.url = &url.URL{
Scheme: strings.ToLower(string(g.Protocol)),
Host: g.Hostname,
Host: host,
}
}
return g.url
Expand Down Expand Up @@ -109,6 +116,7 @@ func (r *OIDCPolicyReconciler) Reconcile(ctx context.Context, request reconcile.
oidcPolicy,
`{"protocol": self.findGateways()[0].spec.listeners[0].protocol,
"hostname": self.findGateways()[0].spec.listeners[0].hostname,
"port": self.findGateways()[0].spec.listeners[0].port,
"name": self.findGateways()[0].metadata.name,
"namespace": self.findGateways()[0].metadata.namespace}`,
true)
Expand Down Expand Up @@ -282,6 +290,11 @@ func (r *OIDCPolicyReconciler) reconcileHTTPRoute(ctx context.Context, desired *
return err
}

func buildTargetCookieExpression(hostname string, protocol gatewayapiv1.ProtocolType) string {
return fmt.Sprintf(`
"target=" + request.path + (has(request.query) && request.query != "" ? "?" + request.query : "") + "; domain=%s; HttpOnly; %s SameSite=Lax; Path=/; Max-Age=3600"`, hostname, getSecureFlag(protocol))
}
Comment on lines +294 to +296

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that request.path in Authorino's CEL context already includes the query string (it's the full request URI, e.g. /get?foo=bar&baz=qux). Appending "?" + request.query on top of that duplicates the query parameters: e.g target=/get?foo=bar&baz=qux?foo=bar&baz=qux


func buildMainAuthPolicy(pol *v1alpha1.OIDCPolicy, igw *ingressGatewayInfo) (*kuadrantv1.AuthPolicy, error) {
authorizeURL, err := pol.GetAuthorizeURL(igw.GetURL())
if err != nil {
Expand All @@ -292,8 +305,7 @@ func buildMainAuthPolicy(pol *v1alpha1.OIDCPolicy, igw *ingressGatewayInfo) (*ku
return nil, err
}

setCookie := fmt.Sprintf(`
"target=" + request.path + "; domain=%s; HttpOnly; %s SameSite=Lax; Path=/; Max-Age=3600"`, igw.Hostname, getSecureFlag(igw.Protocol))
setCookie := buildTargetCookieExpression(igw.Hostname, igw.Protocol)

var authorization = map[string]kuadrantv1.MergeableAuthorizationSpec{}
var authPatterns []authorinov1beta3.PatternExpressionOrRef
Expand Down Expand Up @@ -427,6 +439,14 @@ func buildCallbackHTTPRoute(pol *v1alpha1.OIDCPolicy, igw *ingressGatewayInfo) *
}
}

func buildOpaAuthorizationRule(baseURL *url.URL, igwURL *url.URL, authorizeURL string) string {
return fmt.Sprintf(`cookies := { name: value | raw_cookies := input.request.headers.cookie; cookie_parts := split(raw_cookies, ";"); part := cookie_parts[_]; trimmed := trim(part, " "); eq_idx := indexof(trimmed, "="); eq_idx != -1; name := trim(substring(trimmed, 0, eq_idx), " "); value := trim(substring(trimmed, eq_idx + 1, -1), " ")}
location := concat("", ["%s", cookies.target]) { input.auth.metadata.token.id_token; cookies.target }
location := "%s" { input.auth.metadata.token.id_token; not cookies.target }
location := "%s" { not input.auth.metadata.token.id_token }
allow = true`, baseURL, igwURL, authorizeURL)
}

func buildCallbackAuthPolicy(pol *v1alpha1.OIDCPolicy, igw *ingressGatewayInfo) (*kuadrantv1.AuthPolicy, error) {
igwURL := igw.GetURL()
tokenRequestURL, err := pol.GetTokenRequestURL()
Expand All @@ -443,6 +463,12 @@ func buildCallbackAuthPolicy(pol *v1alpha1.OIDCPolicy, igw *ingressGatewayInfo)
return nil, err
}

// Get the base URL for post-auth redirects (respects custom redirectURI if set)
baseURL, err := pol.GetBaseURL(igwURL)
if err != nil {
return nil, err
}

callbackRoute := gatewayapiv1alpha2.LocalPolicyTargetReference{
Group: gatewayapiv1alpha2.GroupName,
Kind: gatewayapiv1alpha2.Kind("HTTPRoute"),
Expand All @@ -455,11 +481,7 @@ func buildCallbackAuthPolicy(pol *v1alpha1.OIDCPolicy, igw *ingressGatewayInfo)
Expression: authorinov1beta3.CelExpression(callBodyCelExpression),
}

opaAuthorizationRule := fmt.Sprintf(`cookies := { name: value | raw_cookies := input.request.headers.cookie; cookie_parts := split(raw_cookies, ";"); part := cookie_parts[_]; kv := split(trim(part, " "), "="); count(kv) == 2; name := trim(kv[0], " "); value := trim(kv[1], " ")}
location := concat("", ["%s", cookies.target]) { input.auth.metadata.token.id_token; cookies.target }
location := "%s" { input.auth.metadata.token.id_token; not cookies.target }
location := "%s" { not input.auth.metadata.token.id_token }
allow = true`, igwURL, igwURL, authorizeURL)
opaAuthorizationRule := buildOpaAuthorizationRule(baseURL, igwURL, authorizeURL)

return &kuadrantv1.AuthPolicy{
TypeMeta: metav1.TypeMeta{
Expand Down
Loading
Loading