From b13ff51ddaccbdf3cf496d52094226edba8195a6 Mon Sep 17 00:00:00 2001 From: Matthieu Huin Date: Tue, 9 Nov 2021 15:27:38 +0100 Subject: [PATCH] web UI: user login with OpenID Connect Under the hood, this uses AuthProvider as supplied by oidc-react. Most of the theory is explained in the comment in ZuulAuthProvider.jsx The benefit of doing this is that we allow the AuthProvider and userManager to handle the callback logic, so we don't need to handle the callback logic ourselves. A callback page is still required though in order to deal with the parameters passed in a successful redirection from the Identity Provider. The challenge in using these classes as-is is that our authority endpoints (eg, the IDP itself) may change from one tenant to the next; these classes aren't set up for that. So we need to be careful about how and when we change those authority URLs. In terms of functionalities: if the default realm's authentication driver is set to "OpenIDConnect", display a "Sign in" button. If the the user is logged in, redirect to the last page visited prior to logging in; fetch user authorizations and add them to the redux store; display the user's preferred username in the upper right corner. Clicking on the user icon in the right corner displays a modal with user information such as the user's zuul-client configuration, and a sign out button. Clicking on the sign out button removes user information from the store (note that it does not log the user out from the Identity Provider). Add some basic documentation explaining how to configure Zuul with Google's authentication, and with a Keycloak server. (This squashes https://review.opendev.org/c/zuul/zuul/+/816208 into https://review.opendev.org/c/zuul/zuul/+/734082 ) Co-authored-by: James E. Blair Change-Id: I31e71f2795f3f7c4253d0d5b8ed309bfd7d4f98e --- doc/source/howtos/admin.rst | 1 + doc/source/howtos/openid-connect-examples.rst | 21 ++ doc/source/howtos/openid-with-google.rst | 65 +++++ doc/source/howtos/openid-with-keycloak.rst | 110 ++++++++ .../webui-openidconnect-514c09b26f7fd15e.yaml | 5 + web/package.json | 2 + web/src/App.jsx | 34 ++- web/src/App.test.jsx | 14 +- web/src/ZuulAuthProvider.jsx | 76 ++++++ web/src/actions/auth.js | 85 ++++++ web/src/actions/info.js | 5 +- web/src/actions/user.js | 72 ++++++ web/src/api.js | 66 +++-- web/src/containers/auth/Auth.jsx | 242 ++++++++++++++++++ web/src/containers/config/Config.jsx | 2 +- web/src/index.js | 5 +- web/src/pages/AuthCallback.jsx | 41 +++ web/src/reducers/auth.js | 76 ++++++ web/src/reducers/index.js | 4 + web/src/reducers/info.js | 2 + web/src/reducers/initialState.js | 2 + web/src/reducers/user.js | 74 ++++++ web/src/routes.js | 16 +- web/src/store.dev.js | 6 +- web/yarn.lock | 65 +++++ 25 files changed, 1045 insertions(+), 46 deletions(-) create mode 100644 doc/source/howtos/openid-connect-examples.rst create mode 100644 doc/source/howtos/openid-with-google.rst create mode 100644 doc/source/howtos/openid-with-keycloak.rst create mode 100644 releasenotes/notes/webui-openidconnect-514c09b26f7fd15e.yaml create mode 100644 web/src/ZuulAuthProvider.jsx create mode 100644 web/src/actions/auth.js create mode 100644 web/src/actions/user.js create mode 100644 web/src/containers/auth/Auth.jsx create mode 100644 web/src/pages/AuthCallback.jsx create mode 100644 web/src/reducers/auth.js create mode 100644 web/src/reducers/user.js diff --git a/doc/source/howtos/admin.rst b/doc/source/howtos/admin.rst index 9fd4933f27..c4301f97ea 100644 --- a/doc/source/howtos/admin.rst +++ b/doc/source/howtos/admin.rst @@ -6,5 +6,6 @@ Admin How-to Guides installation zuul-from-scratch + openid-connect-examples troubleshooting zookeeper diff --git a/doc/source/howtos/openid-connect-examples.rst b/doc/source/howtos/openid-connect-examples.rst new file mode 100644 index 0000000000..a4fedc43d3 --- /dev/null +++ b/doc/source/howtos/openid-connect-examples.rst @@ -0,0 +1,21 @@ +OpenID Connect Integration Examples +=================================== + +This document lists simple How-Tos to help administrators enable OpenID +Connect authentication in Zuul and Zuul's Web UI. + +.. toctree:: + :maxdepth: 1 + + openid-with-google + openid-with-keycloak + +Debugging +--------- + +If problems appear: + +* Make sure your configuration is correct, especially callback URIs. +* More information can be found in Zuul's web service logs. +* From the user's side, activating the web console in the browser can be helpful + to debug API calls. diff --git a/doc/source/howtos/openid-with-google.rst b/doc/source/howtos/openid-with-google.rst new file mode 100644 index 0000000000..4b25337ef3 --- /dev/null +++ b/doc/source/howtos/openid-with-google.rst @@ -0,0 +1,65 @@ +Configuring Google Authentication +================================= + +This document explains how to configure Zuul in order to enable authentication +with Google. + +Prerequisites +------------- + +* The Zuul instance must be able to query Google's OAUTH API servers. This + simply generally means that the Zuul instance must be able to send and + receive HTTPS data to and from the Internet. +* You must set up a project in `Google's developers console `_. + +Setting up credentials with Google +---------------------------------- + +In the developers console, choose your project and click `APIs & Services`. + +Choose `Credentials` in the menu on the left, then click `Create Credentials`. + +Choose `Create OAuth client ID`. You might need to configure a consent screen first. + +Create OAuth client ID +...................... + +Choose `Web application` as Application Type. + +In `Authorized JavaScript Origins`, add the base URL of Zuul's Web UI. For example, +if you are running a yarn development server on your computer, it would be +`http://localhost:3000` . + +In `Authorized redirect URIs`, write down the base URL of Zuul's Web UI followed +by "/t//auth_callback", for each tenant on which you want to enable +authentication. For example, if you are running a yarn development server on +your computer and want to set up authentication for tenant "local", +write `http://localhost:3000/t/local/auth_callback` . + +Click Save. Google will generate a Client ID and a Client secret for your new +credentials; we will only need the Client ID for the rest of this How-To. + +Configure Zuul +.............. + +Edit the ``/etc/zuul/zuul.conf`` to add the google authenticator: + +.. code-block:: ini + + [auth google_auth] + default=true + driver=OpenIDConnect + realm=my_realm + issuer_id=https://accounts.google.com + client_id= + + +Restart Zuul services (scheduler, web). + +Head to your tenant's status page. If all went well, you should see a "Sign in" +button in the upper right corner of the page. Congratulations! + +Further Reading +--------------- + +This How-To is based on `Google's documentation on their implementation of OpenID Connect `_. diff --git a/doc/source/howtos/openid-with-keycloak.rst b/doc/source/howtos/openid-with-keycloak.rst new file mode 100644 index 0000000000..e803768010 --- /dev/null +++ b/doc/source/howtos/openid-with-keycloak.rst @@ -0,0 +1,110 @@ +Configuring Keycloak Authentication +=================================== + +This document explains how to configure Zuul and Keycloak in order to enable +authentication in Zuul with Keycloak. + +Prerequisites +------------- + +* The Zuul instance must be able to query Keycloak over HTTPS. +* Authenticating users must be able to reach Keycloak's web UI. +* Have a realm set up in Keycloak. + `Instructions on how to do so can be found here `_ . + +By convention, we will assume the Keycloak server's FQDN is ``keycloak``, and +Zuul's Web UI's base URL is ``https://zuul/``. We will use the realm ``my_realm``. + +Most operations below regarding the configuration of Keycloak can be performed through +Keycloak's admin CLI. The following steps must be performed as an admin on Keycloak's +GUI. + +Setting up Keycloak +------------------- + +Create a client +............... + +Choose the realm ``my_realm``, then click ``clients`` in the Configure panel. +Click ``Create``. + +Name your client as you please. We will pick ``zuul`` for this example. Make sure +to fill the following fields: + +* Client Protocol: ``openid-connect`` +* Access Type: ``public`` +* Implicit Flow Enabled: ``ON`` +* Valid Redirect URIs: ``https://zuul/*`` +* Web Origins: ``https://zuul/`` + +Click "Save" when done. + +Create a client scope +...................... + +Keycloak maps the client ID to a specific claim, instead of the usual `aud` claim. +We need to configure Keycloak to add our client ID to the `aud` claim by creating +a custom client scope for our client. + +Choose the realm ``my_realm``, then click ``client scopes`` in the Configure panel. +Click ``Create``. + +Name your scope as you please. We will name it ``zuul_aud`` for this example. +Make sure you fill the following fields: + +* Protocol: ``openid-connect`` +* Include in Token Scope: ``ON`` + +Click "Save" when done. + +On the Client Scopes page, click on ``zuul_aud`` to configure it; click on +``Mappers`` then ``create``. + +Make sure to fill the following: + +* Mapper Type: ``Audience`` +* Included Client Audience: ``zuul`` +* Add to ID token: ``ON`` +* Add to access token: ``ON`` + +Then save. + +Finally, go back to the clients list and pick the ``zuul`` client again. Click +on ``Client Scopes``, and add the ``zuul_aud`` scope to the ``Assigned Default +Client Scopes``. + +(Optional) Set up a social identity provider +............................................ + +Keycloak can delegate authentication to predefined social networks. Follow +`these steps to find out how. `_ + +If you don't set up authentication delegation, make sure to create at least one +user in your realm, or allow self-registration. See Keycloak's documentation section +on `user management `_ +for more details on how to do so. + +Setting up Zuul +--------------- + +Edit the ``/etc/zuul/zuul.conf`` to add the keycloak authenticator: + +.. code-block:: ini + + [auth keycloak] + default=true + driver=OpenIDConnect + realm=my_realm + issuer_id=https://keycloak/auth/realms/my_realm + client_id=zuul + +Restart Zuul services (scheduler, web). + +Head to your tenant's status page. If all went well, you should see a "Sign in" +button in the upper right corner of the page. Congratulations! + +Further Reading +--------------- + +This How-To is based on `Keycloak's documentation `_, +specifically `the documentation about clients `_. diff --git a/releasenotes/notes/webui-openidconnect-514c09b26f7fd15e.yaml b/releasenotes/notes/webui-openidconnect-514c09b26f7fd15e.yaml new file mode 100644 index 0000000000..030eacf452 --- /dev/null +++ b/releasenotes/notes/webui-openidconnect-514c09b26f7fd15e.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add authentication in the web UI. Zuul's web UI can be configured to authenticate + users against an Identity Provider supporting the OpenID Connect protocol. diff --git a/web/package.json b/web/package.json index e13a7d9c3b..16c3e88c14 100644 --- a/web/package.json +++ b/web/package.json @@ -17,6 +17,8 @@ "moment": "^2.22.2", "moment-duration-format": "2.3.2", "moment-timezone": "^0.5.28", + "oidc-client": "^1.10.1", + "oidc-react": "^1.5.1", "patternfly-react": "^2.39.16", "prop-types": "^15.6.2", "react": "^16.13.1", diff --git a/web/src/App.jsx b/web/src/App.jsx index c64f09ccf2..68e7ca3790 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -58,6 +58,7 @@ import { UsersIcon, } from '@patternfly/react-icons' +import AuthContainer from './containers/auth/Auth' import ErrorBoundary from './containers/ErrorBoundary' import { Fetching } from './containers/Fetching' import SelectTz from './containers/timezone/SelectTz' @@ -67,6 +68,7 @@ import { clearError } from './actions/errors' import { fetchConfigErrorsAction } from './actions/configErrors' import { routes } from './routes' import { setTenantAction } from './actions/tenant' +import { configureAuthFromTenant, configureAuthFromInfo } from './actions/auth' class App extends React.Component { static propTypes = { @@ -79,6 +81,7 @@ class App extends React.Component { history: PropTypes.object, dispatch: PropTypes.func, isKebabDropdownOpen: PropTypes.bool, + user: PropTypes.object, } state = { @@ -106,7 +109,7 @@ class App extends React.Component { ) } else { // Return an empty navigation bar in case we don't have an active tenant - return