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
22 changes: 14 additions & 8 deletions lib/web/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ func (h *Handler) addMFADeviceHandle(w http.ResponseWriter, r *http.Request, par
}

type createAuthenticateChallengeRequest struct {
IsMFARequired *isMFARequiredRequest `json:"is_mfa_required"`
IsMFARequiredRequest *isMFARequiredRequest `json:"is_mfa_required_req"`
ChallengeScope int `json:"challenge_scope"`
ChallengeAllowReuse bool `json:"challenge_allow_reuse"`
}

// createAuthenticateChallengeHandle creates and returns MFA authentication challenges for the user in context (logged in user).
Expand All @@ -148,23 +150,27 @@ func (h *Handler) createAuthenticateChallengeHandle(w http.ResponseWriter, r *ht
return nil, trace.Wrap(err)
}

var isMFARequiredProtoReq *proto.IsMFARequiredRequest
if req.IsMFARequired != nil {
isMFARequiredProtoReq, err = req.IsMFARequired.checkAndGetProtoRequest()
var mfaRequiredCheckProto *proto.IsMFARequiredRequest
if req.IsMFARequiredRequest != nil {
mfaRequiredCheckProto, err = req.IsMFARequiredRequest.checkAndGetProtoRequest()
if err != nil {
return nil, trace.Wrap(err)
}
}

allowReuse := mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO
if req.ChallengeAllowReuse {
allowReuse = mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES
}

chal, err := clt.CreateAuthenticateChallenge(r.Context(), &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
MFARequiredCheck: isMFARequiredProtoReq,
MFARequiredCheck: mfaRequiredCheckProto,
ChallengeExtensions: &mfav1.ChallengeExtensions{
// TODO(Joerger): Web client needs to provide scope and allow reuse
Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_UNSPECIFIED,
AllowReuse: mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_UNSPECIFIED,
Scope: mfav1.ChallengeScope(req.ChallengeScope),
AllowReuse: allowReuse,
},
})
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions web/packages/teleport/src/Account/AccountNew.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { FeatureBox } from 'teleport/components/Layout';
import ReAuthenticate from 'teleport/components/ReAuthenticate';
import { RemoveDialog } from 'teleport/components/MfaDeviceList';

import { MFAChallengeScope } from 'teleport/services/auth/auth';

