From 7c1597d47952ffabbb50a691a05ed3fb3357f116 Mon Sep 17 00:00:00 2001 From: Alfredo Oliviero Date: Fri, 22 Mar 2024 15:43:59 +0100 Subject: [PATCH] Token exchange (#27099) Header X-D4Science-Context in query exchange and refresh - performQueryTokenWithPOST accepts also optional headers - performURLEncodedPOSTSendData accepts also optional headers - implemented queryExchangeToken --- CHANGELOG.md | 9 +++ pom.xml | 2 +- .../oidc/rest/OpenIdConnectRESTHelper.java | 76 ++++++++++++++++++- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17dda65..2222206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm # Changelog for "oidc-library" +## [v1.3.2-SNAPSHOT] +Token exchange (#27099) +Header X-D4Science-Context in query exchange and refresh + +- performQueryTokenWithPOST accepts also optional headers +- performURLEncodedPOSTSendData accepts also optional headers +- implemented queryExchangeToken + + ## [v1.3.1] Added `Catalogue-Manager` and `Catalogue-Moderator` roles to the enum (#23623) diff --git a/pom.xml b/pom.xml index 4eb3e49..97039cc 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.gcube.common oidc-library - 1.3.1 + 1.3.2-SNAPSHOT diff --git a/src/main/java/org/gcube/oidc/rest/OpenIdConnectRESTHelper.java b/src/main/java/org/gcube/oidc/rest/OpenIdConnectRESTHelper.java index 12ea16e..93e1676 100644 --- a/src/main/java/org/gcube/oidc/rest/OpenIdConnectRESTHelper.java +++ b/src/main/java/org/gcube/oidc/rest/OpenIdConnectRESTHelper.java @@ -98,13 +98,19 @@ public class OpenIdConnectRESTHelper { return performQueryTokenWithPOST(tokenURL, null, params); } + protected static JWTToken performQueryTokenWithPOST(URL tokenURL, String authorization, Map> params) throws OpenIdConnectRESTHelperException { + return performQueryTokenWithPOST(tokenURL, authorization, params, null); + } + + protected static JWTToken performQueryTokenWithPOST(URL tokenURL, String authorization, + Map> params, Map headers) throws OpenIdConnectRESTHelperException { logger.debug("Querying access token from OIDC server with URL: {}", tokenURL); StringBuilder sb; try { - HttpURLConnection httpURLConnection = performURLEncodedPOSTSendData(tokenURL, params, authorization); + HttpURLConnection httpURLConnection = performURLEncodedPOSTSendData(tokenURL, params, authorization, headers); sb = new StringBuilder(); int httpResultCode = httpURLConnection.getResponseCode(); @@ -143,7 +149,12 @@ public class OpenIdConnectRESTHelper { } protected static HttpURLConnection performURLEncodedPOSTSendData(URL url, Map> params, - String authorization) throws IOException, ProtocolException, UnsupportedEncodingException { + String authorization) throws IOException, ProtocolException, UnsupportedEncodingException { + return performURLEncodedPOSTSendData(url, params, authorization, null); + } + + protected static HttpURLConnection performURLEncodedPOSTSendData(URL url, Map> params, + String authorization, Map headers) throws IOException, ProtocolException, UnsupportedEncodingException { HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); @@ -155,6 +166,12 @@ public class OpenIdConnectRESTHelper { logger.debug("Adding authorization header as: {}", authorization); con.setRequestProperty("Authorization", authorization); } + + if (headers != null) { + for (String key : headers.keySet()) { + con.setRequestProperty(key, headers.get(key)); + } + } OutputStream os = con.getOutputStream(); String queryString = mapToQueryString(params); @@ -184,6 +201,61 @@ public class OpenIdConnectRESTHelper { audience, permissions); } + /** + * Queries from the OIDC server an exchanged token by using provided access token, for the given audience (context), + * in URLEncoded form or not, and optionally a list of permissions. + * + * @param tokenUrl the token endpoint {@link URL} of the OIDC server + * @param authorization the auth token (the access token URLEncoded by the "Bearer " string) + * @param audience the audience (context) where to request the issuing of the ticket (URLEncoded) + * @param permissions a list of permissions, can be null + * @return the issued token + * @throws OpenIdConnectRESTHelperException if an error occurs (also an unauthorized call), inspect the exception for details + */ + public static JWTToken queryExchangeToken(URL tokenUrl, String authorization, String audience, String client_id, String client_secret, + List permissions) throws OpenIdConnectRESTHelperException { + + logger.info("Queried exchangeToken for context " + audience); + + Map> params = new HashMap<>(); + params.put("subject_token", Arrays.asList("authorization")); + params.put("grant_type", Arrays.asList("urn:ietf:params:oauth:grant-type:token-exchange")); + params.put("client_id", Arrays.asList(client_id)); + params.put("client_secret", Arrays.asList(client_secret)); + params.put("subject_token_type", Arrays.asList("urn:ietf:params:oauth:token-type:access_token")); + params.put("requested_token_type", Arrays.asList("urn:ietf:params:oauth:token-type:access_token")); + + if (audience.startsWith("/")) { + try { + logger.trace("Audience was provided in non URL encoded form, encoding it"); + audience = URLEncoder.encode(audience, "UTF-8"); + } catch (UnsupportedEncodingException e) { + logger.error("Cannot URL encode 'audience'", e); + } + } + try { + params.put("audience", Arrays.asList(URLEncoder.encode(audience, "UTF-8"))); + } catch (UnsupportedEncodingException e) { + logger.error("Cannot URL encode 'audience'", e); + } + if (permissions != null && !permissions.isEmpty()) { + params.put( + "permission", permissions.stream().map(s -> { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + return ""; + } + }).collect(Collectors.toList())); + } + + + Map headers = new HashMap<>(); + headers.put("X-D4Science-Context", audience); + + return performQueryTokenWithPOST(tokenUrl, authorization, params, headers); + } + /** * Queries from the OIDC server an UMA token, by using provided access token, for the given audience (context), * in URLEncoded form or not, and optionally a list of permissions.