Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
86552d6
feat(rover-ctl): implement PatchAuthentication to map clientAuthMetho…
stefan-ctrl Apr 27, 2026
90e88b3
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 27, 2026
82994b5
test(rover-server): add tests to verify ctl output
stefan-ctrl Apr 27, 2026
de29049
Merge branch 'client-authentication-method-roverctl-only' of https://…
stefan-ctrl Apr 27, 2026
55eeea7
test(rover-handler): update tests to ensure authentication remains un…
stefan-ctrl Apr 27, 2026
dad4b83
fix(timestamps): update snapshot timestamps to reflect correct timezo…
stefan-ctrl Apr 27, 2026
08f3b3c
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 27, 2026
9bc4511
feat(rover): implement mapping for Rover authentication methods and a…
stefan-ctrl Apr 27, 2026
d9fafe0
chore: make generate & manifest
stefan-ctrl Apr 27, 2026
63a4d66
refactor: align with tokenRequest values
stefan-ctrl Apr 28, 2026
848caa5
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 28, 2026
bd3e69a
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 28, 2026
59b6a0a
chore: make generate & manifests
stefan-ctrl Apr 28, 2026
8de6c66
refactor: move to fuzzbuzz converter
stefan-ctrl Apr 28, 2026
10ae067
refactor: align tokenRequests values for RFC 7591
stefan-ctrl Apr 28, 2026
fe7df33
refactor: replace hardcoded tokenRequest values with constants from r…
stefan-ctrl Apr 28, 2026
4e08c17
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl Apr 28, 2026
5e931dd
feat(tokenRequest): add initial tokenRequest values for RFC 7591 clie…
stefan-ctrl Apr 28, 2026
e56556a
Merge remote-tracking branch 'origin/main' into client-authentication…
stefan-ctrl May 5, 2026
4484ab4
feat(auth): normalize clientAuthMethod values in PatchAuthentication …
stefan-ctrl May 5, 2026
ec4f672
Merge remote-tracking branch 'origin/main' into client-authentication…
stefan-ctrl May 13, 2026
99d318d
chore: solve some local issues
stefan-ctrl May 13, 2026
b2bd0ff
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl May 13, 2026
9627521
Merge branch 'main' into client-authentication-method-roverctl-only
stefan-ctrl May 13, 2026
5dcb703
refactor: introduce enum
stefan-ctrl May 13, 2026
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
4 changes: 2 additions & 2 deletions api/api/v1/security_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ type ExternalIdentityProvider struct {
// +kubebuilder:validation:Format=uri
TokenEndpoint string `json:"tokenEndpoint"`

// TokenRequest is the type of token request, "body" or "header"
// TokenRequest configures the token endpoint authentication method (RFC 7591)
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Enum=body;header
// +kubebuilder:validation:Enum=client_secret_basic;client_secret_post
TokenRequest string `json:"tokenRequest,omitempty"`

// GrantType defines the OAuth2 grant type to use for the token request
Expand Down
8 changes: 4 additions & 4 deletions api/config/crd/bases/api.cp.ei.telekom.de_apiexposures.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,11 @@ spec:
format: uri
type: string
tokenRequest:
description: TokenRequest is the type of token request,
"body" or "header"
description: TokenRequest configures the token endpoint
authentication method (RFC 7591)
enum:
- body
- header
- client_secret_basic
- client_secret_post
type: string
required:
- tokenEndpoint
Expand Down
9 changes: 5 additions & 4 deletions api/internal/controller/apiexposure_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import (
"fmt"

"github.com/telekom/controlplane/api/internal/handler/util"

Check failure on line 10 in api/internal/controller/apiexposure_controller_test.go

View workflow job for this annotation

GitHub Actions / Api / Static Checks for api

File is not properly formatted (gci)
applicationapi "github.com/telekom/controlplane/application/api/v1"

adminapi "github.com/telekom/controlplane/admin/api/v1"
Expand Down Expand Up @@ -156,7 +157,7 @@
M2M: &apiapi.Machine2MachineAuthentication{
ExternalIDP: &apiapi.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/token",
TokenRequest: "header",
TokenRequest: "client_secret_basic",
GrantType: "client_credentials",
Client: &apiapi.OAuth2ClientCredentials{
ClientId: "client-id",
Expand Down Expand Up @@ -399,7 +400,7 @@
err := k8sClient.Create(ctx, thirdApiExposure)
Expect(err).To(HaveOccurred())
Expect(apierrors.IsInvalid(err)).To(BeTrue())
Expect(err.Error()).To(ContainSubstring("Unsupported value: \"sky\": supported values: \"body\", \"header\""))
Expect(err.Error()).To(ContainSubstring("Unsupported value: \"sky\": supported values: \"client_secret_basic\", \"client_secret_post\""))

thirdApiExposure.Spec.Security.M2M.ExternalIDP.GrantType = "not_a_valid_grant_type"
err = k8sClient.Create(ctx, thirdApiExposure)
Expand All @@ -413,7 +414,7 @@
thirdApiExposure.Spec.Security.M2M = &apiv1.Machine2MachineAuthentication{
ExternalIDP: &apiv1.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/token",
TokenRequest: "header",
TokenRequest: "client_secret_basic",
GrantType: "client_credentials",
Client: &apiv1.OAuth2ClientCredentials{
ClientId: "team",
Expand All @@ -439,7 +440,7 @@
g.Expect(route.Spec.Security.M2M.Scopes).To(Equal([]string{"team:scope", "api:scope"}))

g.Expect(route.Spec.Security.M2M.ExternalIDP.TokenEndpoint).To(Equal("https://example.com/token"))
g.Expect(route.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal("header"))
g.Expect(route.Spec.Security.M2M.ExternalIDP.TokenRequest).To(Equal("client_secret_basic"))
g.Expect(route.Spec.Security.M2M.ExternalIDP.GrantType).To(Equal("client_credentials"))
}, timeout, interval).Should(Succeed())
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package controller

import (
. "github.com/onsi/ginkgo/v2"

Check failure on line 8 in api/internal/controller/apisubscription_controller_ratelimiting_test.go

View workflow job for this annotation

GitHub Actions / Api / Static Checks for api

File is not properly formatted (gci)
. "github.com/onsi/gomega"
adminapi "github.com/telekom/controlplane/admin/api/v1"
apiapi "github.com/telekom/controlplane/api/api/v1"
Expand Down Expand Up @@ -83,7 +83,7 @@
M2M: &apiapi.Machine2MachineAuthentication{
ExternalIDP: &apiapi.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/token",
TokenRequest: "header",
TokenRequest: "client_secret_basic",
GrantType: "client_credentials",
Client: &apiapi.OAuth2ClientCredentials{
ClientId: "client-id",
Expand Down
14 changes: 13 additions & 1 deletion discovery-server/internal/mapper/apiexposure/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import (
"github.com/telekom/controlplane/discovery-server/internal/mapper/status"
)

// tokenRequestCRDToAPI converts CRD tokenRequest values to discovery-server API enum values.
func tokenRequestCRDToAPI(value string) api.OAuth2TokenRequest {
switch strings.ToLower(value) {
case "client_secret_basic":
return api.Header
case "client_secret_post":
return api.Body
default:
return api.OAuth2TokenRequest(value)
}
}

// MapResponse maps an ApiExposure CRD to an ApiExposureResponse.
func MapResponse(in *apiv1.ApiExposure) api.ApiExposureResponse {
resp := api.ApiExposureResponse{
Expand Down Expand Up @@ -81,7 +93,7 @@ func mapSecurity(in *apiv1.ApiExposure, out *api.ApiExposureResponse) {
if m2m.ExternalIDP != nil {
oauth2 := api.OAuth2{
TokenEndpoint: m2m.ExternalIDP.TokenEndpoint,
TokenRequest: api.OAuth2TokenRequest(m2m.ExternalIDP.TokenRequest),
TokenRequest: tokenRequestCRDToAPI(m2m.ExternalIDP.TokenRequest),
GrantType: m2m.ExternalIDP.GrantType,
}

Expand Down
4 changes: 2 additions & 2 deletions discovery-server/internal/mapper/apiexposure/out_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func TestMapSecurity(t *testing.T) {
{
name: "external idp oauth2",
setup: func(in *apiv1.ApiExposure) {
in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "body", GrantType: "client_credentials", Client: &apiv1.OAuth2ClientCredentials{ClientId: "cid", ClientSecret: "sec"}}, Scopes: []string{"s1"}}}
in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "client_secret_post", GrantType: "client_credentials", Client: &apiv1.OAuth2ClientCredentials{ClientId: "cid", ClientSecret: "sec"}}, Scopes: []string{"s1"}}}
},
assert: func(t *testing.T, out api.ApiExposureResponse) {
t.Helper()
Expand All @@ -157,7 +157,7 @@ func TestMapSecurity(t *testing.T) {
{
name: "external idp oauth2 with basic credentials",
setup: func(in *apiv1.ApiExposure) {
in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "header", GrantType: "password", Basic: &apiv1.BasicAuthCredentials{Username: "bu", Password: "bp"}}}}
in.Spec.Security = &apiv1.Security{M2M: &apiv1.Machine2MachineAuthentication{ExternalIDP: &apiv1.ExternalIdentityProvider{TokenEndpoint: "https://idp/token", TokenRequest: "client_secret_basic", GrantType: "password", Basic: &apiv1.BasicAuthCredentials{Username: "bu", Password: "bp"}}}}
},
assert: func(t *testing.T, out api.ApiExposureResponse) {
t.Helper()
Expand Down
4 changes: 2 additions & 2 deletions gateway/api/v1/security_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ type ExternalIdentityProvider struct {
// +kubebuilder:validation:Format=uri
TokenEndpoint string `json:"tokenEndpoint"`

// TokenRequest is the type of token request, "body" or "header"
// TokenRequest configures the token endpoint authentication method (RFC 7591)
// +kubebuilder:validation:Optional
// +kubebuilder:validation:Enum=body;header
// +kubebuilder:validation:Enum=client_secret_basic;client_secret_post
TokenRequest string `json:"tokenRequest,omitempty"`
// GrantType is the grant type for the external IDP authentication
// +kubebuilder:validation:Optional
Expand Down
16 changes: 8 additions & 8 deletions gateway/config/crd/bases/gateway.cp.ei.telekom.de_routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,11 @@ spec:
format: uri
type: string
tokenRequest:
description: TokenRequest is the type of token request,
"body" or "header"
description: TokenRequest configures the token endpoint
authentication method (RFC 7591)
enum:
- body
- header
- client_secret_basic
- client_secret_post
type: string
required:
- tokenEndpoint
Expand Down Expand Up @@ -354,11 +354,11 @@ spec:
format: uri
type: string
tokenRequest:
description: TokenRequest is the type of token
request, "body" or "header"
description: TokenRequest configures the token
endpoint authentication method (RFC 7591)
enum:
- body
- header
- client_secret_basic
- client_secret_post
type: string
required:
- tokenEndpoint
Expand Down
2 changes: 1 addition & 1 deletion gateway/internal/controller/route_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package controller

import (
. "github.com/onsi/ginkgo/v2"

Check failure on line 8 in gateway/internal/controller/route_controller_test.go

View workflow job for this annotation

GitHub Actions / Gateway / Static Checks for gateway

File is not properly formatted (gci)
. "github.com/onsi/gomega"
"github.com/telekom/controlplane/common/pkg/condition"
"github.com/telekom/controlplane/common/pkg/config"
Expand Down Expand Up @@ -148,7 +148,7 @@
err := k8sClient.Create(ctx, route)
Expect(err).To(HaveOccurred())
Expect(apierrors.IsInvalid(err)).To(BeTrue())
Expect(err.Error()).To(ContainSubstring("spec.security.m2m.externalIDP.tokenRequest: Unsupported value: \"sky\": supported values: \"body\", \"header\""))
Expect(err.Error()).To(ContainSubstring("spec.security.m2m.externalIDP.tokenRequest: Unsupported value: \"sky\": supported values: \"client_secret_basic\", \"client_secret_post\""))
})

It("should not accept a Route with GrantType=\"not_required\"", func() {
Expand Down
6 changes: 3 additions & 3 deletions gateway/internal/features/builder_external_idp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func externalIDPProviderRouteOAuth() *gatewayv1.Route {
M2M: &gatewayv1.Machine2MachineAuthentication{
ExternalIDP: &gatewayv1.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/tokenEndpoint",
TokenRequest: "header",
TokenRequest: "client_secret_basic",
GrantType: "client_credentials",
Client: &gatewayv1.OAuth2ClientCredentials{
ClientId: "gateway",
Expand Down Expand Up @@ -243,7 +243,7 @@ func externalIDPProviderRouteBasic() *gatewayv1.Route {
M2M: &gatewayv1.Machine2MachineAuthentication{
ExternalIDP: &gatewayv1.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/tokenEndpoint",
TokenRequest: "header",
TokenRequest: "client_secret_basic",
GrantType: "password",
Basic: &gatewayv1.BasicAuthCredentials{
Username: "user",
Expand Down Expand Up @@ -271,7 +271,7 @@ func externalIDPProviderRouteOAuthJwt() *gatewayv1.Route {
M2M: &gatewayv1.Machine2MachineAuthentication{
ExternalIDP: &gatewayv1.ExternalIdentityProvider{
TokenEndpoint: "https://example.com/tokenEndpoint",
TokenRequest: "header",
TokenRequest: "client_secret_basic",
GrantType: "client_credentials",
Client: &gatewayv1.OAuth2ClientCredentials{
ClientId: "ClientId",
Expand Down
19 changes: 18 additions & 1 deletion gateway/internal/features/feature/external_idp.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package feature

import (
"context"
"fmt"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -146,7 +147,11 @@ func extendOauth(ctx context.Context, in plugin.OauthCredentials, providerSettin
in.Scopes = strings.Join(scopes, " ")
}

in.TokenRequest = providerSettings.TokenRequest
tokenRequest, err := tokenRequestToJumper(providerSettings.TokenRequest)
if err != nil {
return in, err
}
in.TokenRequest = tokenRequest
in.GrantType = providerSettings.GrantType

return in, nil
Expand Down Expand Up @@ -182,3 +187,15 @@ func extendBasic(ctx context.Context, in plugin.OauthCredentials, providerSettin

return in, nil
}

// tokenRequestToJumper converts CRD tokenRequest values to the values expected by the Jumper plugin.
func tokenRequestToJumper(value string) (string, error) {
switch strings.ToLower(value) {
case "client_secret_basic":
return "header", nil
case "client_secret_post":
return "body", nil
default:
return "", fmt.Errorf("unsupported tokenRequest value %q", value)
}
}
57 changes: 57 additions & 0 deletions rover-ctl/pkg/handlers/v0/rover.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package v0
import (
"context"
"maps"
"strings"

"github.com/pkg/errors"
"github.com/telekom/controlplane/rover-ctl/pkg/handlers/common"
Expand Down Expand Up @@ -65,10 +66,66 @@ func PatchRoverRequest(ctx context.Context, obj types.Object) error {
}
}

PatchAuthentication(spec)

obj.SetContent(spec)
return nil
}

// PatchAuthentication restructures spec.authentication.m2m.clientAuthMethod
// into spec.authentication.clientAuthMethod for the rover-server API format.
// It also normalizes "BODY"/"body" to "POST" since the server schema only accepts
// NONE, POST, BASIC.
func PatchAuthentication(spec map[string]any) {
auth, exists := spec["authentication"]
if !exists {
return
}
authMap, ok := auth.(map[string]any)
if !ok {
return
}

m2m, exists := authMap["m2m"]
if !exists {
return
}
m2mMap, ok := m2m.(map[string]any)
if !ok {
return
}

clientAuthMethod, exists := m2mMap["clientAuthMethod"]
if !exists {
return
}

spec["authentication"] = map[string]any{
"clientAuthMethod": normalizeClientAuthMethod(clientAuthMethod),
}
}

// normalizeClientAuthMethod maps user-friendly aliases to the API enum values.
// "BODY"/"body" is treated as "POST" per RFC 6749.
func normalizeClientAuthMethod(value any) any {
s, ok := value.(string)
if !ok {
return value
}
switch strings.ToUpper(s) {
case "BODY":
return "POST"
case "BASIC":
return "BASIC"
case "NONE":
return "NONE"
case "POST":
return "POST"
default:
return value
}
}

func PatchExposures(exposures []any) []map[string]any {
if len(exposures) == 0 {
return nil
Expand Down
Loading
Loading