diff --git a/packages/app/components/auth/actions.test.ts b/packages/app/components/auth/actions.test.ts
index 2aa64ca..039576e 100644
--- a/packages/app/components/auth/actions.test.ts
+++ b/packages/app/components/auth/actions.test.ts
@@ -16,14 +16,18 @@ import {
     login,
     setLogin,
 } from 'app/components/auth/actions';
-import { OauthData, OAuthValidateResponse } from '../../services/api/oauth';
+import { OAuthValidateResponse } from 'app/services/api/oauth';
 
-const oauthData: OauthData = {
-    clientId: '',
-    redirectUrl: '',
-    responseType: '',
-    scope: '',
-    state: '',
+import { OAuthState } from './reducer';
+
+const oauthData: OAuthState = {
+    params: {
+        clientId: '',
+        redirectUrl: '',
+        responseType: '',
+        scope: '',
+        state: '',
+    },
     prompt: 'none',
 };
 
@@ -64,9 +68,6 @@ describe('components/auth/actions', () => {
                     name: '',
                     description: '',
                 },
-                oAuth: {
-                    state: 123,
-                },
                 session: {
                     scopes: ['account_info'],
                 },
@@ -86,7 +87,6 @@ describe('components/auth/actions', () => {
                     [setClient(resp.client)],
                     [
                         setOAuthRequest({
-                            ...resp.oAuth,
                             prompt: 'none',
                             loginHint: undefined,
                         }),
diff --git a/packages/app/components/auth/actions.ts b/packages/app/components/auth/actions.ts
index e3c9fe1..53bbdeb 100644
--- a/packages/app/components/auth/actions.ts
+++ b/packages/app/components/auth/actions.ts
@@ -13,7 +13,7 @@ import {
     recoverPassword as recoverPasswordEndpoint,
     OAuthResponse,
 } from 'app/services/api/authentication';
-import oauth, { OauthData, Scope } from 'app/services/api/oauth';
+import oauth, { OauthRequestData, Scope } from 'app/services/api/oauth';
 import {
     register as registerEndpoint,
     activate as activateEndpoint,
@@ -314,38 +314,17 @@ const KNOWN_SCOPES: ReadonlyArray<string> = [
     'account_info',
     'account_email',
 ];
-/**
- * @param {object} oauthData
- * @param {string} oauthData.clientId
- * @param {string} oauthData.redirectUrl
- * @param {string} oauthData.responseType
- * @param {string} oauthData.description
- * @param {string} oauthData.scope
- * @param {string} [oauthData.prompt='none'] - comma-separated list of values to adjust auth flow
- *                 Posible values:
- *                  * none - default behaviour
- *                  * consent - forcibly prompt user for rules acceptance
- *                  * select_account - force account choosage, even if user has only one
- * @param {string} oauthData.loginHint - allows to choose the account, which will be used for auth
- *                        The possible values: account id, email, username
- * @param {string} oauthData.state
- *
- * @returns {Promise}
- */
-export function oAuthValidate(oauthData: OauthData) {
+
+export function oAuthValidate(oauthData: Pick<OAuthState, 'params' | 'description' | 'prompt' | 'loginHint'>) {
     // TODO: move to oAuth actions?
-    // test request: /oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session&description=foo
+    // auth code flow: /oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session&description=foo
+    // device code flow: /code?user_code=XOXOXOXO
     return wrapInLoader((dispatch) =>
         oauth
-            .validate(oauthData)
+            .validate(getOAuthRequest(oauthData))
             .then((resp) => {
                 const { scopes } = resp.session;
                 const invalidScopes = scopes.filter((scope) => !KNOWN_SCOPES.includes(scope));
-                let prompt = (oauthData.prompt || 'none').split(',').map((item) => item.trim());
-
-                if (prompt.includes('none')) {
-                    prompt = ['none'];
-                }
 
                 if (invalidScopes.length) {
                     logger.error('Got invalid scopes after oauth validation', {
@@ -353,12 +332,19 @@ export function oAuthValidate(oauthData: OauthData) {
                     });
                 }
 
+                let { prompt } = oauthData;
+
+                if (prompt && !Array.isArray(prompt)) {
+                    prompt = prompt.split(',').map((item) => item.trim());
+                }
+
                 dispatch(setClient(resp.client));
                 dispatch(
                     setOAuthRequest({
-                        ...resp.oAuth,
-                        prompt: oauthData.prompt || 'none',
+                        params: oauthData.params,
+                        description: oauthData.description,
                         loginHint: oauthData.loginHint,
+                        prompt,
                     }),
                 );
                 dispatch(setScopes(scopes));
@@ -375,12 +361,6 @@ export function oAuthValidate(oauthData: OauthData) {
     );
 }
 
-/**
- * @param {object} params
- * @param {bool} params.accept=false
- * @param params.accept
- * @returns {Promise}
- */
 export function oAuthComplete(params: { accept?: boolean } = {}) {
     return wrapInLoader(
         async (
@@ -388,7 +368,7 @@ export function oAuthComplete(params: { accept?: boolean } = {}) {
             getState,
         ): Promise<{
             success: boolean;
-            redirectUri: string;
+            redirectUri?: string;
         }> => {
             const oauthData = getState().auth.oauth;
 
@@ -397,11 +377,14 @@ export function oAuthComplete(params: { accept?: boolean } = {}) {
             }
 
             try {
-                const resp = await oauth.complete(oauthData, params);
+                const resp = await oauth.complete(getOAuthRequest(oauthData), params);
+
                 localStorage.removeItem('oauthData');
 
-                if (resp.redirectUri.startsWith('static_page')) {
-                    const displayCode = /static_page_with_code/.test(resp.redirectUri);
+                if (!resp.redirectUri) {
+                    dispatch(setOAuthCode({ success: resp.success && params.accept }));
+                } else if (resp.redirectUri.startsWith('static_page')) {
+                    const displayCode = resp.redirectUri.includes('static_page_with_code');
 
                     const [, code] = resp.redirectUri.match(/code=(.+)&/) || [];
                     [, resp.redirectUri] = resp.redirectUri.match(/^(.+)\?/) || [];
@@ -437,13 +420,41 @@ export function oAuthComplete(params: { accept?: boolean } = {}) {
     );
 }
 
+function getOAuthRequest({ params, description }: OAuthState): OauthRequestData {
+    let data: OauthRequestData;
+
+    if ('userCode' in params) {
+        data = {
+            user_code: params.userCode,
+        };
+    } else {
+        data = {
+            client_id: params.clientId,
+            redirect_uri: params.redirectUrl,
+            response_type: params.responseType,
+            scope: params.scope,
+            state: params.state,
+        };
+    }
+
+    if (description) {
+        data.description = description;
+    }
+
+    return data;
+}
+
 function handleOauthParamsValidation(
     resp: {
         [key: string]: any;
         userMessage?: string;
     } = {},
 ) {
-    dispatchBsod();
+    // TODO: it would be better to dispatch BSOD from the initial request performers
+    if (resp.error !== 'invalid_user_code') {
+        dispatchBsod();
+    }
+
     localStorage.removeItem('oauthData');
 
     // eslint-disable-next-line no-alert
@@ -468,33 +479,16 @@ export type ClientAction = SetClientAction;
 
 interface SetOauthAction extends ReduxAction {
     type: 'set_oauth';
-    payload: Pick<OAuthState, 'clientId' | 'redirectUrl' | 'responseType' | 'scope' | 'prompt' | 'loginHint' | 'state'>;
+    payload: OAuthState | null;
 }
 
 // Input data is coming right from the query string, so the names
 // are the same, as used for initializing OAuth2 request
-export function setOAuthRequest(data: {
-    client_id?: string;
-    redirect_uri?: string;
-    response_type?: string;
-    scope?: string;
-    prompt?: string;
-    loginHint?: string;
-    state?: string;
-}): SetOauthAction {
+// TODO: filter out allowed properties
+export function setOAuthRequest(payload: OAuthState | null): SetOauthAction {
     return {
         type: 'set_oauth',
-        payload: {
-            // TODO: there is too much default empty string. Maybe we can somehow validate it
-            //       on the level, where this action is called?
-            clientId: data.client_id || '',
-            redirectUrl: data.redirect_uri || '',
-            responseType: data.response_type || '',
-            scope: data.scope || '',
-            prompt: data.prompt || '',
-            loginHint: data.loginHint || '',
-            state: data.state || '',
-        },
+        payload,
     };
 }
 
@@ -503,9 +497,7 @@ interface SetOAuthResultAction extends ReduxAction {
     payload: Pick<OAuthState, 'success' | 'code' | 'displayCode'>;
 }
 
-export const SET_OAUTH_RESULT = 'set_oauth_result'; // TODO: remove
-
-export function setOAuthCode(payload: { success: boolean; code: string; displayCode: boolean }): SetOAuthResultAction {
+export function setOAuthCode(payload: Pick<OAuthState, 'success' | 'code' | 'displayCode'>): SetOAuthResultAction {
     return {
         type: 'set_oauth_result',
         payload,
@@ -515,7 +507,7 @@ export function setOAuthCode(payload: { success: boolean; code: string; displayC
 export function resetOAuth(): AppAction {
     return (dispatch): void => {
         localStorage.removeItem('oauthData');
-        dispatch(setOAuthRequest({}));
+        dispatch(setOAuthRequest(null));
     };
 }
 
diff --git a/packages/app/components/auth/deviceCode/DeviceCode.tsx b/packages/app/components/auth/deviceCode/DeviceCode.tsx
new file mode 100644
index 0000000..5c2ec45
--- /dev/null
+++ b/packages/app/components/auth/deviceCode/DeviceCode.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { FormattedMessage as Message, defineMessages } from 'react-intl';
+
+import factory from '../factory';
+import Body from './DeviceCodeBody';
+
+const messages = defineMessages({
+    deviceCodeTitle: 'Device code',
+});
+
+export default factory({
+    title: messages.deviceCodeTitle,
+    body: Body,
+    footer: {
+        color: 'green',
+        children: <Message key="continueButton" defaultMessage="Continue" />,
+    },
+});
diff --git a/packages/app/components/auth/deviceCode/DeviceCodeBody.tsx b/packages/app/components/auth/deviceCode/DeviceCodeBody.tsx
new file mode 100644
index 0000000..4c64884
--- /dev/null
+++ b/packages/app/components/auth/deviceCode/DeviceCodeBody.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { defineMessages } from 'react-intl';
+
+import { Input } from 'app/components/ui/form';
+import BaseAuthBody from 'app/components/auth/BaseAuthBody';
+
+const messages = defineMessages({
+    deviceCode: 'Device code',
+});
+
+export default class DeviceCodeBody extends BaseAuthBody {
+    static displayName = 'DeviceCodeBody';
+    static panelId = 'deviceCode';
+
+    autoFocusField = 'user_code';
+
+    render() {
+        return (
+            <div>
+                {this.renderErrors()}
+
+                <Input
+                    {...this.bindField('user_code')}
+                    icon="key"
+                    required
+                    placeholder={messages.deviceCode}
+                />
+            </div>
+        );
+    }
+}
diff --git a/packages/app/components/auth/deviceCode/index.ts b/packages/app/components/auth/deviceCode/index.ts
new file mode 100644
index 0000000..38c2691
--- /dev/null
+++ b/packages/app/components/auth/deviceCode/index.ts
@@ -0,0 +1 @@
+export { default } from './DeviceCode';
diff --git a/packages/app/components/auth/finish/Finish.tsx b/packages/app/components/auth/finish/Finish.tsx
index cd875f9..d411e85 100644
--- a/packages/app/components/auth/finish/Finish.tsx
+++ b/packages/app/components/auth/finish/Finish.tsx
@@ -1,8 +1,9 @@
-import React, { MouseEventHandler } from 'react';
+import React, { FC, MouseEventHandler, useEffect } from 'react';
+import { Redirect } from 'react-router-dom';
 import { FormattedMessage as Message } from 'react-intl';
 import { Helmet } from 'react-helmet-async';
 
-import { connect } from 'app/functions';
+import { useReduxSelector } from 'app/functions';
 import { Button } from 'app/components/ui/form';
 import copy from 'app/services/copy';
 
@@ -16,102 +17,93 @@ interface Props {
     success?: boolean;
 }
 
-class Finish extends React.Component<Props> {
-    render() {
-        const { appName, code, state, displayCode, success } = this.props;
-        const authData = JSON.stringify({
-            auth_code: code,
-            state,
+const Finish: FC<Props> = () => {
+    const { client, oauth } = useReduxSelector((state) => state.auth);
+
+    const onCopyClick: MouseEventHandler = (event) => {
+        event.preventDefault();
+        copy(oauth!.code!);
+    };
+
+    let authData: string | undefined;
+
+    if (oauth && 'state' in oauth.params) {
+        authData = JSON.stringify({
+            auth_code: oauth.code,
+            state: oauth.params.state,
         });
+    }
 
-        history.pushState(null, document.title, `#${authData}`);
+    useEffect(() => {
+        if (authData) {
+            history.pushState(null, document.title, `#${authData}`);
+        }
+    }, []);
 
-        return (
-            <div className={styles.finishPage}>
-                <Helmet title={authData} />
+    if (!client || !oauth) {
+        return <Redirect to="/" />;
+    }
 
-                {success ? (
-                    <div>
-                        <div className={styles.successBackground} />
-                        <div className={styles.greenTitle}>
-                            <Message
-                                key="authForAppSuccessful"
-                                defaultMessage="Authorization for {appName} was successfully completed"
-                                values={{
-                                    appName: <span className={styles.appName}>{appName}</span>,
-                                }}
-                            />
-                        </div>
-                        {displayCode ? (
-                            <div data-testid="oauth-code-container">
-                                <div className={styles.description}>
-                                    <Message
-                                        key="passCodeToApp"
-                                        defaultMessage="To complete authorization process, please, provide the following code to {appName}"
-                                        values={{ appName }}
-                                    />
-                                </div>
-                                <div className={styles.codeContainer}>
-                                    <div className={styles.code}>{code}</div>
-                                </div>
-                                <Button color="green" small onClick={this.onCopyClick}>
-                                    <Message key="copy" defaultMessage="Copy" />
-                                </Button>
-                            </div>
-                        ) : (
+    return (
+        <div className={styles.finishPage}>
+            {authData && <Helmet title={authData} />}
+
+            {oauth.success ? (
+                <div>
+                    <div className={styles.successBackground} />
+                    <div className={styles.greenTitle}>
+                        <Message
+                            key="authForAppSuccessful"
+                            defaultMessage="Authorization for {appName} was successfully completed"
+                            values={{
+                                appName: <span className={styles.appName}>{client.name}</span>,
+                            }}
+                        />
+                    </div>
+                    {oauth.displayCode ? (
+                        <div data-testid="oauth-code-container">
                             <div className={styles.description}>
                                 <Message
-                                    key="waitAppReaction"
-                                    defaultMessage="Please, wait till your application response"
+                                    key="passCodeToApp"
+                                    defaultMessage="To complete authorization process, please, provide the following code to {appName}"
+                                    values={{ appName: client.name }}
                                 />
                             </div>
-                        )}
-                    </div>
-                ) : (
-                    <div>
-                        <div className={styles.failBackground} />
-                        <div className={styles.redTitle}>
-                            <Message
-                                key="authForAppFailed"
-                                defaultMessage="Authorization for {appName} was failed"
-                                values={{
-                                    appName: <span className={styles.appName}>{appName}</span>,
-                                }}
-                            />
+                            <div className={styles.codeContainer}>
+                                <div className={styles.code}>{oauth.code}</div>
+                            </div>
+                            <Button color="green" small onClick={onCopyClick}>
+                                <Message key="copy" defaultMessage="Copy" />
+                            </Button>
                         </div>
+                    ) : (
                         <div className={styles.description}>
                             <Message
                                 key="waitAppReaction"
                                 defaultMessage="Please, wait till your application response"
                             />
                         </div>
+                    )}
+                </div>
+            ) : (
+                <div>
+                    <div className={styles.failBackground} />
+                    <div className={styles.redTitle}>
+                        <Message
+                            key="authForAppFailed"
+                            defaultMessage="Authorization for {appName} was failed"
+                            values={{
+                                appName: <span className={styles.appName}>{client.name}</span>,
+                            }}
+                        />
                     </div>
-                )}
-            </div>
-        );
-    }
+                    <div className={styles.description}>
+                        <Message key="waitAppReaction" defaultMessage="Please, wait till your application response" />
+                    </div>
+                </div>
+            )}
+        </div>
+    );
+};
 
-    onCopyClick: MouseEventHandler = (event) => {
-        event.preventDefault();
-
-        const { code } = this.props;
-
-        if (code) {
-            copy(code);
-        }
-    };
-}
-
-export default connect(({ auth }) => {
-    if (!auth || !auth.client || !auth.oauth) {
-        throw new Error('Can not connect Finish component. No auth data in state');
-    }
-
-    return {
-        appName: auth.client.name,
-        code: auth.oauth.code,
-        displayCode: auth.oauth.displayCode,
-        state: auth.oauth.state,
-        success: auth.oauth.success,
-    };
-})(Finish);
+export default Finish;
diff --git a/packages/app/components/auth/finish/index.ts b/packages/app/components/auth/finish/index.ts
new file mode 100644
index 0000000..a5d2a36
--- /dev/null
+++ b/packages/app/components/auth/finish/index.ts
@@ -0,0 +1 @@
+export { default } from './Finish';
diff --git a/packages/app/components/auth/reducer.ts b/packages/app/components/auth/reducer.ts
index 1862399..af398b0 100644
--- a/packages/app/components/auth/reducer.ts
+++ b/packages/app/components/auth/reducer.ts
@@ -38,15 +38,34 @@ export interface Client {
     description: string;
 }
 
-export interface OAuthState {
+export interface OauthAuthCodeFlowParams {
     clientId: string;
     redirectUrl: string;
     responseType: string;
-    description?: string;
-    scope: string;
-    prompt: string;
-    loginHint: string;
     state: string;
+    scope: string;
+}
+
+export interface OauthDeviceCodeFlowParams {
+    userCode: string;
+}
+
+export interface OAuthState {
+    params: OauthAuthCodeFlowParams | OauthDeviceCodeFlowParams;
+    description?: string;
+    /**
+     * Possible values:
+     * - none - default behaviour
+     * - consent - forcibly prompt user for rules acceptance
+     * - select_account - force account choosage, even if user has only one
+     * comma separated list of 'none' | 'consent' | 'select_account';
+     */
+    prompt?: string | Array<string>;
+    /**
+     * Allows to choose the account, which will be used for auth
+     * The possible values: account id, email, username
+     */
+    loginHint?: string;
     success?: boolean;
     code?: string;
     displayCode?: boolean;
diff --git a/packages/app/containers/AuthFlowRoute.tsx b/packages/app/containers/AuthFlowRoute.tsx
index 4058da3..78e777f 100644
--- a/packages/app/containers/AuthFlowRoute.tsx
+++ b/packages/app/containers/AuthFlowRoute.tsx
@@ -1,19 +1,18 @@
-import React from 'react';
+import React, { FC } from 'react';
 import { Route, RouteProps } from 'react-router-dom';
 
 import AuthFlowRouteContents from './AuthFlowRouteContents';
 
-export default function AuthFlowRoute(props: RouteProps) {
-    const { component: Component, ...routeProps } = props;
-
-    if (!Component) {
-        throw new Error('props.component required');
-    }
+// Make "component" prop required
+type Props = Omit<RouteProps, 'component'> & Required<Pick<RouteProps, 'component'>>;
 
+const AuthFlowRoute: FC<Props> = ({ component: Component, ...props }) => {
     return (
         <Route
-            {...routeProps}
+            {...props}
             render={(routerProps) => <AuthFlowRouteContents routerProps={routerProps} component={Component} />}
         />
     );
-}
+};
+
+export default AuthFlowRoute;
diff --git a/packages/app/containers/AuthFlowRouteContents.tsx b/packages/app/containers/AuthFlowRouteContents.tsx
index 2adcd00..f59d53a 100644
--- a/packages/app/containers/AuthFlowRouteContents.tsx
+++ b/packages/app/containers/AuthFlowRouteContents.tsx
@@ -4,7 +4,7 @@ import { Redirect, RouteComponentProps } from 'react-router-dom';
 import authFlow from 'app/services/authFlow';
 
 interface Props {
-    component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
+    component: React.ComponentType<RouteComponentProps> | React.ComponentType<any>;
     routerProps: RouteComponentProps;
 }
 
@@ -74,7 +74,7 @@ export default class AuthFlowRouteContents extends React.Component<Props, State>
         });
     }
 
-    onRouteAllowed(props: Props) {
+    onRouteAllowed(props: Props): void {
         if (!this.mounted) {
             return;
         }
diff --git a/packages/app/pages/auth/AuthPage.tsx b/packages/app/pages/auth/AuthPage.tsx
index a97dc58..4f0a9b3 100644
--- a/packages/app/pages/auth/AuthPage.tsx
+++ b/packages/app/pages/auth/AuthPage.tsx
@@ -1,4 +1,4 @@
-import React, { ComponentType, ReactNode, useCallback, useState } from 'react';
+import React, { FC, useCallback, useState } from 'react';
 import { Route, Switch, Redirect, RouteComponentProps } from 'react-router-dom';
 
 import AppInfo from 'app/components/auth/appInfo/AppInfo';
@@ -7,6 +7,7 @@ import Register from 'app/components/auth/register/Register';
 import Login from 'app/components/auth/login/Login';
 import Permissions from 'app/components/auth/permissions/Permissions';
 import ChooseAccount from 'app/components/auth/chooseAccount/ChooseAccount';
+import DeviceCode from 'app/components/auth/deviceCode';
 import Activation from 'app/components/auth/activation/Activation';
 import ResendActivation from 'app/components/auth/resendActivation/ResendActivation';
 import Password from 'app/components/auth/password/Password';
@@ -14,7 +15,7 @@ import AcceptRules from 'app/components/auth/acceptRules/AcceptRules';
 import ForgotPassword from 'app/components/auth/forgotPassword/ForgotPassword';
 import RecoverPassword from 'app/components/auth/recoverPassword/RecoverPassword';
 import Mfa from 'app/components/auth/mfa/Mfa';
-import Finish from 'app/components/auth/finish/Finish';
+import Finish from 'app/components/auth/finish';
 
 import { useReduxSelector } from 'app/functions';
 import { Factory } from 'app/components/auth/factory';
@@ -27,7 +28,7 @@ import styles from './auth.scss';
 // so that it persist disregarding remounts
 let isSidebarHiddenCache = false;
 
-const AuthPage: ComponentType = () => {
+const AuthPage: FC = () => {
     const [isSidebarHidden, setIsSidebarHidden] = useState<boolean>(isSidebarHiddenCache);
     const client = useReduxSelector((state) => state.auth.client);
 
@@ -54,6 +55,7 @@ const AuthPage: ComponentType = () => {
                     <Route path="/choose-account" render={renderPanelTransition(ChooseAccount)} />
                     <Route path="/oauth/choose-account" render={renderPanelTransition(ChooseAccount)} />
                     <Route path="/oauth/finish" component={Finish} />
+                    <Route path="/code" component={renderPanelTransition(DeviceCode)} />
                     <Route path="/accept-rules" render={renderPanelTransition(AcceptRules)} />
                     <Route path="/forgot-password" render={renderPanelTransition(ForgotPassword)} />
                     <Route path="/recover-password/:key?" render={renderPanelTransition(RecoverPassword)} />
@@ -64,7 +66,7 @@ const AuthPage: ComponentType = () => {
     );
 };
 
-function renderPanelTransition(factory: Factory): (props: RouteComponentProps<any>) => ReactNode {
+function renderPanelTransition(factory: Factory): FC<RouteComponentProps> {
     const { Title, Body, Footer, Links } = factory();
 
     return (props) => (
diff --git a/packages/app/services/api/oauth.ts b/packages/app/services/api/oauth.ts
index abae245..634b110 100644
--- a/packages/app/services/api/oauth.ts
+++ b/packages/app/services/api/oauth.ts
@@ -24,35 +24,27 @@ export interface OauthAppResponse {
     minecraftServerIp?: string;
 }
 
-interface OauthRequestData {
+interface AuthCodeFlowRequestData {
     client_id: string;
     redirect_uri: string;
     response_type: string;
-    description?: string;
     scope: string;
-    prompt: string;
-    login_hint?: string;
     state?: string;
 }
 
-export interface OauthData {
-    clientId: string;
-    redirectUrl: string;
-    responseType: string;
-    description?: string;
-    scope: string;
-    // TODO: why prompt is not nullable?
-    prompt: string; // comma separated list of 'none' | 'consent' | 'select_account';
-    loginHint?: string;
-    state?: string;
+interface DeviceCodeFlowRequestData {
+    user_code: string;
 }
 
+export type OauthRequestData = (AuthCodeFlowRequestData | DeviceCodeFlowRequestData) & {
+    description?: string;
+};
+
 export interface OAuthValidateResponse {
     session: {
         scopes: Scope[];
     };
     client: Client;
-    oAuth: {}; // TODO: improve typing
 }
 
 interface FormPayloads {
@@ -64,20 +56,20 @@ interface FormPayloads {
 }
 
 const api = {
-    validate(oauthData: OauthData) {
+    validate(oauthData: OauthRequestData) {
         return request
-            .get<OAuthValidateResponse>('/api/oauth2/v1/validate', getOAuthRequest(oauthData))
+            .get<OAuthValidateResponse>('/api/oauth2/v1/validate', oauthData)
             .catch(handleOauthParamsValidation);
     },
 
     complete(
-        oauthData: OauthData,
+        oauthData: OauthRequestData,
         params: { accept?: boolean } = {},
     ): Promise<{
         success: boolean;
-        redirectUri: string;
+        redirectUri?: string;
     }> {
-        const query = request.buildQuery(getOAuthRequest(oauthData));
+        const query = request.buildQuery(oauthData);
 
         return request
             .post<{
@@ -146,37 +138,18 @@ if ('Cypress' in window) {
 
 export default api;
 
-/**
- * @param {object} oauthData
- * @param {string} oauthData.clientId
- * @param {string} oauthData.redirectUrl
- * @param {string} oauthData.responseType
- * @param {string} oauthData.description
- * @param {string} oauthData.scope
- * @param {string} oauthData.state
- *
- * @returns {object}
- */
-function getOAuthRequest(oauthData: OauthData): OauthRequestData {
-    return {
-        client_id: oauthData.clientId,
-        redirect_uri: oauthData.redirectUrl,
-        response_type: oauthData.responseType,
-        description: oauthData.description,
-        scope: oauthData.scope,
-        prompt: oauthData.prompt,
-        login_hint: oauthData.loginHint,
-        state: oauthData.state,
-    };
-}
-
 function handleOauthParamsValidation(
     resp:
-        | { [key: string]: any }
+        | Record<string, any>
         | {
               statusCode: number;
               success: false;
-              error: 'invalid_request' | 'unsupported_response_type' | 'invalid_scope' | 'invalid_client';
+              error:
+                  | 'invalid_request'
+                  | 'unsupported_response_type'
+                  | 'invalid_scope'
+                  | 'invalid_client'
+                  | 'invalid_user_code';
               parameter: string;
           } = {},
 ) {
diff --git a/packages/app/services/authFlow/AuthFlow.test.ts b/packages/app/services/authFlow/AuthFlow.test.ts
index ab7a2f5..5b92da3 100644
--- a/packages/app/services/authFlow/AuthFlow.test.ts
+++ b/packages/app/services/authFlow/AuthFlow.test.ts
@@ -5,7 +5,7 @@ import AuthFlow from 'app/services/authFlow/AuthFlow';
 import AbstractState from 'app/services/authFlow/AbstractState';
 import localStorage from 'app/services/localStorage';
 
-import OAuthState from 'app/services/authFlow/OAuthState';
+import InitOAuthAuthCodeFlowState from './InitOAuthAuthCodeFlowState';
 import RegisterState from 'app/services/authFlow/RegisterState';
 import RecoverPasswordState from 'app/services/authFlow/RecoverPasswordState';
 import ForgotPasswordState from 'app/services/authFlow/ForgotPasswordState';
@@ -314,9 +314,9 @@ describe('AuthFlow', () => {
             '/oauth/permissions': LoginState,
             '/oauth/choose-account': LoginState,
             '/oauth/finish': LoginState,
-            '/oauth2/v1/foo': OAuthState,
-            '/oauth2/v1': OAuthState,
-            '/oauth2': OAuthState,
+            '/oauth2/v1/foo': InitOAuthAuthCodeFlowState,
+            '/oauth2/v1': InitOAuthAuthCodeFlowState,
+            '/oauth2': InitOAuthAuthCodeFlowState,
             '/register': RegisterState,
             '/choose-account': ChooseAccountState,
             '/recover-password': RecoverPasswordState,
@@ -379,7 +379,7 @@ describe('AuthFlow', () => {
             flow.handleRequest({ path, query: new URLSearchParams({}), params: {} }, () => {}, callback);
 
             expect(flow.setState, 'was called once');
-            expect(flow.setState, 'to have a call satisfying', [expect.it('to be a', OAuthState)]);
+            expect(flow.setState, 'to have a call satisfying', [expect.it('to be a', InitOAuthAuthCodeFlowState)]);
             expect(callback, 'was called twice');
         });
 
diff --git a/packages/app/services/authFlow/AuthFlow.ts b/packages/app/services/authFlow/AuthFlow.ts
index cc4e1bb..9c11d22 100644
--- a/packages/app/services/authFlow/AuthFlow.ts
+++ b/packages/app/services/authFlow/AuthFlow.ts
@@ -10,10 +10,12 @@ import {
 } from 'app/components/accounts/actions';
 import * as actions from 'app/components/auth/actions';
 import { updateUser } from 'app/components/user/actions';
+import FinishState from './FinishState';
 
 import RegisterState from './RegisterState';
 import LoginState from './LoginState';
-import OAuthState from './OAuthState';
+import InitOAuthAuthCodeFlowState from './InitOAuthAuthCodeFlowState';
+import InitOAuthDeviceCodeFlowState from './InitOAuthDeviceCodeFlowState';
 import ForgotPasswordState from './ForgotPasswordState';
 import RecoverPasswordState from './RecoverPasswordState';
 import ActivationState from './ActivationState';
@@ -22,11 +24,11 @@ import ChooseAccountState from './ChooseAccountState';
 import ResendActivationState from './ResendActivationState';
 import State from './State';
 
-type Request = {
+interface Request {
     path: string;
     query: URLSearchParams;
     params: Record<string, any>;
-};
+}
 
 export const availableActions = {
     updateUser,
@@ -104,7 +106,11 @@ export default class AuthFlow implements AuthContext {
                     this.replace(route);
                 }
 
-                browserHistory[options.replace ? 'replace' : 'push'](route);
+                if (options.replace) {
+                    browserHistory.replace(route);
+                } else {
+                    browserHistory.push(route);
+                }
             }
 
             this.replace = null;
@@ -132,11 +138,7 @@ export default class AuthFlow implements AuthContext {
     }
 
     setState(state: State) {
-        if (!state) {
-            throw new Error('State is required');
-        }
-
-        this.state && this.state.leave(this);
+        this.state?.leave(this);
         this.prevState = this.state;
         this.state = state;
         const resp = this.state.enter(this);
@@ -170,16 +172,8 @@ export default class AuthFlow implements AuthContext {
 
     /**
      * This should be called from onEnter prop of react-router Route component
-     *
-     * @param {object} request
-     * @param {string} request.path
-     * @param {object} request.params
-     * @param {URLSearchParams} request.query
-     * @param {Function} replace
-     * @param {Function} [callback=function() {}] - an optional callback function to be called, when state will be stabilized
-     * (state's enter function's promise resolved)
      */
-    handleRequest(request: Request, replace: (path: string) => void, callback: () => void = () => {}) {
+    handleRequest(request: Request, replace: (path: string) => void, callback: () => void = () => {}): void {
         const { path } = request;
         this.replace = replace;
         this.onReady = callback;
@@ -218,13 +212,20 @@ export default class AuthFlow implements AuthContext {
                 this.setState(new ChooseAccountState());
                 break;
 
+            case '/code':
+                this.setState(new InitOAuthDeviceCodeFlowState());
+                break;
+
+            case '/oauth/finish':
+                this.setState(new FinishState());
+                break;
+
             case '/':
             case '/login':
             case '/password':
             case '/mfa':
             case '/accept-rules':
             case '/oauth/permissions':
-            case '/oauth/finish':
             case '/oauth/choose-account':
                 this.setState(new LoginState());
                 break;
@@ -234,7 +235,7 @@ export default class AuthFlow implements AuthContext {
                     path.replace(/(.)\/.+/, '$1') // use only first part of an url
                 ) {
                     case '/oauth2':
-                        this.setState(new OAuthState());
+                        this.setState(new InitOAuthAuthCodeFlowState());
                         break;
                     case '/activation':
                         this.setState(new ActivationState());
diff --git a/packages/app/services/authFlow/CompleteState.ts b/packages/app/services/authFlow/CompleteState.ts
index 141aa9f..5ab296d 100644
--- a/packages/app/services/authFlow/CompleteState.ts
+++ b/packages/app/services/authFlow/CompleteState.ts
@@ -1,4 +1,6 @@
+import { OAuthState } from 'app/components/auth/reducer';
 import { getActiveAccount } from 'app/components/accounts/reducer';
+
 import AbstractState from './AbstractState';
 import LoginState from './LoginState';
 import PermissionsState from './PermissionsState';
@@ -11,8 +13,16 @@ import { AuthContext } from './AuthFlow';
 const PROMPT_ACCOUNT_CHOOSE = 'select_account';
 const PROMPT_PERMISSIONS = 'consent';
 
+function hasPrompt(prompt: OAuthState['prompt'], needle: string): boolean {
+    if (Array.isArray(prompt)) {
+        return prompt.includes(needle);
+    }
+
+    return prompt === needle;
+}
+
 export default class CompleteState extends AbstractState {
-    isPermissionsAccepted: boolean | void;
+    private readonly isPermissionsAccepted?: boolean;
 
     constructor(
         options: {
@@ -36,7 +46,7 @@ export default class CompleteState extends AbstractState {
             context.setState(new LoginState());
         } else if (user.shouldAcceptRules && !user.isDeleted) {
             context.setState(new AcceptRulesState());
-        } else if (oauth && oauth.clientId) {
+        } else if (oauth?.params) {
             return this.processOAuth(context);
         } else {
             context.navigate('/');
@@ -44,6 +54,8 @@ export default class CompleteState extends AbstractState {
     }
 
     processOAuth(context: AuthContext): Promise<void> | void {
+        console.log('process oauth', this.isPermissionsAccepted);
+
         const { auth, accounts, user } = context.getState();
 
         let { isSwitcherEnabled } = auth;
@@ -80,7 +92,7 @@ export default class CompleteState extends AbstractState {
                 // so that they can see, that their account was deleted
                 // (this info is displayed on switcher)
                 user.isDeleted ||
-                oauth.prompt.includes(PROMPT_ACCOUNT_CHOOSE))
+                hasPrompt(oauth.prompt, PROMPT_ACCOUNT_CHOOSE))
         ) {
             context.setState(new ChooseAccountState());
         } else if (user.isDeleted) {
@@ -92,35 +104,35 @@ export default class CompleteState extends AbstractState {
         } else if (oauth.code) {
             context.setState(new FinishState());
         } else {
-            const data: { [key: string]: any } = {};
+            const data: Record<string, any> = {};
 
             if (typeof this.isPermissionsAccepted !== 'undefined') {
                 data.accept = this.isPermissionsAccepted;
-            } else if (oauth.acceptRequired || oauth.prompt.includes(PROMPT_PERMISSIONS)) {
+            } else if (oauth.acceptRequired || hasPrompt(oauth.prompt, PROMPT_PERMISSIONS)) {
                 context.setState(new PermissionsState());
 
                 return;
             }
 
             // TODO: it seems that oAuthComplete may be a separate state
-            return context.run('oAuthComplete', data).then(
-                (resp: { redirectUri: string }) => {
+            return context
+                .run('oAuthComplete', data)
+                .then((resp: { redirectUri?: string }) => {
                     // TODO: пусть в стейт попадает флаг или тип авторизации
                     //       вместо волшебства над редирект урлой
-                    if (resp.redirectUri.includes('static_page')) {
+                    if (!resp.redirectUri || resp.redirectUri.includes('static_page')) {
                         context.setState(new FinishState());
                     } else {
                         return context.run('redirect', resp.redirectUri);
                     }
-                },
-                (resp) => {
+                })
+                .catch((resp) => {
                     if (resp.unauthorized) {
                         context.setState(new LoginState());
                     } else if (resp.acceptRequired) {
                         context.setState(new PermissionsState());
                     }
-                },
-            );
+                });
         }
     }
 }
diff --git a/packages/app/services/authFlow/DeviceCodeState.ts b/packages/app/services/authFlow/DeviceCodeState.ts
new file mode 100644
index 0000000..4d80169
--- /dev/null
+++ b/packages/app/services/authFlow/DeviceCodeState.ts
@@ -0,0 +1,26 @@
+import AbstractState from './AbstractState';
+import { AuthContext } from './AuthFlow';
+import CompleteState from './CompleteState';
+
+export default class DeviceCodeState extends AbstractState {
+    async resolve(context: AuthContext, payload: { user_code: string }): Promise<void> {
+        const { query } = context.getRequest();
+
+        context
+            .run('oAuthValidate', {
+                params: {
+                    userCode: payload.user_code,
+                },
+                description: query.get('description')!,
+                prompt: query.get('prompt')!,
+            })
+            .then(() => context.setState(new CompleteState()))
+            .catch((err) => {
+                if (err.error === 'invalid_user_code') {
+                    return context.run('setErrors', { [err.parameter]: err.error });
+                }
+
+                throw err;
+            });
+    }
+}
diff --git a/packages/app/services/authFlow/OAuthState.test.ts b/packages/app/services/authFlow/InitOAuthAuthCodeFlowState.test.ts
similarity index 96%
rename from packages/app/services/authFlow/OAuthState.test.ts
rename to packages/app/services/authFlow/InitOAuthAuthCodeFlowState.test.ts
index 1ae70ae..bf1ba78 100644
--- a/packages/app/services/authFlow/OAuthState.test.ts
+++ b/packages/app/services/authFlow/InitOAuthAuthCodeFlowState.test.ts
@@ -1,17 +1,17 @@
 import sinon, { SinonMock } from 'sinon';
 
-import OAuthState from 'app/services/authFlow/OAuthState';
+import InitOAuthAuthCodeFlowState from './InitOAuthAuthCodeFlowState';
 import CompleteState from 'app/services/authFlow/CompleteState';
 
 import { bootstrap, expectState, expectRun, MockedAuthContext } from './helpers';
 
 describe('OAuthState', () => {
-    let state: OAuthState;
+    let state: InitOAuthAuthCodeFlowState;
     let context: MockedAuthContext;
     let mock: SinonMock;
 
     beforeEach(() => {
-        state = new OAuthState();
+        state = new InitOAuthAuthCodeFlowState();
 
         const data = bootstrap();
         context = data.context;
diff --git a/packages/app/services/authFlow/OAuthState.ts b/packages/app/services/authFlow/InitOAuthAuthCodeFlowState.ts
similarity index 55%
rename from packages/app/services/authFlow/OAuthState.ts
rename to packages/app/services/authFlow/InitOAuthAuthCodeFlowState.ts
index fef2fab..a1bb88b 100644
--- a/packages/app/services/authFlow/OAuthState.ts
+++ b/packages/app/services/authFlow/InitOAuthAuthCodeFlowState.ts
@@ -2,20 +2,22 @@ import AbstractState from './AbstractState';
 import { AuthContext } from './AuthFlow';
 import CompleteState from './CompleteState';
 
-export default class OAuthState extends AbstractState {
+export default class InitOAuthAuthCodeFlowState extends AbstractState {
     enter(context: AuthContext): Promise<void> | void {
         const { query, params } = context.getRequest();
 
         return context
             .run('oAuthValidate', {
-                clientId: query.get('client_id') || params.clientId,
-                redirectUrl: query.get('redirect_uri')!,
-                responseType: query.get('response_type')!,
+                params: {
+                    clientId: query.get('client_id') || params.clientId,
+                    redirectUrl: query.get('redirect_uri')!,
+                    responseType: query.get('response_type')!,
+                    scope: (query.get('scope') || '').replace(/,/g, ' '),
+                    state: query.get('state')!,
+                },
                 description: query.get('description')!,
-                scope: (query.get('scope') || '').replace(/,/g, ' '),
                 prompt: query.get('prompt')!,
                 loginHint: query.get('login_hint')!,
-                state: query.get('state')!,
             })
             .then(() => context.setState(new CompleteState()));
     }
diff --git a/packages/app/services/authFlow/InitOAuthDeviceCodeFlowState.ts b/packages/app/services/authFlow/InitOAuthDeviceCodeFlowState.ts
new file mode 100644
index 0000000..022f1db
--- /dev/null
+++ b/packages/app/services/authFlow/InitOAuthDeviceCodeFlowState.ts
@@ -0,0 +1,27 @@
+import AbstractState from './AbstractState';
+import { AuthContext } from './AuthFlow';
+import CompleteState from './CompleteState';
+import DeviceCodeState from './DeviceCodeState';
+
+export default class InitOAuthDeviceCodeFlowState extends AbstractState {
+    async enter(context: AuthContext): Promise<void> {
+        const { query } = context.getRequest();
+
+        const userCode = query.get('user_code');
+
+        if (userCode) {
+            try {
+                await context.run('oAuthValidate', {
+                    params: { userCode },
+                    description: query.get('description')!,
+                    prompt: query.get('prompt')!,
+                });
+                await context.setState(new CompleteState());
+            } catch {
+                // Ok, fallback to the default
+            }
+        }
+
+        return context.setState(new DeviceCodeState());
+    }
+}
diff --git a/packages/app/services/errorsDict/errorsDict.tsx b/packages/app/services/errorsDict/errorsDict.tsx
index 7095cf5..12fdad6 100644
--- a/packages/app/services/errorsDict/errorsDict.tsx
+++ b/packages/app/services/errorsDict/errorsDict.tsx
@@ -127,6 +127,8 @@ const errorsMap: Record<string, (props?: Record<string, any>) => ReactElement> =
 
     'error.redirectUri_required': () => <Message key="redirectUriRequired" defaultMessage="Redirect URI is required" />,
     'error.redirectUri_invalid': () => <Message key="redirectUriInvalid" defaultMessage="Redirect URI is invalid" />,
+
+    'invalid_user_code': () => <Message key="invalidUserCode" defaultMessage="Invalid Device Code" />,
 };
 
 interface ErrorLiteral {