Skip to content

Commit 9eb31a1

Browse files
committed
Fix 2FA handling; closes pterodactyl#1962
1 parent 2cf1c7f commit 9eb31a1

File tree

3 files changed

+100
-98
lines changed

3 files changed

+100
-98
lines changed

app/Http/Controllers/Auth/LoginCheckpointController.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public function __construct(
6666
* provided a valid username and password.
6767
*
6868
* @param \Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest $request
69-
* @return \Illuminate\Http\JsonResponse
69+
* @return \Illuminate\Http\JsonResponse|void
7070
*
7171
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
7272
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
@@ -75,18 +75,19 @@ public function __construct(
7575
*/
7676
public function __invoke(LoginCheckpointRequest $request): JsonResponse
7777
{
78+
$token = $request->input('confirmation_token');
79+
7880
try {
79-
$user = $this->repository->find(
80-
$this->cache->pull($request->input('confirmation_token'), 0)
81-
);
81+
$user = $this->repository->find($this->cache->get($token, 0));
8282
} catch (RecordNotFoundException $exception) {
8383
return $this->sendFailedLoginResponse($request);
8484
}
8585

8686
$decrypted = $this->encrypter->decrypt($user->totp_secret);
87-
$window = $this->config->get('pterodactyl.auth.2fa.window');
8887

89-
if ($this->google2FA->verifyKey($decrypted, $request->input('authentication_code'), $window)) {
88+
if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) {
89+
$this->cache->delete($token);
90+
9091
return $this->sendLoginResponse($user, $request);
9192
}
9293

app/Http/Controllers/Auth/LoginController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public function index(): View
7070
* Handle a login request to the application.
7171
*
7272
* @param \Illuminate\Http\Request $request
73-
* @return \Illuminate\Http\JsonResponse
73+
* @return \Illuminate\Http\JsonResponse|void
7474
*
7575
* @throws \Pterodactyl\Exceptions\DisplayException
7676
* @throws \Illuminate\Validation\ValidationException
Lines changed: 92 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,116 @@
1-
import React, { useState } from 'react';
1+
import React from 'react';
22
import { Link, RouteComponentProps } from 'react-router-dom';
33
import loginCheckpoint from '@/api/auth/loginCheckpoint';
44
import { httpErrorToHuman } from '@/api/http';
55
import LoginFormContainer from '@/components/auth/LoginFormContainer';
6-
import { Actions, useStoreActions } from 'easy-peasy';
6+
import { ActionCreator } from 'easy-peasy';
77
import { StaticContext } from 'react-router';
8-
import FlashMessageRender from '@/components/FlashMessageRender';
9-
import { ApplicationStore } from '@/state';
108
import Spinner from '@/components/elements/Spinner';
11-
import styled from 'styled-components';
12-
import { breakpoint } from 'styled-components-breakpoint';
9+
import { useFormikContext, withFormik } from 'formik';
10+
import { object, string } from 'yup';
11+
import useFlash from '@/plugins/useFlash';
12+
import { FlashStore } from '@/state/flashes';
13+
import Field from '@/components/elements/Field';
1314

14-
const Container = styled.div`
15-
${breakpoint('sm')`
16-
${tw`w-4/5 mx-auto`}
17-
`};
15+
interface Values {
16+
code: string;
17+
}
1818

19-
${breakpoint('md')`
20-
${tw`p-10`}
21-
`};
19+
type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }>
2220

23-
${breakpoint('lg')`
24-
${tw`w-3/5`}
25-
`};
21+
type Props = OwnProps & {
22+
addError: ActionCreator<FlashStore['addError']['payload']>;
23+
clearFlashes: ActionCreator<FlashStore['clearFlashes']['payload']>;
24+
}
2625

27-
${breakpoint('xl')`
28-
${tw`w-full`}
29-
max-width: 660px;
30-
`};
31-
`;
26+
const LoginCheckpointContainer = () => {
27+
const { isSubmitting } = useFormikContext<Values>();
3228

33-
export default ({ history, location: { state } }: RouteComponentProps<{}, StaticContext, { token?: string }>) => {
34-
const [ code, setCode ] = useState('');
35-
const [ isLoading, setIsLoading ] = useState(false);
36-
37-
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
38-
39-
if (!state || !state.token) {
40-
history.replace('/auth/login');
41-
42-
return null;
43-
}
44-
45-
const onChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
46-
if (e.target.value.length <= 6) {
47-
setCode(e.target.value);
48-
}
49-
};
50-
51-
const submit = (e: React.FormEvent<HTMLFormElement>) => {
52-
e.preventDefault();
29+
return (
30+
<LoginFormContainer
31+
title={'Device Checkpoint'}
32+
className={'w-full flex'}
33+
>
34+
<div className={'mt-6'}>
35+
<Field
36+
light={true}
37+
name={'code'}
38+
title={'Authentication Code'}
39+
description={'Enter the two-factor token generated by your device.'}
40+
type={'number'}
41+
autoFocus={true}
42+
/>
43+
</div>
44+
<div className={'mt-6'}>
45+
<button
46+
type={'submit'}
47+
className={'btn btn-primary btn-jumbo'}
48+
disabled={isSubmitting}
49+
>
50+
{isSubmitting ?
51+
<Spinner size={'tiny'} className={'mx-auto'}/>
52+
:
53+
'Continue'
54+
}
55+
</button>
56+
</div>
57+
<div className={'mt-6 text-center'}>
58+
<Link
59+
to={'/auth/login'}
60+
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
61+
>
62+
Return to Login
63+
</Link>
64+
</div>
65+
</LoginFormContainer>
66+
);
67+
};
5368

54-
setIsLoading(true);
69+
const EnhancedForm = withFormik<Props, Values>({
70+
handleSubmit: ({ code }, { setSubmitting, props: { addError, clearFlashes, location } }) => {
5571
clearFlashes();
56-
57-
loginCheckpoint(state.token!, code)
72+
console.log(location.state.token, code);
73+
loginCheckpoint(location.state?.token || '', code)
5874
.then(response => {
5975
if (response.complete) {
6076
// @ts-ignore
6177
window.location = response.intended || '/';
78+
return;
6279
}
80+
å
81+
setSubmitting(false);
6382
})
6483
.catch(error => {
6584
console.error(error);
66-
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
67-
setIsLoading(false);
85+
setSubmitting(false);
86+
addError({ message: httpErrorToHuman(error) });
6887
});
69-
};
88+
},
7089

71-
return (
72-
<React.Fragment>
73-
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
74-
Device Checkpoint
75-
</h2>
76-
<Container>
77-
<FlashMessageRender/>
78-
<LoginFormContainer onSubmit={submit}>
79-
<div className={'mt-6'}>
80-
<label htmlFor={'authentication_code'}>Authentication Code</label>
81-
<input
82-
id={'authentication_code'}
83-
type={'number'}
84-
autoFocus={true}
85-
className={'input'}
86-
value={code}
87-
onChange={onChangeHandler}
88-
/>
89-
</div>
90-
<div className={'mt-6'}>
91-
<button
92-
type={'submit'}
93-
className={'btn btn-primary btn-jumbo'}
94-
disabled={isLoading || code.length !== 6}
95-
>
96-
{isLoading ?
97-
<Spinner size={'tiny'} className={'mx-auto'}/>
98-
:
99-
'Continue'
100-
}
101-
</button>
102-
</div>
103-
<div className={'mt-6 text-center'}>
104-
<Link
105-
to={'/auth/login'}
106-
className={'text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
107-
>
108-
Return to Login
109-
</Link>
110-
</div>
111-
</LoginFormContainer>
112-
</Container>
113-
</React.Fragment>
114-
);
90+
mapPropsToValues: () => ({
91+
code: '',
92+
}),
93+
94+
validationSchema: object().shape({
95+
code: string().required('An authentication code must be provided.')
96+
.length(6, 'Authentication code must be 6 digits in length.'),
97+
}),
98+
})(LoginCheckpointContainer);
99+
100+
export default ({ history, location, ...props }: OwnProps) => {
101+
const { addError, clearFlashes } = useFlash();
102+
103+
if (!location.state?.token) {
104+
history.replace('/auth/login');
105+
106+
return null;
107+
}
108+
109+
return <EnhancedForm
110+
addError={addError}
111+
clearFlashes={clearFlashes}
112+
history={history}
113+
location={location}
114+
{...props}
115+
/>;
115116
};

0 commit comments

Comments
 (0)