Skip to content

Commit 3e99142

Browse files
committed
Update auth to use keycloak
1 parent bc6b2e3 commit 3e99142

File tree

14 files changed

+223
-127
lines changed

14 files changed

+223
-127
lines changed

package-lock.json

Lines changed: 14 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/client/.env

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ APP_DESCRIPTION=Plugin based STAC editor
1010
REACT_APP_STAC_BROWSER=
1111
REACT_APP_STAC_API=
1212

13-
# Auth variables
14-
REACT_APP_AUTH0_DOMAIN=
15-
REACT_APP_AUTH0_CLIENT_ID=
13+
## Keycloak auth variables
14+
REACT_APP_KEYCLOAK_URL=
15+
REACT_APP_KEYCLOAK_CLIENT_ID=
16+
REACT_APP_KEYCLOAK_REALM=
1617

1718
## Theming
1819
# REACT_APP_THEME_PRIMARY_COLOR='#6A5ACD'

packages/client/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ PUBLIC_URL
2121
REACT_APP_STAC_BROWSER
2222
REACT_APP_STAC_API
2323
24+
# Auth
25+
REACT_APP_KEYCLOAK_URL
26+
REACT_APP_KEYCLOAK_CLIENT_ID
27+
REACT_APP_KEYCLOAK_REALM
28+
2429
# Theming
2530
REACT_APP_THEME_PRIMARY_COLOR
2631
REACT_APP_THEME_SECONDARY_COLOR
@@ -31,7 +36,5 @@ You must provide a value for the `REACT_APP_STAC_API` environment variable. This
3136
If the `REACT_APP_STAC_BROWSER` environment variable is not set, [Radiant Earth's STAC Browser](https://radiantearth.github.io/stac-browser/) will be used by default, which will connect to the STAC API specified in `REACT_APP_STAC_API`.
3237

3338
**Auth**
34-
The client uses Auth0 for authentication, which is disabled by default. To
35-
enable it you must provide values for the `REACT_APP_AUTH0_DOMAIN` and
36-
`REACT_APP_AUTH0_CLIENT_ID` environment variables. These can be obtained by
37-
creating an Auth0 account and setting up a new application.
39+
The client uses Keycloack for authentication, which is disabled by default. To
40+
enable it you must provide values for the `REACT_APP_KEYCLOAK_*` environment variables. These can be obtained through the Keycloak server.

packages/client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
"watcher": "^2.3.1"
5151
},
5252
"dependencies": {
53-
"@auth0/auth0-react": "^2.3.0",
5453
"@chakra-ui/react": "^2.8.2",
5554
"@developmentseed/stac-react": "^0.1.0-alpha.10",
5655
"@devseed-ui/collecticons-chakra": "^3.0.3",
@@ -70,11 +69,13 @@
7069
"@turf/bbox": "^7.1.0",
7170
"@turf/bbox-polygon": "^7.1.0",
7271
"@types/jest": "^29.5.14",
72+
"@types/keycloak-js": "^2.5.4",
7373
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
7474
"@types/react": "^18.3.12",
7575
"@types/react-dom": "^18.3.1",
7676
"formik": "^2.4.6",
7777
"framer-motion": "^10.16.5",
78+
"keycloak-js": "^26.1.4",
7879
"mapbox-gl-draw-rectangle-mode": "^1.0.4",
7980
"maplibre-gl": "^3.6.2",
8081
"polished": "^4.3.1",

packages/client/src/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { Route, Routes } from 'react-router-dom';
1414
import { CollecticonCog } from '@devseed-ui/collecticons-chakra';
1515

1616
import { RequireAuth } from '$components/auth/RequireAuth';
17-
import { useAuth0IfEnabled } from '$components/auth/authIfEnabled';
1817
import MainNavigation from '$components/MainNavigation';
1918
import Home from '$pages/Home';
2019
import CollectionList from '$pages/CollectionList';
@@ -24,6 +23,8 @@ import NotFound from '$pages/NotFound';
2423
import CollectionDetail from '$pages/CollectionDetail';
2524
import Sandbox from '$pages/Sandbox';
2625

26+
import { useKeycloak } from './auth/Context';
27+
2728
const rotate = keyframes`
2829
from {
2930
transform: rotate(0deg);
@@ -43,7 +44,9 @@ const rotate2 = keyframes`
4344
`;
4445

4546
export function App() {
46-
const { isLoading } = useAuth0IfEnabled();
47+
const { initStatus } = useKeycloak();
48+
49+
const isLoading = initStatus === 'loading';
4750

4851
return (
4952
<>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, {
2+
createContext,
3+
useContext,
4+
useEffect,
5+
useRef,
6+
useState
7+
} from 'react';
8+
import Keycloak, { KeycloakInstance } from 'keycloak-js';
9+
10+
const url = process.env.REACT_APP_KEYCLOAK_URL;
11+
const realm = process.env.REACT_APP_KEYCLOAK_REALM;
12+
const clientId = process.env.REACT_APP_KEYCLOAK_CLIENT_ID;
13+
14+
const isAuthEnabled = !!(url && realm && clientId);
15+
16+
const keycloak: KeycloakInstance | undefined = isAuthEnabled
17+
? new (Keycloak as any)({
18+
url,
19+
realm,
20+
clientId
21+
})
22+
: undefined;
23+
24+
export type KeycloakContextProps = {
25+
initStatus: 'loading' | 'success' | 'error';
26+
isLoading: boolean;
27+
profile?: Keycloak.KeycloakProfile;
28+
} & (
29+
| {
30+
keycloak: KeycloakInstance;
31+
isEnabled: true;
32+
}
33+
| {
34+
keycloak: undefined;
35+
isEnabled: false;
36+
}
37+
);
38+
39+
const KeycloakContext = createContext<KeycloakContextProps>({
40+
initStatus: 'loading',
41+
isEnabled: isAuthEnabled
42+
} as KeycloakContextProps);
43+
44+
export const KeycloakProvider = (props: { children: React.ReactNode }) => {
45+
const [initStatus, setInitStatus] =
46+
useState<KeycloakContextProps['initStatus']>('loading');
47+
const [profile, setProfile] = useState<
48+
Keycloak.KeycloakProfile | undefined
49+
>();
50+
51+
const wasInit = useRef(false);
52+
53+
useEffect(() => {
54+
async function initialize() {
55+
if (!keycloak) return;
56+
// Keycloak can only be initialized once. This is a workaround to avoid
57+
// multiple initialization attempts, specially by React double rendering.
58+
if (wasInit.current) return;
59+
wasInit.current = true;
60+
61+
try {
62+
await keycloak.init({
63+
// onLoad: 'login-required',
64+
onLoad: 'check-sso',
65+
checkLoginIframe: false
66+
});
67+
if (keycloak.authenticated) {
68+
const profile =
69+
await (keycloak.loadUserProfile() as unknown as Promise<Keycloak.KeycloakProfile>);
70+
setProfile(profile);
71+
}
72+
73+
setInitStatus('success');
74+
} catch (err) {
75+
setInitStatus('error');
76+
// eslint-disable-next-line no-console
77+
console.error('Failed to initialize keycloak adapter:', err);
78+
}
79+
}
80+
initialize();
81+
}, []);
82+
83+
const base = {
84+
initStatus,
85+
isLoading: isAuthEnabled && initStatus === 'loading',
86+
profile
87+
};
88+
89+
return (
90+
<KeycloakContext.Provider
91+
value={
92+
isAuthEnabled
93+
? {
94+
...base,
95+
keycloak: keycloak!,
96+
isEnabled: true
97+
}
98+
: {
99+
...base,
100+
keycloak: undefined,
101+
isEnabled: false
102+
}
103+
}
104+
>
105+
{props.children}
106+
</KeycloakContext.Provider>
107+
);
108+
};
109+
110+
export const useKeycloak = () => {
111+
const ctx = useContext(KeycloakContext);
112+
113+
if (!ctx) {
114+
throw new Error('useKeycloak must be used within a KeycloakProvider');
115+
}
116+
117+
return ctx;
118+
};

packages/client/src/components/MainNavigation.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import React from 'react';
2-
import { List, ListItem, Button, ButtonProps, Flex } from '@chakra-ui/react';
2+
import {
3+
List,
4+
ListItem,
5+
Button,
6+
ButtonProps,
7+
Flex,
8+
Divider
9+
} from '@chakra-ui/react';
310
import {
411
CollecticonFolder,
512
CollecticonPlusSmall
613
} from '@devseed-ui/collecticons-chakra';
714

815
import SmartLink, { SmartLinkProps } from './SmartLink';
916
import { UserInfo } from './auth/UserInfo';
10-
import { useAuth0 } from '@auth0/auth0-react';
17+
import { useKeycloak } from 'src/auth/Context';
1118

1219
function NavItem(props: ButtonProps & SmartLinkProps) {
1320
return (
@@ -27,20 +34,26 @@ function NavItem(props: ButtonProps & SmartLinkProps) {
2734
}
2835

2936
function MainNavigation() {
30-
const { isAuthenticated } = useAuth0();
37+
const { keycloak } = useKeycloak();
3138

3239
return (
33-
<Flex as='nav' aria-label='Main' gap={8}>
40+
<Flex as='nav' aria-label='Main' gap={4} alignItems='center'>
3441
<List display='flex' gap={2}>
3542
<NavItem to='/collections/' leftIcon={<CollecticonFolder />}>
3643
Browse
3744
</NavItem>
38-
{isAuthenticated && (
45+
{keycloak?.authenticated && (
3946
<NavItem to='/collections/new' leftIcon={<CollecticonPlusSmall />}>
4047
Create
4148
</NavItem>
4249
)}
4350
</List>
51+
<Divider
52+
orientation='vertical'
53+
borderLeftWidth='2px'
54+
borderColor='base.200'
55+
h='1rem'
56+
/>
4457
<UserInfo />
4558
</Flex>
4659
);

packages/client/src/components/auth/Auth0ProviderNavigate.tsx

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import React from 'react';
22
import { Button, ButtonProps, forwardRef } from '@chakra-ui/react';
33
import SmartLink, { SmartLinkProps } from '../SmartLink';
4-
import { useAuth0IfEnabled } from './authIfEnabled';
4+
import { useKeycloak } from 'src/auth/Context';
55

66
export const ButtonWithAuth = forwardRef<
77
SmartLinkProps & ButtonProps,
88
typeof Button
99
>((props, ref) => {
10-
const auth = useAuth0IfEnabled();
10+
const { isEnabled, keycloak } = useKeycloak();
1111

12-
if (!auth.isEnabled) {
12+
if (!isEnabled) {
1313
return <Button ref={ref} as={SmartLink} {...props} />;
1414
}
1515

16-
return auth.isAuthenticated && <Button ref={ref} as={SmartLink} {...props} />;
16+
return (
17+
keycloak.authenticated && <Button ref={ref} as={SmartLink} {...props} />
18+
);
1719
});

0 commit comments

Comments
 (0)