diff options
27 files changed, 123 insertions, 447 deletions
@@ -4,15 +4,12 @@ import ( "net/http" "path/filepath" - "github.com/coreos/go-oidc/v3/oidc" "github.com/rs/zerolog" "github.com/xlgmokha/x/pkg/ioc" "github.com/xlgmokha/x/pkg/log" "github.com/xlgmokha/x/pkg/x" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/controllers/dashboard" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/controllers/sparkles" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/middleware" ) @@ -35,17 +32,10 @@ func New(rootDir string) http.Handler { mux.Handle("GET /", http.FileServer(dir)) logger := ioc.MustResolve[*zerolog.Logger](ioc.Default) - users := ioc.MustResolve[domain.Repository[*domain.User]](ioc.Default) return x.Middleware[http.Handler]( mux, log.HTTP(logger), - middleware.IDToken( - ioc.MustResolve[*oidc.Provider](ioc.Default), - ioc.MustResolve[*oidc.Config](ioc.Default), - middleware.FromCustomHeader("x-jwt-payload"), - middleware.FromCookie(cfg.IDTokenCookie), - ), - middleware.User(users), + middleware.User(), ) } diff --git a/app/cfg/cfg.go b/app/cfg/cfg.go index b423413..7c5e717 100644 --- a/app/cfg/cfg.go +++ b/app/cfg/cfg.go @@ -1,14 +1,13 @@ package cfg import ( - "github.com/coreos/go-oidc/v3/oidc" "github.com/xlgmokha/x/pkg/context" "github.com/xlgmokha/x/pkg/env" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" ) var CurrentUser context.Key[*domain.User] = context.Key[*domain.User]("current_user") -var IDToken context.Key[*oidc.IDToken] = context.Key[*oidc.IDToken]("id_token") + var OIDCIssuer string = env.Fetch("OIDC_ISSUER", "https://gitlab.com") var OAuthClientID string = env.Fetch("OAUTH_CLIENT_ID", "client_id") diff --git a/app/controllers/dashboard/controller.go b/app/controllers/dashboard/controller.go index 04a7ed1..d279930 100644 --- a/app/controllers/dashboard/controller.go +++ b/app/controllers/dashboard/controller.go @@ -41,14 +41,12 @@ func (c *Controller) Show(w http.ResponseWriter, r *http.Request) { } func (c *Controller) Navigation(w http.ResponseWriter, r *http.Request) { - currentUser := cfg.CurrentUser.From(r.Context()) - w.WriteHeader(http.StatusOK) w.Header().Add("Content-Type", "text/html") dto := &NavigationDTO{ - CurrentUser: currentUser, - IsLoggedIn: currentUser != nil, + CurrentUser: cfg.CurrentUser.From(r.Context()), + IsLoggedIn: middleware.IsLoggedIn(r), } if err := views.Render(w, "dashboard/nav", dto); err != nil { pls.LogError(r.Context(), err) diff --git a/app/controllers/dashboard/controller_test.go b/app/controllers/dashboard/controller_test.go index c717a74..ddbfd34 100644 --- a/app/controllers/dashboard/controller_test.go +++ b/app/controllers/dashboard/controller_test.go @@ -28,7 +28,7 @@ func TestController(t *testing.T) { }) t.Run("when authenticated", func(t *testing.T) { - ctx := cfg.CurrentUser.With(t.Context(), &domain.User{}) + ctx := cfg.CurrentUser.With(t.Context(), &domain.User{ID: domain.ID("1")}) r, w := test.RequestResponse("GET", "/dashboard", test.WithContext(ctx)) mux.ServeHTTP(w, r) @@ -55,6 +55,7 @@ func TestController(t *testing.T) { t.Run("when authenticated", func(t *testing.T) { ctx := cfg.CurrentUser.With(t.Context(), &domain.User{ + ID: domain.ID("1"), Username: "root", }) r, w := test.RequestResponse("GET", "/dashboard/nav", test.WithContext(ctx)) diff --git a/app/controllers/sparkles/controller_test.go b/app/controllers/sparkles/controller_test.go index 8a1717d..0619b99 100644 --- a/app/controllers/sparkles/controller_test.go +++ b/app/controllers/sparkles/controller_test.go @@ -44,9 +44,10 @@ func TestSparkles(t *testing.T) { t.Run("POST /sparkles", func(t *testing.T) { t.Run("when a user is logged in", func(t *testing.T) { - currentUser := &domain.User{} + currentUser := domain.NewUser(domain.WithID[*domain.User](domain.ID("1"))) t.Run("saves a new sparkle", func(t *testing.T) { + repository := db.NewRepository[*domain.Sparkle]() mux := http.NewServeMux() controller := New(repository) diff --git a/app/domain/entity.go b/app/domain/entity.go index 0377c51..b2c2166 100644 --- a/app/domain/entity.go +++ b/app/domain/entity.go @@ -1,6 +1,12 @@ package domain +import "github.com/xlgmokha/x/pkg/x" + type Entity interface { Identifiable Validate() error } + +func New[T Entity](options ...x.Configure[T]) T { + return x.New[T](x.Map[x.Configure[T], x.Option[T]](options, x.With[T])...) +} diff --git a/app/domain/identifiable.go b/app/domain/identifiable.go index 8fbc1e4..06bec07 100644 --- a/app/domain/identifiable.go +++ b/app/domain/identifiable.go @@ -1,7 +1,15 @@ package domain +import "github.com/xlgmokha/x/pkg/x" + type Identifiable interface { GetID() ID SetID(id ID) error ToGID() string } + +func WithID[T Identifiable](id ID) x.Configure[T] { + return func(item T) { + item.SetID(id) + } +} diff --git a/app/domain/user.go b/app/domain/user.go index 02ddd26..52cd780 100644 --- a/app/domain/user.go +++ b/app/domain/user.go @@ -1,15 +1,16 @@ package domain +import "github.com/xlgmokha/x/pkg/x" + type User struct { ID ID `json:"id" jsonapi:"primary,users"` Username string `json:"username" jsonapi:"attr,username"` - Email string `json:"email" jsonapi:"attr,email"` ProfileURL string `json:"profile" jsonapi:"attr,profile"` Picture string `json:"picture" jsonapi:"attr,picture"` } -func NewUser() *User { - return &User{} +func NewUser(options ...x.Configure[*User]) *User { + return New[*User](options...) } func (u *User) GetID() ID { diff --git a/app/init.go b/app/init.go index 935c962..5057fe4 100644 --- a/app/init.go +++ b/app/init.go @@ -1,24 +1,20 @@ package app import ( - "context" "net/http" "os" - "github.com/coreos/go-oidc/v3/oidc" "github.com/rs/zerolog" "github.com/xlgmokha/x/pkg/env" "github.com/xlgmokha/x/pkg/ioc" "github.com/xlgmokha/x/pkg/log" "github.com/xlgmokha/x/pkg/mapper" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/rpc" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/controllers/dashboard" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/controllers/sparkles" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/db" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/web" - "golang.org/x/oauth2" ) func init() { @@ -28,9 +24,6 @@ func init() { ioc.RegisterSingleton[domain.Repository[*domain.Sparkle]](ioc.Default, func() domain.Repository[*domain.Sparkle] { return db.NewRepository[*domain.Sparkle]() }) - ioc.RegisterSingleton[domain.Repository[*domain.User]](ioc.Default, func() domain.Repository[*domain.User] { - return db.NewRepository[*domain.User]() - }) ioc.RegisterSingleton[*http.ServeMux](ioc.Default, func() *http.ServeMux { return http.NewServeMux() }) @@ -47,21 +40,6 @@ func init() { }, } }) - ioc.RegisterSingleton[*oidc.Provider](ioc.Default, func() *oidc.Provider { - ctx := context.WithValue( - context.Background(), - oauth2.HTTPClient, - ioc.MustResolve[*http.Client](ioc.Default), - ) - return web.NewOIDCProvider(ctx, cfg.OIDCIssuer, func(err error) { - ioc.MustResolve[*zerolog.Logger](ioc.Default).Err(err).Send() - }) - }) - ioc.Register[*oidc.Config](ioc.Default, func() *oidc.Config { - return &oidc.Config{ - ClientID: cfg.OAuthClientID, - } - }) ioc.Register[rpc.Ability](ioc.Default, func() rpc.Ability { return rpc.NewAbilityProtobufClient( env.Fetch("AUTHZD_HOST", ""), diff --git a/app/middleware/from_cookie.go b/app/middleware/from_cookie.go deleted file mode 100644 index 316d6e4..0000000 --- a/app/middleware/from_cookie.go +++ /dev/null @@ -1,15 +0,0 @@ -package middleware - -import "net/http" - -func FromCookie(name string) TokenParser { - return func(r *http.Request) RawToken { - cookies := r.CookiesNamed(name) - - if len(cookies) != 1 { - return "" - } - - return RawToken(cookies[0].Value) - } -} diff --git a/app/middleware/from_custom_header.go b/app/middleware/from_custom_header.go deleted file mode 100644 index f385911..0000000 --- a/app/middleware/from_custom_header.go +++ /dev/null @@ -1,9 +0,0 @@ -package middleware - -import "net/http" - -func FromCustomHeader(name string) TokenParser { - return func(r *http.Request) RawToken { - return RawToken(r.Header.Get(name)) - } -} diff --git a/app/middleware/id_token.go b/app/middleware/id_token.go deleted file mode 100644 index 0c1503e..0000000 --- a/app/middleware/id_token.go +++ /dev/null @@ -1,38 +0,0 @@ -package middleware - -import ( - "net/http" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/xlgmokha/x/pkg/log" - "github.com/xlgmokha/x/pkg/x" - xcfg "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls" -) - -func IDToken(provider *oidc.Provider, config *oidc.Config, parsers ...TokenParser) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - for _, parser := range parsers { - rawIDToken := parser(r) - if x.IsPresent(rawIDToken) { - verifier := provider.VerifierContext(r.Context(), config) - idToken, err := verifier.Verify(r.Context(), rawIDToken.String()) - - if err != nil { - pls.LogError(r.Context(), err) - } else { - log.WithFields(r.Context(), log.Fields{"id_token": idToken}) - next.ServeHTTP( - w, - r.WithContext(xcfg.IDToken.With(r.Context(), idToken)), - ) - return - } - } - } - - next.ServeHTTP(w, r) - }) - } -} diff --git a/app/middleware/id_token_test.go b/app/middleware/id_token_test.go deleted file mode 100644 index 9d8521a..0000000 --- a/app/middleware/id_token_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package middleware - -import ( - "net/http" - "testing" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/oauth2-proxy/mockoidc" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/xlgmokha/x/pkg/test" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" - xcfg "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/web" -) - -func TestIDToken(t *testing.T) { - srv := web.NewOIDCServer(t) - defer srv.Close() - - middleware := IDToken(srv.Provider, &oidc.Config{ClientID: srv.MockOIDC.ClientID}, FromCookie(cfg.IDTokenCookie)) - - t.Run("when an active id_token cookie is provided", func(t *testing.T) { - t.Run("attaches the token to the request context", func(t *testing.T) { - user := mockoidc.DefaultUser() - _, rawIDToken := srv.CreateTokensFor(user) - - server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := xcfg.IDToken.From(r.Context()) - require.NotNil(t, token) - assert.Equal(t, user.Subject, token.Subject) - - w.WriteHeader(http.StatusTeapot) - })) - - r, w := test.RequestResponse( - "GET", - "/example", - test.WithCookie(web.NewCookie(xcfg.IDTokenCookie, rawIDToken)), - ) - server.ServeHTTP(w, r) - - assert.Equal(t, http.StatusTeapot, w.Code) - }) - }) - - t.Run("when an invalid id_token cookie is provided", func(t *testing.T) { - t.Run("forwards the request", func(t *testing.T) { - server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Nil(t, xcfg.IDToken.From(r.Context())) - - w.WriteHeader(http.StatusTeapot) - })) - - r, w := test.RequestResponse( - "GET", - "/example", - test.WithCookie(web.NewCookie(xcfg.IDTokenCookie, "invalid")), - ) - server.ServeHTTP(w, r) - - assert.Equal(t, http.StatusTeapot, w.Code) - }) - }) - - t.Run("when no cookies are provided", func(t *testing.T) { - t.Run("forwards the request", func(t *testing.T) { - server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Nil(t, xcfg.IDToken.From(r.Context())) - - w.WriteHeader(http.StatusTeapot) - })) - - r, w := test.RequestResponse("GET", "/example") - server.ServeHTTP(w, r) - - assert.Equal(t, http.StatusTeapot, w.Code) - }) - }) -} diff --git a/app/middleware/init.go b/app/middleware/init.go index 874ca52..770bd19 100644 --- a/app/middleware/init.go +++ b/app/middleware/init.go @@ -1,33 +1,19 @@ package middleware import ( - "github.com/coreos/go-oidc/v3/oidc" + "net/http" + "github.com/xlgmokha/x/pkg/mapper" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" ) -type CustomClaims struct { - Name string `json:"name"` - Nickname string `json:"nickname"` - Email string `json:"email"` - ProfileURL string `json:"profile"` - Picture string `json:"picture"` - Groups []string `json:"groups_direct"` -} - func init() { - mapper.Register(func(idToken *oidc.IDToken) *domain.User { - customClaims := &CustomClaims{} - if err := idToken.Claims(customClaims); err != nil { - return &domain.User{ID: domain.ID(idToken.Subject)} - } - + mapper.Register(func(h http.Header) *domain.User { return &domain.User{ - ID: domain.ID(idToken.Subject), - Username: customClaims.Nickname, - Email: customClaims.Email, - ProfileURL: customClaims.ProfileURL, - Picture: customClaims.Picture, + ID: domain.ID(h.Get("x-jwt-claim-sub")), + Username: h.Get("x-jwt-claim-username"), + ProfileURL: h.Get("x-jwt-claim-profile-url"), + Picture: h.Get("x-jwt-claim-picture-url"), } }) } diff --git a/app/middleware/is_logged_in.go b/app/middleware/is_logged_in.go index e2f0445..f70a03b 100644 --- a/app/middleware/is_logged_in.go +++ b/app/middleware/is_logged_in.go @@ -8,5 +8,6 @@ import ( ) var IsLoggedIn x.Predicate[*http.Request] = x.Predicate[*http.Request](func(r *http.Request) bool { - return x.IsPresent(cfg.CurrentUser.From(r.Context())) + user := cfg.CurrentUser.From(r.Context()) + return x.IsPresent(user) && x.IsPresent(user.ID) }) diff --git a/app/middleware/raw_token.go b/app/middleware/raw_token.go deleted file mode 100644 index f7aa264..0000000 --- a/app/middleware/raw_token.go +++ /dev/null @@ -1,7 +0,0 @@ -package middleware - -type RawToken string - -func (r RawToken) String() string { - return string(r) -} diff --git a/app/middleware/require_user_test.go b/app/middleware/require_user_test.go index 07cbf92..20b5f94 100644 --- a/app/middleware/require_user_test.go +++ b/app/middleware/require_user_test.go @@ -28,7 +28,8 @@ func TestRequireUser(t *testing.T) { t.Run("when a user is logged in", func(t *testing.T) { t.Run("forwards the request", func(t *testing.T) { - r, w := test.RequestResponse("GET", "/example", test.WithContextKeyValue(t.Context(), cfg.CurrentUser, &domain.User{})) + user := &domain.User{ID: domain.ID("1")} + r, w := test.RequestResponse("GET", "/example", test.WithContextKeyValue(t.Context(), cfg.CurrentUser, user)) server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) diff --git a/app/middleware/token_parser.go b/app/middleware/token_parser.go deleted file mode 100644 index 48034f0..0000000 --- a/app/middleware/token_parser.go +++ /dev/null @@ -1,9 +0,0 @@ -package middleware - -import ( - "net/http" - - "github.com/xlgmokha/x/pkg/x" -) - -type TokenParser x.Mapper[*http.Request, RawToken] diff --git a/app/middleware/user.go b/app/middleware/user.go index 2a6bf71..2865477 100644 --- a/app/middleware/user.go +++ b/app/middleware/user.go @@ -3,42 +3,18 @@ package middleware import ( "net/http" - "github.com/coreos/go-oidc/v3/oidc" - "github.com/xlgmokha/x/pkg/log" "github.com/xlgmokha/x/pkg/mapper" - "github.com/xlgmokha/x/pkg/x" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls" ) -func User(db domain.Repository[*domain.User]) func(http.Handler) http.Handler { +func User() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - subject := r.Header.Get("x-jwt-claim-sub") - log.WithFields(r.Context(), log.Fields{"sub": subject}) - user := db.Find(r.Context(), domain.ID(subject)) - - if !x.IsPresent(user) { - idToken := cfg.IDToken.From(r.Context()) - - if x.IsZero(idToken) { - next.ServeHTTP(w, r) - return - } - - user = db.Find(r.Context(), domain.ID(idToken.Subject)) - if !x.IsPresent(user) { - user = mapper.MapFrom[*oidc.IDToken, *domain.User](idToken) - if err := db.Save(r.Context(), user); err != nil { - pls.LogError(r.Context(), err) - next.ServeHTTP(w, r) - return - } - } - } - - next.ServeHTTP(w, r.WithContext(cfg.CurrentUser.With(r.Context(), user))) + next.ServeHTTP(w, r.WithContext(cfg.CurrentUser.With( + r.Context(), + mapper.MapFrom[http.Header, *domain.User](r.Header), + ))) }) } } diff --git a/app/middleware/user_test.go b/app/middleware/user_test.go index 7653684..371605c 100644 --- a/app/middleware/user_test.go +++ b/app/middleware/user_test.go @@ -4,90 +4,52 @@ import ( "net/http" "testing" - "github.com/coreos/go-oidc/v3/oidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/xlgmokha/x/pkg/test" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/db" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls" ) func TestUser(t *testing.T) { - repository := db.NewRepository[*domain.User]() - middleware := User(repository) + middleware := User() - knownUser := &domain.User{ID: domain.ID(pls.GenerateULID())} - require.NoError(t, repository.Save(t.Context(), knownUser)) + t.Run("when x-jwt-claim-* headers are not provided", func(t *testing.T) { + server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.False(t, IsLoggedIn(r)) - t.Run("when ID Token is provided", func(t *testing.T) { - t.Run("when user is known", func(t *testing.T) { - server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user := cfg.CurrentUser.From(r.Context()) - require.NotNil(t, user) - assert.Equal(t, knownUser.ID, user.ID) + w.WriteHeader(http.StatusTeapot) + })) - w.WriteHeader(http.StatusTeapot) - })) + r, w := test.RequestResponse("GET", "/example") + server.ServeHTTP(w, r) - ctx := cfg.IDToken.With(t.Context(), &oidc.IDToken{Subject: knownUser.ID.String()}) - - r, w := test.RequestResponse("GET", "/example", test.WithContext(ctx)) - server.ServeHTTP(w, r) - - assert.Equal(t, http.StatusTeapot, w.Code) - }) - - t.Run("when user is unknown", func(t *testing.T) { - unknownID := pls.GenerateULID() - - server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user := cfg.CurrentUser.From(r.Context()) - require.NotNil(t, user) - assert.Equal(t, domain.ID(unknownID), user.ID) - - w.WriteHeader(http.StatusTeapot) - })) - - ctx := cfg.IDToken.With(t.Context(), &oidc.IDToken{Subject: unknownID}) - - r, w := test.RequestResponse("GET", "/example", test.WithContext(ctx)) - server.ServeHTTP(w, r) - - assert.Equal(t, http.StatusTeapot, w.Code) - require.NotNil(t, repository.Find(t.Context(), domain.ID(unknownID))) - }) + assert.Equal(t, http.StatusTeapot, w.Code) }) - t.Run("when ID Token is not provided", func(t *testing.T) { - t.Run("without custom headers", func(t *testing.T) { - server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user := cfg.CurrentUser.From(r.Context()) - require.Nil(t, user) - - w.WriteHeader(http.StatusTeapot) - })) - - r, w := test.RequestResponse("GET", "/example") - server.ServeHTTP(w, r) + t.Run("when x-jwt-claim-* headers are provided", func(t *testing.T) { + server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.True(t, IsLoggedIn(r)) - assert.Equal(t, http.StatusTeapot, w.Code) - }) + user := cfg.CurrentUser.From(r.Context()) + require.NotNil(t, user) - t.Run("with x-jwt-claim-sub header", func(t *testing.T) { - server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user := cfg.CurrentUser.From(r.Context()) - require.NotNil(t, user) - require.Equal(t, knownUser.ID, user.ID) + assert.Equal(t, domain.ID("1"), user.ID) + assert.Equal(t, "root", user.Username) + assert.Equal(t, "https://gitlab.com/tanuki", user.ProfileURL) + assert.Equal(t, "https://example.com/profile.png", user.Picture) - w.WriteHeader(http.StatusTeapot) - })) + w.WriteHeader(http.StatusTeapot) + })) - r, w := test.RequestResponse("GET", "/example", test.WithRequestHeader("x-jwt-claim-sub", knownUser.ID.String())) - server.ServeHTTP(w, r) + r, w := test.RequestResponse("GET", "/", + test.WithRequestHeader("x-jwt-claim-sub", "1"), + test.WithRequestHeader("x-jwt-claim-username", "root"), + test.WithRequestHeader("x-jwt-claim-profile-url", "https://gitlab.com/tanuki"), + test.WithRequestHeader("x-jwt-claim-picture-url", "https://example.com/profile.png"), + ) + server.ServeHTTP(w, r) - assert.Equal(t, http.StatusTeapot, w.Code) - }) + assert.Equal(t, http.StatusTeapot, w.Code) }) } diff --git a/etc/envoy/envoy.yaml b/etc/envoy/envoy.yaml index b18a0ac..a6977d1 100644 --- a/etc/envoy/envoy.yaml +++ b/etc/envoy/envoy.yaml @@ -172,13 +172,19 @@ static_resources: typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication providers: - provider1: + id_token_provider: audiences: - OAUTH_CLIENT_ID claim_to_headers: - - header_name: x-jwt-claim-sub - claim_name: sub - forward: true + - claim_name: sub + header_name: x-jwt-claim-sub + - claim_name: nickname + header_name: x-jwt-claim-username + - claim_name: profile + header_name: x-jwt-claim-profile-url + - claim_name: picture + header_name: x-jwt-claim-picture-url + forward: false forward_payload_header: x-jwt-payload from_cookies: - id_token @@ -190,20 +196,15 @@ static_resources: timeout: 5s rules: - match: - path: /health - - match: - prefix: /sparkles - - match: - prefix: /dashboard/nav - - match: safe_regex: regex: .*\\.(css|js|png|html|ico)$ - match: - path: / - - match: - path: /dashboard + prefix: / requires: - provider_name: provider1 + requires_any: + requirements: + - provider_name: id_token_provider + - allow_missing: {} - name: envoy.filters.http.ext_authz typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz @@ -217,6 +218,10 @@ static_resources: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router suppress_envoy_headers: true route_config: + request_headers_to_remove: + - authorization + - cookie + - user-agent virtual_hosts: - name: local domains: ["*"] diff --git a/pkg/authz/id_token.go b/pkg/authz/id_token.go index ccc96de..3271af8 100644 --- a/pkg/authz/id_token.go +++ b/pkg/authz/id_token.go @@ -5,21 +5,35 @@ import ( "encoding/json" "errors" "strings" - "time" ) +type CustomClaims struct { + Name string `json:"name"` + Nickname string `json:"nickname"` + Email string `json:"email"` + ProfileURL string `json:"profile"` + Picture string `json:"picture"` + Groups []string `json:"groups_direct"` +} + type IDToken struct { - // Audience []string `json:"aud"` - Email string `json:"email"` - EmailVerified bool `json:"email_verified"` - ExpiredAt int64 `json:"exp"` - IssuedAt int64 `json:"iat"` - Issuer string `json:"iss"` - Name string `json:"name"` - Nickname string `json:"nickname"` - Picture string `json:"picture"` - Subject string `json:"sub"` - UpdatedAt time.Time `json:"updated_at"` + Issuer string `json:"iss"` + Subject string `json:"sub"` + Audience any `json:"aud"` + Expiry any `json:"exp"` + IssuedAt any `json:"iat"` + NotBefore any `json:"nbf"` + Nonce string `json:"nonce"` + AtHash string `json:"at_hash"` + ClaimNames map[string]string `json:"_claim_names"` + ClaimSources map[string]ClaimSource `json:"_claim_sources"` + + CustomClaims +} + +type ClaimSource struct { + Endpoint string `json:"endpoint"` + AccessToken string `json:"access_token"` } func NewIDToken(raw string) (*IDToken, error) { diff --git a/pkg/web/cookie.go b/pkg/web/cookie.go deleted file mode 100644 index 11cc807..0000000 --- a/pkg/web/cookie.go +++ /dev/null @@ -1,35 +0,0 @@ -package web - -import ( - "net/http" - - "github.com/xlgmokha/x/pkg/cookie" - "github.com/xlgmokha/x/pkg/x" -) - -func NewCookie(name, value string, options ...x.Option[*http.Cookie]) *http.Cookie { - return x.New[*http.Cookie](x.Prepend[x.Option[*http.Cookie]]( - options, - cookie.WithName(name), - cookie.WithValue(value), - cookie.WithPath("/"), - cookie.WithHttpOnly(true), - cookie.WithSecure(true), - )...) -} - -func ExpireCookie(w http.ResponseWriter, name string) error { - return WriteCookie(w, cookie.Reset(name, - cookie.WithPath("/"), - cookie.WithHttpOnly(true), - cookie.WithSecure(true), - )) -} - -func WriteCookie(w http.ResponseWriter, c *http.Cookie) error { - if err := c.Valid(); err != nil { - return err - } - cookie.Write(w, c) - return nil -} diff --git a/pkg/web/cookie_test.go b/pkg/web/cookie_test.go deleted file mode 100644 index 1a3bfb0..0000000 --- a/pkg/web/cookie_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package web - -import ( - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewCookie(t *testing.T) { - cookie := NewCookie("name", "value") - assert.True(t, cookie.HttpOnly) - assert.True(t, cookie.Secure) -} - -func TestExpireCookie(t *testing.T) { - w := httptest.NewRecorder() - - ExpireCookie(w, "example") - - result, err := http.ParseSetCookie(w.Header().Get("Set-Cookie")) - require.NoError(t, err) - - assert.Empty(t, result.Value) - assert.Equal(t, -1, result.MaxAge) - assert.Equal(t, time.Unix(0, 0).Unix(), result.Expires.Unix()) - assert.True(t, result.HttpOnly) - assert.True(t, result.Secure) - assert.Zero(t, result.SameSite) -} diff --git a/pkg/web/oidc.go b/pkg/web/oidc.go deleted file mode 100644 index 707a1b5..0000000 --- a/pkg/web/oidc.go +++ /dev/null @@ -1,27 +0,0 @@ -package web - -import ( - "context" - - "github.com/coreos/go-oidc/v3/oidc" -) - -func NewOIDCProvider(ctx context.Context, issuer string, report func(error)) *oidc.Provider { - provider, err := oidc.NewProvider(ctx, issuer) - if err == nil { - return provider - } - - report(err) - - config := &oidc.ProviderConfig{ - IssuerURL: issuer, - AuthURL: issuer + "/oauth/authorize", - TokenURL: issuer + "/oauth/token", - DeviceAuthURL: "", - UserInfoURL: issuer + "/oauth/userinfo", - JWKSURL: issuer + "/oauth/disovery/keys", - Algorithms: []string{"RS256"}, - } - return config.NewProvider(ctx) -} diff --git a/share/man/ENVOY.md b/share/man/ENVOY.md index 7ad8b64..4b5d765 100644 --- a/share/man/ENVOY.md +++ b/share/man/ENVOY.md @@ -775,9 +775,8 @@ and will immediately reject tokens that are invalid. audiences: - OAUTH_CLIENT_ID claim_to_headers: - - header_name: x-jwt-claim-sub - claim_name: sub - forward: true + - claim_name: sub + header_name: x-jwt-claim-sub forward_payload_header: x-jwt-payload from_cookies: - id_token @@ -787,9 +786,12 @@ and will immediately reject tokens that are invalid. uri: https://gitlab.com/oauth/discovery/keys rules: - match: - path: / + prefix: / requires: - provider_name: gitlab_provider + requires_any: + requirements: + - provider_name: gitlab_provider + - allow_missing: {} - name: envoy.filters.http.router # ... ``` diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go b/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go index f6a7ea8..e06286c 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/oidc.go @@ -162,7 +162,7 @@ var supportedAlgorithms = map[string]bool{ // parsing. // // // Directly fetch the metadata document. -// resp, err := http.Get("https://login.example.com/custom-metadata-path") +// resp, err := http.Get("https://login.example.com/custom-metadata-path") // if err != nil { // // ... // } |
