diff --git a/pkg/initialize/init.go b/pkg/initialize/init.go
index 32e067ac2..87fb940f7 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 302b9f99e..779aed133 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 2b943b44c..1bc36ba58 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 04ef02b3c..5c06a7688 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 55a28ce4c..c438217f4 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 e4ec0838d..701c77648 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 1ec3062bf..5b9477a86 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 000000000..cd24aa0c8
--- /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 3cdc88b31..9e0484e69 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 aee8a7bc3..af8eb15d0 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: