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 }