1
1
mirror of https://github.com/pterodactyl/panel.git synced 2024-11-23 09:32:29 +01:00

Fix 2FA handling; closes #1962

This commit is contained in:
Dane Everitt 2020-04-25 13:01:16 -07:00
parent 2cf1c7f712
commit 9eb31a16d9
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
3 changed files with 105 additions and 103 deletions

View File

@ -66,7 +66,7 @@ class LoginCheckpointController extends AbstractLoginController
* provided a valid username and password.
*
* @param \Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest $request
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\JsonResponse|void
*
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
@ -75,18 +75,19 @@ class LoginCheckpointController extends AbstractLoginController
*/
public function __invoke(LoginCheckpointRequest $request): JsonResponse
{
$token = $request->input('confirmation_token');
try {
$user = $this->repository->find(
$this->cache->pull($request->input('confirmation_token'), 0)
);
$user = $this->repository->find($this->cache->get($token, 0));
} catch (RecordNotFoundException $exception) {
return $this->sendFailedLoginResponse($request);
}
$decrypted = $this->encrypter->decrypt($user->totp_secret);
$window = $this->config->get('pterodactyl.auth.2fa.window');
if ($this->google2FA->verifyKey($decrypted, $request->input('authentication_code'), $window)) {
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
$this->cache->delete($token);
return $this->sendLoginResponse($user, $request);
}

View File

@ -70,7 +70,7 @@ class LoginController extends AbstractLoginController
* Handle a login request to the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\JsonResponse|void
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Illuminate\Validation\ValidationException

View File

@ -1,115 +1,116 @@
import React, { useState } from 'react';
import React from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import loginCheckpoint from '@/api/auth/loginCheckpoint';
import { httpErrorToHuman } from '@/api/http';
import LoginFormContainer from '@/components/auth/LoginFormContainer';
import { Actions, useStoreActions } from 'easy-peasy';
import { ActionCreator } from 'easy-peasy';
import { StaticContext } from 'react-router';
import FlashMessageRender from '@/components/FlashMessageRender';
import { ApplicationStore } from '@/state';
import Spinner from '@/components/elements/Spinner';
import styled from 'styled-components';
import { breakpoint } from 'styled-components-breakpoint';
import { useFormikContext, withFormik } from 'formik';
import { object, string } from 'yup';
import useFlash from '@/plugins/useFlash';
import { FlashStore } from '@/state/flashes';
import Field from '@/components/elements/Field';
const Container = styled.div`
${breakpoint('sm')`
${tw`w-4/5 mx-auto`}
`};
interface Values {
code: string;
}
${breakpoint('md')`
${tw`p-10`}
`};
type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }>
${breakpoint('lg')`
${tw`w-3/5`}
`};
type Props = OwnProps & {
addError: ActionCreator<FlashStore['addError']['payload']>;
clearFlashes: ActionCreator<FlashStore['clearFlashes']['payload']>;
}
${breakpoint('xl')`
${tw`w-full`}
max-width: 660px;
`};
`;
const LoginCheckpointContainer = () => {
const { isSubmitting } = useFormikContext<Values>();
export default ({ history, location: { state } }: RouteComponentProps<{}, StaticContext, { token?: string }>) => {
const [ code, setCode ] = useState('');
const [ isLoading, setIsLoading ] = useState(false);
return (
<LoginFormContainer
title={'Device Checkpoint'}
className={'w-full flex'}
>
<div className={'mt-6'}>
<Field
light={true}
name={'code'}
title={'Authentication Code'}
description={'Enter the two-factor token generated by your device.'}
type={'number'}
autoFocus={true}
/>
</div>
<div className={'mt-6'}>
<button
type={'submit'}
className={'btn btn-primary btn-jumbo'}
disabled={isSubmitting}
>
{isSubmitting ?
<Spinner size={'tiny'} className={'mx-auto'}/>
:
'Continue'
}
</button>
</div>
<div className={'mt-6 text-center'}>
<Link
to={'/auth/login'}
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
>
Return to Login
</Link>
</div>
</LoginFormContainer>
);
};
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const EnhancedForm = withFormik<Props, Values>({
handleSubmit: ({ code }, { setSubmitting, props: { addError, clearFlashes, location } }) => {
clearFlashes();
console.log(location.state.token, code);
loginCheckpoint(location.state?.token || '', code)
.then(response => {
if (response.complete) {
// @ts-ignore
window.location = response.intended || '/';
return;
}
å
setSubmitting(false);
})
.catch(error => {
console.error(error);
setSubmitting(false);
addError({ message: httpErrorToHuman(error) });
});
},
if (!state || !state.token) {
mapPropsToValues: () => ({
code: '',
}),
validationSchema: object().shape({
code: string().required('An authentication code must be provided.')
.length(6, 'Authentication code must be 6 digits in length.'),
}),
})(LoginCheckpointContainer);
export default ({ history, location, ...props }: OwnProps) => {
const { addError, clearFlashes } = useFlash();
if (!location.state?.token) {
history.replace('/auth/login');
return null;
}
const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value.length <= 6) {
setCode(e.target.value);
}
};
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
clearFlashes();
loginCheckpoint(state.token!, code)
.then(response => {
if (response.complete) {
// @ts-ignore
window.location = response.intended || '/';
}
})
.catch(error => {
console.error(error);
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
setIsLoading(false);
});
};
return (
<React.Fragment>
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
Device Checkpoint
</h2>
<Container>
<FlashMessageRender/>
<LoginFormContainer onSubmit={submit}>
<div className={'mt-6'}>
<label htmlFor={'authentication_code'}>Authentication Code</label>
<input
id={'authentication_code'}
type={'number'}
autoFocus={true}
className={'input'}
value={code}
onChange={onChangeHandler}
/>
</div>
<div className={'mt-6'}>
<button
type={'submit'}
className={'btn btn-primary btn-jumbo'}
disabled={isLoading || code.length !== 6}
>
{isLoading ?
<Spinner size={'tiny'} className={'mx-auto'}/>
:
'Continue'
}
</button>
</div>
<div className={'mt-6 text-center'}>
<Link
to={'/auth/login'}
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
>
Return to Login
</Link>
</div>
</LoginFormContainer>
</Container>
</React.Fragment>
);
return <EnhancedForm
addError={addError}
clearFlashes={clearFlashes}
history={history}
location={location}
{...props}
/>;
};