Skip to content
Merged
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
4 changes: 2 additions & 2 deletions lib/auth/auth_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1727,7 +1727,7 @@ func TestSSOPasswordBypass(t *testing.T) {
// proxyClient.AuthenticateSSHUser to something else (eg,
// proxyClient.AuthenticateWebUser).
// Optional.
authenticateOverride func(context.Context, authclient.AuthenticateSSHRequest) (*authclient.SSHLoginResponse, error)
authenticateOverride func(context.Context, authclient.AuthenticateSSHRequest) (*authclient.CLILoginResponse, error)
}{
{
name: "OTP",
Expand All @@ -1749,7 +1749,7 @@ func TestSSOPasswordBypass(t *testing.T) {
{
name: "AuthenticateWeb",
setSecondFactor: solveWebauthn,
authenticateOverride: func(ctx context.Context, req authclient.AuthenticateSSHRequest) (*authclient.SSHLoginResponse, error) {
authenticateOverride: func(ctx context.Context, req authclient.AuthenticateSSHRequest) (*authclient.CLILoginResponse, error) {
// We only care about the error here, it's OK to swallow the session.
_, err := proxyClient.AuthenticateWebUser(ctx, req.AuthenticateUserRequest)
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -974,7 +974,7 @@ func TestAuthenticateUser_mfaDeviceLocked(t *testing.T) {
proxyClient, err := testServer.NewClient(authtest.TestBuiltin(types.RoleProxy))
require.NoError(t, err, "NewClient")

authenticateSSH := func(dev *authtest.Device) (*authclient.SSHLoginResponse, error) {
authenticateSSH := func(dev *authtest.Device) (*authclient.CLILoginResponse, error) {
chal, err := proxyClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_UserCredentials{
UserCredentials: &proto.UserCredentials{
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ func (a *ServerWithRoles) AuthenticateWebUser(ctx context.Context, req authclien

// AuthenticateSSHUser authenticates SSH console user, creates and returns a pair of signed TLS and SSH
// short lived certificates as a result
func (a *ServerWithRoles) AuthenticateSSHUser(ctx context.Context, req authclient.AuthenticateSSHRequest) (*authclient.SSHLoginResponse, error) {
func (a *ServerWithRoles) AuthenticateSSHUser(ctx context.Context, req authclient.AuthenticateSSHRequest) (*authclient.CLILoginResponse, error) {
// authentication request has its own authentication, however this limits the requests
// types to proxies to make it harder to break
if !a.hasBuiltinRole(types.RoleProxy) {
Expand Down
13 changes: 10 additions & 3 deletions lib/auth/authclient/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -1515,9 +1515,9 @@ func (a *AuthenticateSSHRequest) CheckAndSetDefaults() error {
return nil
}

// SSHLoginResponse is a response returned by web proxy, it preserves backwards compatibility
// CLILoginResponse is a response returned by web proxy, it preserves backwards compatibility
// on the wire, which is the primary reason for non-matching json tags
type SSHLoginResponse struct {
type CLILoginResponse struct {
// User contains a logged-in user information
Username string `json:"username"`
// Cert is a PEM encoded signed certificate
Expand All @@ -1533,8 +1533,15 @@ type SSHLoginResponse struct {
// ClientOptions contains some options that the cluster wants the client to
// use.
ClientOptions ClientOptions `json:"client_options"`
// 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.go).
BrowserMFAWebauthnResponse *wantypes.CredentialAssertionResponse `json:"browser_mfa_webauthn_response,omitempty"`
Comment thread
danielashare marked this conversation as resolved.
}

// TODO(danielashare): Remove this alias once e no longer references it
type SSHLoginResponse = CLILoginResponse

// ClientOptions contains options passed from the control plane to the client at
// login time.
type ClientOptions struct {
Expand Down Expand Up @@ -1694,7 +1701,7 @@ type ClientI interface {
AuthenticateWebUser(ctx context.Context, req AuthenticateUserRequest) (types.WebSession, error)
// AuthenticateSSHUser authenticates SSH console user, creates and returns a pair of signed TLS and SSH
// short-lived certificates as a result
AuthenticateSSHUser(ctx context.Context, req AuthenticateSSHRequest) (*SSHLoginResponse, error)
AuthenticateSSHUser(ctx context.Context, req AuthenticateSSHRequest) (*CLILoginResponse, error)

// Ping gets basic info about the auth server.
Ping(ctx context.Context) (proto.PingResponse, error)
Expand Down
4 changes: 2 additions & 2 deletions lib/auth/authclient/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ func (c *HTTPClient) AuthenticateWebUser(ctx context.Context, req AuthenticateUs

// AuthenticateSSHUser authenticates SSH console user, creates and returns a pair of signed TLS and SSH
// short lived certificates as a result
func (c *HTTPClient) AuthenticateSSHUser(ctx context.Context, req AuthenticateSSHRequest) (*SSHLoginResponse, error) {
func (c *HTTPClient) AuthenticateSSHUser(ctx context.Context, req AuthenticateSSHRequest) (*CLILoginResponse, error) {
out, err := c.PostJSON(
ctx,
c.Endpoint("users", url.PathEscape(req.Username), "ssh", "authenticate"),
Expand All @@ -507,7 +507,7 @@ func (c *HTTPClient) AuthenticateSSHUser(ctx context.Context, req AuthenticateSS
if err != nil {
return nil, trace.Wrap(err)
}
var re SSHLoginResponse
var re CLILoginResponse
if err := json.Unmarshal(out.Bytes(), &re); err != nil {
return nil, trace.Wrap(err)
}
Expand Down
64 changes: 64 additions & 0 deletions lib/auth/browser_mfa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Teleport
// Copyright (C) 2026 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package auth

import (
"context"
"net/url"

"github.com/gravitational/trace"

webauthnpb "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/lib/auth/internal/browsermfa"
wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes"
"github.com/gravitational/teleport/lib/authz"
)

// CompleteBrowserMFAChallenge completes an MFA challenge response by returning the redirect URL with encrypted response.
func (a *Server) CompleteBrowserMFAChallenge(ctx context.Context, requestID string, webauthnResponse *webauthnpb.CredentialAssertionResponse) (string, error) {
const notFoundErrMsg = "mfa session data not found"
// Retrieve the MFA session
mfaSession, err := a.GetSSOMFASession(ctx, requestID)
if trace.IsNotFound(err) {
return "", trace.AccessDenied("%s", notFoundErrMsg)
} else if err != nil {
return "", trace.Wrap(err)
}

user, err := authz.UserFromContext(ctx)
if err != nil {
return "", trace.Wrap(err)
}

if mfaSession.Username != user.GetIdentity().Username {
return "", trace.AccessDenied("%s", notFoundErrMsg)
}

// Valid WebAuthn response, encrypt and return it
u, err := url.Parse(mfaSession.ClientRedirectURL)
if err != nil {
return "", trace.Wrap(err)
}

wr := wantypes.CredentialAssertionResponseFromProto(webauthnResponse)
clientRedirectURL, err := browsermfa.EncryptBrowserMFAResponse(u, wr)
if err != nil {
return "", trace.Wrap(err)
}

return clientRedirectURL, nil
}
Loading
Loading