mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-27 14:59:09 +01:00
[client] Force interactive login when extending the auth session
A session extend must be answered from the account the peer is registered under. With a silent PKCE flow (DisablePromptLogin or max_age=0) the IdP answers from whatever session it already holds, which need not be the peer's account when several are signed in; the token then fails the user match in ExtendAuthSession with no way to pick another account. Mark the PKCE flow request as a session extend so the management server can force prompt=login for it, overriding the configured silent flow.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/client/common"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
func TestApplySessionExtendFlowPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flow *proto.PKCEAuthorizationFlow
|
||||
sessionExtend bool
|
||||
disablePromptLogin bool
|
||||
loginFlag uint32
|
||||
}{
|
||||
{
|
||||
name: "extend forces prompt=login over a silent flow",
|
||||
flow: &proto.PKCEAuthorizationFlow{
|
||||
ProviderConfig: &proto.ProviderConfig{
|
||||
DisablePromptLogin: true,
|
||||
LoginFlag: uint32(common.LoginFlagMaxAge0),
|
||||
},
|
||||
},
|
||||
sessionExtend: true,
|
||||
disablePromptLogin: false,
|
||||
loginFlag: uint32(common.LoginFlagPromptLogin),
|
||||
},
|
||||
{
|
||||
name: "extend replaces max_age=0 so login_hint is honoured",
|
||||
flow: &proto.PKCEAuthorizationFlow{
|
||||
ProviderConfig: &proto.ProviderConfig{
|
||||
DisablePromptLogin: false,
|
||||
LoginFlag: uint32(common.LoginFlagMaxAge0),
|
||||
},
|
||||
},
|
||||
sessionExtend: true,
|
||||
disablePromptLogin: false,
|
||||
loginFlag: uint32(common.LoginFlagPromptLogin),
|
||||
},
|
||||
{
|
||||
name: "login keeps the configured flow untouched",
|
||||
flow: &proto.PKCEAuthorizationFlow{
|
||||
ProviderConfig: &proto.ProviderConfig{
|
||||
DisablePromptLogin: true,
|
||||
LoginFlag: uint32(common.LoginFlagMaxAge0),
|
||||
},
|
||||
},
|
||||
sessionExtend: false,
|
||||
disablePromptLogin: true,
|
||||
loginFlag: uint32(common.LoginFlagMaxAge0),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
applySessionExtendFlowPolicy(tc.flow, tc.sessionExtend)
|
||||
cfg := tc.flow.GetProviderConfig()
|
||||
assert.Equal(t, tc.disablePromptLogin, cfg.GetDisablePromptLogin())
|
||||
assert.Equal(t, tc.loginFlag, cfg.GetLoginFlag())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A provider config is not guaranteed to be present on the response; clearing
|
||||
// the flag must not panic when the validator returned an empty flow.
|
||||
func TestApplySessionExtendFlowPolicyWithoutProviderConfig(t *testing.T) {
|
||||
assert.NotPanics(t, func() {
|
||||
applySessionExtendFlowPolicy(&proto.PKCEAuthorizationFlow{}, true)
|
||||
applySessionExtendFlowPolicy(nil, true)
|
||||
})
|
||||
}
|
||||
@@ -1180,7 +1180,8 @@ func (s *Server) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.Encryp
|
||||
return nil, status.Errorf(codes.Internal, "failed to get server key")
|
||||
}
|
||||
|
||||
err = encryption.DecryptMessage(peerKey, key, req.Body, &proto.PKCEAuthorizationFlowRequest{})
|
||||
flowReq := &proto.PKCEAuthorizationFlowRequest{}
|
||||
err = encryption.DecryptMessage(peerKey, key, req.Body, flowReq)
|
||||
if err != nil {
|
||||
errMSG := fmt.Sprintf("error while decrypting peer's message with Wireguard public key %s.", req.WgPubKey)
|
||||
log.WithContext(ctx).Warn(errMSG)
|
||||
@@ -1224,6 +1225,7 @@ func (s *Server) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.Encryp
|
||||
}
|
||||
|
||||
flowInfoResp := s.integratedPeerValidator.ValidateFlowResponse(ctx, peerKey.String(), initInfoFlow)
|
||||
applySessionExtendFlowPolicy(flowInfoResp, flowReq.GetSessionExtend())
|
||||
|
||||
encryptedResp, err := encryption.EncryptMessage(peerKey, key, flowInfoResp)
|
||||
if err != nil {
|
||||
@@ -1236,6 +1238,32 @@ func (s *Server) GetPKCEAuthorizationFlow(ctx context.Context, req *proto.Encryp
|
||||
}, nil
|
||||
}
|
||||
|
||||
// applySessionExtendFlowPolicy forces a prompt=login flow for a session extend.
|
||||
//
|
||||
// An extend renews the session of one specific peer, so its token has to come
|
||||
// from the account that peer is registered under. A flow that does not prompt
|
||||
// leaves the choice to the IdP, which answers a silent authorization from any
|
||||
// session it already holds — not necessarily this peer's account when several
|
||||
// are signed in, and login_hint is a suggestion the IdP may ignore. The token
|
||||
// then fails the jwt.UserID == peer.UserID check in ExtendAuthSession, and the
|
||||
// user is given no opportunity to pick a different account.
|
||||
//
|
||||
// LoginFlagPromptLogin rather than max_age=0: both re-authenticate, but with
|
||||
// prompt=login the IdP honours login_hint and offers the peer's own account,
|
||||
// whereas max_age=0 leaves the user to find it among every account signed in.
|
||||
//
|
||||
// Called after ValidateFlowResponse so that a per-peer override cannot reinstate
|
||||
// the silent flow for an extend.
|
||||
func applySessionExtendFlowPolicy(flow *proto.PKCEAuthorizationFlow, sessionExtend bool) {
|
||||
if !sessionExtend {
|
||||
return
|
||||
}
|
||||
if cfg := flow.GetProviderConfig(); cfg != nil {
|
||||
cfg.DisablePromptLogin = false
|
||||
cfg.LoginFlag = uint32(common.LoginFlagPromptLogin)
|
||||
}
|
||||
}
|
||||
|
||||
// SyncMeta endpoint is used to synchronize peer's system metadata and notifies the connected,
|
||||
// peer's under the same account of any updates.
|
||||
func (s *Server) SyncMeta(ctx context.Context, req *proto.EncryptedMessage) (*proto.Empty, error) {
|
||||
|
||||
Reference in New Issue
Block a user