From b2b5463a574806b611c4065a0a27c154e7cbe0bd Mon Sep 17 00:00:00 2001 From: Michael Marshall Date: Thu, 2 Dec 2021 22:55:44 -0600 Subject: [PATCH] [Java Client] Make Audience Field Optional in OAuth2 Client Credentials (#11988) * [Java Client] Make Audience Field in Client Credentials Optional * Update site2/website-next/docs * Remove update to 2.8.1 docs * Update more docs based on code review --- .../oauth2/AuthenticationFactoryOAuth2.java | 6 ++--- .../auth/oauth2/ClientCredentialsFlow.java | 4 +-- .../pulsar/client/impl/auth/oauth2/README.md | 4 +-- .../auth/oauth2/protocol/TokenClient.java | 25 +++++++++++-------- .../auth/oauth2/AuthenticationOAuth2Test.java | 13 ++++++++++ .../auth/oauth2/protocol/TokenClientTest.java | 18 +++---------- site2/docs/security-oauth2.md | 4 +-- site2/website-next/docs/security-oauth2.md | 4 +-- 8 files changed, 41 insertions(+), 37 deletions(-) diff --git a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/AuthenticationFactoryOAuth2.java b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/AuthenticationFactoryOAuth2.java index 707fcaf99c6d3..cf567747567e4 100644 --- a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/AuthenticationFactoryOAuth2.java +++ b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/AuthenticationFactoryOAuth2.java @@ -33,7 +33,7 @@ public final class AuthenticationFactoryOAuth2 { * * @param issuerUrl the issuer URL * @param credentialsUrl the credentials URL - * @param audience the audience identifier + * @param audience An optional field. The audience identifier used by some Identity Providers, like Auth0. * @return an Authentication object */ public static Authentication clientCredentials(URL issuerUrl, URL credentialsUrl, String audience) { @@ -45,9 +45,9 @@ public static Authentication clientCredentials(URL issuerUrl, URL credentialsUrl * * @param issuerUrl the issuer URL * @param credentialsUrl the credentials URL - * @param audience the audience identifier + * @param audience An optional field. The audience identifier used by some Identity Providers, like Auth0. * @param scope An optional field. The value of the scope parameter is expressed as a list of space-delimited, - * case-sensitive strings. The strings are defined by the authorization server. + * case-sensitive strings. The strings are defined by the authorization server. * If the value contains multiple space-delimited strings, their order does not matter, * and each string adds an additional access range to the requested scope. * From here: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.2 diff --git a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/ClientCredentialsFlow.java b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/ClientCredentialsFlow.java index b011e85dde0f5..bf0c289c0df60 100644 --- a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/ClientCredentialsFlow.java +++ b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/ClientCredentialsFlow.java @@ -118,10 +118,10 @@ public void close() throws Exception { */ public static ClientCredentialsFlow fromParameters(Map params) { URL issuerUrl = parseParameterUrl(params, CONFIG_PARAM_ISSUER_URL); - String audience = parseParameterString(params, CONFIG_PARAM_AUDIENCE); String privateKeyUrl = parseParameterString(params, CONFIG_PARAM_KEY_FILE); - // This is an optional parameter + // These are optional parameters, so we only perform a get String scope = params.get(CONFIG_PARAM_SCOPE); + String audience = params.get(CONFIG_PARAM_AUDIENCE); return ClientCredentialsFlow.builder() .issuerUrl(issuerUrl) .audience(audience) diff --git a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/README.md b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/README.md index 55ffe58cf0503..b8b1237aa153b 100644 --- a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/README.md +++ b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/README.md @@ -46,7 +46,7 @@ The following parameters are supported: | `type` | Oauth 2.0 auth type. Optional. | default: `client_credentials` | | `issuerUrl` | URL of the provider which allows Pulsar to obtain an access token. Required. | `https://accounts.google.com` | | `privateKey` | URL to a JSON credentials file (in JSON format; see below). Required. | See "Supported Pattern Formats" | -| `audience` | An OAuth 2.0 "resource server" identifier for the Pulsar cluster. Required. | `https://broker.example.com` | +| `audience` | An OAuth 2.0 "resource server" identifier for the Pulsar cluster. Required by some Identity Providers. Optional for client. | `https://broker.example.com` | ### Supported Pattern Formats of `privateKey` The `privateKey` parameter supports the following three pattern formats, and contains client Credentials: @@ -88,7 +88,7 @@ curl --request POST \ In which, - `issuerUrl` parameter in this plugin is mapped to `--url https://dev-kt-aa9ne.us.auth0.com` - `privateKey` file parameter in this plugin should at least contains fields `client_id` and `client_secret`. -- `audience` parameter in this plugin is mapped to `"audience":"https://dev-kt-aa9ne.us.auth0.com/api/v2/"` +- `audience` parameter in this plugin is mapped to `"audience":"https://dev-kt-aa9ne.us.auth0.com/api/v2/"`. This field is only used by some identity providers. ## Pulsar Client Config You can use the provider with the following Pulsar clients. diff --git a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/protocol/TokenClient.java b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/protocol/TokenClient.java index f8667e8625a77..c2b97793e3512 100644 --- a/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/protocol/TokenClient.java +++ b/pulsar-client/src/main/java/org/apache/pulsar/client/impl/auth/oauth2/protocol/TokenClient.java @@ -73,10 +73,21 @@ public void close() throws Exception { /** * Constructing http request parameters. - * @param bodyMap List of parameters to be requested. + * @param req object with relevant request parameters * @return Generate the final request body from a map. */ - String buildClientCredentialsBody(Map bodyMap) { + String buildClientCredentialsBody(ClientCredentialsExchangeRequest req) { + Map bodyMap = new TreeMap<>(); + bodyMap.put("grant_type", "client_credentials"); + bodyMap.put("client_id", req.getClientId()); + bodyMap.put("client_secret", req.getClientSecret()); + // Only set audience and scope if they are non-empty. + if (!StringUtils.isBlank(req.getAudience())) { + bodyMap.put("audience", req.getAudience()); + } + if (!StringUtils.isBlank(req.getScope())) { + bodyMap.put("scope", req.getScope()); + } return bodyMap.entrySet().stream() .map(e -> { try { @@ -96,15 +107,7 @@ String buildClientCredentialsBody(Map bodyMap) { */ public TokenResult exchangeClientCredentials(ClientCredentialsExchangeRequest req) throws TokenExchangeException, IOException { - Map bodyMap = new TreeMap<>(); - bodyMap.put("grant_type", "client_credentials"); - bodyMap.put("client_id", req.getClientId()); - bodyMap.put("client_secret", req.getClientSecret()); - bodyMap.put("audience", req.getAudience()); - if (!StringUtils.isBlank(req.getScope())) { - bodyMap.put("scope", req.getScope()); - } - String body = buildClientCredentialsBody(bodyMap); + String body = buildClientCredentialsBody(req); try { diff --git a/pulsar-client/src/test/java/org/apache/pulsar/client/impl/auth/oauth2/AuthenticationOAuth2Test.java b/pulsar-client/src/test/java/org/apache/pulsar/client/impl/auth/oauth2/AuthenticationOAuth2Test.java index ac14dd2aee105..3ae578c34845c 100644 --- a/pulsar-client/src/test/java/org/apache/pulsar/client/impl/auth/oauth2/AuthenticationOAuth2Test.java +++ b/pulsar-client/src/test/java/org/apache/pulsar/client/impl/auth/oauth2/AuthenticationOAuth2Test.java @@ -86,6 +86,19 @@ public void testConfigure() throws Exception { params.put("privateKey", "data:base64,e30="); params.put("issuerUrl", "http://localhost"); params.put("audience", "http://localhost"); + params.put("scope", "http://localhost"); + ObjectMapper mapper = new ObjectMapper(); + String authParams = mapper.writeValueAsString(params); + this.auth.configure(authParams); + assertNotNull(this.auth.flow); + } + + @Test + public void testConfigureWithoutOptionalParams() throws Exception { + Map params = new HashMap<>(); + params.put("type", "client_credentials"); + params.put("privateKey", "data:base64,e30="); + params.put("issuerUrl", "http://localhost"); ObjectMapper mapper = new ObjectMapper(); String authParams = mapper.writeValueAsString(params); this.auth.configure(authParams); diff --git a/pulsar-client/src/test/java/org/apache/pulsar/client/impl/auth/oauth2/protocol/TokenClientTest.java b/pulsar-client/src/test/java/org/apache/pulsar/client/impl/auth/oauth2/protocol/TokenClientTest.java index 1617359ad08a9..da70d6cd58510 100644 --- a/pulsar-client/src/test/java/org/apache/pulsar/client/impl/auth/oauth2/protocol/TokenClientTest.java +++ b/pulsar-client/src/test/java/org/apache/pulsar/client/impl/auth/oauth2/protocol/TokenClientTest.java @@ -47,19 +47,13 @@ public void exchangeClientCredentialsSuccessByScopeTest() throws DefaultAsyncHttpClient defaultAsyncHttpClient = mock(DefaultAsyncHttpClient.class); URL url = new URL("http://localhost"); TokenClient tokenClient = new TokenClient(url, defaultAsyncHttpClient); - Map bodyMap = new TreeMap<>(); ClientCredentialsExchangeRequest request = ClientCredentialsExchangeRequest.builder() .audience("test-audience") .clientId("test-client-id") .clientSecret("test-client-secret") .scope("test-scope") .build(); - bodyMap.put("grant_type", "client_credentials"); - bodyMap.put("client_id", request.getClientId()); - bodyMap.put("client_secret", request.getClientSecret()); - bodyMap.put("audience", request.getAudience()); - bodyMap.put("scope", request.getScope()); - String body = tokenClient.buildClientCredentialsBody(bodyMap); + String body = tokenClient.buildClientCredentialsBody(request); BoundRequestBuilder boundRequestBuilder = mock(BoundRequestBuilder.class); Response response = mock(Response.class); ListenableFuture listenableFuture = mock(ListenableFuture.class); @@ -80,22 +74,16 @@ public void exchangeClientCredentialsSuccessByScopeTest() throws @Test @SuppressWarnings("unchecked") - public void exchangeClientCredentialsSuccessByNoScopeTest() throws + public void exchangeClientCredentialsSuccessWithoutOptionalClientCredentialsTest() throws IOException, TokenExchangeException, ExecutionException, InterruptedException { DefaultAsyncHttpClient defaultAsyncHttpClient = mock(DefaultAsyncHttpClient.class); URL url = new URL("http://localhost"); TokenClient tokenClient = new TokenClient(url, defaultAsyncHttpClient); - Map bodyMap = new TreeMap<>(); ClientCredentialsExchangeRequest request = ClientCredentialsExchangeRequest.builder() - .audience("test-audience") .clientId("test-client-id") .clientSecret("test-client-secret") .build(); - bodyMap.put("grant_type", "client_credentials"); - bodyMap.put("client_id", request.getClientId()); - bodyMap.put("client_secret", request.getClientSecret()); - bodyMap.put("audience", request.getAudience()); - String body = tokenClient.buildClientCredentialsBody(bodyMap); + String body = tokenClient.buildClientCredentialsBody(request); BoundRequestBuilder boundRequestBuilder = mock(BoundRequestBuilder.class); Response response = mock(Response.class); ListenableFuture listenableFuture = mock(ListenableFuture.class); diff --git a/site2/docs/security-oauth2.md b/site2/docs/security-oauth2.md index 67641d7bf7c2a..cbb4f9e1eaa52 100644 --- a/site2/docs/security-oauth2.md +++ b/site2/docs/security-oauth2.md @@ -28,7 +28,7 @@ The following table lists parameters supported for the `client credentials` auth | `type` | Oauth 2.0 authentication type. | `client_credentials` (default) | Optional | | `issuerUrl` | URL of the authentication provider which allows the Pulsar client to obtain an access token | `https://accounts.google.com` | Required | | `privateKey` | URL to a JSON credentials file | Support the following pattern formats:
  • `file:///path/to/file`
  • `file:/path/to/file`
  • `data:application/json;base64,` | Required | -| `audience` | An OAuth 2.0 "resource server" identifier for the Pulsar cluster | `https://broker.example.com` | Required | +| `audience` | An OAuth 2.0 "resource server" identifier for the Pulsar cluster | `https://broker.example.com` | Optional | | `scope` | Scope of an access request.
    For more more information, see [access token scope](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). | api://pulsar-cluster-1/.default | Optional | The credentials file contains service account credentials used with the client authentication type. The following shows an example of a credentials file `credentials_file.json`. @@ -64,7 +64,7 @@ In the above example, the mapping relationship is shown as below. - The `issuerUrl` parameter in this plugin is mapped to `--url https://dev-kt-aa9ne.us.auth0.com`. - The `privateKey` file parameter in this plugin should at least contains the `client_id` and `client_secret` fields. -- The `audience` parameter in this plugin is mapped to `"audience":"https://dev-kt-aa9ne.us.auth0.com/api/v2/"`. +- The `audience` parameter in this plugin is mapped to `"audience":"https://dev-kt-aa9ne.us.auth0.com/api/v2/"`. This field is only used by some identity providers. ## Client Configuration diff --git a/site2/website-next/docs/security-oauth2.md b/site2/website-next/docs/security-oauth2.md index f3cad38316a15..eb0ccdb935745 100644 --- a/site2/website-next/docs/security-oauth2.md +++ b/site2/website-next/docs/security-oauth2.md @@ -28,7 +28,7 @@ The following table lists parameters supported for the `client credentials` auth | `type` | Oauth 2.0 authentication type. | `client_credentials` (default) | Optional | | `issuerUrl` | URL of the authentication provider which allows the Pulsar client to obtain an access token | `https://accounts.google.com` | Required | | `privateKey` | URL to a JSON credentials file | Support the following pattern formats:
  • `file:///path/to/file`
  • `file:/path/to/file`
  • `data:application/json;base64,`
  • | Required | -| `audience` | An OAuth 2.0 "resource server" identifier for the Pulsar cluster | `https://broker.example.com` | Required | +| `audience` | An OAuth 2.0 "resource server" identifier for the Pulsar cluster | `https://broker.example.com` | Optional | | `scope` | Scope of an access request.
    For more more information, see [access token scope](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). | api://pulsar-cluster-1/.default | Optional | The credentials file contains service account credentials used with the client authentication type. The following shows an example of a credentials file `credentials_file.json`. @@ -68,7 +68,7 @@ In the above example, the mapping relationship is shown as below. - The `issuerUrl` parameter in this plugin is mapped to `--url https://dev-kt-aa9ne.us.auth0.com`. - The `privateKey` file parameter in this plugin should at least contains the `client_id` and `client_secret` fields. -- The `audience` parameter in this plugin is mapped to `"audience":"https://dev-kt-aa9ne.us.auth0.com/api/v2/"`. +- The `audience` parameter in this plugin is mapped to `"audience":"https://dev-kt-aa9ne.us.auth0.com/api/v2/"`. This field is only used by some identity providers. ## Client Configuration