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_CERT_SERVICE_URL=https://dev-gw.cloud.fiskerinc.com/certificate
|
||||
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_GROUP_ID=68273225-9da4-4fa7-aea5-38e16ec471fe
|
||||
REACT_APP_OTA_SERVICE_URL=https://gateway.mini.cloud.fiskerinc.com/ota_update
|
||||
|
||||
@@ -3,10 +3,10 @@ ingress:
|
||||
className: traefik
|
||||
|
||||
image:
|
||||
registry: gitea.mini.cloud.fiskerinc.com
|
||||
name: admin/ota-admin-portal
|
||||
tag: latest
|
||||
pullPolicy: Never
|
||||
registry: fiskercloud.azurecr.io
|
||||
name: ota-admin-portal
|
||||
tag: keycloak
|
||||
pullPolicy: Always
|
||||
|
||||
resources:
|
||||
requests:
|
||||
|
||||
106
package-lock.json
generated
106
package-lock.json
generated
@@ -34,10 +34,12 @@
|
||||
"leaflet": "^1.8.0",
|
||||
"material-ui-dropzone": "^3.5.0",
|
||||
"moment": "^2.29.4",
|
||||
"oidc-client-ts": "^3.4.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-leaflet": "^3.2.5",
|
||||
"react-leaflet-cluster": "^1.0.3",
|
||||
"react-oidc-context": "^3.3.0",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
@@ -3952,6 +3954,21 @@
|
||||
"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": {
|
||||
"version": "5.4.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.4.567",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -14170,6 +14194,19 @@
|
||||
"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": {
|
||||
"version": "0.11.0",
|
||||
"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",
|
||||
"integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==",
|
||||
"requires": {
|
||||
"ejs": "^3.1.6",
|
||||
"ejs": "3.1.9",
|
||||
"json5": "^2.2.0",
|
||||
"magic-string": "^0.25.0",
|
||||
"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": {
|
||||
@@ -22470,14 +22517,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"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": {
|
||||
"version": "1.4.567",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -27253,6 +27307,12 @@
|
||||
"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": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
|
||||
|
||||
@@ -29,10 +29,12 @@
|
||||
"leaflet": "^1.8.0",
|
||||
"material-ui-dropzone": "^3.5.0",
|
||||
"moment": "^2.29.4",
|
||||
"oidc-client-ts": "^3.4.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-leaflet": "^3.2.5",
|
||||
"react-leaflet-cluster": "^1.0.3",
|
||||
"react-oidc-context": "^3.3.0",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from "react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { AuthProvider } from "react-oidc-context";
|
||||
import { UserProvider } from "../Contexts/UserContext";
|
||||
import { StatusProvider } from "../Contexts/StatusContext";
|
||||
import { CssBaseline } from "@material-ui/core";
|
||||
import MenuDrawer from "../Layouts/MenuDrawer";
|
||||
import SiteRoutes from "../Routes/SiteRoutes";
|
||||
import { } from "../../services/monitoring";
|
||||
import { keycloakConfig, isKeycloakEnabled } from "../../services/keycloak";
|
||||
|
||||
function App() {
|
||||
function AppContent() {
|
||||
return (
|
||||
<StatusProvider>
|
||||
<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;
|
||||
|
||||
@@ -3,6 +3,7 @@ import auth from "../../services/auth";
|
||||
import getTimerWorker from "../../services/getTimerWorker";
|
||||
import { parsePayload } from "../../utils/jwt";
|
||||
import {getGroups, getProviders} from "../../utils/roles";
|
||||
import { isKeycloakEnabled } from "../../services/keycloak";
|
||||
|
||||
const UserContext = React.createContext();
|
||||
|
||||
@@ -12,6 +13,7 @@ export const UserProvider = ({ children }) => {
|
||||
const [groups, setGroups] = useState(null);
|
||||
const [providers, setProviders] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [authSource, setAuthSource] = useState(null); // 'cognito' or 'keycloak'
|
||||
let timer;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -19,8 +21,18 @@ export const UserProvider = ({ children }) => {
|
||||
if (!localStorage) return;
|
||||
const t = JSON.parse(localStorage.getItem("token"));
|
||||
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");
|
||||
setToken(t);
|
||||
setAuthSource('cognito');
|
||||
} catch (e) {
|
||||
document.location = signOut();
|
||||
}
|
||||
@@ -29,12 +41,16 @@ export const UserProvider = ({ children }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return;
|
||||
verifyToken();
|
||||
if (authSource === 'keycloak') {
|
||||
verifyKeycloakToken();
|
||||
} else {
|
||||
verifyToken();
|
||||
}
|
||||
return () => {
|
||||
if (timer) timer.terminate();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
}, [token, authSource]);
|
||||
|
||||
const refreshTokens = async () => {
|
||||
if (!token || !token.refreshToken || !token.refreshToken.token) return null;
|
||||
@@ -42,25 +58,57 @@ export const UserProvider = ({ children }) => {
|
||||
};
|
||||
|
||||
const startSessionTimer = () => {
|
||||
if (!token || !token.idToken || !token.idToken.jwtToken) {
|
||||
throw new Error("No id token");
|
||||
let jwtToken;
|
||||
if (authSource === 'keycloak') {
|
||||
jwtToken = token.idToken;
|
||||
} else {
|
||||
if (!token || !token.idToken || !token.idToken.jwtToken) {
|
||||
throw new Error("No id token");
|
||||
}
|
||||
jwtToken = token.idToken.jwtToken;
|
||||
}
|
||||
const payload = parsePayload(token.idToken.jwtToken);
|
||||
|
||||
const payload = parsePayload(jwtToken);
|
||||
if (!payload || !payload.exp) throw new Error("Bad id token payload");
|
||||
const duration = 1000 * payload.exp - new Date().getTime();
|
||||
if (!timer) {
|
||||
timer = getTimerWorker();
|
||||
timer.onMessage(async (e) => {
|
||||
if (e.data === "timeout") {
|
||||
const t = await refreshTokens();
|
||||
if (t && !t.error) return;
|
||||
document.location = signOut();
|
||||
if (authSource === 'keycloak') {
|
||||
// For Keycloak, just sign out - OIDC library handles refresh
|
||||
document.location = signOut();
|
||||
} else {
|
||||
const t = await refreshTokens();
|
||||
if (t && !t.error) return;
|
||||
document.location = signOut();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
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 () => {
|
||||
try {
|
||||
const {
|
||||
@@ -96,6 +144,7 @@ export const UserProvider = ({ children }) => {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
|
||||
setAuthSource('cognito');
|
||||
signedIn(result);
|
||||
} catch (err) {
|
||||
setError(`Sign in error. ${err.message}`);
|
||||
@@ -106,13 +155,42 @@ export const UserProvider = ({ children }) => {
|
||||
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 = () => {
|
||||
setGroups(null);
|
||||
setProviders(null);
|
||||
setToken(null);
|
||||
setAuthSource(null);
|
||||
if (localStorage) {
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -158,10 +236,12 @@ export const UserProvider = ({ children }) => {
|
||||
groups,
|
||||
providers,
|
||||
token,
|
||||
authSource,
|
||||
getAuthorizeURL,
|
||||
getLogoutURL,
|
||||
setError,
|
||||
signIn,
|
||||
signInWithKeycloak,
|
||||
signOut,
|
||||
refresh,
|
||||
}}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useEffect } from "react";
|
||||
|
||||
import { useUserContext } from "../Contexts/UserContext";
|
||||
import useStyles from "../useStyles";
|
||||
import { isKeycloakEnabled } from "../../services/keycloak";
|
||||
|
||||
const getCode = (search) => {
|
||||
if (!search) return null;
|
||||
@@ -11,7 +12,68 @@ const getCode = (search) => {
|
||||
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 { getAuthorizeURL, signIn, fetching } = useUserContext();
|
||||
|
||||
@@ -38,3 +100,10 @@ export default function SignInForm() {
|
||||
</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