diff --git a/.env b/.env
index 95e86688b..799e92c23 100644
--- a/.env
+++ b/.env
@@ -12,6 +12,8 @@ REACT_APP_FEATURE_PUBLIC_HANKKEET=1
REACT_APP_FEATURE_HANKE=1
REACT_APP_FEATURE_CABLE_REPORT_PAPER_DECISION=1
REACT_APP_FEATURE_INFORMATION_REQUEST=1
+REACT_APP_USE_AD_FILTER=0
+REACT_APP_ALLOWED_AD_GROUPS=""
REACT_APP_MAINTENANCE_TEXT_FI=""
REACT_APP_MAINTENANCE_TEXT_SV=""
REACT_APP_MAINTENANCE_TEXT_EN=""
diff --git a/src/common/utils/toStringArray.ts b/src/common/utils/toStringArray.ts
new file mode 100644
index 000000000..ff7e93a8f
--- /dev/null
+++ b/src/common/utils/toStringArray.ts
@@ -0,0 +1,10 @@
+/**
+ * Converts an unknown value to an array of strings.
+ */
+export default function toStringArray(value: unknown): string[] {
+ if (Array.isArray(value)) {
+ return value.filter((item) => typeof item === 'string');
+ } else {
+ return [];
+ }
+}
diff --git a/src/domain/auth/components/ADGroupsError.tsx b/src/domain/auth/components/ADGroupsError.tsx
new file mode 100644
index 000000000..74cb03420
--- /dev/null
+++ b/src/domain/auth/components/ADGroupsError.tsx
@@ -0,0 +1,49 @@
+import { Box, Flex } from '@chakra-ui/react';
+import { Link, useOidcClient } from 'hds-react';
+import MainHeading from '../../../common/components/mainHeading/MainHeading';
+import { Trans, useTranslation } from 'react-i18next';
+
+export default function ADGroupsError() {
+ const { t } = useTranslation();
+ const oidcClient = useOidcClient();
+
+ function logout() {
+ oidcClient.logout();
+ }
+
+ return (
+
+ {t('authentication:noADPermissionHeading')}
+
+
+ Kirjaudu ulos
+
+ ),
+ }}
+ />
+
+
+ );
+}
diff --git a/src/domain/auth/components/OidcCallback.test.tsx b/src/domain/auth/components/OidcCallback.test.tsx
index 5ba51364e..d6af15ecc 100644
--- a/src/domain/auth/components/OidcCallback.test.tsx
+++ b/src/domain/auth/components/OidcCallback.test.tsx
@@ -1,6 +1,6 @@
import { Routes, Route, MemoryRouter } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
-import { cleanup, waitFor } from '@testing-library/react';
+import { cleanup } from '@testing-library/react';
import OidcCallback from './OidcCallback';
import { LOGIN_CALLBACK_PATH } from '../constants';
import i18n from '../../../locales/i18nForTests';
@@ -9,15 +9,17 @@ import {
renderWithLoginProvider,
} from '../testUtils/renderWithLoginProvider';
-function getWrapper({ state, returnUser, errorType }: RenderWithLoginProviderProps) {
+function getWrapper({ state, returnUser, errorType, userADGroups }: RenderWithLoginProviderProps) {
return renderWithLoginProvider({
state,
returnUser,
errorType,
+ userADGroups,
children: (
+ Home page} />
} />
@@ -59,9 +61,69 @@ describe('', () => {
).toBeInTheDocument();
});
- it('should redirect user after successful login', async () => {
- getWrapper({ state: 'NO_SESSION', returnUser: true, errorType: 'RENEWAL_FAILED' });
+ it('should redirect user to home page after successful login', async () => {
+ const { findByText } = getWrapper({
+ state: 'NO_SESSION',
+ returnUser: true,
+ errorType: undefined,
+ });
- await waitFor(() => expect(window.location.pathname).toEqual('/'));
+ expect(await findByText('Home page')).toBeInTheDocument();
+ });
+
+ describe('helsinki AD login', () => {
+ it('should show AD group error message when user does not have required AD groups', async () => {
+ const OLD_ENV = { ...window._env_ };
+ window._env_ = {
+ ...OLD_ENV,
+ REACT_APP_USE_AD_FILTER: '1',
+ REACT_APP_ALLOWED_AD_GROUPS: 'test_group_2;test_group_3',
+ };
+ const { findByText } = getWrapper({
+ state: 'NO_SESSION',
+ returnUser: true,
+ errorType: undefined,
+ userADGroups: ['test_group'],
+ });
+
+ expect(await findByText('Ei käyttöoikeutta Haitaton-asiointiin')).toBeInTheDocument();
+ window._env_ = OLD_ENV;
+ });
+
+ it('should redirect user to home page when user has required AD groups', async () => {
+ const OLD_ENV = { ...window._env_ };
+ window._env_ = {
+ ...OLD_ENV,
+ REACT_APP_USE_AD_FILTER: '1',
+ REACT_APP_ALLOWED_AD_GROUPS: 'test_group;test_group_2;test_group_3',
+ };
+ const { findByText } = getWrapper({
+ state: 'NO_SESSION',
+ returnUser: true,
+ errorType: undefined,
+ userADGroups: ['test_group'],
+ });
+
+ expect(await findByText('Home page')).toBeInTheDocument();
+ window._env_ = OLD_ENV;
+ });
+
+ it('should redirect user to home page when REACT_APP_USE_AD_FILTER is not in use even when user does not have required AD groups', async () => {
+ const OLD_ENV = { ...window._env_ };
+ window._env_ = {
+ ...OLD_ENV,
+ REACT_APP_USE_AD_FILTER: '0',
+ REACT_APP_ALLOWED_AD_GROUPS: 'test_group_2',
+ };
+ const { findByText } = getWrapper({
+ state: 'NO_SESSION',
+ returnUser: true,
+ errorType: undefined,
+ userADGroups: ['test_group'],
+ });
+
+ expect(await findByText('Home page')).toBeInTheDocument();
+ window._env_ = OLD_ENV;
+ });
});
});
diff --git a/src/domain/auth/components/OidcCallback.tsx b/src/domain/auth/components/OidcCallback.tsx
index b8f7a191b..b53d370ae 100644
--- a/src/domain/auth/components/OidcCallback.tsx
+++ b/src/domain/auth/components/OidcCallback.tsx
@@ -1,12 +1,22 @@
import { useState } from 'react';
import { Flex } from '@chakra-ui/react';
import { Trans, useTranslation } from 'react-i18next';
-import { Link as HDSLink, LoginCallbackHandler, OidcClientError } from 'hds-react';
+import {
+ Link as HDSLink,
+ LoginCallbackHandler,
+ OidcClientError,
+ User,
+ useOidcClient,
+} from 'hds-react';
import Text from '../../../common/components/text/Text';
import { useLocalizedRoutes } from '../../../common/hooks/useLocalizedRoutes';
import Link from '../../../common/components/Link/Link';
+import ADGroupsError from './ADGroupsError';
+import hasAllowedADGroups from '../hasAllowedADGroups';
+import { useNavigate } from 'react-router-dom';
+import toStringArray from '../../../common/utils/toStringArray';
-type AuthenticationError = 'permissionDeniedByUserError' | 'unknown';
+type AuthenticationError = 'permissionDeniedByUserError' | 'unknown' | 'adGroupsError';
type AuthErrorProps = {
errorText: string;
@@ -52,9 +62,22 @@ const AuthError = ({ errorText }: Readonly) => {
const OidcCallback = () => {
const { t } = useTranslation();
const [authenticationError, setAuthenticationError] = useState(null);
+ const navigate = useNavigate();
+ const oidcClient = useOidcClient();
- function onSuccess() {
- window.location.pathname = '/';
+ function onSuccess(user: User) {
+ const { ad_groups } = user.profile;
+ const useADFilter = window._env_.REACT_APP_USE_AD_FILTER === '1';
+ const amr = oidcClient.getAmr();
+ const helsinkiADUsed = amr?.includes('helsinkiad');
+
+ // Check if user has required AD groups (when login is done with AD and AD filtering is enabled).
+ if (helsinkiADUsed && useADFilter && !hasAllowedADGroups(toStringArray(ad_groups))) {
+ setAuthenticationError('adGroupsError');
+ return;
+ }
+
+ navigate('/', { replace: true });
}
function onError(error?: OidcClientError) {
@@ -74,6 +97,7 @@ const OidcCallback = () => {
{authenticationError === 'unknown' && (
)}
+ {authenticationError === 'adGroupsError' && }
>
);
diff --git a/src/domain/auth/hasAllowedADGroups.ts b/src/domain/auth/hasAllowedADGroups.ts
new file mode 100644
index 000000000..ee55fe298
--- /dev/null
+++ b/src/domain/auth/hasAllowedADGroups.ts
@@ -0,0 +1,10 @@
+/**
+ * Checks if the user belongs to any of the allowed Active Directory (AD) groups.
+ *
+ * @param {string[]} ad_groups - An array of AD group names the user belongs to.
+ * @returns {boolean} - Returns `true` if the user belongs to at least one of the allowed AD groups, otherwise `false`.
+ */
+export default function hasAllowedADGroups(ad_groups: string[]): boolean {
+ const ALLOWED_AD_GROUPS = window._env_?.REACT_APP_ALLOWED_AD_GROUPS?.split(';') ?? [];
+ return ad_groups.some((group) => ALLOWED_AD_GROUPS.includes(group));
+}
diff --git a/src/domain/auth/testUtils/renderWithLoginProvider.tsx b/src/domain/auth/testUtils/renderWithLoginProvider.tsx
index 528700a80..cd6fe3f6a 100644
--- a/src/domain/auth/testUtils/renderWithLoginProvider.tsx
+++ b/src/domain/auth/testUtils/renderWithLoginProvider.tsx
@@ -27,6 +27,7 @@ export type RenderWithLoginProviderProps = {
state: OidcClientState;
returnUser: boolean;
placeUserToStorage?: boolean;
+ userADGroups?: string[];
errorType?: 'SIGNIN_ERROR' | 'INVALID_OR_EXPIRED_USER' | 'RENEWAL_FAILED';
children?: React.ReactNode;
};
@@ -35,6 +36,7 @@ export function renderWithLoginProvider({
state,
returnUser,
placeUserToStorage = true,
+ userADGroups,
errorType,
children,
}: RenderWithLoginProviderProps) {
@@ -45,10 +47,13 @@ export function renderWithLoginProvider({
connect: (targetBeacon) => {
beacon = targetBeacon;
beacon.addListener(triggerForAllOidcClientSignals, (signal) => {
- const user = createUser(placeUserToStorage);
+ const user = createUser(placeUserToStorage, userADGroups);
const oidcClient = signal.context as OidcClient;
jest.spyOn(oidcClient, 'getState').mockReturnValue(state);
jest.spyOn(oidcClient, 'getUser').mockReturnValue(user);
+ jest
+ .spyOn(oidcClient, 'getAmr')
+ .mockReturnValue(userADGroups === undefined ? ['suomi_fi'] : ['helsinkiad']);
if (!returnUser) {
jest.spyOn(oidcClient, 'handleCallback').mockRejectedValue(handleError);
} else {
diff --git a/src/domain/auth/testUtils/userTestUtil.ts b/src/domain/auth/testUtils/userTestUtil.ts
index 920a4e612..16b870480 100644
--- a/src/domain/auth/testUtils/userTestUtil.ts
+++ b/src/domain/auth/testUtils/userTestUtil.ts
@@ -5,7 +5,7 @@ const client_id = 'test-client';
const tokenExpirationTimeInSeconds = 3600;
-export function createUser(placeUserToStorage = true): User {
+export function createUser(placeUserToStorage = true, ad_groups?: string[]): User {
const nowAsSeconds = Math.round(Date.now() / 1000);
const expires_in = tokenExpirationTimeInSeconds;
const expires_at = nowAsSeconds + expires_in;
@@ -23,6 +23,7 @@ export function createUser(placeUserToStorage = true): User {
name: 'Test User',
email: 'test.user@mail.com',
amr: ['validAmr'],
+ ad_groups,
},
refresh_token: 'refresh_token',
scope: 'openid profile',
diff --git a/src/locales/fi.json b/src/locales/fi.json
index f97067a1b..3bdf07c97 100644
--- a/src/locales/fi.json
+++ b/src/locales/fi.json
@@ -10,7 +10,9 @@
"loggingIn": "Kirjaudutaan sisään...",
"loggingOut": "Kirjaudutaan ulos...",
"loggingInErrorLabel": "Kirjautumisessa tapahtui virhe",
- "logoutError": "Uloskirjautumisessa tapahtui virhe"
+ "logoutError": "Uloskirjautumisessa tapahtui virhe",
+ "noADPermissionHeading": "Ei käyttöoikeutta Haitaton-asiointiin",
+ "noADPermissionText": "Sinulla ei ole oikeutta AD-tunnistautumiseen. Kirjaudu ulos Haitattomasta ja yritä uudelleen suomi.fi-tunnistautumisen kautta."
},
"common": {
"increment": "Lisää",