Add Keycloak OIDC authentication alongside Cognito
This commit is contained in:
@@ -2,6 +2,13 @@ REACT_APP_AUTH_CALLBACK_URL=https://ota-admin.mini.cloud.fiskerinc.com
|
|||||||
REACT_APP_AUTH_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/compute_auth
|
REACT_APP_AUTH_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/compute_auth
|
||||||
REACT_APP_CERT_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/certificate
|
REACT_APP_CERT_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/certificate
|
||||||
REACT_APP_ENV=mini
|
REACT_APP_ENV=mini
|
||||||
|
|
||||||
|
# Keycloak OIDC (set to true to enable)
|
||||||
|
REACT_APP_KEYCLOAK_ENABLED=true
|
||||||
|
REACT_APP_KEYCLOAK_URL=https://keycloak.mini.cloud.fiskerinc.com
|
||||||
|
REACT_APP_KEYCLOAK_REALM=compute-auth
|
||||||
|
REACT_APP_KEYCLOAK_CLIENT_ID=ota-portal
|
||||||
|
|
||||||
REACT_APP_MAGNA_PROVIDER=Magna
|
REACT_APP_MAGNA_PROVIDER=Magna
|
||||||
REACT_APP_MAGNA_GROUP_ID=68273225-9da4-4fa7-aea5-38e16ec471fe
|
REACT_APP_MAGNA_GROUP_ID=68273225-9da4-4fa7-aea5-38e16ec471fe
|
||||||
REACT_APP_OTA_SERVICE_URL=https://gateway.mini.cloud.fiskerinc.com/ota_update
|
REACT_APP_OTA_SERVICE_URL=https://gateway.mini.cloud.fiskerinc.com/ota_update
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ ingress:
|
|||||||
className: traefik
|
className: traefik
|
||||||
|
|
||||||
image:
|
image:
|
||||||
registry: gitea.mini.cloud.fiskerinc.com
|
registry: fiskercloud.azurecr.io
|
||||||
name: admin/ota-admin-portal
|
name: ota-admin-portal
|
||||||
tag: latest
|
tag: keycloak
|
||||||
pullPolicy: Never
|
pullPolicy: Always
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
|
|||||||
106
package-lock.json
generated
106
package-lock.json
generated
@@ -34,10 +34,12 @@
|
|||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"material-ui-dropzone": "^3.5.0",
|
"material-ui-dropzone": "^3.5.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"oidc-client-ts": "^3.4.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-leaflet": "^3.2.5",
|
"react-leaflet": "^3.2.5",
|
||||||
"react-leaflet-cluster": "^1.0.3",
|
"react-leaflet-cluster": "^1.0.3",
|
||||||
|
"react-oidc-context": "^3.3.0",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^5.3.0",
|
||||||
"react-router-hash-link": "^2.4.3",
|
"react-router-hash-link": "^2.4.3",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
@@ -3952,6 +3954,21 @@
|
|||||||
"string.prototype.matchall": "^4.0.6"
|
"string.prototype.matchall": "^4.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@surma/rollup-plugin-off-main-thread/node_modules/ejs": {
|
||||||
|
"version": "3.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
|
||||||
|
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jake": "^10.8.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ejs": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz",
|
||||||
@@ -7361,20 +7378,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||||
},
|
},
|
||||||
"node_modules/ejs": {
|
|
||||||
"version": "3.1.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
|
||||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
|
||||||
"dependencies": {
|
|
||||||
"jake": "^10.8.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"ejs": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.567",
|
"version": "1.4.567",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.567.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.567.tgz",
|
||||||
@@ -12215,6 +12218,27 @@
|
|||||||
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
||||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
|
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/oidc-client-ts": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jwt-decode": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/oidc-client-ts/node_modules/jwt-decode": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@@ -14170,6 +14194,19 @@
|
|||||||
"react-leaflet": "^3.0.2"
|
"react-leaflet": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-oidc-context": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-302T/ma4AOVAxrHdYctDSKXjCq9KNHT564XEO2yOPxRfxEP58xa4nz+GQinNl8x7CnEXECSM5JEjQJk3Cr5BvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"oidc-client-ts": "^3.1.0",
|
||||||
|
"react": ">=16.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
@@ -19970,10 +20007,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
|
||||||
"integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==",
|
"integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ejs": "^3.1.6",
|
"ejs": "3.1.9",
|
||||||
"json5": "^2.2.0",
|
"json5": "^2.2.0",
|
||||||
"magic-string": "^0.25.0",
|
"magic-string": "^0.25.0",
|
||||||
"string.prototype.matchall": "^4.0.6"
|
"string.prototype.matchall": "^4.0.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ejs": {
|
||||||
|
"version": "3.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
|
||||||
|
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
|
||||||
|
"requires": {
|
||||||
|
"jake": "^10.8.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@svgr/babel-plugin-add-jsx-attribute": {
|
"@svgr/babel-plugin-add-jsx-attribute": {
|
||||||
@@ -22470,14 +22517,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||||
},
|
},
|
||||||
"ejs": {
|
|
||||||
"version": "3.1.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
|
||||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
|
||||||
"requires": {
|
|
||||||
"jake": "^10.8.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.4.567",
|
"version": "1.4.567",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.567.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.567.tgz",
|
||||||
@@ -26023,6 +26062,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
||||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
|
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
|
||||||
},
|
},
|
||||||
|
"oidc-client-ts": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==",
|
||||||
|
"requires": {
|
||||||
|
"jwt-decode": "^4.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jwt-decode": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"on-finished": {
|
"on-finished": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
@@ -27253,6 +27307,12 @@
|
|||||||
"leaflet.markercluster": "^1.4.1"
|
"leaflet.markercluster": "^1.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-oidc-context": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-302T/ma4AOVAxrHdYctDSKXjCq9KNHT564XEO2yOPxRfxEP58xa4nz+GQinNl8x7CnEXECSM5JEjQJk3Cr5BvA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-refresh": {
|
"react-refresh": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||||
|
|||||||
@@ -29,10 +29,12 @@
|
|||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"material-ui-dropzone": "^3.5.0",
|
"material-ui-dropzone": "^3.5.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"oidc-client-ts": "^3.4.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-leaflet": "^3.2.5",
|
"react-leaflet": "^3.2.5",
|
||||||
"react-leaflet-cluster": "^1.0.3",
|
"react-leaflet-cluster": "^1.0.3",
|
||||||
|
"react-oidc-context": "^3.3.0",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^5.3.0",
|
||||||
"react-router-hash-link": "^2.4.3",
|
"react-router-hash-link": "^2.4.3",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { AuthProvider } from "react-oidc-context";
|
||||||
import { UserProvider } from "../Contexts/UserContext";
|
import { UserProvider } from "../Contexts/UserContext";
|
||||||
import { StatusProvider } from "../Contexts/StatusContext";
|
import { StatusProvider } from "../Contexts/StatusContext";
|
||||||
import { CssBaseline } from "@material-ui/core";
|
import { CssBaseline } from "@material-ui/core";
|
||||||
import MenuDrawer from "../Layouts/MenuDrawer";
|
import MenuDrawer from "../Layouts/MenuDrawer";
|
||||||
import SiteRoutes from "../Routes/SiteRoutes";
|
import SiteRoutes from "../Routes/SiteRoutes";
|
||||||
import { } from "../../services/monitoring";
|
import { } from "../../services/monitoring";
|
||||||
|
import { keycloakConfig, isKeycloakEnabled } from "../../services/keycloak";
|
||||||
|
|
||||||
function App() {
|
function AppContent() {
|
||||||
return (
|
return (
|
||||||
<StatusProvider>
|
<StatusProvider>
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
@@ -22,4 +24,17 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// Only wrap with AuthProvider if Keycloak is enabled
|
||||||
|
if (isKeycloakEnabled()) {
|
||||||
|
return (
|
||||||
|
<AuthProvider {...keycloakConfig}>
|
||||||
|
<AppContent />
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AppContent />;
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import auth from "../../services/auth";
|
|||||||
import getTimerWorker from "../../services/getTimerWorker";
|
import getTimerWorker from "../../services/getTimerWorker";
|
||||||
import { parsePayload } from "../../utils/jwt";
|
import { parsePayload } from "../../utils/jwt";
|
||||||
import {getGroups, getProviders} from "../../utils/roles";
|
import {getGroups, getProviders} from "../../utils/roles";
|
||||||
|
import { isKeycloakEnabled } from "../../services/keycloak";
|
||||||
|
|
||||||
const UserContext = React.createContext();
|
const UserContext = React.createContext();
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ export const UserProvider = ({ children }) => {
|
|||||||
const [groups, setGroups] = useState(null);
|
const [groups, setGroups] = useState(null);
|
||||||
const [providers, setProviders] = useState(null);
|
const [providers, setProviders] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [authSource, setAuthSource] = useState(null); // 'cognito' or 'keycloak'
|
||||||
let timer;
|
let timer;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -19,8 +21,18 @@ export const UserProvider = ({ children }) => {
|
|||||||
if (!localStorage) return;
|
if (!localStorage) return;
|
||||||
const t = JSON.parse(localStorage.getItem("token"));
|
const t = JSON.parse(localStorage.getItem("token"));
|
||||||
if (!t) return;
|
if (!t) return;
|
||||||
|
|
||||||
|
// Check if it's a Keycloak token (different structure)
|
||||||
|
if (t.authSource === 'keycloak') {
|
||||||
|
setToken(t);
|
||||||
|
setAuthSource('keycloak');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cognito token format
|
||||||
if (!t.idToken || !t.idToken.jwtToken) throw new Error("Invalid token");
|
if (!t.idToken || !t.idToken.jwtToken) throw new Error("Invalid token");
|
||||||
setToken(t);
|
setToken(t);
|
||||||
|
setAuthSource('cognito');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.location = signOut();
|
document.location = signOut();
|
||||||
}
|
}
|
||||||
@@ -29,12 +41,16 @@ export const UserProvider = ({ children }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
if (authSource === 'keycloak') {
|
||||||
|
verifyKeycloakToken();
|
||||||
|
} else {
|
||||||
verifyToken();
|
verifyToken();
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (timer) timer.terminate();
|
if (timer) timer.terminate();
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [token]);
|
}, [token, authSource]);
|
||||||
|
|
||||||
const refreshTokens = async () => {
|
const refreshTokens = async () => {
|
||||||
if (!token || !token.refreshToken || !token.refreshToken.token) return null;
|
if (!token || !token.refreshToken || !token.refreshToken.token) return null;
|
||||||
@@ -42,25 +58,57 @@ export const UserProvider = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startSessionTimer = () => {
|
const startSessionTimer = () => {
|
||||||
|
let jwtToken;
|
||||||
|
if (authSource === 'keycloak') {
|
||||||
|
jwtToken = token.idToken;
|
||||||
|
} else {
|
||||||
if (!token || !token.idToken || !token.idToken.jwtToken) {
|
if (!token || !token.idToken || !token.idToken.jwtToken) {
|
||||||
throw new Error("No id token");
|
throw new Error("No id token");
|
||||||
}
|
}
|
||||||
const payload = parsePayload(token.idToken.jwtToken);
|
jwtToken = token.idToken.jwtToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = parsePayload(jwtToken);
|
||||||
if (!payload || !payload.exp) throw new Error("Bad id token payload");
|
if (!payload || !payload.exp) throw new Error("Bad id token payload");
|
||||||
const duration = 1000 * payload.exp - new Date().getTime();
|
const duration = 1000 * payload.exp - new Date().getTime();
|
||||||
if (!timer) {
|
if (!timer) {
|
||||||
timer = getTimerWorker();
|
timer = getTimerWorker();
|
||||||
timer.onMessage(async (e) => {
|
timer.onMessage(async (e) => {
|
||||||
if (e.data === "timeout") {
|
if (e.data === "timeout") {
|
||||||
|
if (authSource === 'keycloak') {
|
||||||
|
// For Keycloak, just sign out - OIDC library handles refresh
|
||||||
|
document.location = signOut();
|
||||||
|
} else {
|
||||||
const t = await refreshTokens();
|
const t = await refreshTokens();
|
||||||
if (t && !t.error) return;
|
if (t && !t.error) return;
|
||||||
document.location = signOut();
|
document.location = signOut();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
timer.start(duration);
|
timer.start(duration);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const verifyKeycloakToken = async () => {
|
||||||
|
try {
|
||||||
|
const idToken = token.idToken;
|
||||||
|
const payload = parsePayload(idToken);
|
||||||
|
|
||||||
|
// For Keycloak, we trust the token if it's not expired
|
||||||
|
if (!payload || !payload.exp) throw new Error("Invalid Keycloak token");
|
||||||
|
if (payload.exp * 1000 < Date.now()) throw new Error("Token expired");
|
||||||
|
|
||||||
|
// Extract groups from Keycloak token (realm_access.roles or groups claim)
|
||||||
|
const keycloakGroups = payload.groups || payload.realm_access?.roles || [];
|
||||||
|
setGroups(keycloakGroups);
|
||||||
|
setProviders([]);
|
||||||
|
startSessionTimer();
|
||||||
|
} catch (e) {
|
||||||
|
setError(`Keycloak verify error. ${e.message}`);
|
||||||
|
document.location = signOut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const verifyToken = async () => {
|
const verifyToken = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@@ -96,6 +144,7 @@ export const UserProvider = ({ children }) => {
|
|||||||
throw new Error(result.message);
|
throw new Error(result.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAuthSource('cognito');
|
||||||
signedIn(result);
|
signedIn(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Sign in error. ${err.message}`);
|
setError(`Sign in error. ${err.message}`);
|
||||||
@@ -106,13 +155,42 @@ export const UserProvider = ({ children }) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sign in with Keycloak OIDC token
|
||||||
|
const signInWithKeycloak = (oidcUser) => {
|
||||||
|
if (!oidcUser || !oidcUser.id_token) return;
|
||||||
|
|
||||||
|
const keycloakToken = {
|
||||||
|
authSource: 'keycloak',
|
||||||
|
idToken: oidcUser.id_token,
|
||||||
|
accessToken: oidcUser.access_token,
|
||||||
|
refreshToken: oidcUser.refresh_token,
|
||||||
|
profile: oidcUser.profile,
|
||||||
|
};
|
||||||
|
|
||||||
|
setAuthSource('keycloak');
|
||||||
|
setToken(keycloakToken);
|
||||||
|
if (localStorage) {
|
||||||
|
localStorage.setItem("token", JSON.stringify(keycloakToken));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const signOut = () => {
|
const signOut = () => {
|
||||||
setGroups(null);
|
setGroups(null);
|
||||||
setProviders(null);
|
setProviders(null);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
|
setAuthSource(null);
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For Keycloak, we need to redirect to Keycloak logout
|
||||||
|
if (authSource === 'keycloak' && isKeycloakEnabled()) {
|
||||||
|
const keycloakUrl = process.env.REACT_APP_KEYCLOAK_URL || 'https://keycloak.mini.cloud.fiskerinc.com';
|
||||||
|
const realm = process.env.REACT_APP_KEYCLOAK_REALM || 'compute-auth';
|
||||||
|
const redirectUri = process.env.REACT_APP_AUTH_CALLBACK_URL;
|
||||||
|
return `${keycloakUrl}/realms/${realm}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(redirectUri)}`;
|
||||||
|
}
|
||||||
|
|
||||||
return getLogoutURL();
|
return getLogoutURL();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -158,10 +236,12 @@ export const UserProvider = ({ children }) => {
|
|||||||
groups,
|
groups,
|
||||||
providers,
|
providers,
|
||||||
token,
|
token,
|
||||||
|
authSource,
|
||||||
getAuthorizeURL,
|
getAuthorizeURL,
|
||||||
getLogoutURL,
|
getLogoutURL,
|
||||||
setError,
|
setError,
|
||||||
signIn,
|
signIn,
|
||||||
|
signInWithKeycloak,
|
||||||
signOut,
|
signOut,
|
||||||
refresh,
|
refresh,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useEffect } from "react";
|
|||||||
|
|
||||||
import { useUserContext } from "../Contexts/UserContext";
|
import { useUserContext } from "../Contexts/UserContext";
|
||||||
import useStyles from "../useStyles";
|
import useStyles from "../useStyles";
|
||||||
|
import { isKeycloakEnabled } from "../../services/keycloak";
|
||||||
|
|
||||||
const getCode = (search) => {
|
const getCode = (search) => {
|
||||||
if (!search) return null;
|
if (!search) return null;
|
||||||
@@ -11,7 +12,68 @@ const getCode = (search) => {
|
|||||||
return s.get("code");
|
return s.get("code");
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SignInForm() {
|
// Keycloak-enabled version of the form
|
||||||
|
function KeycloakSignInForm() {
|
||||||
|
const { useAuth } = require("react-oidc-context");
|
||||||
|
const classes = useStyles();
|
||||||
|
const { getAuthorizeURL, signIn, signInWithKeycloak, fetching } = useUserContext();
|
||||||
|
const auth = useAuth();
|
||||||
|
|
||||||
|
// Handle Cognito callback
|
||||||
|
useEffect(() => {
|
||||||
|
const code = getCode(document.location.search);
|
||||||
|
// Only process if it's a Cognito code (not a Keycloak callback)
|
||||||
|
// Keycloak uses 'state' param that react-oidc-context handles
|
||||||
|
if (!code || auth.isLoading || auth.isAuthenticated) return;
|
||||||
|
signIn(code);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle Keycloak authentication callback
|
||||||
|
useEffect(() => {
|
||||||
|
if (auth.isAuthenticated && auth.user) {
|
||||||
|
signInWithKeycloak(auth.user);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [auth.isAuthenticated, auth.user]);
|
||||||
|
|
||||||
|
const handleKeycloakLogin = () => {
|
||||||
|
auth.signinRedirect();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx(classes.paper, classes.textJustifyAlign)}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
href={getAuthorizeURL()}
|
||||||
|
disabled={fetching}
|
||||||
|
>
|
||||||
|
{fetching ? "Please wait..." : "Sign In with Cognito"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div style={{ margin: '16px 0', textAlign: 'center', color: '#666' }}>
|
||||||
|
— or —
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
onClick={handleKeycloakLogin}
|
||||||
|
disabled={fetching || auth.isLoading}
|
||||||
|
>
|
||||||
|
{auth.isLoading ? "Loading..." : "Sign In with Keycloak"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<p><strong>Note: Your email address will be used as the user id</strong></p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard Cognito-only form
|
||||||
|
function CognitoSignInForm() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { getAuthorizeURL, signIn, fetching } = useUserContext();
|
const { getAuthorizeURL, signIn, fetching } = useUserContext();
|
||||||
|
|
||||||
@@ -38,3 +100,10 @@ export default function SignInForm() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function SignInForm() {
|
||||||
|
if (isKeycloakEnabled()) {
|
||||||
|
return <KeycloakSignInForm />;
|
||||||
|
}
|
||||||
|
return <CognitoSignInForm />;
|
||||||
|
}
|
||||||
|
|||||||
21
src/services/keycloak.js
Normal file
21
src/services/keycloak.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Keycloak OIDC configuration
|
||||||
|
const KEYCLOAK_URL = process.env.REACT_APP_KEYCLOAK_URL || 'https://keycloak.mini.cloud.fiskerinc.com';
|
||||||
|
const KEYCLOAK_REALM = process.env.REACT_APP_KEYCLOAK_REALM || 'compute-auth';
|
||||||
|
const KEYCLOAK_CLIENT_ID = process.env.REACT_APP_KEYCLOAK_CLIENT_ID || 'ota-portal';
|
||||||
|
const CALLBACK_URL = process.env.REACT_APP_AUTH_CALLBACK_URL;
|
||||||
|
|
||||||
|
export const keycloakConfig = {
|
||||||
|
authority: `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}`,
|
||||||
|
client_id: KEYCLOAK_CLIENT_ID,
|
||||||
|
redirect_uri: CALLBACK_URL,
|
||||||
|
post_logout_redirect_uri: CALLBACK_URL,
|
||||||
|
response_type: 'code',
|
||||||
|
scope: 'openid profile email roles',
|
||||||
|
// Handle silent renew and callback automatically
|
||||||
|
automaticSilentRenew: true,
|
||||||
|
loadUserInfo: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isKeycloakEnabled = () => {
|
||||||
|
return process.env.REACT_APP_KEYCLOAK_ENABLED === 'true';
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user