feat: add multiple authentication methods, token and oidc. token is the current token comparison, and oidc generates oidc token using client-credentials flow. in addition - add ping verification using the same method
This commit is contained in:
parent
83d80857fd
commit
72626ac2b8
@ -25,6 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
"github.com/fatedier/frp/models/auth"
|
||||||
"github.com/fatedier/frp/models/config"
|
"github.com/fatedier/frp/models/config"
|
||||||
"github.com/fatedier/frp/models/msg"
|
"github.com/fatedier/frp/models/msg"
|
||||||
frpNet "github.com/fatedier/frp/utils/net"
|
frpNet "github.com/fatedier/frp/utils/net"
|
||||||
@ -82,13 +83,17 @@ type Control struct {
|
|||||||
|
|
||||||
// service context
|
// service context
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
|
// sets authentication based on selected method
|
||||||
|
authSetter auth.Setter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewControl(ctx context.Context, runId string, conn net.Conn, session *fmux.Session,
|
func NewControl(ctx context.Context, runId string, conn net.Conn, session *fmux.Session,
|
||||||
clientCfg config.ClientCommonConf,
|
clientCfg config.ClientCommonConf,
|
||||||
pxyCfgs map[string]config.ProxyConf,
|
pxyCfgs map[string]config.ProxyConf,
|
||||||
visitorCfgs map[string]config.VisitorConf,
|
visitorCfgs map[string]config.VisitorConf,
|
||||||
serverUDPPort int) *Control {
|
serverUDPPort int,
|
||||||
|
authSetter auth.Setter) *Control {
|
||||||
|
|
||||||
// new xlog instance
|
// new xlog instance
|
||||||
ctl := &Control{
|
ctl := &Control{
|
||||||
@ -107,6 +112,7 @@ func NewControl(ctx context.Context, runId string, conn net.Conn, session *fmux.
|
|||||||
serverUDPPort: serverUDPPort,
|
serverUDPPort: serverUDPPort,
|
||||||
xl: xlog.FromContextSafe(ctx),
|
xl: xlog.FromContextSafe(ctx),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
authSetter: authSetter,
|
||||||
}
|
}
|
||||||
ctl.pm = proxy.NewProxyManager(ctl.ctx, ctl.sendCh, clientCfg, serverUDPPort)
|
ctl.pm = proxy.NewProxyManager(ctl.ctx, ctl.sendCh, clientCfg, serverUDPPort)
|
||||||
|
|
||||||
@ -282,7 +288,12 @@ func (ctl *Control) msgHandler() {
|
|||||||
case <-hbSend.C:
|
case <-hbSend.C:
|
||||||
// send heartbeat to server
|
// send heartbeat to server
|
||||||
xl.Debug("send heartbeat to server")
|
xl.Debug("send heartbeat to server")
|
||||||
ctl.sendCh <- &msg.Ping{}
|
pingMsg := &msg.Ping{}
|
||||||
|
if err := ctl.authSetter.SetPing(pingMsg); err != nil {
|
||||||
|
xl.Warn("error during ping authentication: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctl.sendCh <- pingMsg
|
||||||
case <-hbCheck.C:
|
case <-hbCheck.C:
|
||||||
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartBeatTimeout)*time.Second {
|
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartBeatTimeout)*time.Second {
|
||||||
xl.Warn("heartbeat timeout")
|
xl.Warn("heartbeat timeout")
|
||||||
|
@ -26,11 +26,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/frp/assets"
|
"github.com/fatedier/frp/assets"
|
||||||
|
"github.com/fatedier/frp/models/auth"
|
||||||
"github.com/fatedier/frp/models/config"
|
"github.com/fatedier/frp/models/config"
|
||||||
"github.com/fatedier/frp/models/msg"
|
"github.com/fatedier/frp/models/msg"
|
||||||
"github.com/fatedier/frp/utils/log"
|
"github.com/fatedier/frp/utils/log"
|
||||||
frpNet "github.com/fatedier/frp/utils/net"
|
frpNet "github.com/fatedier/frp/utils/net"
|
||||||
"github.com/fatedier/frp/utils/util"
|
|
||||||
"github.com/fatedier/frp/utils/version"
|
"github.com/fatedier/frp/utils/version"
|
||||||
"github.com/fatedier/frp/utils/xlog"
|
"github.com/fatedier/frp/utils/xlog"
|
||||||
|
|
||||||
@ -46,6 +46,9 @@ type Service struct {
|
|||||||
ctl *Control
|
ctl *Control
|
||||||
ctlMu sync.RWMutex
|
ctlMu sync.RWMutex
|
||||||
|
|
||||||
|
// Sets authentication based on selected method
|
||||||
|
authSetter auth.Setter
|
||||||
|
|
||||||
cfg config.ClientCommonConf
|
cfg config.ClientCommonConf
|
||||||
pxyCfgs map[string]config.ProxyConf
|
pxyCfgs map[string]config.ProxyConf
|
||||||
visitorCfgs map[string]config.VisitorConf
|
visitorCfgs map[string]config.VisitorConf
|
||||||
@ -70,6 +73,7 @@ func NewService(cfg config.ClientCommonConf, pxyCfgs map[string]config.ProxyConf
|
|||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
svr = &Service{
|
svr = &Service{
|
||||||
|
authSetter: auth.NewAuthSetter(cfg),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
cfgFile: cfgFile,
|
cfgFile: cfgFile,
|
||||||
pxyCfgs: pxyCfgs,
|
pxyCfgs: pxyCfgs,
|
||||||
@ -105,7 +109,7 @@ func (svr *Service) Run() error {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// login success
|
// login success
|
||||||
ctl := NewControl(svr.ctx, svr.runId, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort)
|
ctl := NewControl(svr.ctx, svr.runId, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
|
||||||
ctl.Run()
|
ctl.Run()
|
||||||
svr.ctlMu.Lock()
|
svr.ctlMu.Lock()
|
||||||
svr.ctl = ctl
|
svr.ctl = ctl
|
||||||
@ -159,7 +163,7 @@ func (svr *Service) keepControllerWorking() {
|
|||||||
// reconnect success, init delayTime
|
// reconnect success, init delayTime
|
||||||
delayTime = time.Second
|
delayTime = time.Second
|
||||||
|
|
||||||
ctl := NewControl(svr.ctx, svr.runId, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort)
|
ctl := NewControl(svr.ctx, svr.runId, conn, session, svr.cfg, svr.pxyCfgs, svr.visitorCfgs, svr.serverUDPPort, svr.authSetter)
|
||||||
ctl.Run()
|
ctl.Run()
|
||||||
svr.ctlMu.Lock()
|
svr.ctlMu.Lock()
|
||||||
svr.ctl = ctl
|
svr.ctl = ctl
|
||||||
@ -212,19 +216,22 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
|
|||||||
conn = stream
|
conn = stream
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().Unix()
|
|
||||||
loginMsg := &msg.Login{
|
loginMsg := &msg.Login{
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Os: runtime.GOOS,
|
Os: runtime.GOOS,
|
||||||
PoolCount: svr.cfg.PoolCount,
|
PoolCount: svr.cfg.PoolCount,
|
||||||
User: svr.cfg.User,
|
User: svr.cfg.User,
|
||||||
Version: version.Full(),
|
Version: version.Full(),
|
||||||
PrivilegeKey: util.GetAuthKey(svr.cfg.Token, now),
|
Timestamp: time.Now().Unix(),
|
||||||
Timestamp: now,
|
|
||||||
RunId: svr.runId,
|
RunId: svr.runId,
|
||||||
Metas: svr.cfg.Metas,
|
Metas: svr.cfg.Metas,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add auth
|
||||||
|
if err = svr.authSetter.SetLogin(loginMsg); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = msg.WriteMsg(conn, loginMsg); err != nil {
|
if err = msg.WriteMsg(conn, loginMsg); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
4
go.mod
4
go.mod
@ -4,6 +4,7 @@ go 1.12
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
|
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||||
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
|
github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb
|
||||||
github.com/fatedier/golib v0.0.0-20181107124048-ff8cd814b049
|
github.com/fatedier/golib v0.0.0-20181107124048-ff8cd814b049
|
||||||
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
|
github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible
|
||||||
@ -17,6 +18,7 @@ require (
|
|||||||
github.com/mattn/go-runewidth v0.0.4 // indirect
|
github.com/mattn/go-runewidth v0.0.4 // indirect
|
||||||
github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc
|
github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc
|
||||||
github.com/pkg/errors v0.8.0 // indirect
|
github.com/pkg/errors v0.8.0 // indirect
|
||||||
|
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||||
github.com/rakyll/statik v0.1.1
|
github.com/rakyll/statik v0.1.1
|
||||||
github.com/rodaine/table v1.0.0
|
github.com/rodaine/table v1.0.0
|
||||||
github.com/spf13/cobra v0.0.3
|
github.com/spf13/cobra v0.0.3
|
||||||
@ -28,6 +30,8 @@ require (
|
|||||||
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
|
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
|
||||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect
|
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect
|
||||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||||
golang.org/x/text v0.3.2 // indirect
|
golang.org/x/text v0.3.2 // indirect
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||||
|
gopkg.in/square/go-jose.v2 v2.4.1 // indirect
|
||||||
)
|
)
|
||||||
|
65
models/auth/auth.go
Normal file
65
models/auth/auth.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// 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 (
|
||||||
|
"github.com/fatedier/frp/models/config"
|
||||||
|
"github.com/fatedier/frp/models/consts"
|
||||||
|
"github.com/fatedier/frp/models/msg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Setter interface {
|
||||||
|
SetLogin(*msg.Login) error
|
||||||
|
SetPing(*msg.Ping) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthSetter(cfg config.ClientCommonConf) (authProvider Setter) {
|
||||||
|
switch cfg.AuthenticationMethod {
|
||||||
|
case consts.TokenAuthMethod:
|
||||||
|
authProvider = NewTokenAuth(cfg.Token)
|
||||||
|
case consts.OidcAuthMethod:
|
||||||
|
authProvider = NewOidcAuthSetter(
|
||||||
|
cfg.OidcClientId,
|
||||||
|
cfg.OidcClientSecret,
|
||||||
|
cfg.OidcAudience,
|
||||||
|
cfg.OidcTokenEndpointUrl,
|
||||||
|
cfg.AuthenticateHeartBeats,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Verifier interface {
|
||||||
|
VerifyLogin(*msg.Login) error
|
||||||
|
VerifyPing(*msg.Ping) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthVerifier(cfg config.ServerCommonConf) (authVerifier Verifier) {
|
||||||
|
switch cfg.AuthenticationMethod {
|
||||||
|
case consts.TokenAuthMethod:
|
||||||
|
authVerifier = NewTokenAuth(cfg.Token)
|
||||||
|
case consts.OidcAuthMethod:
|
||||||
|
authVerifier = NewOidcAuthVerifier(
|
||||||
|
cfg.OidcIssuer,
|
||||||
|
cfg.OidcAudience,
|
||||||
|
cfg.OidcSkipExpiryCheck,
|
||||||
|
cfg.OidcSkipIssuerCheck,
|
||||||
|
cfg.AuthenticateHeartBeats,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
118
models/auth/oidc.go
Normal file
118
models/auth/oidc.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
// 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 (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/models/msg"
|
||||||
|
|
||||||
|
"github.com/coreos/go-oidc"
|
||||||
|
"golang.org/x/oauth2/clientcredentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OidcAuthProvider struct {
|
||||||
|
tokenGenerator *clientcredentials.Config
|
||||||
|
authenticateHeartBeats bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOidcAuthSetter(clientId string, clientSecret string, audience string, tokenEndpointUrl string, authenticateHeartBeats bool) *OidcAuthProvider {
|
||||||
|
tokenGenerator := &clientcredentials.Config{
|
||||||
|
ClientID: clientId,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
Scopes: []string{audience},
|
||||||
|
TokenURL: tokenEndpointUrl,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OidcAuthProvider{
|
||||||
|
tokenGenerator: tokenGenerator,
|
||||||
|
authenticateHeartBeats: authenticateHeartBeats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OidcAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
|
||||||
|
tokenObj, err := auth.tokenGenerator.Token(context.Background())
|
||||||
|
if tokenObj == nil {
|
||||||
|
return fmt.Errorf("couldn't generate OIDC token for login: %s", err)
|
||||||
|
}
|
||||||
|
loginMsg.PrivilegeKey = tokenObj.AccessToken
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
||||||
|
if !auth.authenticateHeartBeats {
|
||||||
|
// if heartbeat authentication is disabled - don't set
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenObj, err := auth.tokenGenerator.Token(context.Background())
|
||||||
|
if tokenObj == nil {
|
||||||
|
return fmt.Errorf("couldn't generate OIDC token for ping: %s", err)
|
||||||
|
}
|
||||||
|
pingMsg.PrivilegeKey = tokenObj.AccessToken
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type OidcAuthConsumer struct {
|
||||||
|
verifier *oidc.IDTokenVerifier
|
||||||
|
authenticateHeartBeats bool
|
||||||
|
subjectFromLogin string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOidcAuthVerifier(issuer string, audience string, skipExpiryCheck bool, skipIssuerCheck bool, authenticateHeartBeats bool) *OidcAuthConsumer {
|
||||||
|
provider, err := oidc.NewProvider(context.Background(), issuer)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
verifierConf := oidc.Config{
|
||||||
|
ClientID: audience,
|
||||||
|
SkipClientIDCheck: audience == "",
|
||||||
|
SkipExpiryCheck: skipExpiryCheck,
|
||||||
|
SkipIssuerCheck: skipIssuerCheck,
|
||||||
|
}
|
||||||
|
return &OidcAuthConsumer{
|
||||||
|
verifier: provider.Verifier(&verifierConf),
|
||||||
|
authenticateHeartBeats: authenticateHeartBeats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
|
||||||
|
token, err := auth.verifier.Verify(context.Background(), loginMsg.PrivilegeKey)
|
||||||
|
if token != nil {
|
||||||
|
auth.subjectFromLogin = token.Subject
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) {
|
||||||
|
if !auth.authenticateHeartBeats {
|
||||||
|
// if heartbeat authentication is disabled - don't verify
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := auth.verifier.Verify(context.Background(), pingMsg.PrivilegeKey)
|
||||||
|
if token == nil {
|
||||||
|
return fmt.Errorf("invalid OIDC token in ping: %v", err)
|
||||||
|
}
|
||||||
|
if token.Subject != auth.subjectFromLogin {
|
||||||
|
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
||||||
|
"original subject: %s, "+
|
||||||
|
"new subject: %s",
|
||||||
|
auth.subjectFromLogin, token.Subject)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
58
models/auth/token.go
Normal file
58
models/auth/token.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/models/msg"
|
||||||
|
"github.com/fatedier/frp/utils/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenAuthProviderConsumer struct {
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTokenAuth(token string) *TokenAuthProviderConsumer {
|
||||||
|
return &TokenAuthProviderConsumer{
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *TokenAuthProviderConsumer) SetLogin(loginMsg *msg.Login) (err error) {
|
||||||
|
loginMsg.PrivilegeKey = util.GetAuthKey(auth.Token, loginMsg.Timestamp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *TokenAuthProviderConsumer) SetPing(*msg.Ping) error {
|
||||||
|
// ping doesn't include authentication in token method
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenAuthConsumer struct {
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *TokenAuthProviderConsumer) VerifyLogin(loginMsg *msg.Login) error {
|
||||||
|
if util.GetAuthKey(auth.Token, loginMsg.Timestamp) != loginMsg.PrivilegeKey {
|
||||||
|
return fmt.Errorf("token in login doesn't match token from configuration")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (auth *TokenAuthProviderConsumer) VerifyPing(*msg.Ping) error {
|
||||||
|
// ping doesn't include authentication in token method
|
||||||
|
return nil
|
||||||
|
}
|
@ -60,6 +60,31 @@ type ClientCommonConf struct {
|
|||||||
// to the server. The server must have a matching token for authorization
|
// to the server. The server must have a matching token for authorization
|
||||||
// to succeed. By default, this value is "".
|
// to succeed. By default, this value is "".
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
// AuthenticationMethod specifies what authentication method to use to
|
||||||
|
// authenticate frpc with frps. If "token" is specified - token will be
|
||||||
|
// read into login message. If "oidc" is specified - OIDC (Open ID Connect)
|
||||||
|
// token will be issued using OIDC settings. By default, this value is "token".
|
||||||
|
AuthenticationMethod string `json:"authentication_method"`
|
||||||
|
// AuthenticateHeartBeats specifies whether to include authentication token in
|
||||||
|
// heartbeats sent to frps. By default, this value is false.
|
||||||
|
AuthenticateHeartBeats bool `json:"authenticate_heartbeats"`
|
||||||
|
|
||||||
|
// OidcClientId specifies the client ID to use to get a token in OIDC
|
||||||
|
// authentication if AuthenticationMethod == "oidc". By default, this value
|
||||||
|
// is "".
|
||||||
|
OidcClientId string `json:"oidc_client_id"`
|
||||||
|
// OidcClientSecret specifies the client secret to use to get a token in OIDC
|
||||||
|
// authentication if AuthenticationMethod == "oidc". By default, this value
|
||||||
|
// is "".
|
||||||
|
OidcClientSecret string `json:"oidc_client_secret"`
|
||||||
|
// OidcAudience specifies the audience of the token in OIDC authentication
|
||||||
|
//if AuthenticationMethod == "oidc". By default, this value is "".
|
||||||
|
OidcAudience string `json:"oidc_audience"`
|
||||||
|
// OidcTokenEndpointUrl specifies the URL which implements OIDC Token Endpoint.
|
||||||
|
// It will be used to get an OIDC token if AuthenticationMethod == "oidc".
|
||||||
|
// By default, this value is "".
|
||||||
|
OidcTokenEndpointUrl string `json:"oidc_token_endpoint_url"`
|
||||||
|
|
||||||
// AdminAddr specifies the address that the admin server binds to. By
|
// AdminAddr specifies the address that the admin server binds to. By
|
||||||
// default, this value is "127.0.0.1".
|
// default, this value is "127.0.0.1".
|
||||||
AdminAddr string `json:"admin_addr"`
|
AdminAddr string `json:"admin_addr"`
|
||||||
@ -131,6 +156,12 @@ func GetDefaultClientConf() ClientCommonConf {
|
|||||||
LogMaxDays: 3,
|
LogMaxDays: 3,
|
||||||
DisableLogColor: false,
|
DisableLogColor: false,
|
||||||
Token: "",
|
Token: "",
|
||||||
|
AuthenticationMethod: "token",
|
||||||
|
AuthenticateHeartBeats: false,
|
||||||
|
OidcClientId: "",
|
||||||
|
OidcClientSecret: "",
|
||||||
|
OidcAudience: "",
|
||||||
|
OidcTokenEndpointUrl: "",
|
||||||
AdminAddr: "127.0.0.1",
|
AdminAddr: "127.0.0.1",
|
||||||
AdminPort: 0,
|
AdminPort: 0,
|
||||||
AdminUser: "",
|
AdminUser: "",
|
||||||
@ -207,6 +238,32 @@ func UnmarshalClientConfFromIni(content string) (cfg ClientCommonConf, err error
|
|||||||
cfg.Token = tmpStr
|
cfg.Token = tmpStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "authentication_method"); ok {
|
||||||
|
cfg.AuthenticationMethod = tmpStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "authenticate_heartbeats"); ok && tmpStr == "true" {
|
||||||
|
cfg.AuthenticateHeartBeats = true
|
||||||
|
} else {
|
||||||
|
cfg.AuthenticateHeartBeats = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "oidc_client_id"); ok {
|
||||||
|
cfg.OidcClientId = tmpStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "oidc_client_secret"); ok {
|
||||||
|
cfg.OidcClientSecret = tmpStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "oidc_audience"); ok {
|
||||||
|
cfg.OidcAudience = tmpStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "oidc_token_endpoint_url"); ok {
|
||||||
|
cfg.OidcTokenEndpointUrl = tmpStr
|
||||||
|
}
|
||||||
|
|
||||||
if tmpStr, ok = conf.Get("common", "admin_addr"); ok {
|
if tmpStr, ok = conf.Get("common", "admin_addr"); ok {
|
||||||
cfg.AdminAddr = tmpStr
|
cfg.AdminAddr = tmpStr
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,34 @@ type ServerCommonConf struct {
|
|||||||
// received from clients. Clients must have a matching token to be
|
// received from clients. Clients must have a matching token to be
|
||||||
// authorized to use the server. By default, this value is "".
|
// authorized to use the server. By default, this value is "".
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
// AuthenticationMethod specifies what authentication method to use to
|
||||||
|
// authenticate frpc with frps. If "token" is specified - token comparison
|
||||||
|
// will be used. If "oidc" is specified - OIDC (Open ID Connect) will be
|
||||||
|
// used. By default, this value is "token".
|
||||||
|
AuthenticationMethod string `json:"authentication_method"`
|
||||||
|
// AuthenticateHeartBeats specifies whether to expect and verify authentication
|
||||||
|
// token in heartbeats sent from frpc. By default, this value is false.
|
||||||
|
AuthenticateHeartBeats bool `json:"authenticate_heartbeats"`
|
||||||
|
|
||||||
|
// OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer
|
||||||
|
// will be used to load public keys to verify signature and will be compared
|
||||||
|
// with the issuer claim in the OIDC token. It will be used if
|
||||||
|
// AuthenticationMethod == "oidc". By default, this value is "".
|
||||||
|
OidcIssuer string `json:"oidc_issuer"`
|
||||||
|
// OidcAudience specifies the audience OIDC tokens should contain when validated.
|
||||||
|
// If this value is empty, audience ("client ID") verification will be skipped.
|
||||||
|
// It will be used when AuthenticationMethod == "oidc". By default, this
|
||||||
|
// value is "".
|
||||||
|
OidcAudience string `json:"oidc_audience"`
|
||||||
|
// OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is
|
||||||
|
// expired. It will be used when AuthenticationMethod == "oidc". By default, this
|
||||||
|
// value is false.
|
||||||
|
OidcSkipExpiryCheck bool `json:"oidc_skip_expiry_check"`
|
||||||
|
// OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's
|
||||||
|
// issuer claim matches the issuer specified in OidcIssuer. It will be used when
|
||||||
|
// AuthenticationMethod == "oidc". By default, this value is false.
|
||||||
|
OidcSkipIssuerCheck bool `json:"oidc_skip_issuer_check"`
|
||||||
|
|
||||||
// SubDomainHost specifies the domain that will be attached to sub-domains
|
// SubDomainHost specifies the domain that will be attached to sub-domains
|
||||||
// requested by the client when using Vhost proxying. For example, if this
|
// requested by the client when using Vhost proxying. For example, if this
|
||||||
// value is set to "frps.com" and the client requested the subdomain
|
// value is set to "frps.com" and the client requested the subdomain
|
||||||
@ -169,6 +197,12 @@ func GetDefaultServerConf() ServerCommonConf {
|
|||||||
DisableLogColor: false,
|
DisableLogColor: false,
|
||||||
DetailedErrorsToClient: true,
|
DetailedErrorsToClient: true,
|
||||||
Token: "",
|
Token: "",
|
||||||
|
AuthenticationMethod: "token",
|
||||||
|
AuthenticateHeartBeats: false,
|
||||||
|
OidcIssuer: "",
|
||||||
|
OidcAudience: "",
|
||||||
|
OidcSkipExpiryCheck: false,
|
||||||
|
OidcSkipIssuerCheck: false,
|
||||||
SubDomainHost: "",
|
SubDomainHost: "",
|
||||||
TcpMux: true,
|
TcpMux: true,
|
||||||
AllowPorts: make(map[int]struct{}),
|
AllowPorts: make(map[int]struct{}),
|
||||||
@ -330,6 +364,36 @@ func UnmarshalServerConfFromIni(content string) (cfg ServerCommonConf, err error
|
|||||||
|
|
||||||
cfg.Token, _ = conf.Get("common", "token")
|
cfg.Token, _ = conf.Get("common", "token")
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "authentication_method"); ok {
|
||||||
|
cfg.AuthenticationMethod = tmpStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "authenticate_heartbeats"); ok && tmpStr == "true" {
|
||||||
|
cfg.AuthenticateHeartBeats = true
|
||||||
|
} else {
|
||||||
|
cfg.AuthenticateHeartBeats = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "oidc_issuer"); ok {
|
||||||
|
cfg.OidcIssuer = tmpStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "oidc_audience"); ok {
|
||||||
|
cfg.OidcAudience = tmpStr
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "oidc_skip_expiry_check"); ok && tmpStr == "true" {
|
||||||
|
cfg.OidcSkipExpiryCheck = true
|
||||||
|
} else {
|
||||||
|
cfg.OidcSkipExpiryCheck = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmpStr, ok = conf.Get("common", "oidc_skip_issuer_check"); ok && tmpStr == "true" {
|
||||||
|
cfg.OidcSkipIssuerCheck = true
|
||||||
|
} else {
|
||||||
|
cfg.OidcSkipIssuerCheck = false
|
||||||
|
}
|
||||||
|
|
||||||
if allowPortsStr, ok := conf.Get("common", "allow_ports"); ok {
|
if allowPortsStr, ok := conf.Get("common", "allow_ports"); ok {
|
||||||
// e.g. 1000-2000,2001,2002,3000-4000
|
// e.g. 1000-2000,2001,2002,3000-4000
|
||||||
ports, errRet := util.ParseRangeNumbers(allowPortsStr)
|
ports, errRet := util.ParseRangeNumbers(allowPortsStr)
|
||||||
|
@ -29,4 +29,8 @@ var (
|
|||||||
HttpsProxy string = "https"
|
HttpsProxy string = "https"
|
||||||
StcpProxy string = "stcp"
|
StcpProxy string = "stcp"
|
||||||
XtcpProxy string = "xtcp"
|
XtcpProxy string = "xtcp"
|
||||||
|
|
||||||
|
// authentication method
|
||||||
|
TokenAuthMethod string = "token"
|
||||||
|
OidcAuthMethod string = "oidc"
|
||||||
)
|
)
|
||||||
|
@ -148,6 +148,7 @@ type NewVisitorConnResp struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Ping struct {
|
type Ping struct {
|
||||||
|
PrivilegeKey string `json:"privilege_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Pong struct {
|
type Pong struct {
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/models/auth"
|
||||||
"github.com/fatedier/frp/models/config"
|
"github.com/fatedier/frp/models/config"
|
||||||
"github.com/fatedier/frp/models/consts"
|
"github.com/fatedier/frp/models/consts"
|
||||||
frpErr "github.com/fatedier/frp/models/errors"
|
frpErr "github.com/fatedier/frp/models/errors"
|
||||||
@ -94,6 +95,9 @@ type Control struct {
|
|||||||
// stats collector to store stats info of clients and proxies
|
// stats collector to store stats info of clients and proxies
|
||||||
statsCollector stats.Collector
|
statsCollector stats.Collector
|
||||||
|
|
||||||
|
// verifies authentication based on selected method
|
||||||
|
authVerifier auth.Verifier
|
||||||
|
|
||||||
// login message
|
// login message
|
||||||
loginMsg *msg.Login
|
loginMsg *msg.Login
|
||||||
|
|
||||||
@ -149,6 +153,7 @@ func NewControl(
|
|||||||
pxyManager *proxy.ProxyManager,
|
pxyManager *proxy.ProxyManager,
|
||||||
pluginManager *plugin.Manager,
|
pluginManager *plugin.Manager,
|
||||||
statsCollector stats.Collector,
|
statsCollector stats.Collector,
|
||||||
|
authVerifier auth.Verifier,
|
||||||
ctlConn net.Conn,
|
ctlConn net.Conn,
|
||||||
loginMsg *msg.Login,
|
loginMsg *msg.Login,
|
||||||
serverCfg config.ServerCommonConf,
|
serverCfg config.ServerCommonConf,
|
||||||
@ -163,6 +168,7 @@ func NewControl(
|
|||||||
pxyManager: pxyManager,
|
pxyManager: pxyManager,
|
||||||
pluginManager: pluginManager,
|
pluginManager: pluginManager,
|
||||||
statsCollector: statsCollector,
|
statsCollector: statsCollector,
|
||||||
|
authVerifier: authVerifier,
|
||||||
conn: ctlConn,
|
conn: ctlConn,
|
||||||
loginMsg: loginMsg,
|
loginMsg: loginMsg,
|
||||||
sendCh: make(chan msg.Message, 10),
|
sendCh: make(chan msg.Message, 10),
|
||||||
@ -454,6 +460,9 @@ func (ctl *Control) manager() {
|
|||||||
ctl.CloseProxy(m)
|
ctl.CloseProxy(m)
|
||||||
xl.Info("close proxy [%s] success", m.ProxyName)
|
xl.Info("close proxy [%s] success", m.ProxyName)
|
||||||
case *msg.Ping:
|
case *msg.Ping:
|
||||||
|
if err := ctl.authVerifier.VerifyPing(m); err != nil {
|
||||||
|
xl.Warn("received invalid ping: %v", err)
|
||||||
|
}
|
||||||
ctl.lastPing = time.Now()
|
ctl.lastPing = time.Now()
|
||||||
xl.Debug("receive heartbeat")
|
xl.Debug("receive heartbeat")
|
||||||
ctl.sendCh <- &msg.Pong{}
|
ctl.sendCh <- &msg.Pong{}
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/frp/assets"
|
"github.com/fatedier/frp/assets"
|
||||||
|
"github.com/fatedier/frp/models/auth"
|
||||||
"github.com/fatedier/frp/models/config"
|
"github.com/fatedier/frp/models/config"
|
||||||
"github.com/fatedier/frp/models/msg"
|
"github.com/fatedier/frp/models/msg"
|
||||||
"github.com/fatedier/frp/models/nathole"
|
"github.com/fatedier/frp/models/nathole"
|
||||||
@ -86,6 +87,9 @@ type Service struct {
|
|||||||
// All resource managers and controllers
|
// All resource managers and controllers
|
||||||
rc *controller.ResourceController
|
rc *controller.ResourceController
|
||||||
|
|
||||||
|
// Verifies authentication based on selected method
|
||||||
|
authVerifier auth.Verifier
|
||||||
|
|
||||||
// stats collector to store server and proxies stats info
|
// stats collector to store server and proxies stats info
|
||||||
statsCollector stats.Collector
|
statsCollector stats.Collector
|
||||||
|
|
||||||
@ -105,6 +109,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) {
|
|||||||
UdpPortManager: ports.NewPortManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts),
|
UdpPortManager: ports.NewPortManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts),
|
||||||
},
|
},
|
||||||
httpVhostRouter: vhost.NewVhostRouters(),
|
httpVhostRouter: vhost.NewVhostRouters(),
|
||||||
|
authVerifier: auth.NewAuthVerifier(cfg),
|
||||||
tlsConfig: generateTLSConfig(),
|
tlsConfig: generateTLSConfig(),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
@ -399,12 +404,11 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check auth.
|
// Check auth.
|
||||||
if util.GetAuthKey(svr.cfg.Token, loginMsg.Timestamp) != loginMsg.PrivilegeKey {
|
if err = svr.authVerifier.VerifyLogin(loginMsg); err != nil {
|
||||||
err = fmt.Errorf("authorization failed")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.statsCollector, ctlConn, loginMsg, svr.cfg)
|
ctl := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, svr.statsCollector, svr.authVerifier, ctlConn, loginMsg, svr.cfg)
|
||||||
|
|
||||||
if oldCtl := svr.ctlManager.Add(loginMsg.RunId, ctl); oldCtl != nil {
|
if oldCtl := svr.ctlManager.Add(loginMsg.RunId, ctl); oldCtl != nil {
|
||||||
oldCtl.allShutdown.WaitDone()
|
oldCtl.allShutdown.WaitDone()
|
||||||
|
Loading…
Reference in New Issue
Block a user