Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 94 additions & 39 deletions api/gen/proto/go/teleport/mfa/v1/service.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions api/proto/teleport/mfa/v1/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ message VerifyValidatedMFAChallengeRequest {
SessionIdentifyingPayload payload = 2;
// Name of the cluster where the validated challenge originated.
string source_cluster = 3;
// Name of the user for whom the challenge was issued. The client calling VerifyValidatedMFAChallenge MUST
// independently determine this value from session state. The server will verify it matches the user associated with
// the validated challenge to ensure the challenge is tied to the correct user.
string user = 4;
}

// VerifyValidatedMFAChallengeResponse is the response message for VerifyValidatedMFAChallenge.
Expand Down
23 changes: 18 additions & 5 deletions lib/auth/mfa/mfav1/mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,31 @@ func (m *mockAuthServerIdentity) GetMFADevices(
}

type mockMFAService struct {
// If ReturnError is set, methods will return this error.
ReturnError error
createValidatedMFAChallengeError error

chal *mfav1.ValidatedMFAChallenge
}

func (m *mockMFAService) CreateValidatedMFAChallenge(
ctx context.Context,
username string,
chal *mfav1.ValidatedMFAChallenge,
) (*mfav1.ValidatedMFAChallenge, error) {
if m.ReturnError != nil {
return nil, m.ReturnError
if m.createValidatedMFAChallengeError != nil {
return nil, m.createValidatedMFAChallengeError
}

return m.chal, nil
}

func (m *mockMFAService) GetValidatedMFAChallenge(
ctx context.Context,
username string,
challengeName string,
) (*mfav1.ValidatedMFAChallenge, error) {
if m.createValidatedMFAChallengeError != nil {
return nil, m.createValidatedMFAChallengeError
}

return chal, nil
return m.chal, nil
}
75 changes: 75 additions & 0 deletions lib/auth/mfa/mfav1/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package mfav1

import (
"bytes"
"cmp"
"context"
"log/slog"
Expand Down Expand Up @@ -80,11 +81,18 @@ type Identity interface {
}

// MFAService defines the interface for managing MFA resources.
// See lib/auth.MFAService.
type MFAService interface {
CreateValidatedMFAChallenge(ctx context.Context,
username string,
chal *mfav1.ValidatedMFAChallenge,
) (*mfav1.ValidatedMFAChallenge, error)

GetValidatedMFAChallenge(
ctx context.Context,
username string,
challengeName string,
) (*mfav1.ValidatedMFAChallenge, error)
}

// ServiceConfig holds creation parameters for [Service].
Expand Down Expand Up @@ -377,6 +385,48 @@ func (s *Service) ValidateSessionChallenge(
return &mfav1.ValidateSessionChallengeResponse{}, nil
}

func (s *Service) VerifyValidatedMFAChallenge(
ctx context.Context,
req *mfav1.VerifyValidatedMFAChallengeRequest,
) (*mfav1.VerifyValidatedMFAChallengeResponse, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if !authz.HasBuiltinRole(*authCtx, types.RoleNode.String()) {
return nil, trace.AccessDenied("only SSH nodes can verify validated MFA challenges")
}

if err := validateVerifyValidatedMFAChallengeRequest(req); err != nil {
return nil, trace.Wrap(err)
}

chal, err := s.storage.GetValidatedMFAChallenge(ctx, req.GetUser(), req.GetName())
if err != nil {
return nil, trace.Wrap(err)
}

// Ensure the SIP that was initially used to create the challenge matches the SIP provided in the request.
switch payload := chal.GetSpec().GetPayload().GetPayload().(type) {
case *mfav1.SessionIdentifyingPayload_SshSessionId:
if !bytes.Equal(req.GetPayload().GetSshSessionId(), payload.SshSessionId) {
return nil, trace.AccessDenied("request payload does not match validated challenge payload")
}

default:
return nil, trace.BadParameter("unknown ValidatedMFAChallenge payload type %T retrieved from storage", payload)
}

// Ensure the source cluster that was initially used to create the challenge matches the source cluster provided in
// the request.
if req.GetSourceCluster() != chal.GetSpec().GetSourceCluster() {
return nil, trace.AccessDenied("request source cluster does not match validated challenge source cluster")
}

return &mfav1.VerifyValidatedMFAChallengeResponse{}, nil
}

func validateCreateSessionChallengeRequest(req *mfav1.CreateSessionChallengeRequest) error {
payload := req.GetPayload()
if payload == nil {
Expand Down Expand Up @@ -605,3 +655,28 @@ func mfaPreferences(pref types.AuthPreference) (*types.U2F, *types.Webauthn, err

return u2f, webauthn, nil
}

func validateVerifyValidatedMFAChallengeRequest(req *mfav1.VerifyValidatedMFAChallengeRequest) error {
if req == nil {
return trace.BadParameter("param VerifyValidatedMFAChallengeRequest is nil")
}

if req.GetUser() == "" {
return trace.BadParameter("missing VerifyValidatedMFAChallengeRequest user")
}

if req.GetName() == "" {
return trace.BadParameter("missing VerifyValidatedMFAChallengeRequest name")
}

payload := req.GetPayload()
if payload == nil {
return trace.BadParameter("missing VerifyValidatedMFAChallengeRequest payload")
}

if len(payload.GetSshSessionId()) == 0 {
return trace.BadParameter("empty SshSessionId in payload")
}

return nil
}
Loading
Loading