From 5895a1603000bbb99b63e5a2813099db1787be22 Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Sun, 16 Jun 2024 13:13:59 +0200 Subject: [PATCH] Implement handling OAuth2 client token lifespans. --- api/v1alpha1/oauth2client_types.go | 67 +++++++++++++++++++ api/v1alpha1/oauth2client_types_test.go | 41 +++++++----- api/v1alpha1/zz_generated.deepcopy.go | 16 +++++ .../crd/bases/hydra.ory.sh_oauth2clients.yaml | 66 ++++++++++++++++++ hydra/client_test.go | 1 + hydra/types.go | 58 ++++++++++------ 6 files changed, 215 insertions(+), 34 deletions(-) diff --git a/api/v1alpha1/oauth2client_types.go b/api/v1alpha1/oauth2client_types.go index ffd1829..c4eac2d 100644 --- a/api/v1alpha1/oauth2client_types.go +++ b/api/v1alpha1/oauth2client_types.go @@ -51,6 +51,69 @@ type HydraAdmin struct { ForwardedProto string `json:"forwardedProto,omitempty"` } +// TokenLifespans defines the desired token durations by grant type for OAuth2Client +type TokenLifespans struct { + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // AuthorizationCodeGrantAccessTokenLifespan is the access token lifespan + // issued on an authorization_code grant. + AuthorizationCodeGrantAccessTokenLifespan string `json:"authorization_code_grant_access_token_lifespan,omitempty"` + + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // AuthorizationCodeGrantIdTokenLifespan is the id token lifespan + // issued on an authorization_code grant. + AuthorizationCodeGrantIdTokenLifespan string `json:"authorization_code_grant_id_token_lifespan,omitempty"` + + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // AuthorizationCodeGrantRefreshTokenLifespan is the refresh token lifespan + // issued on an authorization_code grant. + AuthorizationCodeGrantRefreshTokenLifespan string `json:"authorization_code_grant_refresh_token_lifespan,omitempty"` + + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // AuthorizationCodeGrantRefreshTokenLifespan is the access token lifespan + // issued on a client_credentials grant. + ClientCredentialsGrantAccessTokenLifespan string `json:"client_credentials_grant_access_token_lifespan,omitempty"` + + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // ImplicitGrantAccessTokenLifespan is the access token lifespan + // issued on an implicit grant. + ImplicitGrantAccessTokenLifespan string `json:"implicit_grant_access_token_lifespan,omitempty"` + + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // ImplicitGrantIdTokenLifespan is the id token lifespan + // issued on an implicit grant. + ImplicitGrantIdTokenLifespan string `json:"implicit_grant_id_token_lifespan,omitempty"` + + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // JwtBearerGrantAccessTokenLifespan is the access token lifespan + // issued on a jwt_bearer grant. + JwtBearerGrantAccessTokenLifespan string `json:"jwt_bearer_grant_access_token_lifespan,omitempty"` + + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // RefreshTokenGrantAccessTokenLifespan is the access token lifespan + // issued on a refresh_token grant. + RefreshTokenGrantAccessTokenLifespan string `json:"refresh_token_grant_access_token_lifespan,omitempty"` + + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // RefreshTokenGrantIdTokenLifespan is the id token lifespan + // issued on a refresh_token grant. + RefreshTokenGrantIdTokenLifespan string `json:"refresh_token_grant_id_token_lifespan,omitempty"` + + // +kubebuilder:validation:Pattern=[0-9]+(ns|us|ms|s|m|h) + // + // RefreshTokenGrantRefreshTokenLifespan is the refresh token lifespan + // issued on a refresh_token grant. + RefreshTokenGrantRefreshTokenLifespan string `json:"refresh_token_grant_refresh_token_lifespan,omitempty"` +} + // OAuth2ClientSpec defines the desired state of OAuth2Client type OAuth2ClientSpec struct { @@ -110,6 +173,10 @@ type OAuth2ClientSpec struct { // Indication which authentication method shoud be used for the token endpoint TokenEndpointAuthMethod TokenEndpointAuthMethod `json:"tokenEndpointAuthMethod,omitempty"` + // TokenLifespans is the configuration to use for managing different token lifespans + // depending on the used grant type. + TokenLifespans TokenLifespans `json:"tokenLifespans,omitempty"` + // +kubebuilder:validation:Type=object // +nullable // +optional diff --git a/api/v1alpha1/oauth2client_types_test.go b/api/v1alpha1/oauth2client_types_test.go index 0c33d00..3d92f7f 100644 --- a/api/v1alpha1/oauth2client_types_test.go +++ b/api/v1alpha1/oauth2client_types_test.go @@ -89,17 +89,27 @@ func TestCreateAPI(t *testing.T) { t.Run("by failing if the requested object doesn't meet CRD requirements", func(t *testing.T) { for desc, modifyClient := range map[string]func(){ - "invalid grant type": func() { created.Spec.GrantTypes = []GrantType{"invalid"} }, - "invalid response type": func() { created.Spec.ResponseTypes = []ResponseType{"invalid", "code"} }, - "invalid composite response type": func() { created.Spec.ResponseTypes = []ResponseType{"invalid code", "code id_token"} }, - "invalid scope": func() { created.Spec.Scope = "" }, - "missing secret name": func() { created.Spec.SecretName = "" }, - "invalid redirect URI": func() { created.Spec.RedirectURIs = []RedirectURI{"invalid"} }, - "invalid logout redirect URI": func() { created.Spec.PostLogoutRedirectURIs = []RedirectURI{"invalid"} }, - "invalid hydra url": func() { created.Spec.HydraAdmin.URL = "invalid" }, - "invalid hydra port high": func() { created.Spec.HydraAdmin.Port = 65536 }, - "invalid hydra endpoint": func() { created.Spec.HydraAdmin.Endpoint = "invalid" }, - "invalid hydra forwarded proto": func() { created.Spec.HydraAdmin.Endpoint = "invalid" }, + "invalid grant type": func() { created.Spec.GrantTypes = []GrantType{"invalid"} }, + "invalid response type": func() { created.Spec.ResponseTypes = []ResponseType{"invalid", "code"} }, + "invalid composite response type": func() { created.Spec.ResponseTypes = []ResponseType{"invalid code", "code id_token"} }, + "invalid scope": func() { created.Spec.Scope = "" }, + "missing secret name": func() { created.Spec.SecretName = "" }, + "invalid redirect URI": func() { created.Spec.RedirectURIs = []RedirectURI{"invalid"} }, + "invalid logout redirect URI": func() { created.Spec.PostLogoutRedirectURIs = []RedirectURI{"invalid"} }, + "invalid hydra url": func() { created.Spec.HydraAdmin.URL = "invalid" }, + "invalid hydra port high": func() { created.Spec.HydraAdmin.Port = 65536 }, + "invalid hydra endpoint": func() { created.Spec.HydraAdmin.Endpoint = "invalid" }, + "invalid hydra forwarded proto": func() { created.Spec.HydraAdmin.ForwardedProto = "invalid" }, + "invalid lifespan authorization code access token": func() { created.Spec.TokenLifespans.AuthorizationCodeGrantAccessTokenLifespan = "invalid" }, + "invalid lifespan authorization code id token": func() { created.Spec.TokenLifespans.AuthorizationCodeGrantIdTokenLifespan = "invalid" }, + "invalid lifespan authorization code refresh token": func() { created.Spec.TokenLifespans.AuthorizationCodeGrantRefreshTokenLifespan = "invalid" }, + "invalid lifespan client credentials access token": func() { created.Spec.TokenLifespans.ClientCredentialsGrantAccessTokenLifespan = "invalid" }, + "invalid lifespan implicit access token": func() { created.Spec.TokenLifespans.ImplicitGrantAccessTokenLifespan = "invalid" }, + "invalid lifespan implicit id token": func() { created.Spec.TokenLifespans.ImplicitGrantIdTokenLifespan = "invalid" }, + "invalid lifespan jwt bearer access token": func() { created.Spec.TokenLifespans.JwtBearerGrantAccessTokenLifespan = "invalid" }, + "invalid lifespan refresh token access token": func() { created.Spec.TokenLifespans.RefreshTokenGrantAccessTokenLifespan = "invalid" }, + "invalid lifespan refresh token id token": func() { created.Spec.TokenLifespans.RefreshTokenGrantIdTokenLifespan = "invalid" }, + "invalid lifespan refresh token refresh token": func() { created.Spec.TokenLifespans.RefreshTokenGrantRefreshTokenLifespan = "invalid" }, } { t.Run(fmt.Sprintf("case=%s", desc), func(t *testing.T) { resetTestClient() @@ -158,10 +168,11 @@ func resetTestClient() { Namespace: "default", }, Spec: OAuth2ClientSpec{ - GrantTypes: []GrantType{"implicit", "client_credentials", "authorization_code", "refresh_token"}, - ResponseTypes: []ResponseType{"id_token", "code", "token"}, - Scope: "read,write", - SecretName: "secret-name", + GrantTypes: []GrantType{"implicit", "client_credentials", "authorization_code", "refresh_token"}, + ResponseTypes: []ResponseType{"id_token", "code", "token"}, + Scope: "read,write", + SecretName: "secret-name", + TokenLifespans: TokenLifespans{}, }, } } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 22a8546..9548b97 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -148,6 +148,7 @@ func (in *OAuth2ClientSpec) DeepCopyInto(out *OAuth2ClientSpec) { copy(*out, *in) } out.HydraAdmin = in.HydraAdmin + out.TokenLifespans = in.TokenLifespans in.Metadata.DeepCopyInto(&out.Metadata) } @@ -196,3 +197,18 @@ func (in *ReconciliationError) DeepCopy() *ReconciliationError { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TokenLifespans) DeepCopyInto(out *TokenLifespans) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TokenLifespans. +func (in *TokenLifespans) DeepCopy() *TokenLifespans { + if in == nil { + return nil + } + out := new(TokenLifespans) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/hydra.ory.sh_oauth2clients.yaml b/config/crd/bases/hydra.ory.sh_oauth2clients.yaml index 2564888..281a3aa 100644 --- a/config/crd/bases/hydra.ory.sh_oauth2clients.yaml +++ b/config/crd/bases/hydra.ory.sh_oauth2clients.yaml @@ -233,6 +233,72 @@ spec: Indication which authentication method shoud be used for the token endpoint type: string + tokenLifespans: + description: |- + TokenLifespans is the configuration to use for managing different token lifespans + depending on the used grant type. + properties: + authorization_code_grant_access_token_lifespan: + description: |- + AuthorizationCodeGrantAccessTokenLifespan is the access token lifespan + issued on an authorization_code grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + authorization_code_grant_id_token_lifespan: + description: |- + AuthorizationCodeGrantIdTokenLifespan is the id token lifespan + issued on an authorization_code grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + authorization_code_grant_refresh_token_lifespan: + description: |- + AuthorizationCodeGrantRefreshTokenLifespan is the refresh token lifespan + issued on an authorization_code grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + client_credentials_grant_access_token_lifespan: + description: |- + AuthorizationCodeGrantRefreshTokenLifespan is the access token lifespan + issued on a client_credentials grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + implicit_grant_access_token_lifespan: + description: |- + ImplicitGrantAccessTokenLifespan is the access token lifespan + issued on an implicit grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + implicit_grant_id_token_lifespan: + description: |- + ImplicitGrantIdTokenLifespan is the id token lifespan + issued on an implicit grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + jwt_bearer_grant_access_token_lifespan: + description: |- + JwtBearerGrantAccessTokenLifespan is the access token lifespan + issued on a jwt_bearer grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + refresh_token_grant_access_token_lifespan: + description: |- + RefreshTokenGrantAccessTokenLifespan is the access token lifespan + issued on a refresh_token grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + refresh_token_grant_id_token_lifespan: + description: |- + RefreshTokenGrantIdTokenLifespan is the id token lifespan + issued on a refresh_token grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + refresh_token_grant_refresh_token_lifespan: + description: |- + RefreshTokenGrantRefreshTokenLifespan is the refresh token lifespan + issued on a refresh_token grant. + pattern: "[0-9]+(ns|us|ms|s|m|h)" + type: string + type: object required: - grantTypes - scope diff --git a/hydra/client_test.go b/hydra/client_test.go index 42415ee..887e17d 100644 --- a/hydra/client_test.go +++ b/hydra/client_test.go @@ -53,6 +53,7 @@ var testOAuthJSONPost = &hydra.OAuth2ClientJSON{ FrontChannelLogoutSessionRequired: false, BackChannelLogoutURI: "https://localhost/backchannel-logout", BackChannelLogoutSessionRequired: false, + AuthorizationCodeGrantAccessTokenLifespan: "6h", } var testOAuthJSONPut = &hydra.OAuth2ClientJSON{ diff --git a/hydra/types.go b/hydra/types.go index dddc3e2..af8ed0d 100644 --- a/hydra/types.go +++ b/hydra/types.go @@ -14,25 +14,35 @@ import ( // OAuth2ClientJSON represents an OAuth2 client digestible by ORY Hydra type OAuth2ClientJSON struct { - ClientName string `json:"client_name,omitempty"` - ClientID *string `json:"client_id,omitempty"` - Secret *string `json:"client_secret,omitempty"` - GrantTypes []string `json:"grant_types"` - RedirectURIs []string `json:"redirect_uris,omitempty"` - PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris,omitempty"` - AllowedCorsOrigins []string `json:"allowed_cors_origins,omitempty"` - ResponseTypes []string `json:"response_types,omitempty"` - Audience []string `json:"audience,omitempty"` - Scope string `json:"scope"` - SkipConsent bool `json:"skip_consent,omitempty"` - Owner string `json:"owner"` - TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` - Metadata json.RawMessage `json:"metadata,omitempty"` - JwksUri string `json:"jwks_uri,omitempty"` - FrontChannelLogoutSessionRequired bool `json:"frontchannel_logout_session_required"` - FrontChannelLogoutURI string `json:"frontchannel_logout_uri"` - BackChannelLogoutSessionRequired bool `json:"backchannel_logout_session_required"` - BackChannelLogoutURI string `json:"backchannel_logout_uri"` + ClientName string `json:"client_name,omitempty"` + ClientID *string `json:"client_id,omitempty"` + Secret *string `json:"client_secret,omitempty"` + GrantTypes []string `json:"grant_types"` + RedirectURIs []string `json:"redirect_uris,omitempty"` + PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris,omitempty"` + AllowedCorsOrigins []string `json:"allowed_cors_origins,omitempty"` + ResponseTypes []string `json:"response_types,omitempty"` + Audience []string `json:"audience,omitempty"` + Scope string `json:"scope"` + SkipConsent bool `json:"skip_consent,omitempty"` + Owner string `json:"owner"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` + JwksUri string `json:"jwks_uri,omitempty"` + FrontChannelLogoutSessionRequired bool `json:"frontchannel_logout_session_required"` + FrontChannelLogoutURI string `json:"frontchannel_logout_uri"` + BackChannelLogoutSessionRequired bool `json:"backchannel_logout_session_required"` + BackChannelLogoutURI string `json:"backchannel_logout_uri"` + AuthorizationCodeGrantAccessTokenLifespan string `json:"authorization_code_grant_access_token_lifespan,omitempty"` + AuthorizationCodeGrantIdTokenLifespan string `json:"authorization_code_grant_id_token_lifespan,omitempty"` + AuthorizationCodeGrantRefreshTokenLifespan string `json:"authorization_code_grant_refresh_token_lifespan,omitempty"` + ClientCredentialsGrantAccessTokenLifespan string `json:"client_credentials_grant_access_token_lifespan,omitempty"` + ImplicitGrantAccessTokenLifespan string `json:"implicit_grant_access_token_lifespan,omitempty"` + ImplicitGrantIdTokenLifespan string `json:"implicit_grant_id_token_lifespan,omitempty"` + JwtBearerGrantAccessTokenLifespan string `json:"jwt_bearer_grant_access_token_lifespan,omitempty"` + RefreshTokenGrantAccessTokenLifespan string `json:"refresh_token_grant_access_token_lifespan,omitempty"` + RefreshTokenGrantIdTokenLifespan string `json:"refresh_token_grant_id_token_lifespan,omitempty"` + RefreshTokenGrantRefreshTokenLifespan string `json:"refresh_token_grant_refresh_token_lifespan,omitempty"` } // Oauth2ClientCredentials represents client ID and password fetched from a @@ -74,6 +84,16 @@ func FromOAuth2Client(c *hydrav1alpha1.OAuth2Client) (*OAuth2ClientJSON, error) FrontChannelLogoutSessionRequired: c.Spec.BackChannelLogoutSessionRequired, BackChannelLogoutSessionRequired: c.Spec.BackChannelLogoutSessionRequired, BackChannelLogoutURI: c.Spec.BackChannelLogoutURI, + AuthorizationCodeGrantAccessTokenLifespan: c.Spec.TokenLifespans.AuthorizationCodeGrantAccessTokenLifespan, + AuthorizationCodeGrantIdTokenLifespan: c.Spec.TokenLifespans.AuthorizationCodeGrantIdTokenLifespan, + AuthorizationCodeGrantRefreshTokenLifespan: c.Spec.TokenLifespans.AuthorizationCodeGrantRefreshTokenLifespan, + ClientCredentialsGrantAccessTokenLifespan: c.Spec.TokenLifespans.ClientCredentialsGrantAccessTokenLifespan, + ImplicitGrantAccessTokenLifespan: c.Spec.TokenLifespans.ImplicitGrantAccessTokenLifespan, + ImplicitGrantIdTokenLifespan: c.Spec.TokenLifespans.ImplicitGrantIdTokenLifespan, + JwtBearerGrantAccessTokenLifespan: c.Spec.TokenLifespans.JwtBearerGrantAccessTokenLifespan, + RefreshTokenGrantAccessTokenLifespan: c.Spec.TokenLifespans.RefreshTokenGrantAccessTokenLifespan, + RefreshTokenGrantIdTokenLifespan: c.Spec.TokenLifespans.RefreshTokenGrantIdTokenLifespan, + RefreshTokenGrantRefreshTokenLifespan: c.Spec.TokenLifespans.RefreshTokenGrantRefreshTokenLifespan, }, nil }