Make IAP middleware an optional follow-up to the JWT middleware

This commit is contained in:
branchmispredictor 2021-05-17 23:09:32 -04:00
parent 440e0e2b27
commit 911dffd8b3
10 changed files with 178 additions and 100 deletions

View File

@ -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()

View File

@ -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

View File

@ -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 <https://www.gnu.org/licenses/>.
package identityawareproxy

View File

@ -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 <https://www.gnu.org/licenses/>.
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 {

View File

@ -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 <https://www.gnu.org/licenses/>.
package identityawareproxy

View File

@ -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 <https://www.gnu.org/licenses/>.
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) {

View File

@ -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 <https://www.gnu.org/licenses/>.
package identityawareproxy

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}
}
}

View File

@ -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())

View File

@ -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: