From 911dffd8b3160a69fdfb4bfd2d13c0464e7ec962 Mon Sep 17 00:00:00 2001 From: branchmispredictor Date: Mon, 17 May 2021 23:09:32 -0400 Subject: [PATCH] Make IAP middleware an optional follow-up to the JWT middleware --- pkg/initialize/init.go | 4 + pkg/modules/auth/auth.go | 32 +++---- pkg/modules/auth/identityawareproxy/error.go | 8 +- .../identityawareproxy/identityawareproxy.go | 95 +++++++++---------- .../identityawareproxy_test.go | 8 +- .../auth/identityawareproxy/middleware.go | 27 +++++- .../identityawareproxy/middleware_test.go | 8 +- pkg/modules/auth/middleware.go | 80 ++++++++++++++++ pkg/routes/routes.go | 13 +-- pkg/swagger/swagger.yaml | 3 +- 10 files changed, 178 insertions(+), 100 deletions(-) create mode 100644 pkg/modules/auth/middleware.go diff --git a/pkg/initialize/init.go b/pkg/initialize/init.go index 32e067ac2e..87fb940f7d 100644 --- a/pkg/initialize/init.go +++ b/pkg/initialize/init.go @@ -27,6 +27,7 @@ import ( "code.vikunja.io/api/pkg/mail" "code.vikunja.io/api/pkg/migration" "code.vikunja.io/api/pkg/models" + "code.vikunja.io/api/pkg/modules/auth/identityawareproxy" "code.vikunja.io/api/pkg/modules/keyvalue" migrator "code.vikunja.io/api/pkg/modules/migration" "code.vikunja.io/api/pkg/notifications" @@ -95,6 +96,9 @@ func FullInit() { models.RegisterReminderCron() models.RegisterOverdueReminderCron() + // Setup + identityawareproxy.Init() + // Start processing events go func() { models.RegisterListeners() diff --git a/pkg/modules/auth/auth.go b/pkg/modules/auth/auth.go index 302b9f99ef..779aed1331 100644 --- a/pkg/modules/auth/auth.go +++ b/pkg/modules/auth/auth.go @@ -29,7 +29,6 @@ import ( "github.com/dgrijalva/jwt-go" petname "github.com/dustinkirkland/golang-petname" "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" ) // These are all valid auth types @@ -42,7 +41,8 @@ const ( AuthTypeIAPUser ) -const authTokenContextKey string = "authToken" +// Key used to store authClaims +const AuthClaimsContextKey string = "authClaims" // Token represents an authentification token in signed string form type Token struct { @@ -78,7 +78,8 @@ type AuthClaims struct { // In these cases, AuthClaims may contain hints to the user identity, // but an outside source is the final source-of-truth for auth (e.g. Identity-Aware Proxy auth) type AuthProvider interface { - GetWebAuth(echo.Context, *AuthClaims) (web.Auth, error) + GetUser(echo.Context, *AuthClaims) (*user.User, error) + RenewToken(echo.Context, *AuthClaims) (string, error) } var authProviders = map[AuthType]AuthProvider{} @@ -147,8 +148,7 @@ func NewLinkShareJWTAuthtoken(share *models.LinkSharing) (token string, err erro // GetAuthFromClaims returns a web.Auth object from jwt claims or from an // alternative authProvider func GetAuthFromClaims(c echo.Context) (a web.Auth, err error) { - jwtinf := c.Get(authTokenContextKey).(*jwt.Token) - claims := jwtinf.Claims.(*AuthClaims) + claims := c.Get(AuthClaimsContextKey).(*AuthClaims) if claims.Type == AuthTypeLinkShare && config.ServiceEnableLinkSharing.GetBool() { return getLinkShareFromClaims(claims) } @@ -156,7 +156,7 @@ func GetAuthFromClaims(c echo.Context) (a web.Auth, err error) { return getUserFromClaims(claims), nil } if authProvider, ok := authProviders[claims.Type]; ok { - return authProvider.GetWebAuth(c, claims) + return authProvider.GetUser(c, claims) } return nil, echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Invalid JWT token."}) } @@ -206,8 +206,7 @@ func GetCurrentUser(c echo.Context) (u *user.User, err error) { // Generates a new jwt token for the types AuthTypeLinkShare and AuthTypeUser func RenewToken(s *xorm.Session, c echo.Context) (token string, err error) { - jwtinf := c.Get(authTokenContextKey).(*jwt.Token) - claims := jwtinf.Claims.(*AuthClaims) + claims := c.Get(AuthClaimsContextKey).(*AuthClaims) if claims.Type == AuthTypeLinkShare { oldShare, err := getLinkShareFromClaims(claims) @@ -230,17 +229,14 @@ func RenewToken(s *xorm.Session, c echo.Context) (token string, err error) { } return NewUserJWTAuthtoken(u) } - return "", echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Invalid JWT token."}) -} - -// GetJWTConfig returns the config for the default JWT middleware -func GetJWTConfig() middleware.JWTConfig { - return middleware.JWTConfig{ - SigningKey: []byte(config.ServiceJWTSecret.GetString()), - SigningMethod: middleware.AlgorithmHS256, - ContextKey: authTokenContextKey, - Claims: &AuthClaims{}, + if authProvider, ok := authProviders[claims.Type]; ok { + token, err := authProvider.RenewToken(c, claims) + if err != nil { + return "", err + } + return token, nil } + return "", echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Invalid JWT token."}) } // GetOrCreateUserFromExternalAuth returns a user after finding or creating a matching user for the provided details diff --git a/pkg/modules/auth/identityawareproxy/error.go b/pkg/modules/auth/identityawareproxy/error.go index 2b943b44c8..1bc36ba58d 100644 --- a/pkg/modules/auth/identityawareproxy/error.go +++ b/pkg/modules/auth/identityawareproxy/error.go @@ -1,17 +1,17 @@ // Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. // // This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by +// it under the terms of the GNU Affero General Public Licensee as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. +// GNU Affero General Public Licensee for more details. // -// You should have received a copy of the GNU General Public License +// You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . package identityawareproxy diff --git a/pkg/modules/auth/identityawareproxy/identityawareproxy.go b/pkg/modules/auth/identityawareproxy/identityawareproxy.go index 04ef02b3c0..5c06a76889 100644 --- a/pkg/modules/auth/identityawareproxy/identityawareproxy.go +++ b/pkg/modules/auth/identityawareproxy/identityawareproxy.go @@ -1,17 +1,17 @@ // Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. // // This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by +// it under the terms of the GNU Affero General Public Licensee as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. +// GNU Affero General Public Licensee for more details. // -// You should have received a copy of the GNU General Public License +// You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . package identityawareproxy @@ -25,7 +25,6 @@ import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/api/pkg/user" - "code.vikunja.io/web" "github.com/dgrijalva/jwt-go" "github.com/labstack/echo/v4" ) @@ -44,8 +43,9 @@ type IAPClaims struct { // Auth provider used to allow auth to get a web.Auth from the IAP provided identity type IAPAuthProvider struct{} -func init() { +func Init() { auth.RegisterAuthProvider(auth.AuthTypeIAPUser, IAPAuthProvider{}) + auth.RegisterAuthMiddleware(auth.AuthTypeIAPUser, Middleware()) } // NewIAPUserJWTAuthtoken generates and signes a new jwt token for a user @@ -55,7 +55,15 @@ func init() { // and a hint to auth.go to retrieve auth data from the IAP. func NewIAPUserJWTAuthtoken(u *user.User) (token string, err error) { // Set claims - claims := &auth.AuthClaims{ + claims := newIAPUserJWTAuthClaims(u) + t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + return t.SignedString([]byte(config.ServiceJWTSecret.GetString())) +} + +func newIAPUserJWTAuthClaims(u *user.User) (claims *auth.AuthClaims) { + return &auth.AuthClaims{ Type: auth.AuthTypeIAPUser, UserID: u.ID, UserUsername: u.Username, @@ -65,57 +73,14 @@ func NewIAPUserJWTAuthtoken(u *user.User) (token string, err error) { ExpiresAt: time.Now().Add(time.Minute * 5).Unix(), }, } - - t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - // Generate encoded token and send it as response. - return t.SignedString([]byte(config.ServiceJWTSecret.GetString())) } -// Token generates a local, short-lived JWT based on the identity from the identity-aware proxy. -// See also the docs for NewIAPUserJWTAuthtoken -// @Summary Authenticate a user from the Identity-Aware Proxy -// @Description Generates a short-lived JWT based on the identity from the identity-aware proxy in order to provide the front-end with user id and username info -// @tags auth -// @Accept N/A -// @Produce json -// @Success 200 {object} auth.Token -// @Failure 500 {object} models.Message "Internal error" -// @Router /auth/identityawareproxy/token [get] -func GetToken(c echo.Context) error { - cl := c.Get(IAPClaimsContextKey).(*IAPClaims) - +func userForIAPClaims(cl *IAPClaims) (u *user.User, err error) { s := db.NewSession() defer s.Close() // Check if we have seen this user before - u, err := auth.GetOrCreateUserFromExternalAuth(s, cl.Issuer, cl.Subject, cl.Email, cl.Name, cl.PreferredUsername) - if err != nil { - _ = s.Rollback() - return err - } - - err = s.Commit() - if err != nil { - return err - } - - // Create token - userToken, err := NewIAPUserJWTAuthtoken(u) - if err != nil { - return err - } - return auth.NewTokenResponse(userToken, c) -} - -// Get a web.Auth object from the identity that the IAP provides -func (p IAPAuthProvider) GetWebAuth(c echo.Context, authClaims *auth.AuthClaims) (web.Auth, error) { - s := db.NewSession() - defer s.Close() - - // Get the user from the IAP identity - cl := c.Get(IAPClaimsContextKey).(*IAPClaims) - u, err := auth.GetOrCreateUserFromExternalAuth(s, cl.Issuer, cl.Subject, cl.Email, cl.Name, cl.PreferredUsername) + u, err = auth.GetOrCreateUserFromExternalAuth(s, cl.Issuer, cl.Subject, cl.Email, cl.Name, cl.PreferredUsername) if err != nil { _ = s.Rollback() return nil, err @@ -126,6 +91,32 @@ func (p IAPAuthProvider) GetWebAuth(c echo.Context, authClaims *auth.AuthClaims) return nil, err } + return u, nil +} + +func (p IAPAuthProvider) RenewToken(c echo.Context, authClaims *auth.AuthClaims) (string, error) { + // Get user + u, err := p.GetUser(c, authClaims) + if err != nil { + return "", nil + } + + // Create token + return NewIAPUserJWTAuthtoken(u) +} + +// Get a web.Auth object from the identity that the IAP provides +func (p IAPAuthProvider) GetUser(c echo.Context, authClaims *auth.AuthClaims) (*user.User, error) { + s := db.NewSession() + defer s.Close() + + // Get the user from the IAP identity + cl := c.Get(IAPClaimsContextKey).(*IAPClaims) + u, err := userForIAPClaims(cl) + if err != nil { + return nil, err + } + // Sanity check that the user the frontend thinks it has (the authClaims from the JWT it passed in) // is the same as the user provided by the IAP. if authClaims.UserID != u.ID { diff --git a/pkg/modules/auth/identityawareproxy/identityawareproxy_test.go b/pkg/modules/auth/identityawareproxy/identityawareproxy_test.go index 55a28ce4cf..c438217f4e 100644 --- a/pkg/modules/auth/identityawareproxy/identityawareproxy_test.go +++ b/pkg/modules/auth/identityawareproxy/identityawareproxy_test.go @@ -1,17 +1,17 @@ // Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. // // This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by +// it under the terms of the GNU Affero General Public Licensee as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. +// GNU Affero General Public Licensee for more details. // -// You should have received a copy of the GNU General Public License +// You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . package identityawareproxy diff --git a/pkg/modules/auth/identityawareproxy/middleware.go b/pkg/modules/auth/identityawareproxy/middleware.go index e4ec0838d3..701c776484 100644 --- a/pkg/modules/auth/identityawareproxy/middleware.go +++ b/pkg/modules/auth/identityawareproxy/middleware.go @@ -1,17 +1,17 @@ // Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. // // This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by +// it under the terms of the GNU Affero General Public Licensee as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. +// GNU Affero General Public Licensee for more details. // -// You should have received a copy of the GNU General Public License +// You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . package identityawareproxy @@ -23,6 +23,7 @@ import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/web/handler" "github.com/dgrijalva/jwt-go" "github.com/labstack/echo/v4" @@ -70,6 +71,12 @@ func Middleware() echo.MiddlewareFunc { cache := &iapCache{} return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { + // Skip if IAP is not enabled + if !config.AuthIdentityAwareProxyEnabled.GetBool() { + return next(c) + } + + // Get and validate claims from the IAP token := c.Request().Header.Get(config.AuthIdentityAwareProxyJwtHeader.GetString()) if token == "" { err := ErrIAPTokenMissing{Header: config.AuthIdentityAwareProxyJwtHeader.GetString()} @@ -85,13 +92,23 @@ func Middleware() echo.MiddlewareFunc { if err != nil { return handler.HandleHTTPError(err, c) } - c.Set(IAPClaimsContextKey, cl) + + // Generate auth.AuthClaims from the IAP identity + user, err := userForIAPClaims(cl) + if err != nil { + return handler.HandleHTTPError(err, c) + } + authClaims := newIAPUserJWTAuthClaims(user) + c.Set(auth.AuthClaimsContextKey, authClaims) return next(c) } } } +// The authMiddleware generates and stores internal auth based on +// those claims. This overwrites any auth from the JWT middleware + func parseAndValidateJwt(token string, keyset *jwk.Set) (*IAPClaims, error) { // Parse the jwt from the identity-aware proxy using the correct key tken, err := jwt.ParseWithClaims(token, &IAPClaims{}, func(unvalidatedToken *jwt.Token) (interface{}, error) { diff --git a/pkg/modules/auth/identityawareproxy/middleware_test.go b/pkg/modules/auth/identityawareproxy/middleware_test.go index 1ec3062bf5..5b9477a862 100644 --- a/pkg/modules/auth/identityawareproxy/middleware_test.go +++ b/pkg/modules/auth/identityawareproxy/middleware_test.go @@ -1,17 +1,17 @@ // Vikunja is a to-do list application to facilitate your life. -// Copyright 2018-2020 Vikunja and contributors. All rights reserved. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. // // This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by +// it under the terms of the GNU Affero General Public Licensee as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. +// GNU Affero General Public Licensee for more details. // -// You should have received a copy of the GNU General Public License +// You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . package identityawareproxy diff --git a/pkg/modules/auth/middleware.go b/pkg/modules/auth/middleware.go new file mode 100644 index 0000000000..cd24aa0c84 --- /dev/null +++ b/pkg/modules/auth/middleware.go @@ -0,0 +1,80 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package auth + +import ( + "code.vikunja.io/api/pkg/config" + "github.com/dgrijalva/jwt-go" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +// Resolves circular dependencies of auth -> IAP -> auth +var authMiddlewares = map[AuthType]echo.MiddlewareFunc{} + +func RegisterAuthMiddleware(t AuthType, f echo.MiddlewareFunc) { + authMiddlewares[t] = f +} + +const authTokenJWTContextKey = "jwtToken" + +// GetJWTConfig returns the config for the default JWT middleware +func GetJWTConfig() middleware.JWTConfig { + return middleware.JWTConfig{ + SigningKey: []byte(config.ServiceJWTSecret.GetString()), + SigningMethod: middleware.AlgorithmHS256, + ContextKey: authTokenJWTContextKey, + Claims: &AuthClaims{}, + } +} + +// The auth middleware uses the JWT middleware to parse and validate a JWT. +// If that does not succeed, it generates a JWT token from the identity-aware proxy +func Middleware() echo.MiddlewareFunc { + // Create a noop next function to let us run middlewares without jumping to the next + // one in the chain + noOpNext := func(_ echo.Context) error { + return nil + } + jwtMiddleware := middleware.JWTWithConfig(GetJWTConfig()) + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // First attempt to get auth from a provided JWT + jwtErr := jwtMiddleware(noOpNext)(c) + if c.Get(authTokenJWTContextKey) != nil && jwtErr == nil { + // If it succeeded, use the authClaims from the JWT + // and continue in the middleware chain + jwtinf := c.Get(authTokenJWTContextKey).(*jwt.Token) + claims := jwtinf.Claims.(*AuthClaims) + c.Set(AuthClaimsContextKey, claims) + return next(c) + } + + // Otherwise, attempt to get auth from authMiddlewares + for _, authMiddleware := range authMiddlewares { + err := authMiddleware(noOpNext)(c) + if c.Get(AuthClaimsContextKey) != nil && err == nil { + // If it succeeded, continue in the middleware chain + return next(c) + } + } + + // Otherwise, return the original error from jwt middleware + return jwtErr + } + } +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 3cdc88b31c..9e0484e694 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -55,7 +55,6 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" - "code.vikunja.io/api/pkg/modules/auth/identityawareproxy" "code.vikunja.io/api/pkg/modules/auth/openid" "code.vikunja.io/api/pkg/modules/background" backgroundHandler "code.vikunja.io/api/pkg/modules/background/handler" @@ -251,19 +250,9 @@ func registerAPIRoutes(a *echo.Group) { n.POST("/shares/:share/auth", apiv1.AuthenticateLinkShare) } - // Identity-Aware Proxy auth, requires the same iap middleware as authenticated routes - if config.AuthIdentityAwareProxyEnabled.GetBool() { - m := n.Group("") - m.Use(identityawareproxy.Middleware()) - m.GET("/auth/identityawareproxy/token", identityawareproxy.GetToken) - } - // ===== Routes with Authentication ===== // Authentification - a.Use(middleware.JWTWithConfig(auth.GetJWTConfig())) - if config.AuthIdentityAwareProxyEnabled.GetBool() { - a.Use(identityawareproxy.Middleware()) - } + a.Use(auth.Middleware()) // Rate limit setupRateLimit(a, config.RateLimitKind.GetString()) diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index aee8a7bc30..af8eb15d0f 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -1340,7 +1340,8 @@ paths: get: consumes: - N/A - description: Generates a short-lived JWT based on the identity from the identity-aware proxy in order to provide the front-end with user id and username info + description: Generates a short-lived JWT based on the identity from the identity-aware + proxy in order to provide the front-end with user id and username info produces: - application/json responses: