Fix 2FA handling; closes #1962
This commit is contained in:
parent
2cf1c7f712
commit
9eb31a16d9
3 changed files with 105 additions and 103 deletions
|
@ -66,7 +66,7 @@ class LoginCheckpointController extends AbstractLoginController
|
||||||
* provided a valid username and password.
|
* provided a valid username and password.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest $request
|
* @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\IncompatibleWithGoogleAuthenticatorException
|
||||||
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
|
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
|
||||||
|
@ -75,18 +75,19 @@ class LoginCheckpointController extends AbstractLoginController
|
||||||
*/
|
*/
|
||||||
public function __invoke(LoginCheckpointRequest $request): JsonResponse
|
public function __invoke(LoginCheckpointRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$token = $request->input('confirmation_token');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$user = $this->repository->find(
|
$user = $this->repository->find($this->cache->get($token, 0));
|
||||||
$this->cache->pull($request->input('confirmation_token'), 0)
|
|
||||||
);
|
|
||||||
} catch (RecordNotFoundException $exception) {
|
} catch (RecordNotFoundException $exception) {
|
||||||
return $this->sendFailedLoginResponse($request);
|
return $this->sendFailedLoginResponse($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
$decrypted = $this->encrypter->decrypt($user->totp_secret);
|
$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);
|
return $this->sendLoginResponse($user, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ class LoginController extends AbstractLoginController
|
||||||
* Handle a login request to the application.
|
* Handle a login request to the application.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse|void
|
||||||
*
|
*
|
||||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
|
|
@ -1,115 +1,116 @@
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import loginCheckpoint from '@/api/auth/loginCheckpoint';
|
import loginCheckpoint from '@/api/auth/loginCheckpoint';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
import LoginFormContainer from '@/components/auth/LoginFormContainer';
|
||||||
import { Actions, useStoreActions } from 'easy-peasy';
|
import { ActionCreator } from 'easy-peasy';
|
||||||
import { StaticContext } from 'react-router';
|
import { StaticContext } from 'react-router';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
|
||||||
import { ApplicationStore } from '@/state';
|
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
import styled from 'styled-components';
|
import { useFormikContext, withFormik } from 'formik';
|
||||||
import { breakpoint } from 'styled-components-breakpoint';
|
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`
|
interface Values {
|
||||||
${breakpoint('sm')`
|
code: string;
|
||||||
${tw`w-4/5 mx-auto`}
|
}
|
||||||
`};
|
|
||||||
|
|
||||||
${breakpoint('md')`
|
type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }>
|
||||||
${tw`p-10`}
|
|
||||||
`};
|
|
||||||
|
|
||||||
${breakpoint('lg')`
|
type Props = OwnProps & {
|
||||||
${tw`w-3/5`}
|
addError: ActionCreator<FlashStore['addError']['payload']>;
|
||||||
`};
|
clearFlashes: ActionCreator<FlashStore['clearFlashes']['payload']>;
|
||||||
|
}
|
||||||
|
|
||||||
${breakpoint('xl')`
|
const LoginCheckpointContainer = () => {
|
||||||
${tw`w-full`}
|
const { isSubmitting } = useFormikContext<Values>();
|
||||||
max-width: 660px;
|
|
||||||
`};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default ({ history, location: { state } }: RouteComponentProps<{}, StaticContext, { token?: string }>) => {
|
return (
|
||||||
const [ code, setCode ] = useState('');
|
<LoginFormContainer
|
||||||
const [ isLoading, setIsLoading ] = useState(false);
|
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');
|
history.replace('/auth/login');
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
return <EnhancedForm
|
||||||
if (e.target.value.length <= 6) {
|
addError={addError}
|
||||||
setCode(e.target.value);
|
clearFlashes={clearFlashes}
|
||||||
}
|
history={history}
|
||||||
};
|
location={location}
|
||||||
|
{...props}
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue