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:36 UTC

[incubator-pinot] branch oidc-ui-temp created (now 9e62f45)

This is an automated email from the ASF dual-hosted git repository.

apucher pushed a change to branch oidc-ui-temp
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git.


      at 9e62f45  Implemented OIDC auth workflow in UI

This branch includes the following new commits:

     new 9e62f45  Implemented OIDC auth workflow in UI

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org


[incubator-pinot] 01/01: Implemented OIDC auth workflow in UI

Posted by ap...@apache.org.
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