Add support to login using identity from an identity-aware proxy #715

Closed
branchmispredictor wants to merge 9 commits from branchmispredictor/api:feature/identity-aware-proxy into main
10 changed files with 178 additions and 100 deletions
Showing only changes of commit 911dffd8b3 - Show all commits

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)

I think this should return a *user.User since the external auth is still represented as a user in Vikunja. It's not a "new type of authentication" like link share is.

I think this should return a `*user.User` since the external auth is still represented as a user in Vikunja. It's not a "new type of authentication" like link share is.
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(),
branchmispredictor marked this conversation as resolved Outdated

Please document this.

Please document this.
},
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
}
branchmispredictor marked this conversation as resolved Outdated

Please make errors like this into custom error types which you'd then return.

Please make errors like this into custom error types which you'd then return.
// 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 {

Is there a special reason this is a separate endpoint instead of reusing the existing token one? It seems like this will require a frontend change?

Is there a special reason this is a separate endpoint instead of reusing the existing token one? It seems like this will require a frontend change?

There's a problem with bootstrapping auth here. The way IAPs work is by setting an http header with claims to the downstream service (vikunja-api), however javascript or front-end code does not have access to http headers.

So on initial load, there is no cookie / vikunja-jwt set so the frontend does not see us as logged in. Similarly, the existing /user/token endpoint is also unavailable because it is behind the jwt middleware and requires a jwt cookie set. So this endpoint exists outside of the jwt middleware so it can be accessed from before a user is "logged in"

There's a problem with bootstrapping auth here. The way IAPs work is by setting an http header with claims to the downstream service (vikunja-api), however javascript or front-end code does not have access to http headers. So on initial load, there is no cookie / vikunja-jwt set so the frontend does not see us as logged in. Similarly, the existing `/user/token` endpoint is also unavailable because it is behind the jwt middleware and requires a jwt cookie set. So this endpoint exists outside of the jwt middleware so it can be accessed from before a user is "logged in"

