// 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 identityawareproxy import ( "fmt" "time" "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/modules/auth" "code.vikunja.io/api/pkg/user" "github.com/dgrijalva/jwt-go" "github.com/labstack/echo/v4" ) const IAPClaimsContextKey string = "iapClaims" // IAPClaims represents the claims made by the authentication JWT // passed in by the identity-aware proxy type IAPClaims struct { Email string `json:"email"` Name string `json:"name,omitempty"` PreferredUsername string `json:"preferred_username,omitempty"` jwt.StandardClaims } // Auth provider used to allow auth to get a web.Auth from the IAP provided identity type IAPAuthProvider struct{} func Init() { auth.RegisterAuthProvider(auth.AuthTypeIAPUser, IAPAuthProvider{}) auth.RegisterAuthMiddleware(auth.AuthTypeIAPUser, Middleware()) } // NewIAPUserJWTAuthtoken generates and signes a new jwt token for a user // These are intentionally short lived because they can be regenerated at // any time from the IAP authn information. They are not related to // session length and are only used to provide user info to the frontend // 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 := 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, UserEmail: u.Email, UserName: u.Name, StandardClaims: jwt.StandardClaims{ ExpiresAt: time.Now().Add(time.Minute * 5).Unix(), }, } } 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 nil, err } err = s.Commit() if err != nil { 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 { return nil, ErrIAPUserFrontendMismatch{} } return u, nil } // Validates the claims in the jwt // Matches the jwt-go Claims interface func (c *IAPClaims) Valid() error { // Validate that expiresAt and issuedAt are set and valid (with up to 1 minute of skew) now := TimeFunc() skew := time.Minute if !c.VerifyExpiresAt(now.Add(-skew).Unix(), true) { delta := now.Sub(time.Unix(c.ExpiresAt, 0)) return fmt.Errorf("token is expired by %v", delta) } if !c.VerifyIssuedAt(now.Add(skew).Unix(), true) { return fmt.Errorf("token used before issued") } // Validate that subject, email, and issuer are all set if c.Subject == "" { return fmt.Errorf("token missing subject") } if c.Email == "" { return fmt.Errorf("token missing email") } if c.Issuer == "" { return fmt.Errorf("token missing issuer") } return nil }