Skip to content

[Browser MFA] Add Browser MFA to challenge request flow#63936

Merged
danielashare merged 1 commit into
masterfrom
danielashare/browser-mfa-challenge-request
Mar 26, 2026
Merged

[Browser MFA] Add Browser MFA to challenge request flow#63936
danielashare merged 1 commit into
masterfrom
danielashare/browser-mfa-challenge-request

Conversation

@danielashare
Copy link
Copy Markdown
Contributor

This PR adds Browser MFA fields to the /webapi/login/mfa/begin flow. The RFD for this addition can be found here. Needs to be merged after #63831.

These changes address this part of the flow from the above RFD:

sequenceDiagram
    participant tsh
    participant proxy as Proxy
    participant auth as Auth

    tsh->>proxy: POST /webapi/mfa/login/begin<br/>w/ redirect /callback?secret_key=xxx
    proxy->>auth: Forward login request
    auth->>auth: Generate request_id<br/>Upsert SSOMFASession
    auth-->>proxy: MFA Challenge
    proxy-->>tsh: Return MFA Challenge
Loading

Manual tests:

  • Test new tsh client receives Browser MFA challenge
  • Confirm Teleport Web UI login page is unchanged
  • Test regular user/pass with MFA (YubiKey, OTP, TouchID) still works
  • Test passwordless still works (TouchID and YubiKey)

@danielashare danielashare self-assigned this Feb 18, 2026
@danielashare danielashare added no-changelog Indicates that a PR does not require a changelog entry backport/branch/v18 labels Feb 18, 2026
@github-actions github-actions Bot requested review from kimlisa and okraport February 18, 2026 16:49
Comment thread lib/auth/auth.go Outdated
Comment thread lib/auth/sso_mfa.go Outdated
@danielashare danielashare marked this pull request as draft February 20, 2026 14:15
@danielashare danielashare force-pushed the danielashare/browser-mfa-challenge-request branch from f9d5208 to 9bd9b9e Compare February 23, 2026 15:52
@danielashare danielashare changed the base branch from danielashare/browser-mfa-proto to danielashare/browser-mfa-proto-2 February 23, 2026 15:54
@danielashare danielashare force-pushed the danielashare/browser-mfa-challenge-request branch 3 times, most recently from e8d3f57 to 66deea0 Compare February 23, 2026 17:40
@danielashare danielashare marked this pull request as ready for review February 23, 2026 17:42
@danielashare danielashare force-pushed the danielashare/browser-mfa-proto-2 branch from 68a5f0c to 7ac607e Compare March 2, 2026 11:08
@danielashare danielashare force-pushed the danielashare/browser-mfa-challenge-request branch from 3e63a80 to 09a1356 Compare March 3, 2026 16:06
BrowserMFATSHRedirectURL string
ProxyAddress string
Ext *mfav1.ChallengeExtensions
SIP *mfav1.SessionIdentifyingPayload
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify if I understand the purpose correctly: does this field have any purpose outside of per-session MFA?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this field is only for in-band per-session MFA

Comment thread lib/auth/auth.go Outdated
// If the user has a WebAuthn device and no SSO MFA configured, return a Browser
// MFA challenge. This challenge is useful in cases where a user only has a
// browser-associated WebAuthn device, but is trying to MFA via a CLI tool (tsh, tctl etc.)
if groupedDevs.Browser != nil && browserMFATSHRedirectURL != "" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this feature unconditionally turned on? I'm asking because the previous conditional has enableSSO as a part of the condition.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't unconditionally enabled, I checked if it was enabled inside this if statement because I didn't want to query the auth preferences if the MFA device or redirect URL wasn't set. However, I realise that authPrefs are checked further up and I can just reuse that response, so I've modified this to match the other MFA methods

Comment thread lib/auth/browser_mfa.go Outdated
return nil, trace.Wrap(err, InvalidClientRedirectErrorMessage)
}

proxyAddr := params.ProxyAddress
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So ProxyAddress is expected to be filled by the proxy service, and it's not a user-provided value, right? Can you explain in which case this value will be empty and will need to be filled here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SSO MFA challenge request flow, which this is heavily inspired by, passes the proxy address and a redirect URL. Browser MFA just has the client send the redirect URL. So, unless some large changes are made to SSO MFA, the proxy address is always going to be present because it is passed as a param to the mfaAuthChallenge function that calls either BeginSSOMFAChallenge or BeginBrowserMFAChallenge. Just in case changes are made, I have a fallback here to get the proxy address. I'll add a comment to explain that.

