[public-api] Measure incoming JWT Sessions - WEB-102 (#17345)

* retest

* retest

* [installer] Add cookie name to config

* Fix

* retest

* [installer] Add cookie name to config

* [public-api] Measure incoming JWT Sessions

* fix

* Fix

* Fix

* fix

* retest
This commit is contained in:
Milan Pavlik 2023-04-24 15:34:45 +02:00 committed by GitHub
parent 3a3d99edd3
commit 0dc46c5bcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 108 additions and 8 deletions

View File

@ -11,6 +11,7 @@ const (
OIDCServiceEnabledFlag = "oidcServiceEnabled" OIDCServiceEnabledFlag = "oidcServiceEnabled"
SupervisorPersistServerAPIChannelWhenStartFlag = "supervisor_persist_serverapi_channel_when_start" SupervisorPersistServerAPIChannelWhenStartFlag = "supervisor_persist_serverapi_channel_when_start"
SupervisorUsePublicAPIFlag = "supervisor_experimental_publicapi" SupervisorUsePublicAPIFlag = "supervisor_experimental_publicapi"
JWTSessionsEnabledFlag = "jwtSessionCookieEnabled"
) )
func IsPersonalAccessTokensEnabled(ctx context.Context, client Client, attributes Attributes) bool { func IsPersonalAccessTokensEnabled(ctx context.Context, client Client, attributes Attributes) bool {
@ -28,3 +29,7 @@ func SupervisorPersistServerAPIChannelWhenStart(ctx context.Context, client Clie
func SupervisorUsePublicAPI(ctx context.Context, client Client, attributes Attributes) bool { func SupervisorUsePublicAPI(ctx context.Context, client Client, attributes Attributes) bool {
return client.GetBoolValue(ctx, SupervisorUsePublicAPIFlag, false, attributes) return client.GetBoolValue(ctx, SupervisorUsePublicAPIFlag, false, attributes)
} }
func JWTSessionsEnabled(ctx context.Context, client Client, attributes Attributes) bool {
return client.GetBoolValue(ctx, JWTSessionsEnabledFlag, false, attributes)
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.
package auth
import (
"strconv"
"github.com/prometheus/client_golang/prometheus"
)
func reportRequestWithJWT(jwtPresent bool) {
requestsWithJWTSessionsTotal.WithLabelValues(strconv.FormatBool(jwtPresent)).Inc()
}
var (
requestsWithJWTSessionsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "gitpod",
Subsystem: "public_api",
Name: "requests_with_jwt_sessions_total",
Help: "Count of sessions with, or without JWT sessions",
}, []string{"with_jwt"})
)
func RegisterMetrics(registry *prometheus.Registry) {
registry.MustRegister(requestsWithJWTSessionsTotal)
}

View File

@ -5,9 +5,14 @@
package auth package auth
import ( import (
"context"
"fmt" "fmt"
"net/http"
"time" "time"
"github.com/bufbuild/connect-go"
"github.com/gitpod-io/gitpod/common-go/experiments"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws" "github.com/gitpod-io/gitpod/public-api-server/pkg/jws"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid" "github.com/google/uuid"
@ -41,3 +46,58 @@ func VerifySessionJWT(token string, verifier jws.Verifier, expectedIssuer string
return claims, nil return claims, nil
} }
func NewJWTCookieInterceptor(exp experiments.Client, cookieName string, expectedIssuer string, verifier jws.Verifier) connect.UnaryInterceptorFunc {
interceptor := func(next connect.UnaryFunc) connect.UnaryFunc {
return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
validJWT := false
defer func() {
reportRequestWithJWT(validJWT)
}()
if req.Spec().IsClient {
return next(ctx, req)
}
token, err := TokenFromContext(ctx)
if err != nil {
return next(ctx, req)
}
if !experiments.JWTSessionsEnabled(ctx, exp, experiments.Attributes{}) {
return next(ctx, req)
}
if token.Type != CookieTokenType {
return next(ctx, req)
}
jwtSessionCookie, err := cookieFromString(token.Value, cookieName)
if err != nil {
return next(ctx, req)
}
claims, err := VerifySessionJWT(jwtSessionCookie.Value, verifier, expectedIssuer)
if err != nil {
log.Extract(ctx).WithError(err).Warnf("Failed to verify JWT session token")
return next(ctx, req)
}
validJWT = claims != nil
return next(ctx, req)
})
}
return connect.UnaryInterceptorFunc(interceptor)
}
func cookieFromString(rawCookieHeader, name string) (*http.Cookie, error) {
// To access the cookie as an http.Cookie, we sadly have to construct a request with the appropriate header such
// that we can then extract the cookie.
header := http.Header{}
header.Add("Cookie", rawCookieHeader)
req := http.Request{Header: header}
return req.Cookie(name)
}

View File

@ -97,7 +97,7 @@ func Start(logger *logrus.Entry, version string, cfg *config.Configuration) erro
if err != nil { if err != nil {
return fmt.Errorf("failed to setup JWS Keyset: %w", err) return fmt.Errorf("failed to setup JWS Keyset: %w", err)
} }
_, err = jws.NewRSA256(keyset) rsa256, err := jws.NewRSA256(keyset)
if err != nil { if err != nil {
return fmt.Errorf("failed to setup jws.RSA256: %w", err) return fmt.Errorf("failed to setup jws.RSA256: %w", err)
} }
@ -139,13 +139,15 @@ func Start(logger *logrus.Entry, version string, cfg *config.Configuration) erro
} }
if registerErr := register(srv, &registerDependencies{ if registerErr := register(srv, &registerDependencies{
connPool: connPool, connPool: connPool,
expClient: expClient, expClient: expClient,
dbConn: dbConn, dbConn: dbConn,
signer: signer, signer: signer,
cipher: cipherSet, cipher: cipherSet,
oidcService: oidcService, oidcService: oidcService,
idpService: idpService, idpService: idpService,
authCfg: cfg.Auth,
sessionVerifier: rsa256,
}); registerErr != nil { }); registerErr != nil {
return fmt.Errorf("failed to register services: %w", registerErr) return fmt.Errorf("failed to register services: %w", registerErr)
} }
@ -165,10 +167,14 @@ type registerDependencies struct {
cipher db.Cipher cipher db.Cipher
oidcService *oidc.Service oidcService *oidc.Service
idpService *identityprovider.Service idpService *identityprovider.Service
sessionVerifier jws.SignerVerifier
authCfg config.AuthConfiguration
} }
func register(srv *baseserver.Server, deps *registerDependencies) error { func register(srv *baseserver.Server, deps *registerDependencies) error {
proxy.RegisterMetrics(srv.MetricsRegistry()) proxy.RegisterMetrics(srv.MetricsRegistry())
auth.RegisterMetrics(srv.MetricsRegistry())
connectMetrics := NewConnectMetrics() connectMetrics := NewConnectMetrics()
err := connectMetrics.Register(srv.MetricsRegistry()) err := connectMetrics.Register(srv.MetricsRegistry())
@ -186,6 +192,7 @@ func register(srv *baseserver.Server, deps *registerDependencies) error {
NewLogInterceptor(log.Log), NewLogInterceptor(log.Log),
auth.NewServerInterceptor(), auth.NewServerInterceptor(),
origin.NewInterceptor(), origin.NewInterceptor(),
auth.NewJWTCookieInterceptor(deps.expClient, deps.authCfg.Session.Cookie.Name, deps.authCfg.Session.Issuer, deps.sessionVerifier),
), ),
} }