import { AuthDeviceList } from './ManageDevices/AuthDeviceList/AuthDeviceList';
import useManageDevices, {
State as ManageDevicesState,
Expand Down Expand Up @@ -214,6 +216,7 @@ export function Account({
onAuthenticated={setToken}
onClose={hideReAuthenticate}
actionText="registering a new device"
challengeScope={MFAChallengeScope.USER_SESSION}
/>
)}
{isAddDeviceVisible && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import MfaDeviceList, { RemoveDialog } from 'teleport/components/MfaDeviceList';

import ReAuthenticate from 'teleport/components/ReAuthenticate';

import { MFAChallengeScope } from 'teleport/services/auth/auth';

import AddDevice from './AddDevice';
import useManageDevices, { State } from './useManageDevices';

Expand Down Expand Up @@ -104,6 +106,7 @@ export function ManageDevices({
onAuthenticated={setToken}
onClose={hideReAuthenticate}
actionText="registering a new device"
challengeScope={MFAChallengeScope.USER_SESSION}
/>
)}
{isAddDeviceVisible && (
Expand Down
6 changes: 4 additions & 2 deletions web/packages/teleport/src/Console/DocumentSsh/useGetScpUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useCallback } from 'react';
import useAttempt from 'shared/hooks/useAttemptNext';

import cfg, { UrlScpParams } from 'teleport/config';
import auth from 'teleport/services/auth/auth';
import auth, { MFAChallengeScope } from 'teleport/services/auth/auth';

export default function useGetScpUrl(addMfaToScpUrls: boolean) {
const { setAttempt, attempt, handleError } = useAttempt('');
Expand All @@ -35,7 +35,9 @@ export default function useGetScpUrl(addMfaToScpUrls: boolean) {
return cfg.getScpUrl(params);
}
try {
let webauthn = await auth.getWebauthnResponse();
let webauthn = await auth.getWebauthnResponse(
MFAChallengeScope.USER_SESSION
);
setAttempt({
status: 'success',
statusText: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import {
import { sortNodeLogins } from 'teleport/services/nodes';
import { ApiError } from 'teleport/services/api/parseError';

import { MFAChallengeScope } from 'teleport/services/auth/auth';

import { NodeMeta } from '../../useDiscover';

import type { Option } from 'shared/components/Select';
Expand Down Expand Up @@ -172,6 +174,7 @@ export function TestConnection(props: AgentStepProps) {
})
}
onClose={cancelMfaDialog}
challengeScope={MFAChallengeScope.USER_SESSION}
/>
)}
<Header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import TextSelectCopy from 'teleport/components/TextSelectCopy';
import { generateTshLoginCommand } from 'teleport/lib/util';
import ReAuthenticate from 'teleport/components/ReAuthenticate';

import { MFAChallengeScope } from 'teleport/services/auth/auth';

import {
ActionButtons,
HeaderSubtitle,
Expand Down Expand Up @@ -89,6 +91,7 @@ export function TestConnectionView({
<ReAuthenticate
onMfaResponse={res => testConnection(makeTestConnRequest(), res)}
onClose={cancelMfaDialog}
challengeScope={MFAChallengeScope.USER_SESSION}
/>
)}
<Header>Test Connection</Header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import TextSelectCopy from 'teleport/components/TextSelectCopy';
import { generateTshLoginCommand } from 'teleport/lib/util';
import ReAuthenticate from 'teleport/components/ReAuthenticate';

import { MFAChallengeScope } from 'teleport/services/auth/auth';

import {
ActionButtons,
HeaderSubtitle,
Expand Down Expand Up @@ -101,6 +103,7 @@ export function TestConnection({
<ReAuthenticate
onMfaResponse={res => testConnection(makeTestConnRequest(), res)}
onClose={cancelMfaDialog}
challengeScope={MFAChallengeScope.USER_SESSION}
/>
)}
<Header>Test Connection</Header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
} from 'teleport/Discover/Shared';
import { sortNodeLogins } from 'teleport/services/nodes';

import { MFAChallengeScope } from 'teleport/services/auth/auth';

import { NodeMeta } from '../../useDiscover';

import type { Option } from 'shared/components/Select';
Expand Down Expand Up @@ -87,6 +89,7 @@ export function TestConnection(props: AgentStepProps) {
<ReAuthenticate
onMfaResponse={res => testConnection(selectedOpt.value, res)}
onClose={cancelMfaDialog}
challengeScope={MFAChallengeScope.USER_SESSION}
/>
)}
<Header>Test Connection</Header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import React from 'react';

import { MFAChallengeScope } from 'teleport/services/auth/auth';

import { State } from './useReAuthenticate';
import { ReAuthenticate } from './ReAuthenticate';

Expand Down Expand Up @@ -47,4 +49,5 @@ const props: State = {
onClose: () => null,
auth2faType: 'on',
actionText: 'performing this action',
challengeScope: MFAChallengeScope.UNSPECIFIED,
};
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function ReAuthenticate({
auth2faType,
preferredMfaType,
actionText,
challengeScope,
}: State) {
const [otpToken, setOtpToken] = useState('');
const mfaOptions = createMfaOptions({
Expand All @@ -60,7 +61,7 @@ export function ReAuthenticate({
e.preventDefault();

if (mfaOption?.value === 'webauthn') {
submitWithWebauthn();
submitWithWebauthn(challengeScope);
}
if (mfaOption?.value === 'otp') {
submitWithTotp(otpToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import useAttempt from 'shared/hooks/useAttemptNext';

import cfg from 'teleport/config';
import auth from 'teleport/services/auth';
import { MFAChallengeScope } from 'teleport/services/auth/auth';

import type { MfaAuthnResponse } from 'teleport/services/mfa';

Expand All @@ -31,7 +32,7 @@ import type { MfaAuthnResponse } from 'teleport/services/mfa';
// token, and after successfully obtaining the token, the function
// `onAuthenticated` will be called with this token.
export default function useReAuthenticate(props: Props) {
const { onClose, actionText = defaultActionText } = props;
const { onClose, actionText = defaultActionText, challengeScope } = props;

// Note that attempt state "success" is not used or required.
// After the user submits, the control is passed back
Expand All @@ -52,12 +53,12 @@ export default function useReAuthenticate(props: Props) {
.catch(handleError);
}

function submitWithWebauthn() {
function submitWithWebauthn(scope: MFAChallengeScope) {
setAttempt({ status: 'processing' });

if ('onMfaResponse' in props) {
auth
.getWebauthnResponse()
.getWebauthnResponse(scope)
.then(webauthnResponse =>
props.onMfaResponse({ webauthn_response: webauthnResponse })
)
Expand All @@ -66,7 +67,7 @@ export default function useReAuthenticate(props: Props) {
}

auth
.createPrivilegeTokenWithWebauthn()
.createPrivilegeTokenWithWebauthn(scope)
.then(props.onAuthenticated)
.catch((err: Error) => {
// This catches a webauthn frontend error that occurs on Firefox and replaces it with a more helpful error message.
Expand Down Expand Up @@ -96,6 +97,7 @@ export default function useReAuthenticate(props: Props) {
auth2faType: cfg.getAuth2faType(),
preferredMfaType: cfg.getPreferredMfaType(),
actionText,
challengeScope,
onClose,
};
}
Expand All @@ -114,6 +116,10 @@ type BaseProps = {
*
* */
actionText?: string;
/**
* The MFA challenge scope of the action to perform, as defined in webauthn.proto.
*/
challengeScope: MFAChallengeScope;
};

// MfaResponseProps defines a function
Expand Down
6 changes: 4 additions & 2 deletions web/packages/teleport/src/services/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/

import 'whatwg-fetch';
import auth from 'teleport/services/auth/auth';
import auth, { MFAChallengeScope } from 'teleport/services/auth/auth';

import { storageService } from '../storageService';
import { WebauthnAssertionResponse } from '../auth';
Expand Down Expand Up @@ -117,7 +117,9 @@ const api = {

let webauthnResponseForRetry;
try {
webauthnResponseForRetry = await auth.getWebauthnResponse();
webauthnResponseForRetry = await auth.getWebauthnResponse(
MFAChallengeScope.ADMIN_ACTION
);
} catch (err) {
throw new Error(
'Failed to fetch webauthn credentials, please connect a registered hardware key and try again. If you do not have a hardware key registered, you can add one from your account settings page.'
Expand Down
42 changes: 29 additions & 13 deletions web/packages/teleport/src/services/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,7 @@ const auth = {

headlessSSOAccept(transactionId: string) {
return auth
.checkWebauthnSupport()
.then(() => api.post(cfg.api.mfaAuthnChallengePath))
.then(res =>
navigator.credentials.get({
publicKey: makeMfaAuthenticateChallenge(res).webauthnPublicKey,
})
)
.fetchWebauthnChallenge(MFAChallengeScope.HEADLESS_LOGIN)
.then(res => {
const request = {
action: 'accept',
Expand All @@ -269,7 +263,11 @@ const auth = {
return api.post(cfg.api.createPrivilegeTokenPath, { secondFactorToken });
},

async fetchWebauthnChallenge(isMFARequiredRequest?: IsMfaRequiredRequest) {
async fetchWebauthnChallenge(
scope: MFAChallengeScope,
allowReuse?: boolean,
isMFARequiredRequest?: IsMfaRequiredRequest
) {
// TODO(Joerger): DELETE IN 16.0.0
// the create mfa challenge endpoint below supports
// MFARequired requests without the extra roundtrip.
Expand All @@ -292,7 +290,9 @@ const auth = {
.then(() =>
api
.post(cfg.api.mfaAuthnChallengePath, {
is_mfa_required: isMFARequiredRequest,
is_mfa_required_req: isMFARequiredRequest,
challenge_scope: scope,
challenge_allow_reuse: allowReuse,
})
.then(makeMfaAuthenticateChallenge)
)
Expand All @@ -303,8 +303,8 @@ const auth = {
);
},

createPrivilegeTokenWithWebauthn() {
return auth.fetchWebauthnChallenge().then(res =>
createPrivilegeTokenWithWebauthn(scope: MFAChallengeScope) {
return auth.fetchWebauthnChallenge(scope).then(res =>
api.post(cfg.api.createPrivilegeTokenPath, {
webauthnAssertionResponse: makeWebauthnAssertionResponse(res),
})
Expand All @@ -315,9 +315,13 @@ const auth = {
return api.post(cfg.api.createPrivilegeTokenPath, {});
},

getWebauthnResponse(isMFARequiredRequest?: IsMfaRequiredRequest) {
getWebauthnResponse(
scope: MFAChallengeScope,
allowReuse?: boolean,
isMFARequiredRequest?: IsMfaRequiredRequest
) {
return auth
.fetchWebauthnChallenge(isMFARequiredRequest)
.fetchWebauthnChallenge(scope, allowReuse, isMFARequiredRequest)
.then(res => makeWebauthnAssertionResponse(res));
},
};
Expand Down Expand Up @@ -394,3 +398,15 @@ export type IsMFARequiredAdminAction = {
name: string;
};
};

// MFAChallengeScope is an mfa challenge scope. Possible values are defined in mfa.proto
export enum MFAChallengeScope {
UNSPECIFIED = 0,
LOGIN = 1,
PASSWORDLESS_LOGIN = 2,
HEADLESS_LOGIN = 3,
MANAGE_DEVICES = 4,
ACCOUNT_RECOVERY = 5,
USER_SESSION = 6,
ADMIN_ACTION = 7,
}