diff --git a/frontend/src/App/Login/LoginByGoogle/index.tsx b/frontend/src/App/Login/LoginByGoogle/index.tsx
new file mode 100644
index 0000000000..b83a93f187
--- /dev/null
+++ b/frontend/src/App/Login/LoginByGoogle/index.tsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import cn from 'classnames';
+
+import { Button } from 'components';
+
+import { goToUrl } from 'libs';
+import { useGoogleAuthorizeMutation } from 'services/auth';
+
+import { ReactComponent as GoogleIcon } from 'assets/icons/google.svg';
+import styles from './styles.module.scss';
+
+export const LoginByGoogle: React.FC<{ className?: string }> = ({ className }) => {
+ const { t } = useTranslation();
+
+ const [googleAuthorize, { isLoading }] = useGoogleAuthorizeMutation();
+
+ const signInClick = () => {
+ googleAuthorize()
+ .unwrap()
+ .then((data) => {
+ goToUrl(data.authorization_url);
+ })
+ .catch(console.log);
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/frontend/src/App/Login/LoginByGoogle/styles.module.scss b/frontend/src/App/Login/LoginByGoogle/styles.module.scss
new file mode 100644
index 0000000000..93407bda8d
--- /dev/null
+++ b/frontend/src/App/Login/LoginByGoogle/styles.module.scss
@@ -0,0 +1,20 @@
+@use '@cloudscape-design/design-tokens/index' as awsui;
+
+.signIn {
+ display: flex;
+ justify-content: center;
+
+ button {
+ .loginButtonInner {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ }
+
+ .loginButtonLabel {
+ height: 20px;
+ line-height: 21px;
+ }
+ }
+}
diff --git a/frontend/src/App/Login/LoginByGoogleCallback/index.tsx b/frontend/src/App/Login/LoginByGoogleCallback/index.tsx
new file mode 100644
index 0000000000..465d0be3ee
--- /dev/null
+++ b/frontend/src/App/Login/LoginByGoogleCallback/index.tsx
@@ -0,0 +1,63 @@
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+
+import { NavigateLink } from 'components';
+import { UnauthorizedLayout } from 'layouts/UnauthorizedLayout';
+
+import { useAppDispatch } from 'hooks';
+import { ROUTES } from 'routes';
+import { useGoogleCallbackMutation } from 'services/auth';
+
+import { AuthErrorMessage } from 'App/AuthErrorMessage';
+import { Loading } from 'App/Loading';
+import { setAuthData } from 'App/slice';
+
+export const LoginByGoogleCallback: React.FC = () => {
+ const { t } = useTranslation();
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
+ const code = searchParams.get('code');
+ const state = searchParams.get('state');
+ const [isInvalidCode, setIsInvalidCode] = useState(false);
+ const dispatch = useAppDispatch();
+
+ const [googleCallback] = useGoogleCallbackMutation();
+
+ const checkCode = () => {
+ if (code && state) {
+ googleCallback({ code, state })
+ .unwrap()
+ .then(({ creds: { token } }) => {
+ dispatch(setAuthData({ token }));
+ navigate('/');
+ })
+ .catch(() => {
+ setIsInvalidCode(true);
+ });
+ }
+ };
+
+ useEffect(() => {
+ if (code && state) {
+ checkCode();
+ } else {
+ setIsInvalidCode(true);
+ }
+ }, []);
+
+ if (isInvalidCode)
+ return (
+
+
+ {t('auth.try_again')}
+
+
+ );
+
+ return (
+
+ ;
+
+ );
+};
diff --git a/frontend/src/App/index.tsx b/frontend/src/App/index.tsx
index 67006b8168..de0f919079 100644
--- a/frontend/src/App/index.tsx
+++ b/frontend/src/App/index.tsx
@@ -19,6 +19,7 @@ const IGNORED_AUTH_PATHS = [
ROUTES.AUTH.GITHUB_CALLBACK,
ROUTES.AUTH.OKTA_CALLBACK,
ROUTES.AUTH.ENTRA_CALLBACK,
+ ROUTES.AUTH.GOOGLE_CALLBACK,
ROUTES.AUTH.TOKEN,
];
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
index 226d5edde3..46b3730f65 100644
--- a/frontend/src/api.ts
+++ b/frontend/src/api.ts
@@ -22,6 +22,12 @@ export const API = {
AUTHORIZE: () => `${API.AUTH.ENTRA.BASE()}/authorize`,
CALLBACK: () => `${API.AUTH.ENTRA.BASE()}/callback`,
},
+ GOOGLE: {
+ BASE: () => `${API.AUTH.BASE()}/google`,
+ INFO: () => `${API.AUTH.GOOGLE.BASE()}/info`,
+ AUTHORIZE: () => `${API.AUTH.GOOGLE.BASE()}/authorize`,
+ CALLBACK: () => `${API.AUTH.GOOGLE.BASE()}/callback`,
+ },
},
USERS: {
diff --git a/frontend/src/assets/icons/google.svg b/frontend/src/assets/icons/google.svg
new file mode 100644
index 0000000000..b352dde5d2
--- /dev/null
+++ b/frontend/src/assets/icons/google.svg
@@ -0,0 +1 @@
+
diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json
index 7aedc2d8ca..e1e0c90f25 100644
--- a/frontend/src/locale/en.json
+++ b/frontend/src/locale/en.json
@@ -32,6 +32,7 @@
"login_github": "Sign in with GitHub",
"login_okta": "Sign in with Okta",
"login_entra": "Sign in with EntraID",
+ "login_google": "Sign in with Google",
"general": "General",
"test": "Test",
"local_storage_unavailable": "Local Storage is unavailable",
diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx
index a953cbc6b3..59f215d28d 100644
--- a/frontend/src/router.tsx
+++ b/frontend/src/router.tsx
@@ -7,6 +7,7 @@ import App from 'App';
import { LoginByEntraIDCallback } from 'App/Login/EntraID/LoginByEntraIDCallback';
import { LoginByGithubCallback } from 'App/Login/LoginByGithubCallback';
import { LoginByOktaCallback } from 'App/Login/LoginByOktaCallback';
+import { LoginByGoogleCallback } from 'App/Login/LoginByGoogleCallback';
import { TokenLogin } from 'App/Login/TokenLogin';
import { Logout } from 'App/Logout';
import { FleetDetails, FleetList } from 'pages/Fleets';
@@ -45,6 +46,10 @@ export const router = createBrowserRouter([
path: ROUTES.AUTH.ENTRA_CALLBACK,
element: ,
},
+ {
+ path: ROUTES.AUTH.GOOGLE_CALLBACK,
+ element: ,
+ },
{
path: ROUTES.AUTH.TOKEN,
element: ,
diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts
index 335d6378be..3df4544656 100644
--- a/frontend/src/routes.ts
+++ b/frontend/src/routes.ts
@@ -8,6 +8,7 @@ export const ROUTES = {
GITHUB_CALLBACK: `/auth/github/callback`,
OKTA_CALLBACK: `/auth/okta/callback`,
ENTRA_CALLBACK: `/auth/entra/callback`,
+ GOOGLE_CALLBACK: `/auth/google/callback`,
TOKEN: `/auth/token`,
},
diff --git a/frontend/src/services/auth.ts b/frontend/src/services/auth.ts
index f566bf3328..5b1f99d406 100644
--- a/frontend/src/services/auth.ts
+++ b/frontend/src/services/auth.ts
@@ -75,6 +75,30 @@ export const authApi = createApi({
body,
}),
}),
+
+ getGoogleInfo: builder.query<{ enabled: boolean }, void>({
+ query: () => {
+ return {
+ url: API.AUTH.GOOGLE.INFO(),
+ method: 'POST',
+ };
+ },
+ }),
+
+ googleAuthorize: builder.mutation<{ authorization_url: string }, void>({
+ query: () => ({
+ url: API.AUTH.GOOGLE.AUTHORIZE(),
+ method: 'POST',
+ }),
+ }),
+
+ googleCallback: builder.mutation({
+ query: (body) => ({
+ url: API.AUTH.GOOGLE.CALLBACK(),
+ method: 'POST',
+ body,
+ }),
+ }),
}),
});
@@ -87,4 +111,7 @@ export const {
useGetEntraInfoQuery,
useEntraAuthorizeMutation,
useEntraCallbackMutation,
+ useGetGoogleInfoQuery,
+ useGoogleAuthorizeMutation,
+ useGoogleCallbackMutation,
} = authApi;