Comment thread lib/services/sso_mfa.go Outdated
// TargetCluster is the optional cluster where the authentication is targeted.
TargetCluster string `json:"target_cluster,omitempty"`
// TshRedirectURL is the redirect URL used to return a WebAuthn response back to tsh
TshRedirectURL string `json:"tsh_redirect_url,omitempty"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be TSHRedirectURL to conform to the naming conventions. Also: I've seen this field written to, but I don't think I've seen it being actually used anywhere. Will it be used in a future PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed that. Yeah, there are other PRs that will read this value

Comment thread lib/web/apiserver.go Outdated
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
}

if req.BrowserMFATSHRedirectURL != "" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue that this conditional is unnecessary, as the URL will be initialized to an empty string anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes it look cleaner 👍

}

// BrowserMFAChallenge contains browser challenge details.
message BrowserMFAChallenge {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this message be used in a future PR? I don't see it used here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, that commit snuck in to this branch, I've removed it now

Comment thread api/proto/teleport/legacy/types/mfa_device.proto Outdated
Comment thread lib/auth/browser_mfa.go Outdated
Comment thread lib/auth/browser_mfa_test.go Outdated
Comment thread lib/auth/sso_mfa.go Outdated
Comment thread lib/services/sso_mfa.go Outdated
Comment thread lib/services/sso_mfa.go Outdated
Base automatically changed from danielashare/browser-mfa-proto-2 to master March 6, 2026 07:23
@danielashare danielashare force-pushed the danielashare/browser-mfa-challenge-request branch from acc34c1 to 655a559 Compare March 6, 2026 07:29
Comment thread lib/auth/auth.go Outdated
Comment thread lib/auth/browser_mfa.go Outdated
Comment thread lib/auth/browser_mfa.go Outdated
Comment thread lib/auth/sso_mfa.go Outdated
Comment thread e
@danielashare danielashare force-pushed the danielashare/browser-mfa-challenge-request branch from 6e6e08b to 80601ac Compare March 20, 2026 16:02
Copy link
Copy Markdown
Contributor

@Joerger Joerger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just a small test request

Comment thread lib/auth/browser_mfa_test.go Outdated
},
},
{
name: "NOK SSO MFA user should not get Browser MFA",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add another test case for an SSO MFA user w/ a webauthn device getting both challenges.

Comment thread api/constants/constants.go Outdated
Comment thread lib/auth/auth.go Outdated
@@ -4459,7 +4444,7 @@ func (a *Server) CreateAuthenticateChallenge(ctx context.Context, req *proto.Cre
}
}

challenges, err := a.mfaAuthChallenge(ctx, username, req.SSOClientRedirectURL, req.ProxyAddress, challengeExtensions)
challenges, err := a.mfaAuthChallenge(ctx, username, req.SSOClientRedirectURL, req.BrowserMFATSHRedirectURL, req.ProxyAddress, challengeExtensions)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's exactly what I was thinking.

I'm not sure if we'll end up liking the change or not. I guess it depends how big of a change it is and how clear the final code is, but give it a shot and see if you like it better with a single redirect URL.

Comment thread lib/auth/browser_mfa_test.go Outdated
Comment thread lib/auth/browser_mfa_test.go Outdated
Webauthn: &types.Webauthn{
RPID: "localhost",
},
AllowBrowserAuthentication: types.NewBoolOption(true),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the new option that enables browser MFA?

If so, I think the name should mention "MFA" - we don't want people to think this is some sort of setting for disabling regular web UI login.

Copy link
Copy Markdown
Contributor Author

@danielashare danielashare Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is, and I agree we should change the name. However, the change to types.proto has already been merged to master with this name. Is there any way we can change this field?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed and in review here

Comment thread lib/auth/browser_mfa_test.go Outdated
Comment thread lib/auth/auth.go
Comment thread lib/auth/browser_mfa.go Outdated
@danielashare danielashare force-pushed the danielashare/browser-mfa-challenge-request branch from 6784a31 to 6143668 Compare March 26, 2026 10:57
@danielashare danielashare force-pushed the danielashare/browser-mfa-challenge-request branch from 6143668 to c039f40 Compare March 26, 2026 11:21
@danielashare danielashare added this pull request to the merge queue Mar 26, 2026
Merged via the queue into master with commit a9fbf8b Mar 26, 2026
46 checks passed
@danielashare danielashare deleted the danielashare/browser-mfa-challenge-request branch March 26, 2026 12:45
@backport-bot-workflows
Copy link
Copy Markdown
Contributor

@danielashare See the table below for backport results.

Branch Result
branch/v18 Failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport/branch/v18 no-changelog Indicates that a PR does not require a changelog entry size/lg size/md

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants