You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by ap...@apache.org on 2021/07/16 18:17:37 UTC
[incubator-pinot] 01/01: Implemented OIDC auth workflow in UI
This is an automated email from the ASF dual-hosted git repository.
apucher pushed a commit to branch oidc-ui-temp
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git
commit 9e62f457a62bbac84920a4435dc40761c40a199c
Author: Gaurav Joshi <68...@users.noreply.github.com>
AuthorDate: Fri Jul 2 07:49:48 2021 -0700
Implemented OIDC auth workflow in UI
---
pinot-controller/src/main/resources/app/App.tsx | 186 ++++++++++++++++-----
.../src/main/resources/app/app_state.ts | 4 +
.../src/main/resources/app/interfaces/types.d.ts | 6 +
.../src/main/resources/app/pages/LoginPage.tsx | 2 +
.../main/resources/app/utils/PinotMethodUtils.ts | 23 ++-
.../src/main/resources/app/utils/axios-config.ts | 15 +-
6 files changed, 191 insertions(+), 45 deletions(-)
diff --git a/pinot-controller/src/main/resources/app/App.tsx b/pinot-controller/src/main/resources/app/App.tsx
index c11dfbe..28735bb 100644
--- a/pinot-controller/src/main/resources/app/App.tsx
+++ b/pinot-controller/src/main/resources/app/App.tsx
@@ -28,6 +28,7 @@ import PinotMethodUtils from './utils/PinotMethodUtils';
import CustomNotification from './components/CustomNotification';
import { NotificationContextProvider } from './components/Notification/NotificationContextProvider';
import app_state from './app_state';
+import { AuthWorkflow } from 'Models';
const useStyles = makeStyles(() =>
createStyles({
@@ -42,7 +43,15 @@ const useStyles = makeStyles(() =>
const App = () => {
const [clusterName, setClusterName] = React.useState('');
const [loading, setLoading] = React.useState(true);
+ const oidcSignInFormRef = React.useRef<HTMLFormElement>(null);
const [isAuthenticated, setIsAuthenticated] = React.useState(null);
+ const [issuer, setIssuer] = React.useState(null);
+ const [redirectUri, setRedirectUri] = React.useState(null);
+ const [clientId, setClientId] = React.useState(null);
+ const [authWorkflow, setAuthWorkflow] = React.useState(null);
+ const [authorizationEndopoint, setAuthorizationEndopoint] = React.useState(
+ null
+ );
const fetchClusterName = async () => {
const clusterNameResponse = await PinotMethodUtils.getClusterName();
@@ -64,22 +73,92 @@ const App = () => {
};
const getAuthInfo = async () => {
- const authInfoResponse = await PinotMethodUtils.getAuthInfo()
- // If authInfoResponse has workflow set to anything but BASIC,
- // it doesn't require authentication.
- if(authInfoResponse?.workflow !== 'BASIC'){
- setIsAuthenticated(true);
- } else {
- setLoading(false);
+ const authInfoResponse = await PinotMethodUtils.getAuthInfo();
+ // Issuer URL, if available
+ setIssuer(
+ authInfoResponse && authInfoResponse.issuer ? authInfoResponse.issuer : ''
+ );
+ // Redirect URI, if available
+ setRedirectUri(
+ authInfoResponse && authInfoResponse.redirectUri
+ ? authInfoResponse.redirectUri
+ : ''
+ );
+ // Client Id, if available
+ setClientId(
+ authInfoResponse && authInfoResponse.clientId
+ ? authInfoResponse.clientId
+ : ''
+ );
+ // Authentication workflow
+ setAuthWorkflow(
+ authInfoResponse && authInfoResponse.workflow
+ ? authInfoResponse.workflow
+ : AuthWorkflow.NONE
+ );
+ };
+
+ const initAuthWorkflow = async () => {
+ switch (authWorkflow) {
+ case AuthWorkflow.NONE: {
+ // No authentication required
+ setIsAuthenticated(true);
+
+ break;
+ }
+ case AuthWorkflow.BASIC: {
+ // Basic authentication, handled by login page
+ setLoading(false);
+
+ break;
+ }
+ case AuthWorkflow.OIDC: {
+ // OIDC authentication, check to see if access token is available in the URL
+ const accessToken = PinotMethodUtils.getAccessTokenFromHashParams();
+ if (accessToken) {
+ app_state.authWorkflow = AuthWorkflow.OIDC;
+ app_state.authToken = accessToken;
+
+ setIsAuthenticated(true);
+ } else {
+ // Get authorization endpoint
+ const openIdConfigResponse = await PinotMethodUtils.getWellKnownOpenIdConfiguration(
+ issuer
+ );
+ setAuthorizationEndopoint(
+ openIdConfigResponse && openIdConfigResponse.authorization_endpoint
+ ? openIdConfigResponse.authorization_endpoint
+ : ''
+ );
+
+ setLoading(false);
+ }
+
+ break;
+ }
+ default: {
+ // Empty
+ }
}
- }
+ };
- React.useEffect(()=>{
+ React.useEffect(() => {
getAuthInfo();
}, []);
- React.useEffect(()=>{
- if(isAuthenticated){
+ React.useEffect(() => {
+ initAuthWorkflow();
+ }, [authWorkflow]);
+
+ React.useEffect(() => {
+ if (authorizationEndopoint && oidcSignInFormRef && oidcSignInFormRef.current) {
+ // Authorization endpoint available; submit signin form
+ oidcSignInFormRef.current.submit();
+ }
+ }, [authorizationEndopoint]);
+
+ React.useEffect(() => {
+ if (isAuthenticated) {
fetchClusterConfig();
fetchClusterName();
}
@@ -109,37 +188,60 @@ const App = () => {
<MuiThemeProvider theme={theme}>
<NotificationContextProvider>
<CustomNotification />
- {
- loading ?
- <CircularProgress className={classes.loader} size={80}/>
- :
- <Router>
- <Switch>
- {getRouterData().map(({ path, Component }, key) => (
- <Route
- exact
- path={path}
- key={key}
- render={props => {
- if(path === '/login'){
- return loginRender(Component, props);
- } else if(isAuthenticated){
- // default render
- return componentRender(Component, props);
- } else {
- return (
- <Redirect to="/login"/>
- );
- }
- }}
- />
- ))}
- <Route path="*">
- <Redirect to={app_state.queryConsoleOnlyView ? "/query" : "/"} />
- </Route>
- </Switch>
- </Router>
- }
+ {/* OIDC auth workflow */}
+ {authWorkflow && authWorkflow === AuthWorkflow.OIDC ? (
+ <>
+ {/* OIDC sign in form */}
+ <form
+ hidden
+ action={authorizationEndopoint}
+ method="post"
+ ref={oidcSignInFormRef}
+ >
+ <input readOnly name="client_id" value={clientId} />
+ <input readOnly name="redirect_uri" value={redirectUri} />
+ <input readOnly name="scope" value="email openid" />
+ <input readOnly name="state" value="true-redirect-uri" />
+ <input readOnly name="response_type" value="id_token token" />
+ <input readOnly name="nonce" value="random_string" />
+ <input type="submit" value="" />
+ </form>
+ </>
+ ) : (
+ <>
+ {/* Non-OIDC auth workflow */}
+ {loading ? (
+ <CircularProgress className={classes.loader} size={80} />
+ ) : (
+ <Router>
+ <Switch>
+ {getRouterData().map(({ path, Component }, key) => (
+ <Route
+ exact
+ path={path}
+ key={key}
+ render={(props) => {
+ if (path === '/login') {
+ return loginRender(Component, props);
+ } else if (isAuthenticated) {
+ // default render
+ return componentRender(Component, props);
+ } else {
+ return <Redirect to="/login" />;
+ }
+ }}
+ />
+ ))}
+ <Route path="*">
+ <Redirect
+ to={app_state.queryConsoleOnlyView ? '/query' : '/'}
+ />
+ </Route>
+ </Switch>
+ </Router>
+ )}
+ </>
+ )}
</NotificationContextProvider>
</MuiThemeProvider>
);
diff --git a/pinot-controller/src/main/resources/app/app_state.ts b/pinot-controller/src/main/resources/app/app_state.ts
index 4b09aa6..caa27a1 100644
--- a/pinot-controller/src/main/resources/app/app_state.ts
+++ b/pinot-controller/src/main/resources/app/app_state.ts
@@ -16,8 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
+
+ import { AuthWorkflow } from "Models";
class app_state {
queryConsoleOnlyView: boolean;
+ authWorkflow: AuthWorkflow;
authToken: string | null;
}
+
export default new app_state();
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/interfaces/types.d.ts b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
index f79cc24..805d675 100644
--- a/pinot-controller/src/main/resources/app/interfaces/types.d.ts
+++ b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
@@ -149,4 +149,10 @@ declare module 'Models' {
tenantName: string,
error: string
}
+
+ export const enum AuthWorkflow {
+ NONE = 'NONE',
+ BASIC = 'BASIC',
+ OIDC = 'OIDC',
+ }
}
diff --git a/pinot-controller/src/main/resources/app/pages/LoginPage.tsx b/pinot-controller/src/main/resources/app/pages/LoginPage.tsx
index 53e5737..320f404 100644
--- a/pinot-controller/src/main/resources/app/pages/LoginPage.tsx
+++ b/pinot-controller/src/main/resources/app/pages/LoginPage.tsx
@@ -23,6 +23,7 @@ import Logo from '../components/SvgIcons/Logo';
import { useForm } from 'react-hook-form';
import PinotMethodUtils from '../utils/PinotMethodUtils';
import app_state from '../app_state';
+import { AuthWorkflow } from 'Models';
interface FormData {
username: string;
@@ -77,6 +78,7 @@ const LoginPage = (props) => {
if(isUserAuthenticated){
setInvalidToken(false);
props.setIsAuthenticated(true);
+ app_state.authWorkflow = AuthWorkflow.BASIC;
app_state.authToken = authToken;
props.history.push(app_state.queryConsoleOnlyView ? '/query' : '/');
} else {
diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
index d67396b..d226466 100644
--- a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
+++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
@@ -68,6 +68,7 @@ import {
getInfo,
authenticateUser
} from '../requests';
+import { baseApi } from './axios-config';
import Utils from './Utils';
const JSONbig = require('json-bigint')({'storeAsString': true})
@@ -758,12 +759,30 @@ const getAuthInfo = () => {
});
};
+const getWellKnownOpenIdConfiguration = (issuer) => {
+ return baseApi
+ .get(`${issuer}/.well-known/openid-configuration`)
+ .then((response) => {
+ return response.data;
+ });
+};
+
const verifyAuth = (authToken) => {
return authenticateUser(authToken).then((response)=>{
return response.data;
});
};
+const getAccessTokenFromHashParams = () => {
+ let accessToken = '';
+ const urlSearchParams = new URLSearchParams(location.hash.substr(1));
+ if (urlSearchParams.has('access_token')) {
+ accessToken = urlSearchParams.get('access_token') as string;
+ }
+
+ return accessToken;
+};
+
export default {
getTenantsData,
getAllInstances,
@@ -813,5 +832,7 @@ export default {
getAllSchemaDetails,
getTableState,
getAuthInfo,
- verifyAuth
+ getWellKnownOpenIdConfiguration,
+ verifyAuth,
+ getAccessTokenFromHashParams
};
diff --git a/pinot-controller/src/main/resources/app/utils/axios-config.ts b/pinot-controller/src/main/resources/app/utils/axios-config.ts
index 0a34237..236d6e4 100644
--- a/pinot-controller/src/main/resources/app/utils/axios-config.ts
+++ b/pinot-controller/src/main/resources/app/utils/axios-config.ts
@@ -20,6 +20,7 @@
/* eslint-disable no-console */
import axios from 'axios';
+import { AuthWorkflow } from 'Models';
import app_state from '../app_state';
const isDev = process.env.NODE_ENV !== 'production';
@@ -39,12 +40,22 @@ const handleResponse = (response: any) => {
};
const handleConfig = (config: any) => {
- if(app_state.authToken){
- Object.assign(config.headers, {"Authorization": app_state.authToken});
+ // Attach auth token for basic auth
+ if (app_state.authWorkflow === AuthWorkflow.BASIC && app_state.authToken) {
+ Object.assign(config.headers, { Authorization: app_state.authToken });
}
+
+ // Attach auth token for OIDC auth
+ if (app_state.authWorkflow === AuthWorkflow.OIDC && app_state.authToken) {
+ Object.assign(config.headers, {
+ Authorization: `Bearer ${app_state.authToken}`,
+ });
+ }
+
if (isDev) {
console.log(config);
}
+
return config;
};
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org