Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Commit

Permalink
load client secret when reading config
Browse files Browse the repository at this point in the history
Signed-off-by: huabing zhao <[email protected]>
  • Loading branch information
zhaohuabing committed Feb 23, 2024
1 parent 21e750f commit 848534d
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 704 deletions.
9 changes: 2 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ require (
google.golang.org/protobuf v1.32.0
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v0.29.0
sigs.k8s.io/controller-runtime v0.17.2
)

Expand All @@ -32,9 +31,9 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.8.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
Expand All @@ -60,11 +59,8 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/yuin/gopher-lua v1.1.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sys v0.16.0 // indirect
Expand All @@ -75,11 +71,10 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.29.0 // indirect
k8s.io/client-go v0.29.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20240215124517-56159419231e // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
Expand Down
418 changes: 0 additions & 418 deletions go.sum

Large diffs are not rendered by default.

139 changes: 60 additions & 79 deletions internal/authz/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ import (
"github.com/tetratelabs/telemetry"
"google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc/codes"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"

oidcv1 "github.com/tetrateio/authservice-go/config/gen/go/v1/oidc"
"github.com/tetrateio/authservice-go/internal"
Expand Down Expand Up @@ -142,7 +137,26 @@ func (o *oidcHandler) Process(ctx context.Context, req *envoy.CheckRequest, resp

// If the request is for the configured logout path,
// then logout and redirect to the configured logout redirect uri.
// TODO (sergicastro): Handle logout request
if matchesLogoutPath(log, o.config, req.GetAttributes().GetRequest().GetHttp()) {
log.Info("handling logout request")
if sessionID != "" {
log.Info("removing session from session store during logout", "session-id", sessionID)
store := o.sessions.Get(o.config)
if err := store.RemoveSession(ctx, sessionID); err != nil {
log.Error("error removing session", err)
setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated)
return nil
}
}
log.Info("Logout complete. Redirecting to logout redirect uri")
deny := newDenyResponse()
// add IDP logout location
setRedirect(deny, o.config.GetLogout().GetRedirectUri())
// add the set-cookie header to delete the session_id cookie
setSetCookieHeader(deny, generateSetCookieHeader(getCookieName(o.config), "deleted", 0))
setDenyResponse(resp, deny, codes.Unauthenticated)
return nil
}

// If the request does not have a session_id cookie,
// then generate a session id, put it in a header, and redirect for login.
Expand All @@ -157,7 +171,7 @@ func (o *oidcHandler) Process(ctx context.Context, req *envoy.CheckRequest, resp
// If the request path is the callback for receiving the authorization code,
// has a session id then exchange it for tokens and redirects end-user back to
// their originally requested URL.
if o.matchesCallbackPath(log, req.GetAttributes().GetRequest().GetHttp()) {
if matchesCallbackPath(log, o.config, req.GetAttributes().GetRequest().GetHttp()) {
log.Debug("handling callback request")
o.retrieveTokens(ctx, log, req, resp, sessionID)
return nil
Expand Down Expand Up @@ -277,18 +291,11 @@ func (o *oidcHandler) redirectToIDP(ctx context.Context, log telemetry.Logger,

// Generate denied response with redirect headers
deny := newDenyResponse()
deny.Status = &typev3.HttpStatus{Code: typev3.StatusCode_Found}
deny.Headers = append(deny.Headers, &corev3.HeaderValueOption{
Header: &corev3.HeaderValue{Key: inthttp.HeaderLocation, Value: redirectURL},
})
setRedirect(deny, redirectURL)

// add the set-cookie header
cookieName := getCookieName(o.config)
cookie := generateSetCookieHeader(cookieName, sessionID, 0)
deny.Headers = append(deny.Headers, &corev3.HeaderValueOption{
Header: &corev3.HeaderValue{Key: inthttp.HeaderSetCookie, Value: cookie},
})

setSetCookieHeader(deny, generateSetCookieHeader(cookieName, sessionID, -1))
setDenyResponse(resp, deny, codes.Unauthenticated)
}

Expand Down Expand Up @@ -347,17 +354,10 @@ func (o *oidcHandler) retrieveTokens(ctx context.Context, log telemetry.Logger,
"redirect_uri": []string{o.config.GetCallbackUri()},
}

clientSecret, err := getClientSecret(ctx, o.config)
if err != nil {
log.Error("error getting client secret", err)
setDenyResponse(resp, newDenyResponse(), codes.Internal)
return
}

// build headers
headers := http.Header{
inthttp.HeaderContentType: []string{inthttp.HeaderContentTypeFormURLEncoded},
inthttp.HeaderAuthorization: []string{inthttp.BasicAuthHeader(o.config.GetClientId(), clientSecret)},
inthttp.HeaderAuthorization: []string{inthttp.BasicAuthHeader(o.config.GetClientId(), o.config.GetClientSecret())},
}

log.Info("performing request to retrieve new tokens")
Expand Down Expand Up @@ -659,6 +659,21 @@ func setDenyResponse(resp *envoy.CheckResponse, deny *envoy.DeniedHttpResponse,
resp.Status = &status.Status{Code: int32(code)}
}

// setRedirect populates the DeniedHttpResponse with the given location and a 302 status code.
func setRedirect(deny *envoy.DeniedHttpResponse, location string) {
deny.Status = &typev3.HttpStatus{Code: typev3.StatusCode_Found}
deny.Headers = append(deny.Headers, &corev3.HeaderValueOption{
Header: &corev3.HeaderValue{Key: inthttp.HeaderLocation, Value: location},
})
}

// setSetCookieHeader populates the DeniedHttpResponse with the given cookie.
func setSetCookieHeader(deny *envoy.DeniedHttpResponse, cookie string) {
deny.Headers = append(deny.Headers, &corev3.HeaderValueOption{
Header: &corev3.HeaderValue{Key: inthttp.HeaderSetCookie, Value: cookie},
})
}

// allowResponse populates the CheckResponse as an OK response with the required tokens.
func (o *oidcHandler) allowResponse(resp *envoy.CheckResponse, tokens *oidc.TokenResponse) {
ok := resp.GetOkResponse()
Expand All @@ -675,13 +690,14 @@ func (o *oidcHandler) allowResponse(resp *envoy.CheckResponse, tokens *oidc.Toke
}

// matchesCallbackPath checks if the request matches the configured callback uri.
func (o *oidcHandler) matchesCallbackPath(log telemetry.Logger, httpReq *envoy.AttributeContext_HttpRequest) bool {
// Request done by the IDP directly to the authservice to exchange the authorization code for tokens.
func matchesCallbackPath(log telemetry.Logger, config *oidcv1.OIDCConfig, httpReq *envoy.AttributeContext_HttpRequest) bool {
reqFullPath := httpReq.GetPath()
reqHost := httpReq.GetHost()
reqPath, _, _ := inthttp.GetPathQueryFragment(reqFullPath)

// no need to handle the error since config validation already checks for this
confURI, _ := url.Parse(o.config.GetCallbackUri())
confURI, _ := url.Parse(config.GetCallbackUri())
confPort := confURI.Port()
confHost := confURI.Hostname()
confScheme := confURI.Scheme
Expand All @@ -704,6 +720,23 @@ func (o *oidcHandler) matchesCallbackPath(log telemetry.Logger, httpReq *envoy.A
return false
}

// matchesLogoutPath checks if the request matches the configured logout uri.
// Request done by the end-user to log out.
func matchesLogoutPath(log telemetry.Logger, config *oidcv1.OIDCConfig, httpReq *envoy.AttributeContext_HttpRequest) bool {
if config.GetLogout() == nil {
return false
}

reqPath, _, _ := inthttp.GetPathQueryFragment(httpReq.GetPath())
confPath := config.GetLogout().GetPath()

if reqPath == confPath {
log.Debug("request matches configured logout uri")
return true
}
return false
}

// encodeTokensToHeaders encodes the tokens to the headers according to the configuration.
func (o *oidcHandler) encodeTokensToHeaders(tokens *oidc.TokenResponse) map[string]string {
headers := make(map[string]string)
Expand Down Expand Up @@ -746,7 +779,7 @@ func generateSetCookieHeader(cookieName, cookieValue string, timeout time.Durati
// getCookieDirectives returns the directives to use in the Set-Cookie header depending on the timeout.
func getCookieDirectives(timeout time.Duration) []string {
directives := []string{inthttp.HeaderSetCookieHTTPOnly, inthttp.HeaderSetCookieSecure, inthttp.HeaderSetCookieSameSiteLax, "Path=/"}
if timeout > 0 {
if timeout >= 0 {
directives = append(directives, fmt.Sprintf("%s=%d", inthttp.HeaderSetCookieMaxAge, int(timeout.Seconds())))
}
return directives
Expand Down Expand Up @@ -808,55 +841,3 @@ func loadWellKnownConfig(client *http.Client, cfg *oidcv1.OIDCConfig) error {

return nil
}

const clientSecretKey = "client-secret"
const defaultNamespace = "default"

// inClusterConfig is used to inject the kubeconfig for testing purposes.
var inClusterConfig *rest.Config

// getClientSecret retrieves the client secret from the given OIDCConfig.
// The client secret can be a literal or a reference to a Kubernetes secret.
// If the client secret is a reference to a Kubernetes secret, it retrieves the secret from the Kubernetes API.
func getClientSecret(ctx context.Context, c *oidcv1.OIDCConfig) (string, error) {
if c.GetClientSecretRef() != nil {
var (
cfg *rest.Config
err error
)

namespace := c.GetClientSecretRef().Namespace
if namespace == "" {
namespace = defaultNamespace
}
secretName := types.NamespacedName{
Namespace: namespace,
Name: c.GetClientSecretRef().Name,
}

cfg = inClusterConfig
if cfg == nil {
cfg, err = config.GetConfig()
if err != nil {
return "", fmt.Errorf("error getting kube config: %w", err)
}
}

cl, err := client.New(cfg, client.Options{})
if err != nil {
return "", fmt.Errorf("errot creating kube client: %w", err)
}

secret := &v1.Secret{}
err = cl.Get(ctx, secretName, secret)
if err != nil {
return "", fmt.Errorf("error getting secret: %w", err)
}
clientSecretBytes, ok := secret.Data[clientSecretKey]
if !ok || len(clientSecretBytes) == 0 {
return "", fmt.Errorf("client secret not found in secret %s", secretName.String())
}
return string(clientSecretBytes), nil
}
return c.GetClientSecret(), nil
}
Loading

0 comments on commit 848534d

Please sign in to comment.