So this is the endpoint to "translate" the http header from the IAP to a Vikunja token? Wouldn't it be possible to just extend the existing middleware to look for that http header? All that'd be left then would be to extend the frontend so it somehow checks if the user is logged in (maybe by calling /api/v1/user and see if there's a 200 response coming back and then setting the frontend state to "logged in"?)

So this is the endpoint to "translate" the http header from the IAP to a Vikunja token? Wouldn't it be possible to just extend the existing middleware to look for that http header? All that'd be left then would be to extend the frontend so it somehow checks if the user is logged in (maybe by calling `/api/v1/user` and see if there's a 200 response coming back and then setting the frontend state to "logged in"?)

What I can do, is abtract this away a bit and make this an /auth/externalprovider/loggedin endpoint, so any future external auth source might also reuse the same endpoint.

Was writing that before I saw the reply. Yes, this basically translates the http header from the IAP to a vikunja token. The only problem is that the jwt middleware will error out for /api/v1/user/* if the frontend does not provide an already valid jwt as a cookie

~~What I can do, is abtract this away a bit and make this an `/auth/externalprovider/loggedin` endpoint, so any future external auth source might also reuse the same endpoint.~~ Was writing that before I saw the reply. Yes, this basically translates the http header from the IAP to a vikunja token. The only problem is that the jwt middleware will error out for /api/v1/user/* if the frontend does not provide an already valid jwt as a cookie

How would that look in the frontend, would it call that endpoint every time on reload?

How would that look in the frontend, would it call that endpoint every time on reload?

Yes, or at least if not already logged in / if current jwt token is expired. Essentially it's similar to an open-id auth from the frontend's point of view except it 1) is hitting vikunja-backend directly instead of first hitting an external service and 2) it is automatically calling the endpoint instead of waiting for the openid button to be hit.

We can actually make num 2 optional, if for whatever reason we prefer being able to have both IAP login and local/openid logins too.

Yes, or at least if not already logged in / if current jwt token is expired. Essentially it's similar to an open-id auth from the frontend's point of view except it 1) is hitting vikunja-backend directly instead of first hitting an external service and 2) it is automatically calling the endpoint instead of waiting for the openid button to be hit. We can actually make num 2 optional, if for whatever reason we prefer being able to have both IAP login and local/openid logins too.

Then I'd prefer modifying the existing jwt middleware to accept a jwt token or an IAP header if no token is provided (and move all the logic to create a new user etc there as well). That way, we won't need the new endpoint and can modify the frontend only a bit to make a call to /user every time it is loaded, regardless if it has a jwt token in localstorage or not. I think that would make the IAP a lot more transparent to the frontend.

Then I'd prefer modifying the existing jwt middleware to accept a jwt token or an IAP header if no token is provided (and move all the logic to create a new user etc there as well). That way, we won't need the new endpoint and can modify the frontend only a bit to make a call to `/user` every time it is loaded, regardless if it has a jwt token in localstorage or not. I think that would make the IAP a lot more transparent to the frontend.

Okay, I'll give it a shot.

Okay, I'll give it a shot.

Following up just to make sure I capture the implicit decisions here, so you want for the backend:

  1. If a valid jwt token is presented from the frontend, use that (e.g. ignore the IAP header)
  2. If no valid jwt token exists, create one from the IAP header. In the backend, treat this newly generated jwt token the same as if it was provided by the frontend

In the frontend:

  1. Call /user/token on load -> will get the jwt token generated from the IAP header
Following up just to make sure I capture the implicit decisions here, so you want for the backend: 1. If a valid jwt token is presented from the frontend, use that (e.g. ignore the IAP header) 2. If no valid jwt token exists, create one from the IAP header. In the backend, treat this newly generated jwt token the same as if it was provided by the frontend In the frontend: 1. Call /user/token on load -> will get the jwt token generated from the IAP header

Almost, I wouldn't generate a jwt token from the backend at all but instead use the IAP header to authenticate the user.

That would look like this in the middleware:

  1. If a valid jwt token is presented from the frontend, use that
  2. If a valid IAP header is present, use that as a means to authenticate the user in place of a jwt token. This includes creating the user internally if none exists yet. There may be a few places which would need to be a bit more abstracted to not only rely on a jwt token to get the user information.

The frontend would then always call user to figure out if the user is authenticated or not. Currently it only does that if a jwt token is present in local storage.

Almost, I wouldn't generate a jwt token from the backend at all but instead use the IAP header to authenticate the user. That would look like this in the middleware: 1. If a valid jwt token is presented from the frontend, use that 2. If a valid IAP header is present, use that as a means to authenticate the user in place of a jwt token. This includes creating the user internally if none exists yet. There may be a few places which would need to be a bit more abstracted to not only rely on a jwt token to get the user information. The frontend would then always call [`user`](https://try.vikunja.io/api/v1/docs#tag/user/paths/~1user/get) to figure out if the user is authenticated or not. Currently it only does that if a jwt token is present in local storage.
_ = s.Rollback()
return nil, err

What if there's more than one Key? Is that even possible? In that case, should we still just use the first one or return a different error?

What if there's more than one Key? Is that even possible? In that case, should we still just use the first one or return a different error?
@ -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
}
branchmispredictor marked this conversation as resolved Outdated

This is almost an exact copy from the method in openid.go. I'll try to refactor into some common helper, but I'm not too familiar with go and how to get them to work with both different claim structs. Open to suggestions here.

This is almost an exact copy from the method in `openid.go`. I'll try to refactor into some common helper, but I'm not too familiar with go and how to get them to work with both different claim structs. Open to suggestions here.

Maybe just have the parameters of the claim like Issuer, Subject, Username and so on as function parameters? Or a separate createUserOpts struct or something like that.

Maybe just have the parameters of the claim like `Issuer`, `Subject`, `Username` and so on as function parameters? Or a separate `createUserOpts` struct or something like that.
// 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: