github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/mfa/mfa.go (about)

     1  /*
     2  Copyright 2023 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package mfa
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/base64"
    23  
    24  	"github.com/gogo/protobuf/jsonpb"
    25  	"github.com/gravitational/trace"
    26  	"google.golang.org/grpc"
    27  	"google.golang.org/grpc/credentials"
    28  	"google.golang.org/grpc/metadata"
    29  
    30  	"github.com/gravitational/teleport/api/client/proto"
    31  )
    32  
    33  // ResponseMetadataKey is the context metadata key for an MFA response in a gRPC request.
    34  const ResponseMetadataKey = "mfa_challenge_response"
    35  
    36  var (
    37  	// ErrAdminActionMFARequired is an error indicating that an admin-level
    38  	// API request failed due to missing MFA verification.
    39  	ErrAdminActionMFARequired = trace.AccessDeniedError{Message: "admin-level API request requires MFA verification"}
    40  
    41  	// ErrMFANotRequired is returned by MFA ceremonies when it is discovered or
    42  	// inferred that an MFA ceremony is not required by the server.
    43  	ErrMFANotRequired = trace.BadParameterError{Message: "re-authentication with MFA is not required"}
    44  
    45  	// ErrMFANotSupported is returned by MFA ceremonies when the client does not
    46  	// support MFA ceremonies, or the server does not support MFA ceremonies for
    47  	// the client user.
    48  	ErrMFANotSupported = trace.BadParameterError{Message: "re-authentication with MFA is not supported for this client"}
    49  )
    50  
    51  // WithCredentials can be called on a GRPC client request to attach
    52  // MFA credentials to the GRPC metadata for requests that require MFA,
    53  // like admin-level requests.
    54  func WithCredentials(resp *proto.MFAAuthenticateResponse) grpc.CallOption {
    55  	return grpc.PerRPCCredentials(&perRPCCredentials{MFAChallengeResponse: resp})
    56  }
    57  
    58  // CredentialsFromContext can be called from a GRPC server method to return
    59  // MFA credentials added to the GRPC metadata for requests that require MFA,
    60  // like admin-level requests. If no MFA credentials are found, an
    61  // ErrAdminActionMFARequired will be returned, aggregated with any other errors
    62  // encountered.
    63  func CredentialsFromContext(ctx context.Context) (*proto.MFAAuthenticateResponse, error) {
    64  	resp, err := getMFACredentialsFromContext(ctx)
    65  	if err != nil {
    66  		return nil, trace.Wrap(err)
    67  	}
    68  	return resp, nil
    69  }
    70  
    71  func getMFACredentialsFromContext(ctx context.Context) (*proto.MFAAuthenticateResponse, error) {
    72  	values := metadata.ValueFromIncomingContext(ctx, ResponseMetadataKey)
    73  	if len(values) == 0 {
    74  		return nil, trace.NotFound("request metadata missing MFA credentials")
    75  	}
    76  	mfaChallengeResponseEnc := values[0]
    77  
    78  	mfaChallengeResponseJSON, err := base64.StdEncoding.DecodeString(mfaChallengeResponseEnc)
    79  	if err != nil {
    80  		return nil, trace.Wrap(err)
    81  	}
    82  
    83  	var mfaChallengeResponse proto.MFAAuthenticateResponse
    84  	if err := jsonpb.Unmarshal(bytes.NewReader(mfaChallengeResponseJSON), &mfaChallengeResponse); err != nil {
    85  		return nil, trace.Wrap(err)
    86  	}
    87  
    88  	return &mfaChallengeResponse, nil
    89  }
    90  
    91  // perRPCCredentials supplies perRPCCredentials from an MFA challenge response.
    92  type perRPCCredentials struct {
    93  	MFAChallengeResponse *proto.MFAAuthenticateResponse
    94  }
    95  
    96  // GetRequestMetadata gets the request metadata as a map from a TokenSource.
    97  func (mc *perRPCCredentials) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
    98  	ri, _ := credentials.RequestInfoFromContext(ctx)
    99  	if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
   100  		return nil, trace.BadParameter("unable to transfer MFA PerRPCCredentials: %v", err)
   101  	}
   102  
   103  	enc, err := EncodeMFAChallengeResponseCredentials(mc.MFAChallengeResponse)
   104  	if err != nil {
   105  		return nil, trace.Wrap(err)
   106  	}
   107  
   108  	return map[string]string{
   109  		ResponseMetadataKey: enc,
   110  	}, nil
   111  }
   112  
   113  // RequireTransportSecurity indicates whether the credentials requires transport security.
   114  func (mc *perRPCCredentials) RequireTransportSecurity() bool {
   115  	return true
   116  }
   117  
   118  // EncodeMFAChallengeResponseCredentials encodes the given MFA challenge response into a string.
   119  func EncodeMFAChallengeResponseCredentials(mfaResp *proto.MFAAuthenticateResponse) (string, error) {
   120  	challengeJSON, err := (&jsonpb.Marshaler{}).MarshalToString(mfaResp)
   121  	if err != nil {
   122  		return "", trace.Wrap(err)
   123  	}
   124  
   125  	return base64.StdEncoding.EncodeToString([]byte(challengeJSON)), nil
   126  }
   127  
   128  type mfaResponseContextKey struct{}
   129  
   130  // ContextWithMFAResponse embeds the MFA response in the context.
   131  func ContextWithMFAResponse(ctx context.Context, mfaResp *proto.MFAAuthenticateResponse) context.Context {
   132  	return context.WithValue(ctx, mfaResponseContextKey{}, mfaResp)
   133  }
   134  
   135  // MFAResponseFromContext returns the MFA response from the context.
   136  func MFAResponseFromContext(ctx context.Context) (*proto.MFAAuthenticateResponse, error) {
   137  	if val := ctx.Value(mfaResponseContextKey{}); val != nil {
   138  		mfaResp, ok := val.(*proto.MFAAuthenticateResponse)
   139  		if !ok {
   140  			return nil, trace.BadParameter("unexpected context value type %T", val)
   141  		}
   142  		if mfaResp == nil {
   143  			return nil, trace.NotFound("mfa response not found in the context")
   144  		}
   145  		return mfaResp, nil
   146  	}
   147  	return nil, trace.NotFound("mfa response not found in the context")
   148  }