Skip to content

Commit d714b42

Browse files
authored
Merge pull request #589 from kartverket/add-jwt-authentication
Add JWT-validation for idporten/maskinporten
2 parents 48089d4 + 352e94c commit d714b42

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1630
-143
lines changed

api/v1alpha1/application_types.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package v1alpha1
33
import (
44
"encoding/json"
55
"errors"
6+
"github.com/kartverket/skiperator/api/v1alpha1/digdirator"
67
"time"
78

8-
"github.com/kartverket/skiperator/api/v1alpha1/digdirator"
99
"github.com/kartverket/skiperator/api/v1alpha1/istiotypes"
1010
"github.com/kartverket/skiperator/api/v1alpha1/podtypes"
1111
corev1 "k8s.io/api/core/v1"
@@ -469,6 +469,10 @@ func (s *ApplicationSpec) Hosts() (HostCollection, error) {
469469
return hosts, errors.Join(errorsFound...)
470470
}
471471

472+
func (s *ApplicationSpec) IsRequestAuthEnabled() bool {
473+
return (s.IDPorten != nil && s.IDPorten.IsRequestAuthEnabled()) || (s.Maskinporten != nil && s.Maskinporten.IsRequestAuthEnabled())
474+
}
475+
472476
type MultiErr interface {
473477
Unwrap() []error
474478
}

api/v1alpha1/digdirator/digdirator.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package digdirator
2+
3+
import (
4+
"github.com/kartverket/skiperator/api/v1alpha1/istiotypes"
5+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6+
)
7+
8+
type DigdiratorName string
9+
10+
type DigdiratorInfo struct {
11+
Name DigdiratorName
12+
IssuerURI string
13+
JwksURI string
14+
ClientID string
15+
}
16+
17+
type DigdiratorClient interface {
18+
GetOwnerReferences() []v1.OwnerReference
19+
GetSecretName() string
20+
}
21+
22+
type DigdiratorProvider interface {
23+
IsRequestAuthEnabled() bool
24+
GetAuthSpec() *istiotypes.RequestAuthentication
25+
GetDigdiratorName() DigdiratorName
26+
GetProvidedSecretName() *string
27+
GetPaths() []string
28+
GetIgnoredPaths() []string
29+
GetIssuerKey() string
30+
GetJwksKey() string
31+
GetClientIDKey() string
32+
GetTokenLocation() string
33+
}

api/v1alpha1/digdirator/idporten.go

+86-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package digdirator
22

3-
import nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1"
3+
import (
4+
"github.com/kartverket/skiperator/api/v1alpha1/istiotypes"
5+
"github.com/nais/digdirator/pkg/secrets"
6+
nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1"
7+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
)
9+
10+
const IDPortenName DigdiratorName = "idporten"
411

512
// Based off NAIS' IDPorten specification as seen here:
613
// https://github.com/nais/liberator/blob/c9da4cf48a52c9594afc8a4325ff49bbd359d9d2/pkg/apis/nais.io/v1/naiserator_types.go#L93C10-L93C10
714
//
815
// +kubebuilder:object:generate=true
916
type IDPorten struct {
1017
// The name of the Client as shown in Digitaliseringsdirektoratet's Samarbeidsportal
11-
// Meant to be a human-readable name for separating clients in the portal
18+
// Meant to be a human-readable name for separating clients in the portal.
1219
ClientName *string `json:"clientName,omitempty"`
1320

1421
// Whether to enable provisioning of an ID-porten client.
15-
// If enabled, an ID-porten client be provisioned.
22+
// If enabled, an ID-porten client will be provisioned.
1623
Enabled bool `json:"enabled"`
1724

1825
// AccessTokenLifetime is the lifetime in seconds for any issued access token from ID-porten.
@@ -75,4 +82,80 @@ type IDPorten struct {
7582
// +kubebuilder:validation:Minimum=3600
7683
// +kubebuilder:validation:Maximum=7200
7784
SessionLifetime *int `json:"sessionLifetime,omitempty"`
85+
86+
// RequestAuthentication specifies how incoming JWTs should be validated.
87+
RequestAuthentication *istiotypes.RequestAuthentication `json:"requestAuthentication,omitempty"`
88+
}
89+
90+
type IDPortenClient struct {
91+
Client nais_io_v1.IDPortenClient
92+
}
93+
94+
func (i *IDPortenClient) GetSecretName() string {
95+
return i.Client.Spec.SecretName
96+
}
97+
98+
func (i *IDPortenClient) GetOwnerReferences() []v1.OwnerReference {
99+
return i.Client.GetOwnerReferences()
100+
}
101+
102+
func (i *IDPorten) IsRequestAuthEnabled() bool {
103+
return i != nil && i.RequestAuthentication != nil && i.RequestAuthentication.Enabled
104+
}
105+
106+
func (i *IDPorten) GetAuthSpec() *istiotypes.RequestAuthentication {
107+
if i != nil && i.RequestAuthentication != nil {
108+
return i.RequestAuthentication
109+
}
110+
return nil
111+
}
112+
113+
func (i *IDPorten) GetDigdiratorName() DigdiratorName {
114+
return IDPortenName
115+
}
116+
117+
func (i *IDPorten) GetProvidedSecretName() *string {
118+
if i != nil && i.RequestAuthentication != nil {
119+
return i.RequestAuthentication.SecretName
120+
}
121+
return nil
122+
}
123+
124+
func (i *IDPorten) GetPaths() []string {
125+
var paths []string
126+
if i.IsRequestAuthEnabled() {
127+
if i.RequestAuthentication.Paths != nil {
128+
paths = append(paths, *i.RequestAuthentication.Paths...)
129+
}
130+
}
131+
return paths
132+
}
133+
134+
func (i *IDPorten) GetIgnoredPaths() []string {
135+
var ignoredPaths []string
136+
if i.IsRequestAuthEnabled() {
137+
if i.RequestAuthentication.IgnorePaths != nil {
138+
ignoredPaths = append(ignoredPaths, *i.RequestAuthentication.IgnorePaths...)
139+
}
140+
}
141+
return ignoredPaths
142+
}
143+
144+
func (i *IDPorten) GetIssuerKey() string {
145+
return secrets.IDPortenIssuerKey
146+
}
147+
148+
func (i *IDPorten) GetJwksKey() string {
149+
return secrets.IDPortenJwksUriKey
150+
}
151+
152+
func (i *IDPorten) GetClientIDKey() string {
153+
return secrets.IDPortenClientIDKey
154+
}
155+
156+
func (i *IDPorten) GetTokenLocation() string {
157+
if i != nil && i.RequestAuthentication != nil && i.RequestAuthentication.TokenLocation != nil {
158+
return *i.RequestAuthentication.TokenLocation
159+
}
160+
return "cookie"
78161
}

api/v1alpha1/digdirator/maskinporten.go

+84-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
package digdirator
22

3-
import nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1"
3+
import (
4+
"github.com/kartverket/skiperator/api/v1alpha1/istiotypes"
5+
"github.com/nais/digdirator/pkg/secrets"
6+
nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1"
7+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
)
9+
10+
const MaskinPortenName = "maskinporten"
411

512
// https://github.com/nais/liberator/blob/c9da4cf48a52c9594afc8a4325ff49bbd359d9d2/pkg/apis/nais.io/v1/naiserator_types.go#L376
613
//
@@ -15,4 +22,80 @@ type Maskinporten struct {
1522

1623
// Schema to configure Maskinporten clients with consumed scopes and/or exposed scopes.
1724
Scopes *nais_io_v1.MaskinportenScope `json:"scopes,omitempty"`
25+
26+
// RequestAuthentication specifies how incoming JWTs should be validated.
27+
RequestAuthentication *istiotypes.RequestAuthentication `json:"requestAuthentication,omitempty"`
28+
}
29+
30+
type MaskinportenClient struct {
31+
Client nais_io_v1.MaskinportenClient
32+
}
33+
34+
func (m *MaskinportenClient) GetOwnerReferences() []v1.OwnerReference {
35+
return m.Client.GetOwnerReferences()
36+
}
37+
38+
func (m *MaskinportenClient) GetSecretName() string {
39+
return m.Client.Spec.SecretName
40+
}
41+
42+
func (i *Maskinporten) IsRequestAuthEnabled() bool {
43+
return i != nil && i.RequestAuthentication != nil && i.RequestAuthentication.Enabled
44+
}
45+
46+
func (i *Maskinporten) GetAuthSpec() *istiotypes.RequestAuthentication {
47+
if i != nil && i.RequestAuthentication != nil {
48+
return i.RequestAuthentication
49+
}
50+
return nil
51+
}
52+
53+
func (i *Maskinporten) GetDigdiratorName() DigdiratorName {
54+
return MaskinPortenName
55+
}
56+
57+
func (i *Maskinporten) GetProvidedSecretName() *string {
58+
if i != nil && i.RequestAuthentication != nil {
59+
return i.RequestAuthentication.SecretName
60+
}
61+
return nil
62+
}
63+
64+
func (i *Maskinporten) GetPaths() []string {
65+
var paths []string
66+
if i.IsRequestAuthEnabled() {
67+
if i.RequestAuthentication.Paths != nil {
68+
paths = append(paths, *i.RequestAuthentication.Paths...)
69+
}
70+
}
71+
return paths
72+
}
73+
74+
func (i *Maskinporten) GetIgnoredPaths() []string {
75+
var ignoredPaths []string
76+
if i.IsRequestAuthEnabled() {
77+
if i.RequestAuthentication.IgnorePaths != nil {
78+
ignoredPaths = append(ignoredPaths, *i.RequestAuthentication.IgnorePaths...)
79+
}
80+
}
81+
return ignoredPaths
82+
}
83+
84+
func (i *Maskinporten) GetIssuerKey() string {
85+
return secrets.MaskinportenIssuerKey
86+
}
87+
88+
func (i *Maskinporten) GetJwksKey() string {
89+
return secrets.MaskinportenJwksUriKey
90+
}
91+
92+
func (i *Maskinporten) GetClientIDKey() string {
93+
return secrets.MaskinportenClientIDKey
94+
}
95+
96+
func (i *Maskinporten) GetTokenLocation() string {
97+
if i != nil && i.RequestAuthentication != nil && i.RequestAuthentication.TokenLocation != nil {
98+
return *i.RequestAuthentication.TokenLocation
99+
}
100+
return "header"
18101
}

api/v1alpha1/digdirator/zz_generated.deepcopy.go

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package istiotypes
2+
3+
// RequestAuthentication specifies how incoming JWTs should be validated.
4+
//
5+
// +kubebuilder:object:generate=true
6+
type RequestAuthentication struct {
7+
// Whether to enable JWT validation.
8+
// If enabled, incoming JWTs will be validated against the issuer specified in the app registration and the generated audience.
9+
Enabled bool `json:"enabled"`
10+
11+
// The name of the Kubernetes Secret containing OAuth2 credentials.
12+
//
13+
// If omitted, the associated client registration in the application manifest is used for JWT validation.
14+
// +kubebuilder:validation:Optional
15+
SecretName *string `json:"secretName,omitempty"`
16+
17+
// If set to `true`, the original token will be kept for the upstream request. Defaults to `true`.
18+
// +kubebuilder:default=true
19+
ForwardJwt bool `json:"forwardJwt,omitempty"`
20+
21+
// Where to find the JWT in the incoming request
22+
//
23+
// An enum value of `header` means that the JWT is present in the `Authorization` header as a `Bearer` token.
24+
// An enum value of `cookie` means that the JWT is present as a cookie called `BearerToken`.
25+
//
26+
// If omitted, its default value depends on the provider type:
27+
// - Defaults to "cookie" for providers supporting user login (e.g. IDPorten).
28+
// - Defaults to "header" for providers not supporting user login (e.g. Maskinporten).
29+
// +kubebuilder:validation:Enum=header;cookie
30+
// +kubebuilder:validation:Optional
31+
TokenLocation *string `json:"tokenLocation,omitempty"`
32+
33+
// This field specifies a list of operations to copy the claim to HTTP headers on a successfully verified token.
34+
// The header specified in each operation in the list must be unique. Nested claims of type string/int/bool is supported as well.
35+
// ```
36+
//
37+
// outputClaimToHeaders:
38+
// - header: x-my-company-jwt-group
39+
// claim: my-group
40+
// - header: x-test-environment-flag
41+
// claim: test-flag
42+
// - header: x-jwt-claim-group
43+
// claim: nested.key.group
44+
//
45+
// ```
46+
// +kubebuilder:validation:Optional
47+
// +kubebuilder:validation:MaxItems=10
48+
OutputClaimToHeaders *[]ClaimToHeader `json:"outputClaimToHeaders,omitempty"`
49+
50+
// Paths specifies paths that require an authenticated JWT.
51+
//
52+
// The specified paths must be a valid URI path. It has to start with '/' and cannot end with '/'.
53+
// The paths can also contain the wildcard operator '*', but only at the end.
54+
// +listType=set
55+
// +kubebuilder:validation:Items.Pattern=`^/[a-zA-Z0-9\-._~!$&'()+,;=:@%/]*(\*)?$`
56+
// +kubebuilder:validation:MaxItems=50
57+
// +kubebuilder:validation:Optional
58+
Paths *[]string `json:"paths,omitempty"`
59+
60+
// IgnorePaths specifies paths that do not require an authenticated JWT.
61+
//
62+
// The specified paths must be a valid URI path. It has to start with '/' and cannot end with '/'.
63+
// The paths can also contain the wildcard operator '*', but only at the end.
64+
// +listType=set
65+
// +kubebuilder:validation:Items.Pattern=`^/[a-zA-Z0-9\-._~!$&'()+,;=:@%/]*(\*)?$`
66+
// +kubebuilder:validation:MaxItems=50
67+
// +kubebuilder:validation:Optional
68+
IgnorePaths *[]string `json:"ignorePaths,omitempty"`
69+
}
70+
71+
type ClaimToHeader struct {
72+
// The name of the HTTP header for which the specified claim will be copied to.
73+
// +kubebuilder:validation:Pattern="^[a-zA-Z0-9-]+$"
74+
// +kubebuilder:validation:MaxLength=64
75+
Header string `json:"header"`
76+
77+
// The claim to be copied.
78+
// +kubebuilder:validation:Pattern="^[a-zA-Z0-9-._]+$"
79+
// +kubebuilder:validation:MaxLength=128
80+
Claim string `json:"claim"`
81+
}

0 commit comments

Comments
 (0)