diff --git a/go.mod b/go.mod index 2a5003d7..84f8bae0 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/coreos/go-oidc/v3 v3.10.0 github.com/fatedier/golib v0.4.2 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.1 diff --git a/go.sum b/go.sum index 8f1610f3..6a94df6d 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,8 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 9d8db7b6..dcb9e52f 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -30,7 +30,7 @@ type Setter interface { func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) { switch cfg.Method { case v1.AuthMethodToken: - authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) + authProvider = NewJWTAuth(cfg.AdditionalScopes, cfg.Token) case v1.AuthMethodOIDC: authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) default: @@ -48,7 +48,7 @@ type Verifier interface { func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) { switch cfg.Method { case v1.AuthMethodToken: - authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) + authVerifier = NewJWTAuth(cfg.AdditionalScopes, cfg.Token) case v1.AuthMethodOIDC: authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC) } diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go new file mode 100644 index 00000000..66c5c973 --- /dev/null +++ b/pkg/auth/jwt.go @@ -0,0 +1,124 @@ +// Copyright 2020 guylewin, guy@lewin.co.il +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "errors" + "fmt" + "slices" + "time" + + "github.com/golang-jwt/jwt/v5" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/msg" +) + +type JWTAuthSetterVerifier struct { + additionalAuthScopes []v1.AuthScope + token string +} + +func NewJWTAuth(additionalAuthScopes []v1.AuthScope, token string) *JWTAuthSetterVerifier { + return &JWTAuthSetterVerifier{ + additionalAuthScopes: additionalAuthScopes, + token: token, + } +} + +func (auth *JWTAuthSetterVerifier) SetLogin(loginMsg *msg.Login) error { + loginMsg.PrivilegeKey = auth.token + return nil +} + +func (auth *JWTAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { + return nil + } + + pingMsg.Timestamp = time.Now().Unix() + pingMsg.PrivilegeKey = auth.token + return nil +} + +func (auth *JWTAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { + return nil + } + + newWorkConnMsg.Timestamp = time.Now().Unix() + newWorkConnMsg.PrivilegeKey = auth.token + return nil +} + +func (auth *JWTAuthSetterVerifier) VerifyLogin(m *msg.Login) error { + return auth.VerifyToken(m.User, m.PrivilegeKey) +} + +func (auth *JWTAuthSetterVerifier) VerifyPing(m *msg.Ping) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { + return nil + } + + return auth.VerifyToken("", m.PrivilegeKey) +} + +func (auth *JWTAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error { + if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { + return nil + } + + return auth.VerifyToken("", m.PrivilegeKey) +} + +func (auth *JWTAuthSetterVerifier) VerifyToken(user, token string) error { + methodKey := map[string]string{jwt.SigningMethodHS256.Alg(): auth.token} + parser := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name})) + parsedToken, err := parser.Parse(token, func(t *jwt.Token) (any, error) { + key, ok := methodKey[t.Method.Alg()] + if !ok { + return nil, fmt.Errorf("method %s is not supported", t.Method) + } + return []byte(key), nil + }) + + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return errors.New("token is expired") + } + return err + } + + if !parsedToken.Valid { + return fmt.Errorf("token %s is invalid", token) + } + + claims, ok := parsedToken.Claims.(jwt.MapClaims) + if !ok { + return fmt.Errorf("claims %v is invalid", parsedToken.Claims) + } + + if len(user) > 0 { + id, found := claims["email"] + if !found { + id, found = claims["id"] + } + if id != user { + return fmt.Errorf("token %s is not for user %s", token, user) + } + } + + return nil +}