diff --git a/.gitignore b/.gitignore index 514f82116de..81e72a27ad0 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ src/main/webapp/resources/images/dataverseproject.png.thumb140 # Docker development volumes /docker-dev-volumes /.vs + +# custom run script for developers +run_dev_env.sh \ No newline at end of file diff --git a/doc/release-notes/PR-10905-OIDC-new-implementation.md b/doc/release-notes/PR-10905-OIDC-new-implementation.md new file mode 100644 index 00000000000..437f2fa55b1 --- /dev/null +++ b/doc/release-notes/PR-10905-OIDC-new-implementation.md @@ -0,0 +1,23 @@ +New OpenID Connect implementation including new log in scenarios (see [the guides](https://dataverse-guide--10905.org.readthedocs.build/en/10905/installation/oidc.html#choosing-provisioned-providers-at-log-in)) for the current JSF frontend, the new Single Page Application (SPA) frontend, and a generic API usage. The API scenario using Bearer Token authorization is illustrated with a Python script that can be found in the `doc/sphinx-guides/_static/api/bearer-token-example` directory. This Python script prompts you to log in to the Keycloak in a new browser window using selenium. You can run that script with the following commands: + +```shell + cd doc/sphinx-guides/_static/api/bearer-token-example + ./run.sh +``` + +This script is safe for production use, as it does not require you to know the client secret or the user credentials. Therefore, you can safely distribute it as a part of your own Python script that lets users run some custom tasks. + +The following settings become deprecated with this change and can be removed from the configuration: +- `dataverse.auth.oidc.pkce.enabled` +- `dataverse.auth.oidc.pkce.method` +- `dataverse.auth.oidc.pkce.max-cache-size` +- `dataverse.auth.oidc.pkce.max-cache-age` + +The following settings new: +- `dataverse.auth.oidc.issuer-identifier` +- `dataverse.auth.oidc.issuer-identifier-field` +- `dataverse.auth.oidc.subject-identifier-field` + +Also, the bearer token authentication is now always enabled. Therefore, the `dataverse.feature.api-bearer-auth` feature flag is no longer used and can be removed from the configuration as well. + +The new implementation relies now on the builtin OIDC support in our application server (Payara). With this change the Nimbus SDK is no longer used and is removed from the dependencies. diff --git a/doc/sphinx-guides/_static/api/bearer-token-example/get_session.py b/doc/sphinx-guides/_static/api/bearer-token-example/get_session.py new file mode 100644 index 00000000000..bccbc3df9f5 --- /dev/null +++ b/doc/sphinx-guides/_static/api/bearer-token-example/get_session.py @@ -0,0 +1,28 @@ +import contextlib +import selenium.webdriver as webdriver +import selenium.webdriver.support.ui as ui +import re +import json +import requests + +with contextlib.closing(webdriver.Firefox()) as driver: + driver.get("http://localhost:8080/oidc/login?target=API&oidcp=oidc-mpconfig") + wait = ui.WebDriverWait(driver, 100) # timeout after 100 seconds + wait.until(lambda driver: "accessToken" in driver.page_source) + driver.get("view-source:http://localhost:8080/api/v1/oidc/session") + result = wait.until( + lambda driver: ( + driver.page_source if "accessToken" in driver.page_source else False + ) + ) + m = re.search("
(.+?)
", result) + if m: + found = m.group(1) + session = json.loads(found) + + token = session["data"]["accessToken"] + endpoint = "http://localhost:8080/api/v1/users/:me" + headers = {"Authorization": "Bearer " + token} + + print() + print(requests.get(endpoint, headers=headers).json()) diff --git a/doc/sphinx-guides/_static/api/bearer-token-example/requirements.txt b/doc/sphinx-guides/_static/api/bearer-token-example/requirements.txt new file mode 100644 index 00000000000..fdd089ed0b9 --- /dev/null +++ b/doc/sphinx-guides/_static/api/bearer-token-example/requirements.txt @@ -0,0 +1,2 @@ +selenium +requests \ No newline at end of file diff --git a/doc/sphinx-guides/_static/api/bearer-token-example/run.sh b/doc/sphinx-guides/_static/api/bearer-token-example/run.sh new file mode 100755 index 00000000000..864555c8f94 --- /dev/null +++ b/doc/sphinx-guides/_static/api/bearer-token-example/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +python3 -m venv run_env +source run_env/bin/activate +python3 -m pip install -r requirements.txt +python3 get_session.py +rm -rf run_env \ No newline at end of file diff --git a/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html new file mode 100644 index 00000000000..e70abbb9030 --- /dev/null +++ b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/doc/sphinx-guides/source/_static/installation/files/root/auth-providers/oidc.json b/doc/sphinx-guides/source/_static/installation/files/root/auth-providers/oidc.json index 9df38988a25..e081a0fc454 100644 --- a/doc/sphinx-guides/source/_static/installation/files/root/auth-providers/oidc.json +++ b/doc/sphinx-guides/source/_static/installation/files/root/auth-providers/oidc.json @@ -3,6 +3,6 @@ "factoryAlias":"oidc", "title":"", "subtitle":"", - "factoryData":"type: oidc | issuer: | clientId: | clientSecret: | pkceEnabled: | pkceMethod: ", + "factoryData":"type: oidc | issuer: | clientId: | clientSecret: | issuerId: | issuerIdField: | subjectIdField: ", "enabled":true } \ No newline at end of file diff --git a/doc/sphinx-guides/source/api/auth.rst b/doc/sphinx-guides/source/api/auth.rst index eae3bd3c969..4cb4a66a455 100644 --- a/doc/sphinx-guides/source/api/auth.rst +++ b/doc/sphinx-guides/source/api/auth.rst @@ -69,17 +69,18 @@ You can reset your API Token from your account page in your Dataverse installati Bearer Tokens ------------- -Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to API tokens if your installation has been set up to use them (see :ref:`bearer-token-auth` in the Installation Guide). +Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to API tokens if your installation has been set up to use OpenID Connect log in (see :ref:`oidc-log-in` in the Installation Guide). .. _RFC 6750: https://tools.ietf.org/html/rfc6750 -To test if bearer tokens are working, you can try something like the following (using the :ref:`User Information` API endpoint), substituting in parameters for your installation and user. +To test if bearer tokens are working, you can use a Python script that prompts you to log in to the Keycloak in a new browser window using selenium. For example, you can run the script inside the `doc/sphinx-guides/_static/api/bearer-token-example` that illustrates this: .. code-block:: bash - export TOKEN=`curl -s -X POST --location "http://keycloak.mydomain.com:8090/realms/test/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "username=user&password=user&grant_type=password&client_id=test&client_secret=94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8" | jq '.access_token' -r | tr -d "\n"` - - curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/users/:me + cd doc/sphinx-guides/_static/api/bearer-token-example + ./run.sh + +This script is safe for production use, as it does not require you to know the client secret or the user credentials. Therefore, you can safely distribute it as a part of your own Python script that lets users run some custom tasks. Signed URLs ----------- diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index f8b8620f121..bf63b562efa 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -6430,3 +6430,46 @@ Parameters: ``per_page`` Number of results returned per page. +.. _oidc-session: + +Session +------- + +The Session API is used to get the information on the current OIDC session (after being successfully authenticated using the OpenID Connect :ref:`oidc-log-in`). +You can be either redirected to that endpoint using the `API` log in flow as illustrated in the :ref:`bearer-tokens` example, or going to this endpoint directly, +after logging-in in your browser. The returned JSON looks like this: + +.. code-block:: json + + { + "status": "OK", + "data": { + "user": { + "id": 3, + "userIdentifier": "aUser", + "lastName": "User", + "firstName": "Dataverse", + "email": "dataverse-user@mailinator.com", + "isSuperuser": false, + "createdTime": "2024-10-07 08:26:29.453", + "lastLoginTime": "2024-10-07 08:26:29.453", + "deactivated": false, + "mutedEmails": [], + "mutedNotifications": [] + }, + "session": "6164900bf35e7f576a92e4f771cc", + "accessToken": "eyJhbGc...7VvYOMYxreH-Uo3RpaA" + } + } + +You can then use the retrieved `session` and `accessToken` for subsequent calls to the API or the session endpoint, as illustrated in the following curl examples: + +.. code-block:: bash + + export BEARER_TOKEN=eyJhbGc...7VvYOMYxreH-Uo3RpaA + export SESSION=6164900bf35e7f576a92e4f771cc + export SERVER_URL=https://demo.dataverse.org + + curl -H "Authorization: Bearer $BEARER_TOKEN" "$SERVER_URL/api/oidc/session" + + curl -v --cookie "JSESSIONID=$SESSION" "$SERVER_URL/api/oidc/session" diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index a2c27598b76..e8afd9f54b4 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -754,12 +754,10 @@ As for the "Remote only" authentication mode, it means that: Bearer Token Authentication --------------------------- -Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to API tokens. This is an experimental feature hidden behind a feature flag. +Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to API tokens. .. _RFC 6750: https://tools.ietf.org/html/rfc6750 -To enable bearer tokens, you must install and configure Keycloak (for now, see :ref:`oidc-dev` in the Developer Guide) and enable ``api-bearer-auth`` under :ref:`feature-flags`. - You can test that bearer tokens are working by following the example under :ref:`bearer-tokens` in the API Guide. .. _smtp-config: diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index d132fd2953d..7a195a42ba8 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -69,26 +69,6 @@ After adding a provider, the Log In page will by default show the "builtin" prov In contrast to our :doc:`oauth2`, you can use multiple providers by creating distinct configurations enabled by the same technology and without modifying the Dataverse Software code base (standards for the win!). - -.. _oidc-pkce: - -Enabling PKCE Security -^^^^^^^^^^^^^^^^^^^^^^ - -Many providers these days support or even require the usage of `PKCE `_ to safeguard against -some attacks and enable public clients that cannot have a secure secret to still use OpenID Connect (or OAuth2). - -The Dataverse-built OIDC client can be configured to use PKCE and the method to use when creating the code challenge can be specified. -See also `this explanation of the flow `_ -for details on how this works. - -As we are using the `Nimbus SDK `_ as our client -library, we support the standard ``PLAIN`` and ``S256`` (SHA-256) code challenge methods. "SHA-256 method" is the default -as recommend in `RFC7636 `_. If your provider needs some -other method, please open an issue. - -The provisioning sections below contain in the example the parameters you may use to configure PKCE. - Provision a Provider -------------------- @@ -106,9 +86,6 @@ requires fewer extra steps and allows you to keep more configuration in a single Provision via REST API ^^^^^^^^^^^^^^^^^^^^^^ -Note: you may omit the PKCE related settings from ``factoryData`` below if you don't plan on using PKCE - default is -disabled. - Please create a :download:`my-oidc-provider.json <../_static/installation/files/root/auth-providers/oidc.json>` file, replacing every ``<...>`` with your values: .. literalinclude:: /_static/installation/files/root/auth-providers/oidc.json @@ -163,14 +140,6 @@ The following options are available: - The base URL of the OpenID Connect (OIDC) server as explained above. - Y - \- - * - ``dataverse.auth.oidc.pkce.enabled`` - - Set to ``true`` to enable :ref:`PKCE ` in auth flow. - - N - - ``false`` - * - ``dataverse.auth.oidc.pkce.method`` - - Set code challenge method. The default value is the current best practice in the literature. - - N - - ``S256`` * - ``dataverse.auth.oidc.title`` - The UI visible name for this provider in login options. - N @@ -179,12 +148,34 @@ The following options are available: - A subtitle, currently not displayed by the UI. - N - ``OpenID Connect`` - * - ``dataverse.auth.oidc.pkce.max-cache-size`` - - Tune the maximum size of all OIDC providers' verifier cache (the number of outstanding PKCE-enabled auth responses). + * - ``dataverse.auth.oidc.issuer-identifier`` + - Issuer identifier value as found in the JWT token claims under ``dataverse.auth.oidc.issuer-identifier-field``. - N - - 10000 - * - ``dataverse.auth.oidc.pkce.max-cache-age`` - - Tune the maximum age, in seconds, of all OIDC providers' verifier cache entries. Default is 5 minutes, equivalent to lifetime - of many OIDC access tokens. + - ``value from dataverse.auth.oidc.auth-server-url`` + * - ``dataverse.auth.oidc.issuer-identifier-field`` + - Issuer identifier field name in the JWT token claims. - N - - 300 \ No newline at end of file + - ``iss`` + * - ``dataverse.auth.oidc.subject-identifier-field`` + - Subject identifier field name in the JWT token claims. + - N + - ``sub`` + +.. _oidc-log-in: + +Choosing Provisioned Providers at Log In +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In the JSF frontend, you can select the provider you wish to log in with at login time. However, you can also use the login link directly, for example, from a Python script as illustrated in the `doc/sphinx-guides/_static/api/bearer-token-example` :ref:`bearer-tokens` (you can copy that link in the +browser, it will prompt you with the Keycloak and redirect you to the API endpoint for retrieving the session :ref:`oidc-session`): +http://localhost:8080/oidc/login?target=API&oidcp=oidc-mpconfig + +The `oidc` parameter is the provisioned provider ID you wish to use and is configured in the previous steps. For example, +`oidc-mpconfig` is the provider configured with the JVM Options, it is also the default provider if this parameter is not included +in the request. The target parameter is the name of the target you want to be redirected to after a successful logging in. First you are +redirected to the callback endpoint of the OpenID Connect flow (`/oidc/callback/*`) which on its turn redirects you to the location +chosen in the target parameter: + + - `JSF` is the default target, and it redirects you to the JSF frontend + - `API` redirects you to the session endpoint of the native API :ref:`oidc-session`, from which you can recover the session ID and the bearer token for the API access + - `SPA` redirects you to the new SPA, if it is already installed on your system diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 384b70b7a7b..e48bf6eea3a 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -16,7 +16,6 @@ services: ENABLE_RELOAD: "1" SKIP_DEPLOY: "${SKIP_DEPLOY}" DATAVERSE_JSF_REFRESH_PERIOD: "1" - DATAVERSE_FEATURE_API_BEARER_AUTH: "1" DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" DATAVERSE_MAIL_MTA_HOST: "smtp" DATAVERSE_AUTH_OIDC_ENABLED: "1" diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index d599967919e..c549f04c79c 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -13,7 +13,6 @@ services: DATAVERSE_DB_HOST: postgres DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: dataverse - DATAVERSE_FEATURE_API_BEARER_AUTH: "1" DATAVERSE_MAIL_SYSTEM_EMAIL: "Demo Dataverse " DATAVERSE_MAIL_MTA_HOST: "smtp" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 diff --git a/pom.xml b/pom.xml index b2344989569..1baa1372b20 100644 --- a/pom.xml +++ b/pom.xml @@ -457,12 +457,6 @@ scribejava-apis 6.9.0 - - - com.nimbusds - oauth2-oidc-sdk - 10.13.2 - com.github.ben-manes.caffeine diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 3257a3cc7ac..b47415f5ed6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -6,7 +6,9 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; @@ -36,14 +38,19 @@ import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; +import fish.payara.security.openid.api.AccessTokenCallerPrincipal; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; +import jakarta.inject.Inject; import jakarta.json.*; import jakarta.json.JsonValue.ValueType; import jakarta.persistence.EntityManager; import jakarta.persistence.NoResultException; import jakarta.persistence.PersistenceContext; +import jakarta.security.enterprise.AuthenticationStatus; +import jakarta.security.enterprise.SecurityContext; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; @@ -240,6 +247,22 @@ String getWrappedMessageWhenJson() { @Context protected HttpServletRequest httpRequest; + @Context + protected HttpServletResponse httpResponse; + +/** +* OIDCLoginBackingBean and SecurityContext injections are a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + @Inject + OIDCLoginBackingBean oidcLoginBackingBean; + + @Inject + private SecurityContext securityContext; + /** * For pretty printing (indenting) of JSON output. */ @@ -322,7 +345,30 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon if (requestUser.isAuthenticated()) { return (AuthenticatedUser) requestUser; } else { - throw new WrappedResponse(authenticatedUserRequired()); + // This is a part of the OpenID Connect solution using security annotations. + // try authenticating with OpenIdContext first + UserRecordIdentifier userRecordIdentifier = oidcLoginBackingBean.getUserRecordIdentifier(); + if (userRecordIdentifier == null) { + // Try SecurityContext and the underlying Bearer token + AuthenticationStatus status = securityContext.authenticate(httpRequest, httpResponse, null); + if (AuthenticationStatus.SUCCESS.equals(status)) { + try { + userRecordIdentifier = securityContext.getPrincipalsByType(AccessTokenCallerPrincipal.class).stream().map(principal -> + oidcLoginBackingBean.getUserRecordIdentifier(principal.getAccessToken())).filter(userId -> userId != null).findFirst().get(); + } catch (Exception e) { + // NOOP + } + } + } + if (userRecordIdentifier == null) { + throw new WrappedResponse(authenticatedUserRequired()); + } + final AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier); + if (authUser != null) { + return authUser; + } else { + throw new WrappedResponse(authenticatedUserRequired()); + } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java b/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java new file mode 100644 index 00000000000..3e5895b5fa1 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java @@ -0,0 +1,55 @@ +package edu.harvard.iq.dataverse.api; + +import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; + +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.util.json.JsonPrinter; +import fish.payara.security.openid.api.OpenIdContext; +import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; + +@Stateless +@Path("oidc") +public class OIDCSession extends AbstractApiBean { + @Inject + OpenIdContext openIdContext; + + @Inject + protected AuthenticationServiceBean authSvc; + + /** + * Retrieve OIDC session and tokens + * + * @param crc + * @return + */ + @Path("session") + @GET + public Response session(@Context ContainerRequestContext crc) { + final UserRecordIdentifier userRecordIdentifier = oidcLoginBackingBean.getUserRecordIdentifier(); + if (userRecordIdentifier == null) { + return notFound("user record identifier not found"); + } + final AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier); + if (authUser != null) { + try { + return ok( + jsonObjectBuilder() + .add("user", JsonPrinter.json(authUser)) + .add("session", crc.getCookies().get("JSESSIONID").getValue()) + .add("accessToken", openIdContext.getAccessToken().getToken())); + } catch (Exception e) { + return badRequest(e.getMessage()); + } + } else { + return notFound("user with record identifier " + userRecordIdentifier + " not found"); + } + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java deleted file mode 100644 index 31f524af3f0..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java +++ /dev/null @@ -1,124 +0,0 @@ -package edu.harvard.iq.dataverse.api.auth; - -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import edu.harvard.iq.dataverse.UserServiceBean; -import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.settings.FeatureFlags; - -import jakarta.inject.Inject; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.core.HttpHeaders; -import java.io.IOException; -import java.util.List; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -public class BearerTokenAuthMechanism implements AuthMechanism { - private static final String BEARER_AUTH_SCHEME = "Bearer"; - private static final Logger logger = Logger.getLogger(BearerTokenAuthMechanism.class.getCanonicalName()); - - public static final String UNAUTHORIZED_BEARER_TOKEN = "Unauthorized bearer token"; - public static final String INVALID_BEARER_TOKEN = "Could not parse bearer token"; - public static final String BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED = "Bearer token detected, no OIDC provider configured"; - - @Inject - protected AuthenticationServiceBean authSvc; - @Inject - protected UserServiceBean userSvc; - - @Override - public User findUserFromRequest(ContainerRequestContext containerRequestContext) throws WrappedAuthErrorResponse { - if (FeatureFlags.API_BEARER_AUTH.enabled()) { - Optional bearerToken = getRequestApiKey(containerRequestContext); - // No Bearer Token present, hence no user can be authenticated - if (bearerToken.isEmpty()) { - return null; - } - - // Validate and verify provided Bearer Token, and retrieve UserRecordIdentifier - // TODO: Get the identifier from an invalidating cache to avoid lookup bursts of the same token. Tokens in the cache should be removed after some (configurable) time. - UserRecordIdentifier userInfo = verifyOidcBearerTokenAndGetUserIdentifier(bearerToken.get()); - - // retrieve Authenticated User from AuthService - AuthenticatedUser authUser = authSvc.lookupUser(userInfo); - if (authUser != null) { - // track the API usage - authUser = userSvc.updateLastApiUseTime(authUser); - return authUser; - } else { - // a valid Token was presented, but we have no associated user account. - logger.log(Level.WARNING, "Bearer token detected, OIDC provider {0} validated Token but no linked UserAccount", userInfo.getUserRepoId()); - // TODO: Instead of returning null, we should throw a meaningful error to the client. - // Probably this will be a wrapped auth error response with an error code and a string describing the problem. - return null; - } - } - return null; - } - - /** - * Verifies the given Bearer token and obtain information about the corresponding user within respective AuthProvider. - * - * @param token The string containing the encoded JWT - * @return - */ - private UserRecordIdentifier verifyOidcBearerTokenAndGetUserIdentifier(String token) throws WrappedAuthErrorResponse { - try { - BearerAccessToken accessToken = BearerAccessToken.parse(token); - // Get list of all authentication providers using Open ID Connect - // @TASK: Limited to OIDCAuthProviders, could be widened to OAuth2Providers. - List providers = authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream() - .map(providerId -> (OIDCAuthProvider) authSvc.getAuthenticationProvider(providerId)) - .collect(Collectors.toUnmodifiableList()); - // If not OIDC Provider are configured we cannot validate a Token - if(providers.isEmpty()){ - logger.log(Level.WARNING, "Bearer token detected, no OIDC provider configured"); - throw new WrappedAuthErrorResponse(BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED); - } - - // Iterate over all OIDC providers if multiple. Sadly needed as do not know which provided the Token. - for (OIDCAuthProvider provider : providers) { - try { - // The OIDCAuthProvider need to verify a Bearer Token and equip the client means to identify the corresponding AuthenticatedUser. - Optional userInfo = provider.getUserIdentifier(accessToken); - if(userInfo.isPresent()) { - logger.log(Level.FINE, "Bearer token detected, provider {0} confirmed validity and provided identifier", provider.getId()); - return userInfo.get(); - } - } catch (IOException e) { - // TODO: Just logging this is not sufficient - if there is an IO error with the one provider - // which would have validated successfully, this is not the users fault. We need to - // take note and refer to that later when occurred. - logger.log(Level.FINE, "Bearer token detected, provider " + provider.getId() + " indicates an invalid Token, skipping", e); - } - } - } catch (ParseException e) { - logger.log(Level.FINE, "Bearer token detected, unable to parse bearer token (invalid Token)", e); - throw new WrappedAuthErrorResponse(INVALID_BEARER_TOKEN); - } - - // No UserInfo returned means we have an invalid access token. - logger.log(Level.FINE, "Bearer token detected, yet no configured OIDC provider validated it."); - throw new WrappedAuthErrorResponse(UNAUTHORIZED_BEARER_TOKEN); - } - - /** - * Retrieve the raw, encoded token value from the Authorization Bearer HTTP header as defined in RFC 6750 - * @return An {@link Optional} either empty if not present or the raw token from the header - */ - private Optional getRequestApiKey(ContainerRequestContext containerRequestContext) { - String headerParamApiKey = containerRequestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - if (headerParamApiKey != null && headerParamApiKey.toLowerCase().startsWith(BEARER_AUTH_SCHEME.toLowerCase() + " ")) { - return Optional.of(headerParamApiKey); - } else { - return Optional.empty(); - } - } -} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanism.java index 801e2752b9e..d3eb0c6898b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanism.java @@ -19,9 +19,9 @@ public class CompoundAuthMechanism implements AuthMechanism { private final List authMechanisms = new ArrayList<>(); @Inject - public CompoundAuthMechanism(ApiKeyAuthMechanism apiKeyAuthMechanism, WorkflowKeyAuthMechanism workflowKeyAuthMechanism, SignedUrlAuthMechanism signedUrlAuthMechanism, SessionCookieAuthMechanism sessionCookieAuthMechanism, BearerTokenAuthMechanism bearerTokenAuthMechanism) { + public CompoundAuthMechanism(ApiKeyAuthMechanism apiKeyAuthMechanism, WorkflowKeyAuthMechanism workflowKeyAuthMechanism, SignedUrlAuthMechanism signedUrlAuthMechanism, SessionCookieAuthMechanism sessionCookieAuthMechanism) { // Auth mechanisms should be ordered by priority here - add(apiKeyAuthMechanism, workflowKeyAuthMechanism, signedUrlAuthMechanism, sessionCookieAuthMechanism,bearerTokenAuthMechanism); + add(apiKeyAuthMechanism, workflowKeyAuthMechanism, signedUrlAuthMechanism, sessionCookieAuthMechanism); } public CompoundAuthMechanism(AuthMechanism... authMechanisms) { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 8f3dc07fdea..9d57d7d8170 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -5,6 +5,8 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.ClockUtil; @@ -78,6 +80,9 @@ public class OAuth2LoginBackingBean implements Serializable { @Inject @ClockUtil.LocalTime Clock clock; + + @EJB + OIDCLoginBackingBean oidcLoginBackingBean; /** * Generate the OAuth2 Provider URL to be used in the login page link for the provider. @@ -87,6 +92,10 @@ public class OAuth2LoginBackingBean implements Serializable { */ public String linkFor(String idpId, String redirectPage) { AbstractOAuth2AuthenticationProvider idp = authenticationSvc.getOAuth2Provider(idpId); + if (idp instanceof OIDCAuthProvider oidcIdP) { + // OIDC has its own Log In endpoint, we use that one instead + return oidcLoginBackingBean.getLogInLink(oidcIdP); + } String state = createState(idp, toOption(redirectPage)); return idp.buildAuthzUrl(state, systemConfig.getOAuth2CallbackUrl()); } @@ -97,6 +106,12 @@ public String linkFor(String idpId, String redirectPage) { */ public void exchangeCodeForToken() throws IOException { HttpServletRequest req = Faces.getRequest(); + final String stateParameter = req.getParameter("state"); + // if no state is present, we know this is OIDC and can return early + if (stateParameter == null || stateParameter.isEmpty()) { + oidcLoginBackingBean.setUser(); + return; + } try { Optional oIdp = parseStateFromRequest(req.getParameter("state")); @@ -104,7 +119,7 @@ public void exchangeCodeForToken() throws IOException { if (oIdp.isPresent() && code.isPresent()) { AbstractOAuth2AuthenticationProvider idp = oIdp.get(); - oauthUser = idp.getUserRecord(code.get(), req.getParameter("state"), systemConfig.getOAuth2CallbackUrl()); + oauthUser = idp.getUserRecord(code.get(), stateParameter, systemConfig.getOAuth2CallbackUrl()); // Throw an error if this authentication method is disabled: // (it's not clear if it's possible at all, for somebody to get here with diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java index 59f659ff297..7fa3355df06 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java @@ -2,6 +2,7 @@ import com.github.scribejava.core.model.OAuth2AccessToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; + import java.io.Serializable; import java.sql.Timestamp; import jakarta.persistence.Column; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java index 5eb2b391eb7..c614b7d72d4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java @@ -1,359 +1,76 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import com.github.scribejava.core.builder.api.DefaultApi20; -import com.nimbusds.oauth2.sdk.AuthorizationCode; -import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; -import com.nimbusds.oauth2.sdk.AuthorizationGrant; -import com.nimbusds.oauth2.sdk.ErrorObject; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.ResponseType; -import com.nimbusds.oauth2.sdk.Scope; -import com.nimbusds.oauth2.sdk.TokenRequest; -import com.nimbusds.oauth2.sdk.TokenResponse; -import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; -import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; -import com.nimbusds.oauth2.sdk.auth.Secret; -import com.nimbusds.oauth2.sdk.http.HTTPRequest; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; -import com.nimbusds.oauth2.sdk.id.ClientID; -import com.nimbusds.oauth2.sdk.id.Issuer; -import com.nimbusds.oauth2.sdk.id.State; -import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod; -import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.openid.connect.sdk.AuthenticationRequest; -import com.nimbusds.openid.connect.sdk.Nonce; -import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; -import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser; -import com.nimbusds.openid.connect.sdk.UserInfoRequest; -import com.nimbusds.openid.connect.sdk.UserInfoResponse; -import com.nimbusds.openid.connect.sdk.claims.UserInfo; -import com.nimbusds.openid.connect.sdk.op.OIDCProviderConfigurationRequest; -import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; -import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationSetupException; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2Exception; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.BundleUtil; -import java.io.IOException; -import java.net.URI; -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; -import java.util.logging.Logger; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; +import fish.payara.security.openid.api.AccessToken; +import fish.payara.security.openid.api.OpenIdConstant; /** - * TODO: this should not EXTEND, but IMPLEMENT the contract to be used in {@link edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean} + * TODO: this should not EXTEND, but IMPLEMENT the contract to be used in + * {@link edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean} */ public class OIDCAuthProvider extends AbstractOAuth2AuthenticationProvider { - - private static final Logger logger = Logger.getLogger(OIDCAuthProvider.class.getName()); - protected String id = "oidc"; protected String title = "Open ID Connect"; - protected List scope = Arrays.asList("openid", "email", "profile"); - - final Issuer issuer; - final ClientAuthentication clientAuth; - final OIDCProviderMetadata idpMetadata; - final boolean pkceEnabled; - final CodeChallengeMethod pkceMethod; - - /** - * Using PKCE, we create and send a special {@link CodeVerifier}. This contains a secret - * we need again when verifying the response by the provider, thus the cache. - * To be absolutely sure this may not be abused to DDoS us and not let unused verifiers rot, - * use an evicting cache implementation and not a standard map. - */ - private final Cache verifierCache = Caffeine.newBuilder() - .maximumSize(JvmSettings.OIDC_PKCE_CACHE_MAXSIZE.lookup(Integer.class)) - .expireAfterWrite(Duration.of(JvmSettings.OIDC_PKCE_CACHE_MAXAGE.lookup(Integer.class), ChronoUnit.SECONDS)) - .build(); - - public OIDCAuthProvider(String aClientId, String aClientSecret, String issuerEndpointURL, - boolean pkceEnabled, String pkceMethod) throws AuthorizationSetupException { - this.clientSecret = aClientSecret; // nedded for state creation - this.clientAuth = new ClientSecretBasic(new ClientID(aClientId), new Secret(aClientSecret)); - this.issuer = new Issuer(issuerEndpointURL); - - this.idpMetadata = getMetadata(); - - this.pkceEnabled = pkceEnabled; - this.pkceMethod = CodeChallengeMethod.parse(pkceMethod); + + final String aClientId; + final String aClientSecret; + final String issuerEndpointURL; + final String issuerIdentifier; + final String issuerIdentifierField; + final String subjectIdentifierField; + + public OIDCAuthProvider(String aClientId, String aClientSecret, String issuerEndpointURL, String issuerIdentifier, String issuerIdentifierField, String subjectIdentifierField) { + this.aClientId = aClientId; + this.aClientSecret = aClientSecret; + this.issuerEndpointURL = issuerEndpointURL; + this.issuerIdentifier = issuerIdentifier == null ? issuerEndpointURL : issuerIdentifier; + this.issuerIdentifierField = issuerIdentifierField == null ? OpenIdConstant.ISSUER_IDENTIFIER : issuerIdentifierField; + this.subjectIdentifierField = subjectIdentifierField == null ? OpenIdConstant.SUBJECT_IDENTIFIER : subjectIdentifierField; } - - /** - * Although this is defined in {@link edu.harvard.iq.dataverse.authorization.AuthenticationProvider}, - * this needs to be present due to bugs in ELResolver (has been modified for Spring). - * TODO: for the future it might be interesting to make this configurable via the provider JSON (it's used for ORCID!) - * @see JBoss Issue 159 - * @see Jakarta EE Bug 43 - * @return false - */ - @Override - public boolean isDisplayIdentifier() { - return false; + + public boolean isIssuerOf(AccessToken accessToken) { + try { + final String issuerIdentifierValue = accessToken.getJwtClaims().getStringClaim(issuerIdentifierField).orElse(null); + return issuerIdentifier.equals(issuerIdentifierValue); + } catch (final Exception ignore) { + return false; + } } - - /** - * Setup metadata from OIDC provider during creation of the provider representation - * @return The OIDC provider metadata, if successfull - * @throws IOException when sth. goes wrong with the retrieval - * @throws ParseException when the metadata is not parsable - */ - OIDCProviderMetadata getMetadata() throws AuthorizationSetupException { + + public String getSubject(AccessToken accessToken) { try { - var metadata = getMetadata(this.issuer); - // Assert that the provider supports the code flow - if (metadata.getResponseTypes().stream().noneMatch(ResponseType::impliesCodeFlow)) { - throw new AuthorizationSetupException("OIDC provider at "+this.issuer.getValue()+" does not support code flow, disabling."); - } - return metadata; - } catch (IOException ex) { - logger.severe("OIDC provider metadata at \"+issuerEndpointURL+\" not retrievable: "+ex.getMessage()); - throw new AuthorizationSetupException("OIDC provider metadata at "+this.issuer.getValue()+" not retrievable."); - } catch (ParseException ex) { - logger.severe("OIDC provider metadata at \"+issuerEndpointURL+\" not parsable: "+ex.getMessage()); - throw new AuthorizationSetupException("OIDC provider metadata at "+this.issuer.getValue()+" not parsable."); + return accessToken.getJwtClaims().getStringClaim(subjectIdentifierField).orElse(null); + } catch (final Exception ignore) { + return null; } } - - /** - * Retrieve metadata from OIDC provider (moved here to be mock-/spyable) - * @param issuer The OIDC provider (basically a wrapped URL to endpoint) - * @return The OIDC provider metadata, if successfull - * @throws IOException when sth. goes wrong with the retrieval - * @throws ParseException when the metadata is not parsable - */ - OIDCProviderMetadata getMetadata(Issuer issuer) throws IOException, ParseException { - // Will resolve the OpenID provider metadata automatically - OIDCProviderConfigurationRequest request = new OIDCProviderConfigurationRequest(issuer); - - // Make HTTP request - HTTPRequest httpRequest = request.toHTTPRequest(); - HTTPResponse httpResponse = httpRequest.send(); - - // Parse OpenID provider metadata - return OIDCProviderMetadata.parse(httpResponse.getContentAsJSONObject()); + + public String getClientId() { + return aClientId; } - - /** - * TODO: remove when refactoring package and {@link AbstractOAuth2AuthenticationProvider} - */ - @Override - public DefaultApi20 getApiInstance() { - throw new UnsupportedOperationException("OIDC provider cannot provide a ScribeJava API instance object"); + + public String getClientSecret() { + return aClientSecret; } - - /** - * TODO: remove when refactoring package and {@link AbstractOAuth2AuthenticationProvider} - */ - @Override - protected ParsedUserResponse parseUserResponse(String responseBody) { - throw new UnsupportedOperationException("OIDC provider uses the SDK to parse the response."); + + public String getIssuerEndpointURL() { + return this.issuerEndpointURL; } - - /** - * Create the authz URL for the OIDC provider - * @param state A randomized state, necessary to secure the authorization flow. @see OAuth2LoginBackingBean.createState() - * @param callbackUrl URL where the provider should send the browser after authn in code flow - * @return - */ + @Override - public String buildAuthzUrl(String state, String callbackUrl) { - State stateObject = new State(state); - URI callback = URI.create(callbackUrl); - Nonce nonce = new Nonce(); - CodeVerifier pkceVerifier = pkceEnabled ? new CodeVerifier() : null; - - AuthenticationRequest req = new AuthenticationRequest.Builder(new ResponseType("code"), - Scope.parse(this.scope), - this.clientAuth.getClientID(), - callback) - .endpointURI(idpMetadata.getAuthorizationEndpointURI()) - .state(stateObject) - // Called method is nullsafe - will disable sending a PKCE challenge in case the verifier is not present - .codeChallenge(pkceVerifier, pkceMethod) - .nonce(nonce) - .build(); - - // Cache the PKCE verifier, as we need the secret in it for verification later again, after the client sends us - // the auth code! We use the state to cache the verifier, as the state is unique per authentication event. - if (pkceVerifier != null) { - this.verifierCache.put(state, pkceVerifier); - } - - return req.toURI().toString(); + public boolean isDisplayIdentifier() { + return false; } - - /** - * Receive user data from OIDC provider after authn/z has been successfull. (Callback view uses this) - * Request a token and access the resource, parse output and return user details. - * @param code The authz code sent from the provider - * @param redirectUrl The redirect URL (some providers require this when fetching the access token, e. g. Google) - * @return A user record containing all user details accessible for us - * @throws IOException Thrown when communication with the provider fails - * @throws OAuth2Exception Thrown when we cannot access the user details for some reason - * @throws InterruptedException Thrown when the requests thread is failing - * @throws ExecutionException Thrown when the requests thread is failing - */ + @Override - public OAuth2UserRecord getUserRecord(String code, String state, String redirectUrl) throws IOException, OAuth2Exception { - // Retrieve the verifier from the cache and clear from the cache. If not found, will be null. - // Will be sent to token endpoint for verification, so if required but missing, will lead to exception. - CodeVerifier verifier = verifierCache.getIfPresent(state); - - // Create grant object - again, this is null-safe for the verifier - AuthorizationGrant codeGrant = new AuthorizationCodeGrant( - new AuthorizationCode(code), URI.create(redirectUrl), verifier); - - // Get Access Token first - Optional accessToken = getAccessToken(codeGrant); - - // Now retrieve User Info - if (accessToken.isPresent()) { - Optional userInfo = getUserInfo(accessToken.get()); - - // Construct our internal user representation - if (userInfo.isPresent()) { - return getUserRecord(userInfo.get()); - } - } - - // this should never happen, as we are throwing exceptions like champs before. - throw new OAuth2Exception(-1, "", "auth.providers.token.failGetUser"); - } - - /** - * Create the OAuth2UserRecord from the OIDC UserInfo. - * TODO: extend to retrieve and insert claims about affiliation and position. - * @param userInfo - * @return the usable user record for processing ing {@link edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean} - */ - OAuth2UserRecord getUserRecord(UserInfo userInfo) { - return new OAuth2UserRecord( - this.getId(), - userInfo.getSubject().getValue(), - userInfo.getPreferredUsername(), - null, - new AuthenticatedUserDisplayInfo(userInfo.getGivenName(), userInfo.getFamilyName(), userInfo.getEmailAddress(), "", ""), - null - ); - } - - /** - * Retrieve the Access Token from provider. Encapsulate for testing. - * @param grant - * @return The bearer access token used in code (grant) flow. May be empty if SDK could not cast internally. - */ - Optional getAccessToken(AuthorizationGrant grant) throws IOException, OAuth2Exception { - // Request token - HTTPResponse response = new TokenRequest(this.idpMetadata.getTokenEndpointURI(), - this.clientAuth, - grant, - Scope.parse(this.scope)) - .toHTTPRequest() - .send(); - - // Parse response - try { - TokenResponse tokenRespone = OIDCTokenResponseParser.parse(response); - - // If error --> oauth2 ex - if (! tokenRespone.indicatesSuccess() ) { - ErrorObject error = tokenRespone.toErrorResponse().getErrorObject(); - throw new OAuth2Exception(error.getHTTPStatusCode(), error.getDescription(), "auth.providers.token.failRetrieveToken"); - } - - // Success --> return token - OIDCTokenResponse successResponse = (OIDCTokenResponse)tokenRespone.toSuccessResponse(); - - return Optional.of(successResponse.getOIDCTokens().getBearerAccessToken()); - - } catch (ParseException ex) { - throw new OAuth2Exception(-1, ex.getMessage(), "auth.providers.token.failParseToken"); - } - } - - /** - * Retrieve User Info from provider. Encapsulate for testing. - * @param accessToken The access token to enable reading data from userinfo endpoint - */ - Optional getUserInfo(BearerAccessToken accessToken) throws IOException, OAuth2Exception { - // Retrieve data - HTTPResponse response = new UserInfoRequest(this.idpMetadata.getUserInfoEndpointURI(), accessToken) - .toHTTPRequest() - .send(); - - // Parse/Extract - try { - UserInfoResponse infoResponse = UserInfoResponse.parse(response); - - // If error --> oauth2 ex - if (! infoResponse.indicatesSuccess() ) { - ErrorObject error = infoResponse.toErrorResponse().getErrorObject(); - throw new OAuth2Exception(error.getHTTPStatusCode(), - error.getDescription(), - BundleUtil.getStringFromBundle("auth.providers.exception.userinfo", Arrays.asList(this.getTitle()))); - } - - // Success --> return info - return Optional.of(infoResponse.toSuccessResponse().getUserInfo()); - - } catch (ParseException ex) { - throw new OAuth2Exception(-1, ex.getMessage(), BundleUtil.getStringFromBundle("auth.providers.exception.userinfo", Arrays.asList(this.getTitle()))); - } + public DefaultApi20 getApiInstance() { + throw new UnsupportedOperationException("OIDC provider cannot provide a ScribeJava API instance object"); } - /** - * Trades an access token for an {@link UserRecordIdentifier} (if valid). - * - * @apiNote The resulting {@link UserRecordIdentifier} may be used with - * {@link edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean#lookupUser(UserRecordIdentifier)} - * to look up an {@link edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser} from the database. - * @see edu.harvard.iq.dataverse.api.auth.BearerTokenAuthMechanism - * - * @param accessToken The token to use when requesting user information from the provider - * @return Returns an {@link UserRecordIdentifier} for a valid access token or an empty {@link Optional}. - * @throws IOException In case communication with the endpoint fails to succeed for an I/O reason - */ - public Optional getUserIdentifier(BearerAccessToken accessToken) throws IOException { - OAuth2UserRecord userRecord; - try { - // Try to retrieve with given token (throws if invalid token) - Optional userInfo = getUserInfo(accessToken); - - if (userInfo.isPresent()) { - // Take this detour to avoid code duplication and potentially hard to track conversion errors. - userRecord = getUserRecord(userInfo.get()); - } else { - // This should not happen - an error at the provider side will lead to an exception. - logger.log(Level.WARNING, - "User info retrieval from {0} returned empty optional but expected exception for token {1}.", - List.of(getId(), accessToken).toArray() - ); - return Optional.empty(); - } - } catch (OAuth2Exception e) { - logger.log(Level.FINE, - "Could not retrieve user info with token {0} at provider {1}: {2}", - List.of(accessToken, getId(), e.getMessage()).toArray()); - logger.log(Level.FINER, "Retrieval failed, details as follows: ", e); - return Optional.empty(); - } - - return Optional.of(userRecord.getUserRecordIdentifier()); + @Override + protected ParsedUserResponse parseUserResponse(String responseBody) { + throw new UnsupportedOperationException("OIDC provider uses the SDK to parse the response."); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java index 3f8c18d0567..5ed75c4d6ea 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java @@ -42,8 +42,9 @@ public AuthenticationProvider buildProvider( AuthenticationProviderRow aRow ) th factoryData.get("clientId"), factoryData.get("clientSecret"), factoryData.get("issuer"), - Boolean.parseBoolean(factoryData.getOrDefault("pkceEnabled", "false")), - factoryData.getOrDefault("pkceMethod", "S256") + factoryData.get("issuerId"), + factoryData.get("issuerIdField"), + factoryData.get("subjectIdField") ); oidc.setId(aRow.getId()); @@ -63,8 +64,9 @@ public static AuthenticationProvider buildFromSettings() throws AuthorizationSet JvmSettings.OIDC_CLIENT_ID.lookup(), JvmSettings.OIDC_CLIENT_SECRET.lookup(), JvmSettings.OIDC_AUTH_SERVER_URL.lookup(), - JvmSettings.OIDC_PKCE_ENABLED.lookupOptional(Boolean.class).orElse(false), - JvmSettings.OIDC_PKCE_METHOD.lookupOptional().orElse("S256") + JvmSettings.OIDC_ISSUER_IDENTIFIER.lookupOptional().orElse(null), + JvmSettings.OIDC_ISSUER_IDENTIFIER_FIELD.lookupOptional().orElse(null), + JvmSettings.OIDC_SUBJECT_IDENTIFIER_FIELD.lookupOptional().orElse(null) ); oidc.setId("oidc-mpconfig"); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java new file mode 100644 index 00000000000..f58f0bfa646 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java @@ -0,0 +1,141 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; + +import java.io.IOException; +import java.io.Serializable; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.omnifaces.util.Faces; + +import edu.harvard.iq.dataverse.DataverseSession; +import edu.harvard.iq.dataverse.UserServiceBean; +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2FirstLoginPage; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; +import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; +import edu.harvard.iq.dataverse.util.SystemConfig; +import fish.payara.security.openid.api.AccessToken; +import fish.payara.security.openid.api.JwtClaims; +import fish.payara.security.openid.api.OpenIdConstant; +import fish.payara.security.openid.api.OpenIdContext; +import jakarta.ejb.EJB; +import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.inject.Named; + +/** +* This code is a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + +/** + * Backing bean of the OIDC login process. Used from the login and the callback pages. + * It also provides UserRecordIdentifier retrieval method used in the AbstractAPIBean for OpenIdContext processing to identify the connected user. + */ +@Stateless +@Named +public class OIDCLoginBackingBean implements Serializable { + private static final Logger logger = Logger.getLogger(OIDCLoginBackingBean.class.getName()); + + @EJB + AuthenticationServiceBean authenticationSvc; + + @EJB + SystemConfig systemConfig; + + @EJB + UserServiceBean userService; + + @Inject + DataverseSession session; + + @Inject + OAuth2FirstLoginPage newAccountPage; + + @Inject + OpenIdContext openIdContext; + + /** + * Generate the OIDC log in link. + */ + public String getLogInLink(final OIDCAuthProvider oidcAuthProvider) { + final UserRecordIdentifier userRecordIdentifier = getUserRecordIdentifier(); + if (userRecordIdentifier != null) { + setUser(); + return SystemConfig.getDataverseSiteUrlStatic(); + } + return "/oidc/login?target=JSF&oidcp=" + oidcAuthProvider.getId(); + } + + /** + * View action for callback.xhtml, the browser redirect target for the OAuth2 + * provider. + * + * @throws IOException + */ + public void setUser() { + try { + final JwtClaims claims = openIdContext.getAccessToken().getJwtClaims(); + final UserRecordIdentifier userRecordIdentifier = getUserRecordIdentifier(); + final String subject = userRecordIdentifier.getUserIdInRepo(); + final String providerId = userRecordIdentifier.getUserRepoId(); + AuthenticatedUser dvUser = authenticationSvc.lookupUser(userRecordIdentifier); + if (dvUser == null) { + if (!systemConfig.isSignupDisabledForRemoteAuthProvider(providerId)) { + final String firstName = claims.getStringClaim(OpenIdConstant.GIVEN_NAME).orElse(""); + final String lastName = claims.getStringClaim(OpenIdConstant.FAMILY_NAME).orElse(""); + final String verifiedEmailAddress = claims.getStringClaim(OpenIdConstant.EMAIL).orElse(""); + final String emailAddress = verifiedEmailAddress == null ? "" : verifiedEmailAddress; + final String affiliation = claims.getStringClaim("affiliation").orElse(""); + final String position = claims.getStringClaim("position").orElse(""); + final OAuth2UserRecord userRecord = new OAuth2UserRecord( + providerId, + subject, + claims.getStringClaim(OpenIdConstant.PREFERRED_USERNAME).orElse(subject), + null, + new AuthenticatedUserDisplayInfo(firstName, lastName, emailAddress, affiliation, position), + List.of(emailAddress)); + logger.log(Level.INFO, "redirect to first login: " + userRecordIdentifier); + newAccountPage.setNewUser(userRecord); + Faces.redirect("/oauth2/firstLogin.xhtml"); + } + } else { + dvUser = userService.updateLastLogin(dvUser); + session.setUser(dvUser); + Faces.redirect("/"); + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Setting user failed: " + e.getMessage()); + } + } + + public UserRecordIdentifier getUserRecordIdentifier() { + return getUserRecordIdentifier(openIdContext.getAccessToken()); + } + + public UserRecordIdentifier getUserRecordIdentifier(final AccessToken accessToken) { + try { + final OIDCAuthProvider provider = getProvider(accessToken); + final String providerId = provider.getId(); + final String subject = provider.getSubject(accessToken); + if (subject == null) { + return null; + } + return new UserRecordIdentifier(providerId, subject); + } catch (final Exception ignore) { + return null; + } + } + + private OIDCAuthProvider getProvider(AccessToken accessToken) { + return authenticationSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream() + .map(providerId -> (OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(providerId)) + .filter(provider -> provider.isIssuerOf(accessToken)).findFirst().get(); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java new file mode 100644 index 00000000000..02223bfbc59 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java @@ -0,0 +1,58 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; + +import java.io.IOException; + +import edu.harvard.iq.dataverse.util.SystemConfig; +import fish.payara.security.annotations.LogoutDefinition; +import fish.payara.security.annotations.OpenIdAuthenticationDefinition; +import fish.payara.security.openid.api.OpenIdConstant; +import jakarta.annotation.security.DeclareRoles; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.HttpConstraint; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** +* This code is a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + +/** + * If we want to use the OIDC annotation, it assumes we also use the security annotations, HttpAuthenticationMechanism, IdentityStoreHandler and the IdentityStore (which we do not use...) and that the endpoints security relies on SecurityContext. + * If I do not add the security annotation (@ServletSecurity(@HttpConstraint(rolesAllowed = "nobodyHasAccess"))) to this authentication servlet, it will not be secured and will not force you to log in, you will just see its content, OIDC annotation has no effect in that situation. + * The authentication servlet is the only location that is really secured with OIDC and security annotations, SecurityContext, IdentityStoreHandler, HttpAuthenticationMechanism and the bearer token IdentityStore. But we do not use it anywhere else (and I assume we do not want to), so this servlet has content that is unreachable (because we do not use security annotations anywhere else in authentication mechanisms we implemented, so you cannot gain access to it by any means). + * You can only make calls to the API with the bearer tokens in this implementation because the bearer token IdentityStore passes the user to the AbstractApiBean, so it can just rely on the "standard" Dataverse implementation, without any security annotations. The authentication servlet is there only to force the use of the OIDC annotation to do its work, so it is not important that it is not reachable. After logging in you are redirected somewhere else anyway. + */ + +/** + * OIDC login implementation + */ +@WebServlet("/oidc/login") +@OpenIdAuthenticationDefinition( + providerURI = "#{openIdConfigBean.providerURI}", + clientId = "#{openIdConfigBean.clientId}", + clientSecret = "#{openIdConfigBean.clientSecret}", + redirectURI = "#{openIdConfigBean.redirectURI}", + logout = @LogoutDefinition(redirectURI = "#{openIdConfigBean.logoutURI}"), + scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE}, + useSession = true // If enabled state & nonce value stored in session otherwise in cookies. + // I do not know if useSession should default to true. I made it explicit because of the comments about the Payara jsession being insecure and that we do not want to use cookies. It looks like these requirements would make this implementation unusable: either jsession cookie or token cookie is used. We might end up not using this code at all. +) +@DeclareRoles("nobodyHasAccess") +@ServletSecurity(@HttpConstraint(rolesAllowed = "nobodyHasAccess")) +public class OpenIDAuthentication extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + // as explained above, this content is unreachable + final String baseURL = SystemConfig.getDataverseSiteUrlStatic(); + final String target = request.getParameter("target"); + final String redirect = "SPA".equals(target) ? baseURL + "/spa/" : baseURL; + response.sendRedirect(redirect); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java new file mode 100644 index 00000000000..a1766bd2b22 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java @@ -0,0 +1,49 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; + +import java.io.IOException; + +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** +* This code is a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + +/** + * I am not sure if we need the redirects implemented here, this entire code might be removed in the future. I think it depends if we want to allow cookies or not. If we assume that nobody wants to use httponly secure cookies as being OK for security and always implement local storage/in memory tokens, like many systems do, then we can delete this routing thing. I just like the simplicity of it. You cannot turn the cookies off anyway if you want to use the OIDC annotation on payara, so we can have some convenience code as well. I see it more like that: either we happily use what is possible on Payara/Jakarta, or we implement everything ourselves, like it was the case until now. If we do use the OIDC annotation, we might as well use the convenience of it and not hide it under a rug for some hacker to figure it out anyway. + */ + +@WebServlet("/oidc/callback/*") +public class OpenIDCallback extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + final String baseURL = SystemConfig.getDataverseSiteUrlStatic(); + final String target = request.getPathInfo(); + final String redirect; + switch (target) { + case "/JSF": + redirect = baseURL + "/oauth2/callback.xhtml"; + break; + case "/SPA": + redirect = baseURL + "/spa/"; + break; + case "/API": + redirect = baseURL + "/api/v1/oidc/session"; + break; + + default: + redirect = baseURL + "/oauth2/callback.xhtml"; + break; + } + response.sendRedirect(redirect); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java new file mode 100644 index 00000000000..2072721893a --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java @@ -0,0 +1,73 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; + +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.servlet.http.HttpServletRequest; + +/** +* This code is a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + +@Named("openIdConfigBean") +public class OpenIDConfigBean implements java.io.Serializable { + @Inject + HttpServletRequest request; + + @Inject + AuthenticationServiceBean authenticationSvc; + + public String getProviderURI() { + final String oidcp = request.getParameter("oidcp"); + if (oidcp == null || oidcp.isBlank()) { + return JvmSettings.OIDC_AUTH_SERVER_URL.lookupOptional().orElse(""); + } + try { + return ((OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(oidcp)).getIssuerEndpointURL(); + } catch (Exception e) { + return ""; + } + } + + public String getClientId() { + final String oidcp = request.getParameter("oidcp"); + if (oidcp == null || oidcp.isBlank()) { + return JvmSettings.OIDC_CLIENT_ID.lookupOptional().orElse(""); + } + try { + return ((OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(oidcp)).getClientId(); + } catch (Exception e) { + return ""; + } + } + + public String getClientSecret() { + final String oidcp = request.getParameter("oidcp"); + if (oidcp == null || oidcp.isBlank()) { + return JvmSettings.OIDC_CLIENT_SECRET.lookupOptional().orElse(""); + } + try { + return ((OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(oidcp)).getClientSecret(); + } catch (Exception e) { + return ""; + } + } + + public String getRedirectURI() { + String target = request.getParameter("target"); + target = target == null || target.isBlank() ? "API" : target; + return SystemConfig.getDataverseSiteUrlStatic() + "/oidc/callback/" + target; + } + + public String getLogoutURI() { + final String target = request.getParameter("target"); + final String baseURL = SystemConfig.getDataverseSiteUrlStatic(); + return "SPA".equals(target) ? baseURL + "/spa/" : baseURL; + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 33e828e619d..ac4fe15a701 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -30,12 +30,6 @@ public enum FeatureFlags { * @since Dataverse 5.14 */ API_SESSION_AUTH("api-session-auth"), - /** - * Enables API authentication via Bearer Token. - * @apiNote Raise flag by setting "dataverse.feature.api-bearer-auth" - * @since Dataverse @TODO: - */ - API_BEARER_AUTH("api-bearer-auth"), /** * For published (public) objects, don't use a join when searching Solr. * Experimental! Requires a reindex with the following feature flag enabled, diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index d7eea970b8a..4848d9d44e9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -230,11 +230,9 @@ public enum JvmSettings { OIDC_AUTH_SERVER_URL(SCOPE_OIDC, "auth-server-url"), OIDC_CLIENT_ID(SCOPE_OIDC, "client-id"), OIDC_CLIENT_SECRET(SCOPE_OIDC, "client-secret"), - SCOPE_OIDC_PKCE(SCOPE_OIDC, "pkce"), - OIDC_PKCE_ENABLED(SCOPE_OIDC_PKCE, "enabled"), - OIDC_PKCE_METHOD(SCOPE_OIDC_PKCE, "method"), - OIDC_PKCE_CACHE_MAXSIZE(SCOPE_OIDC_PKCE, "max-cache-size"), - OIDC_PKCE_CACHE_MAXAGE(SCOPE_OIDC_PKCE, "max-cache-age"), + OIDC_ISSUER_IDENTIFIER(SCOPE_OIDC, "issuer-identifier"), + OIDC_ISSUER_IDENTIFIER_FIELD(SCOPE_OIDC, "issuer-identifier-field"), + OIDC_SUBJECT_IDENTIFIER_FIELD(SCOPE_OIDC, "subject-identifier-field"), // UI SETTINGS SCOPE_UI(PREFIX, "ui"), diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index b0bc92cf975..69e72cccf6e 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -56,5 +56,7 @@ dataverse.oai.server.maxsets=100 #dataverse.oai.server.repositoryname= # AUTHENTICATION -dataverse.auth.oidc.pkce.max-cache-size=10000 -dataverse.auth.oidc.pkce.max-cache-age=300 +# the following setting enables the Multi-tenancy Support +# for the OIDC securitya nnotation base implementation +# see: https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html#multitenancy +payara.security.openid.sessionScopedConfiguration=true diff --git a/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java b/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java deleted file mode 100644 index 7e1c23d26f4..00000000000 --- a/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java +++ /dev/null @@ -1,167 +0,0 @@ -package edu.harvard.iq.dataverse.api.auth; - -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import edu.harvard.iq.dataverse.UserServiceBean; -import edu.harvard.iq.dataverse.api.auth.doubles.BearerTokenKeyContainerRequestTestFake; -import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.testing.JvmSetting; -import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import jakarta.ws.rs.container.ContainerRequestContext; - -import java.io.IOException; -import java.util.Collections; -import java.util.Optional; - -import static edu.harvard.iq.dataverse.api.auth.BearerTokenAuthMechanism.*; -import static org.junit.jupiter.api.Assertions.*; - -@LocalJvmSettings -@JvmSetting(key = JvmSettings.FEATURE_FLAG, value = "true", varArgs = "api-bearer-auth") -class BearerTokenAuthMechanismTest { - - private static final String TEST_API_KEY = "test-api-key"; - - private BearerTokenAuthMechanism sut; - - @BeforeEach - public void setUp() { - sut = new BearerTokenAuthMechanism(); - sut.authSvc = Mockito.mock(AuthenticationServiceBean.class); - sut.userSvc = Mockito.mock(UserServiceBean.class); - } - - @Test - void testFindUserFromRequest_no_token() throws WrappedAuthErrorResponse { - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake(null); - User actual = sut.findUserFromRequest(testContainerRequest); - - assertNull(actual); - } - - @Test - void testFindUserFromRequest_invalid_token() { - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.emptySet()); - - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer "); - WrappedAuthErrorResponse wrappedAuthErrorResponse = assertThrows(WrappedAuthErrorResponse.class, () -> sut.findUserFromRequest(testContainerRequest)); - - //then - assertEquals(INVALID_BEARER_TOKEN, wrappedAuthErrorResponse.getMessage()); - } - @Test - void testFindUserFromRequest_no_OidcProvider() { - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.emptySet()); - - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " +TEST_API_KEY); - WrappedAuthErrorResponse wrappedAuthErrorResponse = assertThrows(WrappedAuthErrorResponse.class, () -> sut.findUserFromRequest(testContainerRequest)); - - //then - assertEquals(BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED, wrappedAuthErrorResponse.getMessage()); - } - - @Test - void testFindUserFromRequest_oneProvider_invalidToken_1() throws ParseException, IOException { - OIDCAuthProvider oidcAuthProvider = Mockito.mock(OIDCAuthProvider.class); - String providerID = "OIEDC"; - Mockito.when(oidcAuthProvider.getId()).thenReturn(providerID); - // ensure that a valid OIDCAuthProvider is available within the AuthenticationServiceBean - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.singleton(providerID)); - Mockito.when(sut.authSvc.getAuthenticationProvider(providerID)).thenReturn(oidcAuthProvider); - - // ensure that the OIDCAuthProvider returns a valid UserRecordIdentifier for a given Token - BearerAccessToken token = BearerAccessToken.parse("Bearer " + TEST_API_KEY); - Mockito.when(oidcAuthProvider.getUserIdentifier(token)).thenReturn(Optional.empty()); - - // when - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " + TEST_API_KEY); - WrappedAuthErrorResponse wrappedAuthErrorResponse = assertThrows(WrappedAuthErrorResponse.class, () -> sut.findUserFromRequest(testContainerRequest)); - - //then - assertEquals(UNAUTHORIZED_BEARER_TOKEN, wrappedAuthErrorResponse.getMessage()); - } - - @Test - void testFindUserFromRequest_oneProvider_invalidToken_2() throws ParseException, IOException { - OIDCAuthProvider oidcAuthProvider = Mockito.mock(OIDCAuthProvider.class); - String providerID = "OIEDC"; - Mockito.when(oidcAuthProvider.getId()).thenReturn(providerID); - // ensure that a valid OIDCAuthProvider is available within the AuthenticationServiceBean - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.singleton(providerID)); - Mockito.when(sut.authSvc.getAuthenticationProvider(providerID)).thenReturn(oidcAuthProvider); - - // ensure that the OIDCAuthProvider returns a valid UserRecordIdentifier for a given Token - BearerAccessToken token = BearerAccessToken.parse("Bearer " + TEST_API_KEY); - Mockito.when(oidcAuthProvider.getUserIdentifier(token)).thenThrow(IOException.class); - - // when - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " + TEST_API_KEY); - WrappedAuthErrorResponse wrappedAuthErrorResponse = assertThrows(WrappedAuthErrorResponse.class, () -> sut.findUserFromRequest(testContainerRequest)); - - //then - assertEquals(UNAUTHORIZED_BEARER_TOKEN, wrappedAuthErrorResponse.getMessage()); - } - @Test - void testFindUserFromRequest_oneProvider_validToken() throws WrappedAuthErrorResponse, ParseException, IOException { - OIDCAuthProvider oidcAuthProvider = Mockito.mock(OIDCAuthProvider.class); - String providerID = "OIEDC"; - Mockito.when(oidcAuthProvider.getId()).thenReturn(providerID); - // ensure that a valid OIDCAuthProvider is available within the AuthenticationServiceBean - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.singleton(providerID)); - Mockito.when(sut.authSvc.getAuthenticationProvider(providerID)).thenReturn(oidcAuthProvider); - - // ensure that the OIDCAuthProvider returns a valid UserRecordIdentifier for a given Token - UserRecordIdentifier userinfo = new UserRecordIdentifier(providerID, "KEY"); - BearerAccessToken token = BearerAccessToken.parse("Bearer " + TEST_API_KEY); - Mockito.when(oidcAuthProvider.getUserIdentifier(token)).thenReturn(Optional.of(userinfo)); - - // ensures that the AuthenticationServiceBean can retrieve an Authenticated user based on the UserRecordIdentifier - AuthenticatedUser testAuthenticatedUser = new AuthenticatedUser(); - Mockito.when(sut.authSvc.lookupUser(userinfo)).thenReturn(testAuthenticatedUser); - Mockito.when(sut.userSvc.updateLastApiUseTime(testAuthenticatedUser)).thenReturn(testAuthenticatedUser); - - // when - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " + TEST_API_KEY); - User actual = sut.findUserFromRequest(testContainerRequest); - - //then - assertEquals(testAuthenticatedUser, actual); - Mockito.verify(sut.userSvc, Mockito.atLeastOnce()).updateLastApiUseTime(testAuthenticatedUser); - - } - @Test - void testFindUserFromRequest_oneProvider_validToken_noAccount() throws WrappedAuthErrorResponse, ParseException, IOException { - OIDCAuthProvider oidcAuthProvider = Mockito.mock(OIDCAuthProvider.class); - String providerID = "OIEDC"; - Mockito.when(oidcAuthProvider.getId()).thenReturn(providerID); - // ensure that a valid OIDCAuthProvider is available within the AuthenticationServiceBean - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.singleton(providerID)); - Mockito.when(sut.authSvc.getAuthenticationProvider(providerID)).thenReturn(oidcAuthProvider); - - // ensure that the OIDCAuthProvider returns a valid UserRecordIdentifier for a given Token - UserRecordIdentifier userinfo = new UserRecordIdentifier(providerID, "KEY"); - BearerAccessToken token = BearerAccessToken.parse("Bearer " + TEST_API_KEY); - Mockito.when(oidcAuthProvider.getUserIdentifier(token)).thenReturn(Optional.of(userinfo)); - - // ensures that the AuthenticationServiceBean can retrieve an Authenticated user based on the UserRecordIdentifier - Mockito.when(sut.authSvc.lookupUser(userinfo)).thenReturn(null); - - - // when - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " + TEST_API_KEY); - User actual = sut.findUserFromRequest(testContainerRequest); - - //then - assertNull(actual); - - } -} diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java deleted file mode 100644 index ee6823ef98a..00000000000 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java +++ /dev/null @@ -1,249 +0,0 @@ -package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; - -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.openid.connect.sdk.claims.UserInfo; -import dasniko.testcontainers.keycloak.KeycloakContainer; -import edu.harvard.iq.dataverse.UserServiceBean; -import edu.harvard.iq.dataverse.api.auth.BearerTokenAuthMechanism; -import edu.harvard.iq.dataverse.api.auth.doubles.BearerTokenKeyContainerRequestTestFake; -import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2Exception; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.mocks.MockAuthenticatedUser; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.testing.JvmSetting; -import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; -import edu.harvard.iq.dataverse.util.testing.Tags; -import org.htmlunit.FailingHttpStatusCodeException; -import org.htmlunit.WebClient; -import org.htmlunit.WebResponse; -import org.htmlunit.html.HtmlForm; -import org.htmlunit.html.HtmlInput; -import org.htmlunit.html.HtmlPage; -import org.htmlunit.html.HtmlSubmitInput; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.OAuth2Constants; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.KeycloakBuilder; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthenticationProviderFactoryIT.clientId; -import static edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthenticationProviderFactoryIT.clientSecret; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.mockito.Mockito.when; - -@Tag(Tags.INTEGRATION_TEST) -@Tag(Tags.USES_TESTCONTAINERS) -@Testcontainers(disabledWithoutDocker = true) -@ExtendWith(MockitoExtension.class) -// NOTE: order is important here - Testcontainers must be first, otherwise it's not ready when we call getAuthUrl() -@LocalJvmSettings -@JvmSetting(key = JvmSettings.OIDC_CLIENT_ID, value = clientId) -@JvmSetting(key = JvmSettings.OIDC_CLIENT_SECRET, value = clientSecret) -@JvmSetting(key = JvmSettings.OIDC_AUTH_SERVER_URL, method = "getAuthUrl") -class OIDCAuthenticationProviderFactoryIT { - - static final String clientId = "test"; - static final String clientSecret = "94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8"; - static final String realm = "test"; - static final String realmAdminUser = "admin"; - static final String realmAdminPassword = "admin"; - - static final String adminUser = "kcadmin"; - static final String adminPassword = "kcpassword"; - - // The realm JSON resides in conf/keycloak/test-realm.json and gets avail here using in pom.xml - @Container - static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:22.0") - .withRealmImportFile("keycloak/test-realm.json") - .withAdminUsername(adminUser) - .withAdminPassword(adminPassword); - - // simple method to retrieve the issuer URL, referenced to by @JvmSetting annotations (do no delete) - private static String getAuthUrl() { - return keycloakContainer.getAuthServerUrl() + "/realms/" + realm; - } - - OIDCAuthProvider getProvider() throws Exception { - OIDCAuthProvider oidcAuthProvider = (OIDCAuthProvider) OIDCAuthenticationProviderFactory.buildFromSettings(); - - assumeTrue(oidcAuthProvider.getMetadata().getTokenEndpointURI().toString() - .startsWith(keycloakContainer.getAuthServerUrl())); - - return oidcAuthProvider; - } - - // NOTE: This requires the "direct access grants" for the client to be enabled! - String getBearerTokenViaKeycloakAdminClient() throws Exception { - try (Keycloak keycloak = KeycloakBuilder.builder() - .serverUrl(keycloakContainer.getAuthServerUrl()) - .grantType(OAuth2Constants.PASSWORD) - .realm(realm) - .clientId(clientId) - .clientSecret(clientSecret) - .username(realmAdminUser) - .password(realmAdminPassword) - .scope("openid") - .build()) { - return keycloak.tokenManager().getAccessTokenString(); - } - } - - /** - * This basic test covers configuring an OIDC provider via MPCONFIG and being able to use it. - */ - @Test - void testCreateProvider() throws Exception { - // given - OIDCAuthProvider oidcAuthProvider = getProvider(); - String token = getBearerTokenViaKeycloakAdminClient(); - assumeFalse(token == null); - - Optional info = Optional.empty(); - - // when - try { - info = oidcAuthProvider.getUserInfo(new BearerAccessToken(token)); - } catch (OAuth2Exception e) { - System.out.println(e.getMessageBody()); - } - - //then - assertTrue(info.isPresent()); - assertEquals(realmAdminUser, info.get().getPreferredUsername()); - } - - @Mock - UserServiceBean userService; - @Mock - AuthenticationServiceBean authService; - - @InjectMocks - BearerTokenAuthMechanism bearerTokenAuthMechanism; - - /** - * This test covers using an OIDC provider as authorization party when accessing the Dataverse API with a - * Bearer Token. See {@link BearerTokenAuthMechanism}. It needs to mock the auth services to avoid adding - * more dependencies. - */ - @Test - @JvmSetting(key = JvmSettings.FEATURE_FLAG, varArgs = "api-bearer-auth", value = "true") - void testApiBearerAuth() throws Exception { - assumeFalse(userService == null); - assumeFalse(authService == null); - assumeFalse(bearerTokenAuthMechanism == null); - - // given - // Get the access token from the remote Keycloak in the container - String accessToken = getBearerTokenViaKeycloakAdminClient(); - assumeFalse(accessToken == null); - - OIDCAuthProvider oidcAuthProvider = getProvider(); - // This will also receive the details from the remote Keycloak in the container - UserRecordIdentifier identifier = oidcAuthProvider.getUserIdentifier(new BearerAccessToken(accessToken)).get(); - String token = "Bearer " + accessToken; - BearerTokenKeyContainerRequestTestFake request = new BearerTokenKeyContainerRequestTestFake(token); - AuthenticatedUser user = new MockAuthenticatedUser(); - - // setup mocks (we don't want or need a database here) - when(authService.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Set.of(oidcAuthProvider.getId())); - when(authService.getAuthenticationProvider(oidcAuthProvider.getId())).thenReturn(oidcAuthProvider); - when(authService.lookupUser(identifier)).thenReturn(user); - when(userService.updateLastApiUseTime(user)).thenReturn(user); - - // when (let's do this again, but now with the actual subject under test!) - User lookedUpUser = bearerTokenAuthMechanism.findUserFromRequest(request); - - // then - assertNotNull(lookedUpUser); - assertEquals(user, lookedUpUser); - } - - /** - * This test covers the {@link OIDCAuthProvider#buildAuthzUrl(String, String)} and - * {@link OIDCAuthProvider#getUserRecord(String, String, String)} methods that are used when - * a user authenticates via the JSF UI. It covers enabling PKCE, which is no hard requirement - * by the protocol, but might be required by some provider (as seen with Microsoft Azure AD). - * As we don't have a real browser, we use {@link WebClient} from HtmlUnit as a replacement. - */ - @Test - @JvmSetting(key = JvmSettings.OIDC_PKCE_ENABLED, value = "true") - void testAuthorizationCodeFlowWithPKCE() throws Exception { - // given - String state = "foobar"; - String callbackUrl = "http://localhost:8080/oauth2callback.xhtml"; - - OIDCAuthProvider oidcAuthProvider = getProvider(); - String authzUrl = oidcAuthProvider.buildAuthzUrl(state, callbackUrl); - //System.out.println(authzUrl); - - try (WebClient webClient = new WebClient()) { - webClient.getOptions().setCssEnabled(false); - webClient.getOptions().setJavaScriptEnabled(false); - // We *want* to know about the redirect, as it contains the data we need! - webClient.getOptions().setRedirectEnabled(false); - - HtmlPage loginPage = webClient.getPage(authzUrl); - assumeTrue(loginPage.getTitleText().contains("Sign in to " + realm)); - - HtmlForm form = loginPage.getForms().get(0); - HtmlInput username = form.getInputByName("username"); - HtmlInput password = form.getInputByName("password"); - HtmlSubmitInput submit = form.getInputByName("login"); - - username.type(realmAdminUser); - password.type(realmAdminPassword); - - FailingHttpStatusCodeException exception = assertThrows(FailingHttpStatusCodeException.class, submit::click); - assertEquals(302, exception.getStatusCode()); - - WebResponse response = exception.getResponse(); - assertNotNull(response); - - String callbackLocation = response.getResponseHeaderValue("Location"); - assertTrue(callbackLocation.startsWith(callbackUrl)); - //System.out.println(callbackLocation); - - String queryPart = callbackLocation.trim().split("\\?")[1]; - Map parameters = Pattern.compile("\\s*&\\s*") - .splitAsStream(queryPart) - .map(s -> s.split("=", 2)) - .collect(Collectors.toMap(a -> a[0], a -> a.length > 1 ? a[1]: "")); - //System.out.println(map); - assertTrue(parameters.containsKey("code")); - assertTrue(parameters.containsKey("state")); - - OAuth2UserRecord userRecord = oidcAuthProvider.getUserRecord( - parameters.get("code"), - parameters.get("state"), - callbackUrl - ); - - assertNotNull(userRecord); - assertEquals(realmAdminUser, userRecord.getUsername()); - } catch (OAuth2Exception e) { - System.out.println(e.getMessageBody()); - throw e; - } - } -} \ No newline at end of file diff --git a/src/test/resources/META-INF/microprofile-config.properties b/src/test/resources/META-INF/microprofile-config.properties index 113a098a1fe..8233eeddce1 100644 --- a/src/test/resources/META-INF/microprofile-config.properties +++ b/src/test/resources/META-INF/microprofile-config.properties @@ -16,3 +16,9 @@ test.filesDir=/tmp/dataverse dataverse.files.directory=${test.filesDir} dataverse.files.uploads=${test.filesDir}/uploads dataverse.files.docroot=${test.filesDir}/docroot + +# AUTHENTICATION +# the following setting enables the Multi-tenancy Support +# for the OIDC securitya nnotation base implementation +# see: https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html#multitenancy +payara.security.openid.sessionScopedConfiguration=true