diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml new file mode 100644 index 0000000..c12545a --- /dev/null +++ b/.github/workflows/ci-cd.yaml @@ -0,0 +1,130 @@ +name: Build and Publish + +on: + push: + branches: [main] + pull_request: + release: + types: [published] + +permissions: + contents: write + packages: write + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: maven + server-id: ${{ github.event_name == 'release' && 'central' || 'github' }} + server-username: ${{ github.event_name == 'release' && 'CENTRAL_USERNAME' || 'GITHUB_ACTOR' }} + server-password: ${{ github.event_name == 'release' && 'CENTRAL_TOKEN' || 'GITHUB_TOKEN' }} + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + + - name: Read version from pom.xml + id: get-version + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "pom_version=$VERSION" >> $GITHUB_OUTPUT + + - name: Validate pom.xml version has -SNAPSHOT suffix + run: | + POM_VERSION=${{ steps.get-version.outputs.pom_version }} + echo "POM version: $POM_VERSION" + if [[ "$POM_VERSION" != *"-SNAPSHOT" ]]; then + echo "::error::pom.xml version must end with -SNAPSHOT, but is '$POM_VERSION'" + exit 1 + fi + + - name: Validate release tag matches POM version + if: github.event_name == 'release' + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/v} + POM_VERSION=${{ steps.get-version.outputs.pom_version }} + EXPECTED_TAG="${POM_VERSION%-SNAPSHOT}" + echo "GitHub tag: $TAG_VERSION" + echo "Expected from POM: $EXPECTED_TAG" + if [ "$TAG_VERSION" != "$EXPECTED_TAG" ]; then + echo "::error::Tag v$TAG_VERSION does not match pom.xml version $POM_VERSION without -SNAPSHOT suffix" + exit 1 + fi + + - name: Build and test + run: mvn --batch-mode verify + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + + - name: Publish Test Results + uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1 + if: success() || failure() + with: + name: Test Results + path: target/surefire-reports/*.xml + reporter: java-junit + fail-on-error: false + + - name: Publish to GitHub Packages + if: github.event_name == 'push' + run: mvn --batch-mode deploy -DskipTests -DaltDeploymentRepository=github::https://maven.pkg.github.com/${{ github.repository }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set release version for Maven Central + if: github.event_name == 'release' + run: | + POM_VERSION=${{ steps.get-version.outputs.pom_version }} + RELEASE_VERSION="${POM_VERSION%-SNAPSHOT}" + mvn versions:set -DnewVersion="$RELEASE_VERSION" -DgenerateBackupPoms=false + + - name: Update release with Maven Central links + if: github.event_name == 'release' + run: | + RELEASE_VERSION="${{ steps.get-version.outputs.pom_version }}" + RELEASE_VERSION="${RELEASE_VERSION%-SNAPSHOT}" + + # Get existing release body first + EXISTING_BODY=$(gh release view ${{ github.event.release.tag_name }} --json body -q .body) + + # Start with existing body if it exists + if [ -n "$EXISTING_BODY" ] && [ "$EXISTING_BODY" != "null" ]; then + echo "$EXISTING_BODY" > release_body.md + echo "" >> release_body.md + echo "---" >> release_body.md + echo "" >> release_body.md + fi + + # Append Maven Central links + cat << EOF >> release_body.md + ## 📦 Maven Central + + This release is available at https://central.sonatype.com/artifact/com.scalepoint/oauth-token-client/${RELEASE_VERSION} + + \`\`\`xml + + com.scalepoint + oauth-token-client + ${RELEASE_VERSION} + + \`\`\` + EOF + + # Update the release + gh release edit ${{ github.event.release.tag_name }} --notes-file release_body.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to Maven Central + if: github.event_name == 'release' + run: mvn --batch-mode deploy -DskipTests + env: + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} diff --git a/README.md b/README.md index 1eb0cd0..23e7e15 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Install with Maven: com.scalepoint oauth-token-client - 1.1.1 + 2.0.0 ``` diff --git a/pom.xml b/pom.xml index acbc55a..490b0f0 100644 --- a/pom.xml +++ b/pom.xml @@ -1,23 +1,24 @@ - + 4.0.0 com.scalepoint oauth-token-client - 1.2-SNAPSHOT + 2.0.0-SNAPSHOT jar OAuth2 Token Endpoint Client Client helper for OAuth 2.0 Token endpoint. Supports "Client Credentials" flow with "client_secret", RS256 JWT "client_assertion" and custom grants. - https://github.com/Scalepoint/oauth-token-java-client + https://github.com/scalepoint-tech/oauth-token-java-client - Scalepoint Technologies Ltd. + Scalepoint A/S Yuriy Ostapenko - uncleyo@users.noreply.github.com - Scalepoint Technologies Ltd. + yuriyostapenko@users.noreply.github.com + Scalepoint A/S https://scalepoint.com @@ -28,149 +29,139 @@ - scm:git:git@github.com:Scalepoint/oauth-token-java-client.git - scm:git:git@github.com:Scalepoint/oauth-token-java-client.git - https://github.com/Scalepoint/oauth-token-java-client.git + scm:git:git@github.com:scalepoint-tech/oauth-token-java-client.git + scm:git:git@github.com:scalepoint-tech/oauth-token-java-client.git + https://github.com/scalepoint-tech/oauth-token-java-client.git UTF-8 + 17 + 0.8.0 + 3.14.0 + 3.3.1 + 3.11.2 + 3.2.8 - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.5.1 - - 1.6 - 1.6 - - - - org.apache.maven.plugins - maven-jar-plugin - 2.6 - - - - true - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.0.0 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.3 - - - attach-javadocs - - jar - - - - - - org.codehaus.mojo - animal-sniffer-maven-plugin - 1.15 - - - org.codehaus.mojo.signature - java16 - 1.1 - - - - - ensure-java-1.6-class-library - compile - - check - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - ossrh - https://oss.sonatype.org/ - false - - - - - - io.jsonwebtoken jjwt - 0.6.0 + 0.12.6 com.fasterxml.jackson.core jackson-databind - 2.12.7.1 - - - net.jodah - expiringmap - 0.5.6 + 2.19.2 org.testng testng - 6.9.10 + 7.11.0 test org.mock-server mockserver-netty - 3.10.4 + 5.15.0 test - - org.apache.httpcomponents - httpclient - 4.5.13 - test - - - + + + + ci + + + env.CI + + + + + + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} + true + + central + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + true + true + + -Xlint:all + -Werror + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + + + + + + + central + https://central.sonatype.com/api/v1/publish + + + github + https://maven.pkg.github.com/scalepoint-tech/oauth-token-java-client + + + + \ No newline at end of file diff --git a/src/main/java/com/scalepoint/oauth_token_client/CertificateUtil.java b/src/main/java/com/scalepoint/oauth_token_client/CertificateUtil.java index 517df0e..e948971 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/CertificateUtil.java +++ b/src/main/java/com/scalepoint/oauth_token_client/CertificateUtil.java @@ -1,7 +1,5 @@ package com.scalepoint.oauth_token_client; -import io.jsonwebtoken.impl.Base64UrlCodec; - import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -30,7 +28,7 @@ static String getThumbprint(Certificate certificate) { md.update(der); byte[] digest = md.digest(); - return new Base64UrlCodec().encode(digest); + return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(digest); } static Boolean checkIfMatch(PrivateKey privateKey, X509Certificate certificate) { diff --git a/src/main/java/com/scalepoint/oauth_token_client/ClientAssertionJwtFactory.java b/src/main/java/com/scalepoint/oauth_token_client/ClientAssertionJwtFactory.java index 4e94011..6628d93 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/ClientAssertionJwtFactory.java +++ b/src/main/java/com/scalepoint/oauth_token_client/ClientAssertionJwtFactory.java @@ -1,17 +1,16 @@ package com.scalepoint.oauth_token_client; -import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import java.security.Key; +import java.security.PrivateKey; +import java.time.Instant; import java.util.Date; import java.util.UUID; class ClientAssertionJwtFactory { private final String tokenEndpointUri; private final String clientId; - private final Key key; + private final PrivateKey key; private final String thumbprint; public ClientAssertionJwtFactory(String tokenEndpointUri, String clientId, CertificateWithPrivateKey keyPair) { @@ -21,23 +20,27 @@ public ClientAssertionJwtFactory(String tokenEndpointUri, String clientId, Certi this.key = keyPair.getPrivateKey(); } - public String CreateAssertionToken() { - Date now = new Date(); + public String createAssertionToken() { + Instant now = Instant.now(); // no need to have a long-lived token (clock skew should be accounted for on the server-side) - Date expires = new Date(now.getTime() + 10000 /* 10 seconds */); + Instant expires = now.plusSeconds(10); return Jwts.builder() - .setHeaderParam("typ", "JWT") - .setHeaderParam(JwsHeader.X509_CERT_SHA1_THUMBPRINT, thumbprint) - .setHeaderParam(JwsHeader.KEY_ID, thumbprint) - .setIssuer(clientId) - .setSubject(clientId) - .setAudience(tokenEndpointUri) - .setId(UUID.randomUUID().toString()) - .setIssuedAt(now) - .setNotBefore(now) - .setExpiration(expires) - .signWith(SignatureAlgorithm.RS256, key) + .header() + .type("JWT") + .add("x5t", thumbprint) + .keyId(thumbprint) + .and() + .claims() + .issuer(clientId) + .subject(clientId) + .audience().add(tokenEndpointUri).and() + .id(UUID.randomUUID().toString()) + .issuedAt(Date.from(now)) + .notBefore(Date.from(now)) + .expiration(Date.from(expires)) + .and() + .signWith(key, Jwts.SIG.RS256) .compact(); } diff --git a/src/main/java/com/scalepoint/oauth_token_client/ClientCredentialsGrantTokenClient.java b/src/main/java/com/scalepoint/oauth_token_client/ClientCredentialsGrantTokenClient.java index 291c319..2a3ce64 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/ClientCredentialsGrantTokenClient.java +++ b/src/main/java/com/scalepoint/oauth_token_client/ClientCredentialsGrantTokenClient.java @@ -40,7 +40,7 @@ public ClientCredentialsGrantTokenClient(String tokenEndpointUri, ClientCredenti * @throws IOException Exception during token endpoint communication */ @SuppressWarnings("UnusedReturnValue") - public String getToken(final String... scopes) throws IOException { + public String getToken(final String... scopes) throws IOException, InterruptedException { return getTokenInternal(Collections.emptyList(), scopes); } diff --git a/src/main/java/com/scalepoint/oauth_token_client/CustomGrantTokenClient.java b/src/main/java/com/scalepoint/oauth_token_client/CustomGrantTokenClient.java index f7647ff..9346e99 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/CustomGrantTokenClient.java +++ b/src/main/java/com/scalepoint/oauth_token_client/CustomGrantTokenClient.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.List; -@SuppressWarnings("WeakerAccess") public abstract class CustomGrantTokenClient { private final ClientCredentials clientCredentials; private final TokenEndpointHttpClient tokenEndpointHttpClient; @@ -26,7 +25,7 @@ public CustomGrantTokenClient(String tokenEndpointUri, ClientCredentials clientC * @return Access token * @throws IOException Exception during token endpoint communication */ - protected String getTokenInternal(final List parameters, final String... scopes) throws IOException { + protected String getTokenInternal(final List parameters, final String... scopes) throws IOException, InterruptedException { final String scopeString = (scopes == null || scopes.length < 1) @@ -37,7 +36,7 @@ protected String getTokenInternal(final List parameters, final St return cache.get(cacheKey, new TokenSource() { @Override - public ExpiringToken get() throws IOException { + public ExpiringToken get() throws IOException, InterruptedException { List form = new ArrayList(); diff --git a/src/main/java/com/scalepoint/oauth_token_client/DigestUtil.java b/src/main/java/com/scalepoint/oauth_token_client/DigestUtil.java index 2250ab1..e0f0314 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/DigestUtil.java +++ b/src/main/java/com/scalepoint/oauth_token_client/DigestUtil.java @@ -1,24 +1,18 @@ package com.scalepoint.oauth_token_client; -import javax.xml.bind.DatatypeConverter; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; class DigestUtil { static String sha1Hex(String data) { - MessageDigest digest = null; try { - digest = MessageDigest.getInstance("SHA-1"); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + byte[] digestBytes = digest.digest(data.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(digestBytes); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } - try { - digest.update(data.getBytes("utf8")); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - byte[] digestBytes = digest.digest(); - return DatatypeConverter.printHexBinary(digestBytes); } } diff --git a/src/main/java/com/scalepoint/oauth_token_client/InMemoryTokenCache.java b/src/main/java/com/scalepoint/oauth_token_client/InMemoryTokenCache.java index 46f2696..d902689 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/InMemoryTokenCache.java +++ b/src/main/java/com/scalepoint/oauth_token_client/InMemoryTokenCache.java @@ -1,27 +1,35 @@ package com.scalepoint.oauth_token_client; -import net.jodah.expiringmap.ExpirationPolicy; -import net.jodah.expiringmap.ExpiringMap; - import java.io.IOException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** - * Simple in-memory token cache implementation + * Simple in-memory token cache implementation using ConcurrentHashMap */ public class InMemoryTokenCache implements TokenCache { - private final ExpiringMap cacheMap; - private final ReentrantLock lock = new ReentrantLock(); + private final ConcurrentMap cache = new ConcurrentHashMap<>(); /** - * Create new in-memory cache instance + * Wrapper class to store token with its expiration time */ - @SuppressWarnings("WeakerAccess") - public InMemoryTokenCache() { - this.cacheMap = ExpiringMap.builder() - .variableExpiration() - .build(); + private static class CachedToken { + private final String token; + private final Instant expirationTime; + + CachedToken(String token, long expiresInSeconds) { + this.token = token; + this.expirationTime = Instant.now().plusSeconds(expiresInSeconds); + } + + String getToken() { + return token; + } + + boolean isExpired() { + return Instant.now().isAfter(expirationTime); + } } /** @@ -31,25 +39,29 @@ public InMemoryTokenCache() { * @throws IOException Exception from underlying source */ @Override - public String get(String cacheKey, TokenSource underlyingSource) throws IOException { - String value = cacheMap.get(cacheKey); - if (value == null) { - lock.lock(); - try { - value = cacheMap.get(cacheKey); - if (value == null) { - ExpiringToken token = underlyingSource.get(); - value = token.getToken(); - if (token.getExpiresInSeconds() > 0) { - cacheMap.put(cacheKey, value, ExpirationPolicy.CREATED, token.getExpiresInSeconds(), TimeUnit.SECONDS); - } else { - throw new IOException("Authorization server does not provide token expiration information. Consider using NoCache or custom cache implementation to avoid performance penalty caused by locking."); - } - } - } finally { - lock.unlock(); - } + public String get(String cacheKey, TokenSource underlyingSource) throws IOException, InterruptedException { + CachedToken cachedToken = cache.get(cacheKey); + + // Check if we have a valid (non-expired) token + if (cachedToken != null && !cachedToken.isExpired()) { + return cachedToken.getToken(); + } + + // Token is missing or expired - fetch a new one + ExpiringToken token = underlyingSource.get(); + if (token.getExpiresInSeconds() <= 0) { + throw new IllegalArgumentException("Authorization server does not provide token expiration information. Consider using NoCache or custom cache implementation."); } - return value; + + // Use compute for thread-safe update, even though we already have the token + CachedToken newToken = new CachedToken(token.getToken(), token.getExpiresInSeconds()); + cache.compute(cacheKey, (key, existingToken) -> { + if (existingToken != null && !existingToken.isExpired()) { + return existingToken; + } + return newToken; + }); + + return newToken.getToken(); } } diff --git a/src/main/java/com/scalepoint/oauth_token_client/JwtBearerClientAssertionCredentials.java b/src/main/java/com/scalepoint/oauth_token_client/JwtBearerClientAssertionCredentials.java index 5e45ad2..bb78503 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/JwtBearerClientAssertionCredentials.java +++ b/src/main/java/com/scalepoint/oauth_token_client/JwtBearerClientAssertionCredentials.java @@ -32,7 +32,7 @@ public JwtBearerClientAssertionCredentials(String tokenEndpointUri, String clien @Override public List getPostParams() { - String assertionToken = assertionFactory.CreateAssertionToken(); + String assertionToken = assertionFactory.createAssertionToken(); ArrayList params = new ArrayList(); params.add(new NameValuePair("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); params.add(new NameValuePair("client_assertion", assertionToken)); diff --git a/src/main/java/com/scalepoint/oauth_token_client/NoCache.java b/src/main/java/com/scalepoint/oauth_token_client/NoCache.java index f34ca2b..6c32aad 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/NoCache.java +++ b/src/main/java/com/scalepoint/oauth_token_client/NoCache.java @@ -14,7 +14,7 @@ public class NoCache implements TokenCache { * @throws IOException Exception from underlying source */ @Override - public String get(String cacheKey, TokenSource underlyingSource) throws IOException { + public String get(String cacheKey, TokenSource underlyingSource) throws IOException, InterruptedException { return underlyingSource.get().getToken(); } } diff --git a/src/main/java/com/scalepoint/oauth_token_client/ResourceScopedAccessGrantTokenClient.java b/src/main/java/com/scalepoint/oauth_token_client/ResourceScopedAccessGrantTokenClient.java index 12c143e..ee6221f 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/ResourceScopedAccessGrantTokenClient.java +++ b/src/main/java/com/scalepoint/oauth_token_client/ResourceScopedAccessGrantTokenClient.java @@ -23,7 +23,7 @@ public ResourceScopedAccessGrantTokenClient(String tokenEndpointUri, ClientCrede * @throws IOException Exception during token endpoint communication */ @SuppressWarnings("UnusedReturnValue") - public String getToken(ResourceScopedAccessGrantParameters parameters) throws IOException { + public String getToken(ResourceScopedAccessGrantParameters parameters) throws IOException, InterruptedException { return getTokenInternal(getPostParams(parameters), parameters.getScope()); } diff --git a/src/main/java/com/scalepoint/oauth_token_client/TokenCache.java b/src/main/java/com/scalepoint/oauth_token_client/TokenCache.java index b718c4a..176484e 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/TokenCache.java +++ b/src/main/java/com/scalepoint/oauth_token_client/TokenCache.java @@ -12,5 +12,5 @@ public interface TokenCache { * @return Token from either cache or underlying source * @throws IOException Exception from underlying cache */ - String get(String cacheKey, TokenSource underlyingSource) throws IOException; + String get(String cacheKey, TokenSource underlyingSource) throws IOException, InterruptedException; } diff --git a/src/main/java/com/scalepoint/oauth_token_client/TokenEndpointHttpClient.java b/src/main/java/com/scalepoint/oauth_token_client/TokenEndpointHttpClient.java index c27f930..9bead75 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/TokenEndpointHttpClient.java +++ b/src/main/java/com/scalepoint/oauth_token_client/TokenEndpointHttpClient.java @@ -3,68 +3,50 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.*; -import java.net.HttpURLConnection; -import java.net.URL; +import java.io.IOException; +import java.net.URI; import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.List; +import java.util.stream.Collectors; class TokenEndpointHttpClient { - private final static String UTF8 = "utf-8"; private final static ObjectMapper MAPPER = new ObjectMapper(); private final String tokenEndpointUri; + private final HttpClient httpClient; public TokenEndpointHttpClient(String tokenEndpointUri) { this.tokenEndpointUri = tokenEndpointUri; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .build(); } - ExpiringToken getToken(List params) throws IOException { - - String body = formatRequest(params); - HttpURLConnection c = makeRequest(body); - String tokenResponse = readResponse(c); - return parseResponse(tokenResponse); - } - - private String formatRequest(List params) throws UnsupportedEncodingException { - String body = ""; - for (NameValuePair p: params) { - body += URLEncoder.encode(p.getName(), UTF8) + "=" + URLEncoder.encode(p.getValue(), UTF8) + "&"; + ExpiringToken getToken(List params) throws IOException, InterruptedException { + String formData = params.stream() + .map(p -> URLEncoder.encode(p.getName(), StandardCharsets.UTF_8) + "=" + + URLEncoder.encode(p.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&")); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(tokenEndpointUri)) + .timeout(Duration.ofSeconds(30)) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(formData, StandardCharsets.UTF_8)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new IOException("HTTP Status Code: " + response.statusCode() + ", " + response.body()); } - body = body.substring(0, body.length()-1); - return body; - } - - private HttpURLConnection makeRequest(String body) throws IOException { - URL u = new URL(tokenEndpointUri); - HttpURLConnection c = (HttpURLConnection)u.openConnection(); - c.setRequestMethod("POST"); - c.setInstanceFollowRedirects(false); - c.setDoInput(true); - c.setDoOutput(true); - c.setReadTimeout(30 * 1000); - c.setConnectTimeout(30 * 1000); - c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - c.setUseCaches(false); - OutputStream outputStream = c.getOutputStream(); - OutputStreamWriter writer = new OutputStreamWriter(outputStream); - writer.write(body); - writer.flush(); - writer.close(); - return c; - } - - private String readResponse(HttpURLConnection c) throws IOException { - int statusCode = c.getResponseCode(); - if (statusCode != 200) { - InputStream errorStream = c.getErrorStream(); - String errorMessage = errorStream != null - ? ", " + readStream(errorStream) - : ""; - throw new IOException("HTTP Status Code: "+statusCode+errorMessage); - } - return readStream(c.getInputStream()); + + return parseResponse(response.body()); } private ExpiringToken parseResponse(String tokenResponse) throws IOException { @@ -80,15 +62,4 @@ private ExpiringToken parseResponse(String tokenResponse) throws IOException { return new ExpiringToken(accessToken, expiresInSeconds); } - - private String readStream(InputStream stream) throws IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); - String content = ""; - String line; - while ((line = reader.readLine()) != null) { - content += line; - } - reader.close(); - return content; - } } diff --git a/src/main/java/com/scalepoint/oauth_token_client/TokenSource.java b/src/main/java/com/scalepoint/oauth_token_client/TokenSource.java index eccd3de..1830422 100644 --- a/src/main/java/com/scalepoint/oauth_token_client/TokenSource.java +++ b/src/main/java/com/scalepoint/oauth_token_client/TokenSource.java @@ -10,5 +10,5 @@ public interface TokenSource { * @return Token * @throws IOException */ - ExpiringToken get() throws IOException; + ExpiringToken get() throws IOException, InterruptedException; } diff --git a/src/test/java/com/scalepoint/oauth_token_client/BadRequestCallback.java b/src/test/java/com/scalepoint/oauth_token_client/BadRequestCallback.java index e61ea63..ac2e2a8 100644 --- a/src/test/java/com/scalepoint/oauth_token_client/BadRequestCallback.java +++ b/src/test/java/com/scalepoint/oauth_token_client/BadRequestCallback.java @@ -1,12 +1,12 @@ package com.scalepoint.oauth_token_client; -import org.mockserver.mock.action.ExpectationCallback; +import org.mockserver.mock.action.ExpectationResponseCallback; import org.mockserver.model.Header; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; @SuppressWarnings("WeakerAccess") -public class BadRequestCallback implements ExpectationCallback { +public class BadRequestCallback implements ExpectationResponseCallback { @Override public HttpResponse handle(HttpRequest httpRequest) { return new HttpResponse() diff --git a/src/test/java/com/scalepoint/oauth_token_client/ClientAssertionJwtFactoryTest.java b/src/test/java/com/scalepoint/oauth_token_client/ClientAssertionJwtFactoryTest.java index 342b73a..82e2f65 100644 --- a/src/test/java/com/scalepoint/oauth_token_client/ClientAssertionJwtFactoryTest.java +++ b/src/test/java/com/scalepoint/oauth_token_client/ClientAssertionJwtFactoryTest.java @@ -10,7 +10,6 @@ /** * Validate that assertion token is generated according to the specification */ -@SuppressWarnings("unused") public class ClientAssertionJwtFactoryTest { private final static String TOKEN_ENDPOINT_URI = "https://foobar"; @@ -23,8 +22,9 @@ public void init() { CertificateWithPrivateKey keyPair = TestCertificateHelper.load(); thumbprint = CertificateUtil.getThumbprint(keyPair.getCertificate()); ClientAssertionJwtFactory factory = new ClientAssertionJwtFactory(TOKEN_ENDPOINT_URI, CLIENT_ID, keyPair); - String tokenString = factory.CreateAssertionToken(); - token = Jwts.parser().setSigningKey(keyPair.getPrivateKey()).parseClaimsJws(tokenString); + String tokenString = factory.createAssertionToken(); + // Use the PUBLIC key from the certificate to verify the JWT signature, not the private key + token = Jwts.parser().verifyWith(keyPair.getCertificate().getPublicKey()).build().parseSignedClaims(tokenString); } @Test @@ -44,31 +44,32 @@ public void testContainsX5t() { @Test public void testContainsValidIssuer() { - Assert.assertEquals(token.getBody().getIssuer(), CLIENT_ID); + Assert.assertEquals(token.getPayload().getIssuer(), CLIENT_ID); } @Test public void testContainsValidSubject() { - Assert.assertEquals(token.getBody().getSubject(), CLIENT_ID); + Assert.assertEquals(token.getPayload().getSubject(), CLIENT_ID); } @Test public void testContainsValidAudience() { - Assert.assertEquals(token.getBody().getAudience(), TOKEN_ENDPOINT_URI); + // In newer JJWT versions, audience is returned as a Set + Assert.assertTrue(token.getPayload().getAudience().contains(TOKEN_ENDPOINT_URI)); } @Test public void testContainsJwtId() { - Assert.assertNotNull(token.getBody().getId()); + Assert.assertNotNull(token.getPayload().getId()); } @Test public void testContainsExpiration() { - Assert.assertNotNull(token.getBody().getExpiration()); + Assert.assertNotNull(token.getPayload().getExpiration()); } @Test public void testContainsIssuedAt() { - Assert.assertNotNull(token.getBody().getIssuedAt()); + Assert.assertNotNull(token.getPayload().getIssuedAt()); } } diff --git a/src/test/java/com/scalepoint/oauth_token_client/ClientAssertionTokenClientTest.java b/src/test/java/com/scalepoint/oauth_token_client/ClientAssertionTokenClientTest.java index 5a8c403..a2d274d 100644 --- a/src/test/java/com/scalepoint/oauth_token_client/ClientAssertionTokenClientTest.java +++ b/src/test/java/com/scalepoint/oauth_token_client/ClientAssertionTokenClientTest.java @@ -1,6 +1,6 @@ package com.scalepoint.oauth_token_client; -import org.mockserver.model.HttpCallback; +import org.mockserver.model.HttpClassCallback; import org.mockserver.model.HttpRequest; import org.testng.Assert; import org.testng.annotations.Test; @@ -10,7 +10,6 @@ import static org.mockserver.matchers.Times.exactly; import static org.mockserver.model.HttpRequest.request; -@SuppressWarnings("unused") public class ClientAssertionTokenClientTest extends MockServerTestBase { @Test @@ -23,7 +22,7 @@ public void testSuccess() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback(SuccessfulExpectationCallback.class)); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient( tokenEndpointUri, @@ -47,7 +46,7 @@ public void testSuccessFromCache() throws Exception { request, exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback(SuccessfulExpectationCallback.class)); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient( tokenEndpointUri, @@ -72,7 +71,7 @@ public void testValidRequest() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(ValidClientAssertionExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback(ValidClientAssertionExpectationCallback.class)); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient( tokenEndpointUri, @@ -95,7 +94,7 @@ public void testFailure() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(BadRequestCallback.class.getName())); + .respond(HttpClassCallback.callback(BadRequestCallback.class)); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient( tokenEndpointUri, @@ -118,7 +117,7 @@ public void testEmptyScopes() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback(SuccessfulExpectationCallback.class)); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient( tokenEndpointUri, diff --git a/src/test/java/com/scalepoint/oauth_token_client/ClientSecretTokenClientTest.java b/src/test/java/com/scalepoint/oauth_token_client/ClientSecretTokenClientTest.java index b6be83b..1b1c3f7 100644 --- a/src/test/java/com/scalepoint/oauth_token_client/ClientSecretTokenClientTest.java +++ b/src/test/java/com/scalepoint/oauth_token_client/ClientSecretTokenClientTest.java @@ -1,6 +1,6 @@ package com.scalepoint.oauth_token_client; -import org.mockserver.model.HttpCallback; +import org.mockserver.model.HttpClassCallback; import org.mockserver.model.HttpRequest; import org.testng.Assert; import org.testng.annotations.Test; @@ -10,7 +10,6 @@ import static org.mockserver.matchers.Times.exactly; import static org.mockserver.model.HttpRequest.request; -@SuppressWarnings("unused") public class ClientSecretTokenClientTest extends MockServerTestBase { @Test @@ -23,7 +22,7 @@ public void testSuccess() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient(tokenEndpointUri, new ClientSecretCredentials("clientid", "password")); tokenClient.getToken("success"); @@ -40,7 +39,7 @@ public void testSuccessFromCache() throws Exception { request, exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient(tokenEndpointUri, new ClientSecretCredentials("clientid", "password")); tokenClient.getToken("cache"); @@ -58,7 +57,7 @@ public void testValidRequest() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(ValidClientSecretExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback().withCallbackClass(ValidClientSecretExpectationCallback.class.getName())); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient(tokenEndpointUri, new ClientSecretCredentials("clientid", "password")); tokenClient.getToken("scope1", "scope2"); @@ -74,7 +73,7 @@ public void testFailure() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(BadRequestCallback.class.getName())); + .respond(HttpClassCallback.callback().withCallbackClass(BadRequestCallback.class.getName())); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient(tokenEndpointUri, new ClientSecretCredentials("clientid", "password")); tokenClient.getToken("badRequest"); @@ -90,7 +89,7 @@ public void testEmptyScopes() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); ClientCredentialsGrantTokenClient tokenClient = new ClientCredentialsGrantTokenClient(tokenEndpointUri, new ClientSecretCredentials("clientid", "password")); tokenClient.getToken(); diff --git a/src/test/java/com/scalepoint/oauth_token_client/ResourceScopedAccessGrantClientTest.java b/src/test/java/com/scalepoint/oauth_token_client/ResourceScopedAccessGrantClientTest.java index 79d64f4..a36b73b 100644 --- a/src/test/java/com/scalepoint/oauth_token_client/ResourceScopedAccessGrantClientTest.java +++ b/src/test/java/com/scalepoint/oauth_token_client/ResourceScopedAccessGrantClientTest.java @@ -1,6 +1,6 @@ package com.scalepoint.oauth_token_client; -import org.mockserver.model.HttpCallback; +import org.mockserver.model.HttpClassCallback; import org.testng.annotations.Test; import java.io.IOException; @@ -8,7 +8,6 @@ import static org.mockserver.matchers.Times.exactly; import static org.mockserver.model.HttpRequest.request; -@SuppressWarnings("unused") public class ResourceScopedAccessGrantClientTest extends MockServerTestBase { @Test @@ -21,7 +20,7 @@ public void testSuccess() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); ResourceScopedAccessGrantTokenClient tokenClient = new ResourceScopedAccessGrantTokenClient( tokenEndpointUri, @@ -44,7 +43,7 @@ public void testValidRequest() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(ValidResourceScopedAccessRequestCallback.class.getName())); + .respond(HttpClassCallback.callback().withCallbackClass(ValidResourceScopedAccessRequestCallback.class.getName())); ResourceScopedAccessGrantTokenClient tokenClient = new ResourceScopedAccessGrantTokenClient( tokenEndpointUri, @@ -67,7 +66,7 @@ public void testFailure() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(BadRequestCallback.class.getName())); + .respond(HttpClassCallback.callback().withCallbackClass(BadRequestCallback.class.getName())); ResourceScopedAccessGrantTokenClient tokenClient = new ResourceScopedAccessGrantTokenClient( tokenEndpointUri, @@ -90,7 +89,7 @@ public void testEmptyScopes() throws Exception { .withPath("/oauth2/token"), exactly(1) ) - .callback(HttpCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); + .respond(HttpClassCallback.callback().withCallbackClass(SuccessfulExpectationCallback.class.getName())); ResourceScopedAccessGrantTokenClient tokenClient = new ResourceScopedAccessGrantTokenClient( tokenEndpointUri, diff --git a/src/test/java/com/scalepoint/oauth_token_client/SuccessfulExpectationCallback.java b/src/test/java/com/scalepoint/oauth_token_client/SuccessfulExpectationCallback.java index 233a58c..bff389e 100644 --- a/src/test/java/com/scalepoint/oauth_token_client/SuccessfulExpectationCallback.java +++ b/src/test/java/com/scalepoint/oauth_token_client/SuccessfulExpectationCallback.java @@ -1,12 +1,11 @@ package com.scalepoint.oauth_token_client; -import org.mockserver.mock.action.ExpectationCallback; +import org.mockserver.mock.action.ExpectationResponseCallback; import org.mockserver.model.Header; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -@SuppressWarnings("WeakerAccess") -public class SuccessfulExpectationCallback implements ExpectationCallback { +public class SuccessfulExpectationCallback implements ExpectationResponseCallback { @Override public HttpResponse handle(HttpRequest httpRequest) { return new HttpResponse() diff --git a/src/test/java/com/scalepoint/oauth_token_client/ValidClientAssertionExpectationCallback.java b/src/test/java/com/scalepoint/oauth_token_client/ValidClientAssertionExpectationCallback.java index 159380a..886190a 100644 --- a/src/test/java/com/scalepoint/oauth_token_client/ValidClientAssertionExpectationCallback.java +++ b/src/test/java/com/scalepoint/oauth_token_client/ValidClientAssertionExpectationCallback.java @@ -14,7 +14,7 @@ protected boolean isValid(HashMap params) { return false; CertificateWithPrivateKey keyPair = TestCertificateHelper.load(); - Jwts.parser().setSigningKey(keyPair.getPrivateKey()).parseClaimsJws(params.get("client_assertion")); + Jwts.parser().verifyWith(keyPair.getCertificate().getPublicKey()).build().parseSignedClaims(params.get("client_assertion")); return true; } diff --git a/src/test/java/com/scalepoint/oauth_token_client/ValidRequestExpectationCallback.java b/src/test/java/com/scalepoint/oauth_token_client/ValidRequestExpectationCallback.java index 3f6ce2c..6dcf595 100644 --- a/src/test/java/com/scalepoint/oauth_token_client/ValidRequestExpectationCallback.java +++ b/src/test/java/com/scalepoint/oauth_token_client/ValidRequestExpectationCallback.java @@ -1,17 +1,16 @@ package com.scalepoint.oauth_token_client; -import org.apache.commons.lang3.CharEncoding; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; -import org.mockserver.mock.action.ExpectationCallback; +import org.mockserver.mock.action.ExpectationResponseCallback; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; -abstract class ValidRequestExpectationCallback implements ExpectationCallback { +abstract class ValidRequestExpectationCallback implements ExpectationResponseCallback { @Override public HttpResponse handle(HttpRequest httpRequest) { @@ -24,7 +23,7 @@ public HttpResponse handle(HttpRequest httpRequest) { private boolean isValid(HttpRequest httpRequest) { String body = (String) httpRequest.getBody().getValue(); - List parsedParams = URLEncodedUtils.parse(body, Charset.forName(CharEncoding.UTF_8)); + List parsedParams = URLEncodedUtils.parse(body, StandardCharsets.UTF_8); HashMap params = new HashMap(); for (NameValuePair p : parsedParams) params.put(p.getName(), p.getValue()); return isValid(params); diff --git a/src/test/java/com/scalepoint/oauth_token_client/ValidResourceScopedAccessRequestCallback.java b/src/test/java/com/scalepoint/oauth_token_client/ValidResourceScopedAccessRequestCallback.java index e8fd313..e66cd20 100644 --- a/src/test/java/com/scalepoint/oauth_token_client/ValidResourceScopedAccessRequestCallback.java +++ b/src/test/java/com/scalepoint/oauth_token_client/ValidResourceScopedAccessRequestCallback.java @@ -5,7 +5,6 @@ import java.util.HashMap; public class ValidResourceScopedAccessRequestCallback extends ValidRequestExpectationCallback { - @SuppressWarnings("RedundantIfStatement") @Override protected boolean isValid(HashMap params) { if (!params.get("grant_type").equals("urn:scalepoint:params:oauth:grant-type:resource-scoped-access")) return false; @@ -14,7 +13,7 @@ protected boolean isValid(HashMap params) { return false; CertificateWithPrivateKey keyPair = TestCertificateHelper.load(); - Jwts.parser().setSigningKey(keyPair.getPrivateKey()).parseClaimsJws(params.get("client_assertion")); + Jwts.parser().verifyWith(keyPair.getCertificate().getPublicKey()).build().parseSignedClaims(params.get("client_assertion")); if (!params.get("resource").equals("resource")) return false; if (!params.get("amr").equals("pwd otp mfa")) return false;