Skip to content

Commit

Permalink
Create DeviceWebToken during Web logins (#38704)
Browse files Browse the repository at this point in the history
* Add Get and SetDeviceWebToken methods to WebSessionV2

* Create DeviceWebToken during Web logins

* Add error wrapping

Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com>

---------

Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com>
  • Loading branch information
codingllama and rosstimothy authored Feb 28, 2024
1 parent 3d8d39f commit a1fcfbd
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 3 deletions.
19 changes: 19 additions & 0 deletions api/types/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ type WebSession interface {
SetSAMLSession(*SAMLSessionData)
// GetSAMLSession gets the SAML session data. Is considered secret.
GetSAMLSession() *SAMLSessionData
// SetDeviceWebToken sets the session's DeviceWebToken.
// The token is considered a secret.
SetDeviceWebToken(*DeviceWebToken)
// GetDeviceWebToken returns the session's DeviceWebToken, if any.
// The token is considered a secret.
GetDeviceWebToken() *DeviceWebToken
}

// NewWebSession returns new instance of the web session based on the V2 spec
Expand Down Expand Up @@ -191,6 +197,7 @@ func (ws *WebSessionV2) WithoutSecrets() WebSession {
cp := proto.Clone(ws).(*WebSessionV2)
cp.Spec.Priv = nil
cp.Spec.SAMLSession = nil
cp.Spec.DeviceWebToken = nil
return cp
}

Expand All @@ -214,6 +221,18 @@ func (ws *WebSessionV2) GetSAMLSession() *SAMLSessionData {
return ws.Spec.SAMLSession
}

// SetDeviceWebToken sets the session's DeviceWebToken.
// The token is considered a secret.
func (ws *WebSessionV2) SetDeviceWebToken(webToken *DeviceWebToken) {
ws.Spec.DeviceWebToken = webToken
}

// GetDeviceWebToken returns the session's DeviceWebToken, if any.
// The token is considered a secret.
func (ws *WebSessionV2) GetDeviceWebToken() *DeviceWebToken {
return ws.Spec.DeviceWebToken
}

// setStaticFields sets static resource header and metadata fields.
func (ws *WebSessionV2) setStaticFields() {
ws.Version = V2
Expand Down
31 changes: 31 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import (
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/gen/proto/go/assist/v1"
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/metadata"
Expand Down Expand Up @@ -716,6 +717,15 @@ var (
// successfully authenticated. An example would be creating objects based on the user.
type LoginHook func(context.Context, types.User) error

// CreateDeviceWebTokenFunc creates a new DeviceWebToken for the logged in user.
//
// Used during a successful Web login, after the user was verified and the
// WebSession created.
//
// May return `nil, nil` if device trust isn't supported (OSS), disabled, or if
// the user has no suitable trusted device.
type CreateDeviceWebTokenFunc func(context.Context, *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error)

// Server keeps the cluster together. It acts as a certificate authority (CA) for
// a cluster and:
// - generates the keypair for the node it's running on
Expand Down Expand Up @@ -868,6 +878,10 @@ type Server struct {

// ulsGenerator is the user login state generator.
ulsGenerator *userloginstate.Generator

// createDeviceWebTokenFunc is the CreateDeviceWebToken implementation.
// Is nil on OSS clusters.
createDeviceWebTokenFunc CreateDeviceWebTokenFunc
}

// SetSAMLService registers svc as the SAMLService that provides the SAML
Expand Down Expand Up @@ -991,6 +1005,23 @@ func (a *Server) SetHeadlessAuthenticationWatcher(headlessAuthenticationWatcher
a.headlessAuthenticationWatcher = headlessAuthenticationWatcher
}

func (a *Server) SetCreateDeviceWebTokenFunc(f CreateDeviceWebTokenFunc) {
a.lock.Lock()
a.createDeviceWebTokenFunc = f
a.lock.Unlock()
}

// createDeviceWebToken safely calls the underlying [CreateDeviceWebTokenFunc].
func (a *Server) createDeviceWebToken(ctx context.Context, webToken *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error) {
a.lock.RLock()
defer a.lock.RUnlock()
if a.createDeviceWebTokenFunc == nil {
return nil, nil
}
token, err := a.createDeviceWebTokenFunc(ctx, webToken)
return token, trace.Wrap(err)
}

// syncUpgradeWindowStartHour attempts to load the cloud UpgradeWindowStartHour value and set
// the ClusterMaintenanceConfig resource's AgentUpgrade.UTCStartHour field to match it.
func (a *Server) syncUpgradeWindowStartHour(ctx context.Context) error {
Expand Down
131 changes: 131 additions & 0 deletions lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/types/header"
Expand Down Expand Up @@ -254,6 +255,136 @@ func TestSessions(t *testing.T) {
require.True(t, trace.IsNotFound(err), "%#v", err)
}

func TestAuthenticateWebUser_deviceWebToken(t *testing.T) {
t.Parallel()
s := newAuthSuite(t)

authServer := s.a

const user = "llama"
const pass = "supersecretpassword!!1!"

// Prepare user and password.
// 2nd factors are not important for this test.
_, _, err := CreateUserAndRole(authServer, user, []string{user}, nil /* allowRules */)
require.NoError(t, err, "CreateUserAndRole failed")
require.NoError(t,
authServer.UpsertPassword(user, []byte(pass)),
"UpsertPassword failed")

const userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
const remoteIP = "40.89.244.232"
const remoteAddr = remoteIP + ":4242"

makeTokenSuccess := func(t *testing.T) CreateDeviceWebTokenFunc {
return func(ctx context.Context, dwt *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error) {
if !assert.NotNil(t, dwt, "dwt parameter is nil") {
return nil, errors.New("dtw parameter is nil")
}
assert.NotEmpty(t, dwt.WebSessionId, "dwt.WebSessionId is empty")
assert.Equal(t, userAgent, dwt.BrowserUserAgent, "dwt.BrowserUserAgent mismatch")
assert.Equal(t, remoteIP, dwt.BrowserIp, "dwt.BrowserIp mismatch")
assert.Equal(t, user, dwt.User, "dwt.User mismatch")

return &devicepb.DeviceWebToken{
Id: "this is an opaque ID",
Token: "this is an opaque token",
}, nil
}
}

makeTokenError := func(t *testing.T) CreateDeviceWebTokenFunc {
return func(ctx context.Context, dwt *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error) {
return nil, errors.New("something bad happened")
}
}

ctx := context.Background()
validReq := &AuthenticateUserRequest{
Username: user,
Pass: &PassCreds{
Password: []byte(pass),
},
ClientMetadata: &ForwardedClientMetadata{
UserAgent: userAgent,
RemoteAddr: remoteAddr,
},
}

tests := []struct {
name string
makeTokenFunc func(t *testing.T) CreateDeviceWebTokenFunc
req *AuthenticateUserRequest
wantErr string
wantToken bool
}{
{
name: "success",
makeTokenFunc: makeTokenSuccess,
req: validReq,
wantToken: true,
},
{
name: "CreateDeviceWebToken fails",
makeTokenFunc: makeTokenError,
req: validReq,
},
{
name: "empty ClientMetadata.UserAgent",
makeTokenFunc: makeTokenSuccess,
req: func() *AuthenticateUserRequest {
req := *validReq
req.ClientMetadata = &ForwardedClientMetadata{
RemoteAddr: remoteAddr, // AuthenticateWebUser fails if RemoteAddr is missing.
}
return &req
}(),
},
{
name: "nil ClientMetadata",
makeTokenFunc: makeTokenSuccess,
req: func() *AuthenticateUserRequest {
req := *validReq
req.ClientMetadata = nil
return &req
}(),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var gotSessionID string // captured session ID from DeviceWebToken
tokenFn := test.makeTokenFunc(t)
captureSessionFn := func(ctx context.Context, dwt *devicepb.DeviceWebToken) (*devicepb.DeviceWebToken, error) {
gotSessionID = dwt.GetWebSessionId()
return tokenFn(ctx, dwt)
}

// Set a fake SetCreateDeviceWebTokenFunc.
// This is set during server creation for Enterprise servers.
authServer.SetCreateDeviceWebTokenFunc(captureSessionFn)

webSession, err := authServer.AuthenticateWebUser(ctx, *test.req)
// AuthenticateWebUser is never expected to fail in this test.
// Either a DeviceWebToken exists in the response, or it doesn't, but the
// method itself always works.
require.NoError(t, err, "AuthenticateWebUser failed unexpectedly")

// Verify the token itself.
deviceToken := webSession.GetDeviceWebToken()
if !test.wantToken {
assert.Nil(t, deviceToken, "WebSession.GetDeviceWebToken is not nil")
return
}
require.NotNil(t, deviceToken, "WebSession.GetDeviceWebToken is nil")
assert.NotEmpty(t, deviceToken.Id, "DeviceWebToken.Id is empty")
assert.NotEmpty(t, deviceToken.Token, "DeviceWebToken.Token is empty")

// Verify the WebSessionId sent to CreateDeviceWebTokenFunc.
assert.Equal(t, webSession.GetName(), gotSessionID, "Captured DeviceWebToken.WebSessionId mismatch")
})
}
}

func TestAuthenticateSSHUser(t *testing.T) {
t.Parallel()
s := newAuthSuite(t)
Expand Down
29 changes: 26 additions & 3 deletions lib/auth/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
devicepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/devicetrust/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
Expand Down Expand Up @@ -644,12 +645,13 @@ func (a *Server) AuthenticateWebUser(ctx context.Context, req AuthenticateUserRe
return nil, trace.Wrap(err)
}

loginIP := ""
if req.ClientMetadata != nil {
loginIP, _, err = net.SplitHostPort(req.ClientMetadata.RemoteAddr)
var loginIP, userAgent string
if cm := req.ClientMetadata; cm != nil {
loginIP, _, err = net.SplitHostPort(cm.RemoteAddr)
if err != nil {
return nil, trace.Wrap(err)
}
userAgent = cm.UserAgent
}

sess, err := a.CreateWebSessionFromReq(ctx, types.NewWebSessionRequest{
Expand All @@ -664,6 +666,27 @@ func (a *Server) AuthenticateWebUser(ctx context.Context, req AuthenticateUserRe
return nil, trace.Wrap(err)
}

// Create the device trust DeviceWebToken.
// We only get a token if the server is enabled for Device Trust and the user
// has a suitable trusted device.
if loginIP != "" && userAgent != "" {
webToken, err := a.createDeviceWebToken(ctx, &devicepb.DeviceWebToken{
WebSessionId: sess.GetName(),
BrowserUserAgent: userAgent,
BrowserIp: loginIP,
User: sess.GetUser(),
})
switch {
case err != nil:
log.WithError(err).Warn("Failed to create DeviceWebToken for user")
case webToken != nil: // May be nil even if err==nil.
sess.SetDeviceWebToken(&types.DeviceWebToken{
Id: webToken.Id,
Token: webToken.Token,
})
}
}

return sess, nil
}

Expand Down

0 comments on commit a1fcfbd

Please sign in to comment.