[Browser MFA] Add ValidateBrowserMFAChallenge gRPC#63873
[Browser MFA] Add ValidateBrowserMFAChallenge gRPC#63873danielashare wants to merge 19 commits intodanielashare/browser-mfa-webauthn-validationfrom
Conversation
d516d0c to
9075cce
Compare
codingllama
left a comment
There was a problem hiding this comment.
Reviewed a good chunk of it, but not all.
As stated in the proto PR, I think the RPC could be in the specialized MFAService instead.
This could also, potentially, be split into a few distinct smaller PRs: lib/webauthn changes, browser MFA encrypt/decrypt, and the RPC itself. Once we add tests in various layers it'll likely grow.
| // use. | ||
| ClientOptions ClientOptions `json:"client_options"` | ||
| // BrowserMFAWebauthnResponse is a webauthn response for the browser MFA flow | ||
| BrowserMFAWebauthnResponse *wantypes.CredentialAssertionResponse `json:"browser_mfa_webauthn_response,omitempty"` |
There was a problem hiding this comment.
I'm confused, why is this field in SSHLoginResponse?
There was a problem hiding this comment.
The redirect handler in tsh expects a SSHLoginResponse containing an MFAToken for SSO MFA. I had a look at returning a plain string from encryptBrowserMFAResponse and modifying tsh's [callback](https://github.com/gravitational/teleport/blob/master/lib/client/sso/redirector.go#L422) handler to guess if the decrypted response was a SSHLoginResponse or a WebAuthn JSON, but it wasn't very confidence inspiring. Or it would require adding a new set of callback handlers or starting up two callback servers, which would be quite a large change.
I think it would be simpler to implement and maintain if we kept BrowserMFAWebauthnResponse as part of SSHLoginResponse, WDYT?
There was a problem hiding this comment.
No wonder I'm confused, it's quite the indirect relationship.
I think a bit more context in the godoc is useful. Eg:
// BrowserMFAWebauthnResponse is a webauthn response for the browser MFA flow.
// Exists in SSHLoginResponse as this is the payload used by the SSO redirector
// (lib/client/sso.Redirector).
I would also consider a similar comment, or a type alias, for encryptBrowserMFAResponse:
// Payload required by lib/client/sso.Redirector.
type ssoRedirectorResponse = authclient.SSHLoginResponse
func encryptBrowserMFAResponse(redirectURL *url.URL, webauthnResponse *wantypes.CredentialAssertionResponse) (string, error) {
consoleResponse := ssoRedirectorResponse{
BrowserMFAWebauthnResponse: webauthnResponse,
}
// (...)
}The type alias could live inside lib/client/sso, but I'm not sure it's best touching that now (or in this PR).
There was a problem hiding this comment.
Added those for clarity
lib/auth/grpcserver.go
Outdated
| return protoResp, nil | ||
| } | ||
|
|
||
| func (g *GRPCServer) ValidateBrowserMFAChallenge(ctx context.Context, req *authpb.ValidateBrowserMFAChallengeRequest) (*authpb.ValidateBrowserMFAChallengeResponse, error) { |
There was a problem hiding this comment.
Let's test this as well, as it should touch all layers.
There was a problem hiding this comment.
Moved to MFA Service and added tests
There was a problem hiding this comment.
I don't see the move to MFAService, did you push?
There was a problem hiding this comment.
Not yet, sorry, still separating in to different PRs
lib/auth/grpcserver.go
Outdated
| return protoResp, nil | ||
| } | ||
|
|
||
| func (g *GRPCServer) ValidateBrowserMFAChallenge(ctx context.Context, req *authpb.ValidateBrowserMFAChallengeRequest) (*authpb.ValidateBrowserMFAChallengeResponse, error) { |
lib/auth/auth_with_roles.go
Outdated
|
|
||
| // ValidateBrowserMFAChallenge validates an MFA challenge response and returns the redirect URL with encrypted response. | ||
| func (a *ServerWithRoles) ValidateBrowserMFAChallenge(ctx context.Context, requestID string, webauthnResponse *wantypes.CredentialAssertionResponse) (string, error) { | ||
| if !authz.HasBuiltinRole(a.context, string(types.RoleProxy)) { |
There was a problem hiding this comment.
Let's include authz in the gRPC tests.
lib/auth/grpcserver.go
Outdated
| req.BrowserMfaResponse.RequestId, | ||
| wantypes.CredentialAssertionResponseFromProto(req.BrowserMfaResponse.WebauthnResponse), |
There was a problem hiding this comment.
I'd be tempted to pass the request proto down all the way instead of unwrapping it here. If we change the request this will break.
| // Unlike Finish, this function: | ||
| // - Doesn't update the device counter | ||
| // - Doesn't persist any changes to the device | ||
| // - Doesn't delete the session data |
There was a problem hiding this comment.
(1) I wonder if we should let this all happen, but retain the data for exactly one following Finish call. (Maybe with caveats for reuse?)
Maybe:
a. Finish(retainData=true) - (update counter, update device, retain session data)
b. Finish() - (noop, update device, delete session data)
(2) If you take the suggestion from (1), let's first refactor Finish to it takes a struct as input. It'll be too many parameters and we'll have to touch all callers anyway.
Eg: Finish(context.Context, FinishParams) (*LoginData, error)
Let's do that in a separate PR, with only the refactor and nothing else.
(3) I was checking the RFD and found this:
After successful validation, the WebAuthn response is encrypted with the secret key from the client redirect URL and returned to be sent to tsh. The SSOMFASession is deleted to prevent reuse attacks.
So it looks like we are deviating by retaining the session data?
There was a problem hiding this comment.
I think it would be best to keep the two functions separate from a DX point of view. I think it's clearer what each function does.
Hmm, yeah, that part of the RFD needs updating because session data is retained if Reuse=true.
There was a problem hiding this comment.
I keep a separate open branch where I collect RFD changes/corrections, so I can mail it the end. Maybe do something like that so you don't lose track of it?
9075cce to
4ffb570
Compare
4ffb570 to
f52aa5e
Compare
2d299c0 to
5b450e8
Compare
|
Apologies @codingllama, your comments have lost their context since I split out the webauthn changes |
No worries, give me a ping when this is good for another pass. |
9f7bc17 to
dd9ef28
Compare
This PR adds the ValidateBrowserMFAChallenge gRPC endpoint. The RFD for this addition can be found here. Needs to be merged after #63978.
These changes address this part of the flow:
sequenceDiagram proxy->>auth: rpc ValidateBrowserMFAChallenge auth->>auth: ValidateMFAResponse() auth->>auth: Encrypt WebAuthn response<br/>with secret_key auth-->>proxy: Return http://127.0.0.1:port/callback?response={encrypted_webauthn} proxy-->>browser: HTTP 200 with redirect URLI would like to draw particular attention to
login.go. In order to validate the WebAuthn response from the browser without consuming it, I had to split the validation logic out oflogin.go'sfinishfunction in to its own function. I want to avoid consuming it because in the full Browser MFA flow, the WebAuthn response is returned totshwhere it will exchange the response for certificates etc. I wonder if it is worth validating the MFA response from the browser before sending it back totshwhere it will then send the MFA response back to the server where it will have to be validated again anyway?Manual tests: