From 7a0b2b03928bc7dce1085fdecd8226bc32951bd5 Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 7 Nov 2024 00:44:27 +0300 Subject: [PATCH 01/22] draft core and entraid --- .github/ISSUE_TEMPLATE | 32 ++ .github/dependabot.yml | 7 + .github/release-drafter-config.yml | 55 ++++ .github/spellcheck-settings.yml | 28 ++ .github/wordlist.txt | 306 ++++++++++++++++++ .github/workflows/codeql-analysis.yml | 63 ++++ .github/workflows/doctests.yml | 38 +++ .github/workflows/release-drafter.yml | 20 ++ .github/workflows/snapshot.yml | 43 +++ .github/workflows/spellcheck.yml | 14 + .github/workflows/stale-issues.yml | 25 ++ .github/workflows/version-and-release.yml | 46 +++ LICENSE | 21 ++ core/pom.xml | 274 ++++++++++++++++ .../authentication/core/AuthXException.java | 12 + .../authentication/core/IdentityProvider.java | 6 + .../core/IdentityProviderConfig.java | 6 + .../authentication/core/SimpleToken.java | 48 +++ .../clients/authentication/core/Token.java | 16 + .../authentication/core/TokenAuthConfig.java | 71 ++++ .../authentication/core/TokenListener.java | 8 + .../authentication/core/TokenManager.java | 117 +++++++ .../core/TokenManagerConfig.java | 72 +++++ .../core/TokenRequestException.java | 25 ++ .../CoreAuthenticationIntegrationTests.java | 10 + .../CoreAuthenticationUnitTests.java | 251 ++++++++++++++ entraid/pom.xml | 288 +++++++++++++++++ .../entraid/EntraIDIdentityProvider.java | 54 ++++ .../EntraIDIdentityProviderConfig.java | 73 +++++ .../entraid/EntraIDTokenAuthConfig.java | 85 +++++ .../authentication/entraid/JWToken.java | 73 +++++ .../entraid/RedisEntraIDException.java | 14 + .../authentication/EndpointConfig.java | 140 ++++++++ .../RedisEntraIDIntegrationTests.java | 32 ++ .../authentication/RedisEntraIDUnitTests.java | 94 ++++++ .../clients/authentication/TestContext.java | 187 +++++++++++ entraid/src/test/resources/endpoints.json | 107 ++++++ hbase-formatter.xml | 291 +++++++++++++++++ pom.xml | 21 ++ 39 files changed, 3073 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE create mode 100644 .github/dependabot.yml create mode 100644 .github/release-drafter-config.yml create mode 100644 .github/spellcheck-settings.yml create mode 100644 .github/wordlist.txt create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/doctests.yml create mode 100644 .github/workflows/release-drafter.yml create mode 100644 .github/workflows/snapshot.yml create mode 100644 .github/workflows/spellcheck.yml create mode 100644 .github/workflows/stale-issues.yml create mode 100644 .github/workflows/version-and-release.yml create mode 100644 LICENSE create mode 100644 core/pom.xml create mode 100644 core/src/main/java/redis/clients/authentication/core/AuthXException.java create mode 100644 core/src/main/java/redis/clients/authentication/core/IdentityProvider.java create mode 100644 core/src/main/java/redis/clients/authentication/core/IdentityProviderConfig.java create mode 100644 core/src/main/java/redis/clients/authentication/core/SimpleToken.java create mode 100644 core/src/main/java/redis/clients/authentication/core/Token.java create mode 100644 core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java create mode 100644 core/src/main/java/redis/clients/authentication/core/TokenListener.java create mode 100644 core/src/main/java/redis/clients/authentication/core/TokenManager.java create mode 100644 core/src/main/java/redis/clients/authentication/core/TokenManagerConfig.java create mode 100644 core/src/main/java/redis/clients/authentication/core/TokenRequestException.java create mode 100644 core/src/test/java/redis/clients/authentication/CoreAuthenticationIntegrationTests.java create mode 100644 core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java create mode 100644 entraid/pom.xml create mode 100644 entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java create mode 100644 entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java create mode 100644 entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfig.java create mode 100644 entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java create mode 100644 entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java create mode 100644 entraid/src/test/java/redis/clients/authentication/EndpointConfig.java create mode 100644 entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java create mode 100644 entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java create mode 100644 entraid/src/test/java/redis/clients/authentication/TestContext.java create mode 100644 entraid/src/test/resources/endpoints.json create mode 100644 hbase-formatter.xml create mode 100644 pom.xml diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE new file mode 100644 index 0000000..43fccfe --- /dev/null +++ b/.github/ISSUE_TEMPLATE @@ -0,0 +1,32 @@ + + +### Expected behavior + +Write here what you're expecting ... + +### Actual behavior + +Write here what happens instead ... + +### Steps to reproduce: + +Please create a reproducible case of your problem. Make sure +that case repeats consistently and it's not random +1. +2. +3. + +### Redis / EntraID Configuration + +#### ClientLibrary and version: + +#### Redis version: + +#### Java version: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..069e8c5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 + +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml new file mode 100644 index 0000000..4607da0 --- /dev/null +++ b/.github/release-drafter-config.yml @@ -0,0 +1,55 @@ +name-template: '$NEXT_MINOR_VERSION' +tag-template: 'v$NEXT_MINOR_VERSION' +filter-by-commitish: true +commitish: master +autolabeler: + - label: 'maintenance' + files: + - '*.md' + - '.github/*' + - label: 'bug' + branch: + - '/bug-.+' + - label: 'maintenance' + branch: + - '/maintenance-.+' + - label: 'feature' + branch: + - '/feature-.+' +categories: + - title: '๐Ÿ”ฅ Breaking Changes' + labels: + - 'breakingchange' + - title: '๐Ÿงช Experimental Features' + labels: + - 'experimental' + - title: '๐Ÿš€ New Features' + labels: + - 'feature' + - 'enhancement' + - title: '๐Ÿ› Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - 'BUG' + - title: '๐Ÿงฐ Maintenance' + labels: + - 'maintenance' + - 'dependencies' + - 'documentation' + - 'docs' + - 'testing' +change-template: '- $TITLE (#$NUMBER)' +exclude-labels: + - 'skip-changelog' +template: | + # Changes + + $CHANGES + + ## Contributors + We'd like to thank all the contributors who worked on this release! + + $CONTRIBUTORS + diff --git a/.github/spellcheck-settings.yml b/.github/spellcheck-settings.yml new file mode 100644 index 0000000..07b400f --- /dev/null +++ b/.github/spellcheck-settings.yml @@ -0,0 +1,28 @@ +matrix: +- name: Markdown + expect_match: false + apsell: + lang: en + d: en_US + ignore-case: true + dictionary: + wordlists: + - .github/wordlist.txt + output: wordlist.dic + pipeline: + - pyspelling.filters.markdown: + markdown_extensions: + - markdown.extensions.extra: + - pyspelling.filters.html: + comments: false + attributes: + - alt + ignores: + - ':matches(code, pre)' + - code + - pre + - blockquote + - img + sources: + - '*.md' + - 'docs/**' diff --git a/.github/wordlist.txt b/.github/wordlist.txt new file mode 100644 index 0000000..32a6f7f --- /dev/null +++ b/.github/wordlist.txt @@ -0,0 +1,306 @@ +!!!Spelling check failed!!! +APM +ARGV +BFCommands +BitOP +BitPosParams +BuilderFactory +CFCommands +CMSCommands +CallNotPermittedException +CircuitBreaker +ClientKillParams +ClusterNode +ClusterNodes +ClusterPipeline +ClusterPubSub +ConnectionPool +CoreCommands +EVAL +EVALSHA +Failback +Failover +FTCreateParams +FTSearchParams +GSON +GenericObjectPool +GenericObjectPoolConfig +GeoAddParams +GeoRadiusParam +GeoRadiusStoreParam +GeoUnit +GraphCommands +Grokzen's +HostAndPort +HostnameVerifier +INCR +IOError +Instrumentations +JDK +JSONArray +JSONCommands +Jaeger +Javadocs +ListPosition +Ludovico +Magnocavallo +McCurdy +NOSCRIPT +NUMPAT +NUMPT +NUMSUB +OSS +OpenCensus +OpenTelemetry +OpenTracing +Otel +POJO +POJOs +PubSub +Queable +READONLY +RediSearch +RediSearchCommands +RedisBloom +RedisCluster +RedisClusterCommands +RedisClusterException +RedisClusters +RedisGraph +RedisInstrumentor +RedisJSON +RedisTimeSeries +SHA +SSLParameters +SSLSocketFactory +SearchCommands +SentinelCommands +SentinelConnectionPool +ShardInfo +Sharded +Solovyov +SortingParams +SpanKind +Specfiying +StatusCode +StreamEntryID +TCP +TOPKCommands +Throwable +TimeSeriesCommands +URI +UnblockType +Uptrace +ValueError +WATCHed +WatchError +XTrimParams +ZAddParams +ZParams +aclDelUser +api +approximateLength +arg +args +async +asyncio +autoclass +automodule +backoff +bdb +behaviour +bitcount +bitop +bitpos +bool +boolean +booleans +bysource +charset +clientId +clientKill +clientUnblock +clusterCountKeysInSlot +clusterKeySlot +configs +consumerName +consumername +cumbersome +dbIndex +dbSize +decr +decrBy +del +destKey +dev +dstKey +dstkey +eg +exc +expireAt +failback +failover +faoliver +firstName +firsttimersonly +fo +genindex +geoadd +georadiusByMemberStore +georadiusStore +getbit +gmail +groupname +hdel +hexists +hincrBy +hincrByFloat +hiredis +hlen +hset +hsetnx +hstrlen +http +idx +iff +incr +incrBy +incrByFloat +ini +json +keyslot +keyspace +keysvalues +kwarg +lastName +lastsave +linsert +linters +llen +localhost +lpush +lpushx +lrem +lua +makeapullrequest +maxLen +maxdepth +maya +memberCoordinateMap +mget +microservice +microservices +millisecondsTimestamp +mset +msetnx +multikey +mykey +newkey +nonatomic +observability +oldkey +opentelemetry +oss +param +params +performant +pexpire +pexpireAt +pfadd +pfcount +pmessage +png +pre +psubscribe +pttl +pubsub +punsubscribe +py +pypi +quickstart +readonly +readwrite +redis +redismodules +reimplemented +reinitialization +renamenx +replicaof +repo +rpush +rpushx +runtime +sadd +scard +scoreMembers +sdiffstore +sedrik +setbit +setnx +setrange +sinterstore +sismember +slowlogLen +smove +sortingParameters +srcKey +srcKeys +srckey +ssl +storeParam +str +strlen +stunnel +subcommands +sunionstore +thevalueofmykey +timeseries +toctree +topk +tox +triaging +ttl +txt +un +unblockType +unicode +unixTime +unlink +untyped +url +virtualenv +waitReplicas +whenver +www +xack +xdel +xgroupDelConsumer +xgroupDestroy +xlen +xtrim +zadd +zcard +zcount +zdiffStore +zincrby +zinterstore +zlexcount +zpopmax +zpopmin +zrandmember +zrandmemberWithScores +zrange +zrangeByLex +zrangeByScore +zrangeByScoreWithScores +zrangeWithScores +zrem +zremrangeByLex +zremrangeByRank +zremrangeByScore +zrevrange +zrevrangeByLex +zrevrangeByScore +zrevrangeByScoreWithScores +zrevrangeWithScores +zunionstore diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..3f472ee --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,63 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '31 4 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: java + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # โ„น๏ธ Command-line programs to run using the OS shell. + # ๐Ÿ“š https://git.io/JvXDl + + # โœ๏ธ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/doctests.yml b/.github/workflows/doctests.yml new file mode 100644 index 0000000..ae06981 --- /dev/null +++ b/.github/workflows/doctests.yml @@ -0,0 +1,38 @@ +name: Documentation Tests + +on: + push: + tags-ignore: + - '*' + branches: [ master ] + pull_request: + workflow_dispatch: + +jobs: + doctests: + runs-on: ubuntu-latest + services: + redis-stack: + image: redis/redis-stack-server:latest + options: >- + --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v3 + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + /var/cache/apt + key: entraid-${{hashFiles('**/pom.xml')}} + - name: Set up Java + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'temurin' + - name: Run doctests + run: | + mvn -Pdoctests test diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..caac3ca --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,20 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + config-name: release-drafter-config.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml new file mode 100644 index 0000000..e11b9a8 --- /dev/null +++ b/.github/workflows/snapshot.yml @@ -0,0 +1,43 @@ +--- + +name: Publish Snapshot + +on: + push: + branches: + - master + - '[0-9].x' + workflow_dispatch: + +jobs: + + snapshot: + name: Deploy Snapshot + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up publishing to maven central + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'temurin' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + /var/cache/apt + key: endtraid-${{hashFiles('**/pom.xml')}} + - name: mvn offline + run: | + mvn -q dependency:go-offline + - name: deploy + run: | + mvn --no-transfer-progress \ + -DskipTests deploy + env: + MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} + MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml new file mode 100644 index 0000000..e152841 --- /dev/null +++ b/.github/workflows/spellcheck.yml @@ -0,0 +1,14 @@ +name: spellcheck +on: + pull_request: +jobs: + check-spelling: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Check Spelling + uses: rojopolis/spellcheck-github-actions@0.33.1 + with: + config_path: .github/spellcheck-settings.yml + task_name: Markdown diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml new file mode 100644 index 0000000..54bf059 --- /dev/null +++ b/.github/workflows/stale-issues.yml @@ -0,0 +1,25 @@ +name: "Close stale issues" +on: + schedule: + - cron: "0 0 * * *" + +permissions: {} +jobs: + stale: + permissions: + issues: write # to close stale issues (actions/stale) + pull-requests: write # to close stale PRs (actions/stale) + + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue is marked stale. It will be closed in 30 days if it is not updated.' + stale-pr-message: 'This pull request is marked stale. It will be closed in 30 days if it is not updated.' + days-before-stale: 365 + days-before-close: 30 + stale-issue-label: "stale" + stale-pr-label: "stale" + operations-per-run: 10 + remove-stale-when-updated: false diff --git a/.github/workflows/version-and-release.yml b/.github/workflows/version-and-release.yml new file mode 100644 index 0000000..7c996e5 --- /dev/null +++ b/.github/workflows/version-and-release.yml @@ -0,0 +1,46 @@ +name: Release + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: get version from tag + id: get_version + run: | + realversion="${GITHUB_REF/refs\/tags\//}" + realversion="${realversion//v/}" + echo "VERSION=$realversion" >> $GITHUB_OUTPUT + + - name: Set up publishing to maven central + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'temurin' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + + - name: mvn versions + run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }} + + - name: Install gpg key + run: | + cat <(echo -e "${{ secrets.OSSH_GPG_SECRET_KEY }}") | gpg --batch --import + gpg --list-secret-keys --keyid-format LONG + + - name: Publish + run: | + mvn --no-transfer-progress \ + --batch-mode \ + -Dgpg.passphrase='${{ secrets.OSSH_GPG_SECRET_KEY_PASSWORD }}' \ + -DskipTests deploy -P release + env: + MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} + MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..15c4dd5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-2023, Redis, inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000..c01c416 --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,274 @@ + + + + redis.clients.authentication + redis-authx + 0.1.0 + + + 4.0.0 + jar + redis.clients.authentication + redis-authx-core + 0.1.0-SNAPSHOT + Redis AuthX Core is the core lib of an extension for Redis Java Clients to support token-based authentication. + https://github.com/redis/redis-authx-core + + + + Redis Authx Mailing List + redis_authx@googlegroups.com + + https://groups.google.com/group/redis_authx + + + + + + + MIT + https://github.com/redis/redis-authx-core/blob/master/LICENSE + repo + + + + + github + https://github.com/redis/redis-authx-core/issues + + + + scm:git:git@github.com:redis/redis-authx-core.git + scm:git:git@github.com:redis/redis-authx-core.git + scm:git:git@github.com:redis/redis-authx-core.git + redis-authx-core-0.1.0 + + + + github + redis.clients.authentication.core + 1.7.36 + 1.7.1 + 2.18.0 + 3.5.1 + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-inline + 4.11.0 + test + + + org.hamcrest + hamcrest + 3.0 + test + + + org.awaitility + awaitility + 4.2.2 + test + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + src/main/resources + true + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + + + report + test + + report + + + + + + maven-compiler-plugin + 3.13.0 + + 1.8 + 1.8 + + + + maven-surefire-plugin + ${maven.surefire.version} + + + ${redis-hosts} + + + **/examples/*Example.java + + + + + + maven-source-plugin + 3.3.1 + + true + + + + attach-sources + + jar + + + + + + maven-javadoc-plugin + 3.10.1 + + 8 + false + + + + + + attach-javadoc + + jar + + + + + + maven-release-plugin + 3.1.1 + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + ossrh + https://oss.sonatype.org/ + true + + + + com.googlecode.maven-java-formatter-plugin + maven-java-formatter-plugin + 0.4 + + ${project.basedir}/hbase-formatter.xml + + + + maven-jar-plugin + 3.4.2 + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + ${core.module.name} + + + + + + org.apache.felix + maven-bundle-plugin + 5.1.9 + + + bundle-manifest + process-classes + + manifest + + + + + + + + + release + + + + + maven-gpg-plugin + 3.2.7 + + + --pinentry-mode + loopback + + + + + sign-artifacts + verify + + sign + + + + + + + + + doctests + + + + maven-surefire-plugin + ${maven.surefire.version} + + **/examples/*Example.java + + + + + + + diff --git a/core/src/main/java/redis/clients/authentication/core/AuthXException.java b/core/src/main/java/redis/clients/authentication/core/AuthXException.java new file mode 100644 index 0000000..7601b50 --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/AuthXException.java @@ -0,0 +1,12 @@ +package redis.clients.authentication.core; + +public class AuthXException extends RuntimeException { + + public AuthXException(String message) { + super(message); + } + + public AuthXException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/redis/clients/authentication/core/IdentityProvider.java b/core/src/main/java/redis/clients/authentication/core/IdentityProvider.java new file mode 100644 index 0000000..7b9701d --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/IdentityProvider.java @@ -0,0 +1,6 @@ +package redis.clients.authentication.core; + +public interface IdentityProvider { + + Token requestToken(); +} \ No newline at end of file diff --git a/core/src/main/java/redis/clients/authentication/core/IdentityProviderConfig.java b/core/src/main/java/redis/clients/authentication/core/IdentityProviderConfig.java new file mode 100644 index 0000000..5fcffec --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/IdentityProviderConfig.java @@ -0,0 +1,6 @@ +package redis.clients.authentication.core; + +public interface IdentityProviderConfig { + + public IdentityProvider getProvider(); +} diff --git a/core/src/main/java/redis/clients/authentication/core/SimpleToken.java b/core/src/main/java/redis/clients/authentication/core/SimpleToken.java new file mode 100644 index 0000000..6579eaa --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/SimpleToken.java @@ -0,0 +1,48 @@ +package redis.clients.authentication.core; + +import java.util.Map; + +public class SimpleToken implements Token { + + private String value; + private long expiresAt; + private long receivedAt; + private Map claims; + + public SimpleToken(String value, long expiresAt, long receivedAt, Map claims) { + this.value = value; + this.expiresAt = expiresAt; + this.receivedAt = receivedAt; + this.claims = claims; + } + + @Override + public boolean isExpired() { + return System.currentTimeMillis() > expiresAt; + } + + @Override + public long ttl() { + return expiresAt - System.currentTimeMillis(); + } + + @Override + public String getValue() { + return value; + } + + @Override + public long getExpiresAt() { + return expiresAt; + } + + @Override + public long getReceivedAt() { + return receivedAt; + } + + @Override + public String tryGet(String key) { + return claims.get(key); + } +} \ No newline at end of file diff --git a/core/src/main/java/redis/clients/authentication/core/Token.java b/core/src/main/java/redis/clients/authentication/core/Token.java new file mode 100644 index 0000000..d8b7679 --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/Token.java @@ -0,0 +1,16 @@ +package redis.clients.authentication.core; + +public interface Token { + + public boolean isExpired(); + + public long ttl(); + + public String getValue(); + + public long getExpiresAt(); + + public long getReceivedAt(); + + public String tryGet(String key); +} \ No newline at end of file diff --git a/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java b/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java new file mode 100644 index 0000000..3d6a1a2 --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java @@ -0,0 +1,71 @@ +package redis.clients.authentication.core; + +public class TokenAuthConfig { + + private TokenManagerConfig tokenManagerConfig; + private IdentityProviderConfig identityProviderConfig; + + public TokenAuthConfig(TokenManagerConfig tokenManagerConfig, + IdentityProviderConfig identityProviderConfig) { + this.tokenManagerConfig = tokenManagerConfig; + this.identityProviderConfig = identityProviderConfig; + } + + public TokenManagerConfig getTokenManagerConfig() { + return tokenManagerConfig; + } + + public IdentityProviderConfig getIdentityProviderConfig() { + return identityProviderConfig; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private IdentityProviderConfig identityProviderConfig; + private int lowerRefreshBoundMillis; + private float expirationRefreshRatio; + private int tokenRequestExecTimeoutInMs; + private int maxAttemptsToRetry; + private int delayInMsToRetry; + + public Builder expirationRefreshRatio(float expirationRefreshRatio) { + this.expirationRefreshRatio = expirationRefreshRatio; + return this; + } + + public Builder lowerRefreshBoundMillis(int lowerRefreshBoundMillis) { + this.lowerRefreshBoundMillis = lowerRefreshBoundMillis; + return this; + } + + public Builder tokenRequestExecTimeoutInMs(int tokenRequestExecTimeoutInMs) { + this.tokenRequestExecTimeoutInMs = tokenRequestExecTimeoutInMs; + return this; + } + + public Builder maxAttemptsToRetry(int maxAttemptsToRetry) { + this.maxAttemptsToRetry = maxAttemptsToRetry; + return this; + } + + public Builder delayInMsToRetry(int delayInMsToRetry) { + this.delayInMsToRetry = delayInMsToRetry; + return this; + } + + public Builder identityProviderConfig(IdentityProviderConfig identityProviderConfig) { + this.identityProviderConfig = identityProviderConfig; + return this; + } + + public TokenAuthConfig build() { + return new TokenAuthConfig(new TokenManagerConfig(expirationRefreshRatio, + lowerRefreshBoundMillis, tokenRequestExecTimeoutInMs, + new TokenManagerConfig.RetryPolicy(maxAttemptsToRetry, delayInMsToRetry)), + identityProviderConfig); + } + } +} diff --git a/core/src/main/java/redis/clients/authentication/core/TokenListener.java b/core/src/main/java/redis/clients/authentication/core/TokenListener.java new file mode 100644 index 0000000..f4815ee --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/TokenListener.java @@ -0,0 +1,8 @@ +package redis.clients.authentication.core; + +public interface TokenListener { + + void onTokenRenewed(Token newToken); + + void onError(Exception reason); +} diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManager.java b/core/src/main/java/redis/clients/authentication/core/TokenManager.java new file mode 100644 index 0000000..5a95158 --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/TokenManager.java @@ -0,0 +1,117 @@ +package redis.clients.authentication.core; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TokenManager { + + private TokenManagerConfig tokenManagerConfig; + private IdentityProvider identityProvider; + private TokenListener listener; + private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private ExecutorService executor = Executors.newSingleThreadExecutor(); + private boolean stopped = false; + private ScheduledFuture scheduledTask; + private int numberOfRetries = 0; + private Exception lastException; + private Logger logger = LoggerFactory.getLogger(getClass()); + + public TokenManager(IdentityProvider identityProvider, TokenManagerConfig tokenManagerConfig) { + this.identityProvider = identityProvider; + this.tokenManagerConfig = tokenManagerConfig; + } + + public void start(TokenListener listener, boolean blockForInitialToken) + throws InterruptedException, ExecutionException, TimeoutException { + + this.listener = listener; + ScheduledFuture currentTask = scheduleNext(0); + scheduledTask = currentTask; + if (blockForInitialToken) { + while (currentTask.get() == null) { + currentTask = scheduledTask; + } + } + } + + public void stop() { + stopped = true; + scheduledTask.cancel(true); + scheduler.shutdown(); + } + + public TokenManagerConfig getConfig() { + return tokenManagerConfig; + } + + private ScheduledFuture scheduleNext(long delay) { + // Schedule the task to run after the calculated delay + return scheduler.schedule(() -> renewToken(), delay, TimeUnit.MILLISECONDS); + } + + protected Token renewToken() { + if (stopped) { + return null; + } + Token newToken = null; + try { + Future requestResult = executor.submit(() -> requestToken()); + newToken = requestResult.get(tokenManagerConfig.getTokenRequestExecTimeoutInMs(), + TimeUnit.MILLISECONDS); + long delay = calculateRenewalDelay(newToken.getExpiresAt(), newToken.getReceivedAt()); + scheduledTask = scheduleNext(delay); + listener.onTokenRenewed(newToken); + return newToken; + } catch (Exception e) { + if (numberOfRetries < tokenManagerConfig.getRetryPolicy().getMaxAttempts()) { + numberOfRetries++; + scheduledTask = scheduleNext(tokenManagerConfig.getRetryPolicy().getdelayInMs()); + } else { + TokenRequestException tre = new TokenRequestException(e, lastException); + listener.onError(tre); + throw tre; + } + } + return null; + } + + protected Token requestToken() { + lastException = null; + try { + return identityProvider.requestToken(); + } catch (Exception e) { + lastException = e; + logger.error("Request to identity provider failed with message: " + e.getMessage(), e); + throw e; + } + } + + public long calculateRenewalDelay(long expireDate, long issueDate) { + long ttlLowerRefresh = ttlForLowerRefresh(expireDate); + long ttlRatioRefresh = ttlForRatioRefresh(expireDate, issueDate); + long delay = Math.min(ttlLowerRefresh, ttlRatioRefresh); + + return delay < 0 ? 0 : delay; + } + + public long ttlForLowerRefresh(long expireDate) { + return expireDate - tokenManagerConfig.getLowerRefreshBoundMillis() + - System.currentTimeMillis(); + } + + protected long ttlForRatioRefresh(long expireDate, long issueDate) { + long validDuration = expireDate - issueDate; + long refreshBefore = validDuration + - (long) (validDuration * tokenManagerConfig.getExpirationRefreshRatio()); + return expireDate - refreshBefore - System.currentTimeMillis(); + } +} diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManagerConfig.java b/core/src/main/java/redis/clients/authentication/core/TokenManagerConfig.java new file mode 100644 index 0000000..a8cb671 --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/TokenManagerConfig.java @@ -0,0 +1,72 @@ +package redis.clients.authentication.core; + +/** + * Token manager example configuration. + */ +public class TokenManagerConfig { + + private final float expirationRefreshRatio; + private final int lowerRefreshBoundMillis; + private final int tokenRequestExecTimeoutInMs; + private final RetryPolicy retryPolicy; + + public static class RetryPolicy { + private final int maxAttempts; + private final int delayInMs; + + public RetryPolicy(int maxAttempts, int delayInMs) { + this.maxAttempts = maxAttempts; + this.delayInMs = delayInMs; + } + + public int getMaxAttempts() { + return maxAttempts; + } + + public int getdelayInMs() { + return delayInMs; + } + + } + + public TokenManagerConfig(float expirationRefreshRatio, int lowerRefreshBoundMillis, + int tokenRequestExecTimeoutInMs, RetryPolicy retryPolicy) { + this.expirationRefreshRatio = expirationRefreshRatio; + this.lowerRefreshBoundMillis = lowerRefreshBoundMillis; + this.tokenRequestExecTimeoutInMs = tokenRequestExecTimeoutInMs; + this.retryPolicy = retryPolicy; + } + + /** + * Represents the ratio of a token's lifetime at which a refresh should be triggered. + * For example, a value of 0.75 means the token should be refreshed when 75% of its + * lifetime has elapsed (or when 25% of its lifetime remains). + */ + public float getExpirationRefreshRatio() { + return expirationRefreshRatio; + } + + /** + * Represents the minimum time in milliseconds before token expiration to trigger a refresh, in milliseconds. + * This value sets a fixed lower bound for when a token refresh should occur, regardless + * of the token's total lifetime. + * If set to 0 there will be no lower bound and the refresh will be triggered based on the expirationRefreshRatio only. + */ + public int getLowerRefreshBoundMillis() { + return lowerRefreshBoundMillis; + } + + /** + * Represents the maximum time in milliseconds to wait for a token request to complete. + */ + public int getTokenRequestExecTimeoutInMs() { + return tokenRequestExecTimeoutInMs; + } + + /** + * Represents the retry policy for token requests. + */ + public RetryPolicy getRetryPolicy() { + return retryPolicy; + } +} diff --git a/core/src/main/java/redis/clients/authentication/core/TokenRequestException.java b/core/src/main/java/redis/clients/authentication/core/TokenRequestException.java new file mode 100644 index 0000000..e095ada --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/TokenRequestException.java @@ -0,0 +1,25 @@ +package redis.clients.authentication.core; + +public class TokenRequestException extends AuthXException { + + private static final String msg = "Token request/renewal failed!"; + private final Exception identityProviderFailedWith; + + public TokenRequestException(Throwable cause, Exception identityProviderFailedWith) { + super(getMessage(identityProviderFailedWith), cause); + this.identityProviderFailedWith = identityProviderFailedWith; + } + + public Exception getIdentityProviderFailedWith() { + return identityProviderFailedWith; + } + + private static String getMessage(Exception identityProviderFailedWith) { + if (identityProviderFailedWith == null) { + return msg; + } + return msg + " Identity provider request failed!" + + identityProviderFailedWith.getMessage(); + } + +} diff --git a/core/src/test/java/redis/clients/authentication/CoreAuthenticationIntegrationTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationIntegrationTests.java new file mode 100644 index 0000000..8858852 --- /dev/null +++ b/core/src/test/java/redis/clients/authentication/CoreAuthenticationIntegrationTests.java @@ -0,0 +1,10 @@ +package redis.clients.authentication; + +import org.junit.Test; + +public class CoreAuthenticationIntegrationTests { + @Test + public void testTokenManager() { + // Test code + } +} diff --git a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java new file mode 100644 index 0000000..327fac3 --- /dev/null +++ b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java @@ -0,0 +1,251 @@ +package redis.clients.authentication; + +import static org.mockito.Mockito.when; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.either; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import redis.clients.authentication.core.IdentityProvider; +import redis.clients.authentication.core.SimpleToken; +import redis.clients.authentication.core.Token; +import redis.clients.authentication.core.TokenListener; +import redis.clients.authentication.core.TokenManager; +import redis.clients.authentication.core.TokenManagerConfig; +import static org.awaitility.Awaitility.await; +import java.util.concurrent.TimeUnit; + +public class CoreAuthenticationUnitTests { + + public static class TokenManagerConfigWrapper extends TokenManagerConfig { + int lower; + float ratio; + + public TokenManagerConfigWrapper() { + super(0, 0, 0, null); + } + + @Override + public int getLowerRefreshBoundMillis() { + return lower; + } + + @Override + public float getExpirationRefreshRatio() { + return ratio; + } + } + + @Test + public void testCalculateRenewalDelay() { + long delay = 0; + long duration = 0; + long issueDate; + long expireDate; + + TokenManagerConfigWrapper config = new TokenManagerConfigWrapper(); + TokenManager manager = new TokenManager(() -> null, config); + + duration = 5000; + config.lower = 2000; + config.ratio = 0.5F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertThat(delay, Matchers + .greaterThanOrEqualTo(Math.min(duration - config.lower, (long) (duration * config.ratio)))); + + duration = 10000; + config.lower = 8000; + config.ratio = 0.2F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertThat(delay, Matchers + .greaterThanOrEqualTo(Math.min(duration - config.lower, (long) (duration * config.ratio)))); + + duration = 10000; + config.lower = 10000; + config.ratio = 0.2F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertEquals(0, delay); + + duration = 0; + config.lower = 5000; + config.ratio = 0.2F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertEquals(0, delay); + + duration = 10000; + config.lower = 1000; + config.ratio = 0.00001F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertEquals(0, delay); + + duration = 10000; + config.lower = 1000; + config.ratio = 0.0001F; + issueDate = System.currentTimeMillis(); + expireDate = issueDate + duration; + + delay = manager.calculateRenewalDelay(expireDate, issueDate); + + assertThat(delay, either(is(0L)).or(is(1L))); + } + + @Test + public void testTokenManagerStart() + throws InterruptedException, ExecutionException, TimeoutException { + + IdentityProvider identityProvider = () -> new SimpleToken("tokenVal", + System.currentTimeMillis() + 5 * 1000, System.currentTimeMillis(), + Collections.singletonMap("oid", "user1")); + + TokenManager tokenManager = new TokenManager(identityProvider, + new TokenManagerConfig(0.7F, 200, 2000, null)); + + TokenListener listener = mock(TokenListener.class); + final Token[] tokenHolder = new Token[1]; + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + tokenHolder[0] = (Token) args[0]; + return null; + }).when(listener).onTokenRenewed(any()); + + tokenManager.start(listener, true); + assertEquals(tokenHolder[0].getValue(), "tokenVal"); + } + + @Test + public void testBlockForInitialToken() { + IdentityProvider identityProvider = () -> { + throw new RuntimeException("Test exception from identity provider!"); + }; + + TokenManager tokenManager = new TokenManager(identityProvider, + new TokenManagerConfig(0.7F, 200, 2000, new TokenManagerConfig.RetryPolicy(5, 100))); + + ExecutionException e = assertThrows(ExecutionException.class, + () -> tokenManager.start(mock(TokenListener.class), true)); + + assertEquals("java.lang.RuntimeException: Test exception from identity provider!", + e.getCause().getCause().getMessage()); + } + + @Test + public void testNoBlockForInitialToken() + throws InterruptedException, ExecutionException, TimeoutException { + int numberOfRetries = 5; + CountDownLatch requesLatch = new CountDownLatch(numberOfRetries); + IdentityProvider identityProvider = () -> { + requesLatch.countDown(); + throw new RuntimeException("Test exception from identity provider!"); + }; + + TokenManager tokenManager = new TokenManager(identityProvider, new TokenManagerConfig(0.7F, 200, + 2000, new TokenManagerConfig.RetryPolicy(numberOfRetries - 1, 100))); + + TokenListener listener = mock(TokenListener.class); + tokenManager.start(listener, false); + + requesLatch.await(); + verify(listener, atLeastOnce()).onError(any()); + verify(listener, never()).onTokenRenewed(any()); + } + + @Test + public void testTokenManagerWithFailingTokenRequest() + throws InterruptedException, ExecutionException, TimeoutException { + int numberOfRetries = 5; + CountDownLatch requesLatch = new CountDownLatch(numberOfRetries); + + IdentityProvider identityProvider = mock(IdentityProvider.class); + when(identityProvider.requestToken()).thenAnswer(invocation -> { + requesLatch.countDown(); + if (requesLatch.getCount() > 0) { + throw new RuntimeException("Test exception from identity provider!"); + } + return new SimpleToken("tokenValX", System.currentTimeMillis() + 50 * 1000, + System.currentTimeMillis(), Collections.singletonMap("oid", "user1")); + }); + + ArgumentCaptor argument = ArgumentCaptor.forClass(Token.class); + + TokenManager tokenManager = new TokenManager(identityProvider, new TokenManagerConfig(0.7F, 200, + 2000, new TokenManagerConfig.RetryPolicy(numberOfRetries - 1, 100))); + + TokenListener listener = mock(TokenListener.class); + tokenManager.start(listener, false); + requesLatch.await(); + verify(identityProvider, times(numberOfRetries)).requestToken(); + verify(listener, never()).onError(any()); + verify(listener).onTokenRenewed(argument.capture()); + assertEquals("tokenValX", argument.getValue().getValue()); + } + + @Test + public void testTokenManagerWithHangingTokenRequest() + throws InterruptedException, ExecutionException, TimeoutException { + int sleepDuration = 200; + int executionTimeout = 100; + int tokenLifetime = 50 * 1000; + int numberOfRetries = 5; + CountDownLatch requesLatch = new CountDownLatch(numberOfRetries); + + IdentityProvider identityProvider = () -> { + requesLatch.countDown(); + if (requesLatch.getCount() > 0) { + try { + Thread.sleep(sleepDuration); + } catch (InterruptedException e) { + } + return null; + } + return new SimpleToken("tokenValX", System.currentTimeMillis() + tokenLifetime, + System.currentTimeMillis(), Collections.singletonMap("oid", "user1")); + }; + + TokenManager tokenManager = new TokenManager(identityProvider, new TokenManagerConfig(0.7F, 200, + executionTimeout, new TokenManagerConfig.RetryPolicy(numberOfRetries, 100))); + + TokenListener listener = mock(TokenListener.class); + tokenManager.start(listener, false); + requesLatch.await(); + verify(listener, never()).onError(any()); + await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> { + verify(listener, times(1)).onTokenRenewed(any()); + }); + } +} diff --git a/entraid/pom.xml b/entraid/pom.xml new file mode 100644 index 0000000..517efe8 --- /dev/null +++ b/entraid/pom.xml @@ -0,0 +1,288 @@ + + + + redis.clients.authentication + redis-authx + 0.1.0 + + + 4.0.0 + jar + redis.clients.authentication + redis-authx-entraid + 0.1.0-SNAPSHOT + Redis AuthX EntraID is an extension for Redis Java Clients to support token-based authentication with Microsoft EntraID. + https://github.com/redis/redis-authx-entraid + + + + Redis Authx Mailing List + redis_authx@googlegroups.com + + https://groups.google.com/group/redis_authx + + + + + + + MIT + https://github.com/redis/redis-authx-entraid/blob/master/LICENSE + repo + + + + + github + https://github.com/redis/redis-authx-entraid/issues + + + + scm:git:git@github.com:redis/redis-authx-entraid.git + scm:git:git@github.com:redis/redis-authx-entraid.git + scm:git:git@github.com:redis/redis-authx-entraid.git + entraid-0.1.0 + + + + github + redis.clients.authentication.entraid + 3.5.1 + + + + + + com.auth0 + java-jwt + 4.4.0 + + + redis.clients.authentication + redis-authx-core + 0.1.0 + + + com.microsoft.azure + msal4j + 1.17.2 + + + redis.clients + jedis + 5.3.0-SNAPSHOT + test + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-inline + 4.11.0 + test + + + org.hamcrest + hamcrest + 3.0 + test + + + org.awaitility + awaitility + 4.2.2 + test + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + + src/main/resources + true + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + + + report + test + + report + + + + + + maven-compiler-plugin + 3.13.0 + + 1.8 + 1.8 + + + + maven-surefire-plugin + ${maven.surefire.version} + + + ${redis-hosts} + + + **/examples/*Example.java + + + + + + maven-source-plugin + 3.3.1 + + true + + + + attach-sources + + jar + + + + + + maven-javadoc-plugin + 3.10.1 + + 8 + false + + + + + + attach-javadoc + + jar + + + + + + maven-release-plugin + 3.1.1 + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + ossrh + https://oss.sonatype.org/ + true + + + + com.googlecode.maven-java-formatter-plugin + maven-java-formatter-plugin + 0.4 + + ${project.basedir}/hbase-formatter.xml + + + + maven-jar-plugin + 3.4.2 + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + ${entraid.module.name} + + + + + + org.apache.felix + maven-bundle-plugin + 5.1.9 + + + bundle-manifest + process-classes + + manifest + + + + + + + + + release + + + + + maven-gpg-plugin + 3.2.7 + + + --pinentry-mode + loopback + + + + + sign-artifacts + verify + + sign + + + + + + + + + doctests + + + + maven-surefire-plugin + ${maven.surefire.version} + + **/examples/*Example.java + + + + + + + diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java new file mode 100644 index 0000000..5fb01ca --- /dev/null +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java @@ -0,0 +1,54 @@ +package redis.clients.authentication.entraid; + +import java.net.MalformedURLException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import com.microsoft.aad.msal4j.ClientCredentialFactory; +import com.microsoft.aad.msal4j.ClientCredentialParameters; +import com.microsoft.aad.msal4j.ConfidentialClientApplication; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IClientCredential; + +import redis.clients.authentication.core.IdentityProvider; +import redis.clients.authentication.core.Token; + +public final class EntraIDIdentityProvider implements IdentityProvider { + + private ConfidentialClientApplication app; + private ClientCredentialParameters clientParams; + + public EntraIDIdentityProvider(String clientId, String authority, String secret, + Set scopes) { + IClientCredential credential = ClientCredentialFactory.createFromSecret(secret); + init(clientId, authority, credential, scopes); + } + + public EntraIDIdentityProvider(String clientId, String authority, PrivateKey key, X509Certificate cert, + Set scopes) { + IClientCredential credential = ClientCredentialFactory.createFromCertificate(key, cert); + init(clientId, authority, credential, scopes); + } + + protected void init(String clientId, String authority, IClientCredential credential, Set scopes) { + try { + app = ConfidentialClientApplication.builder(clientId, credential).authority(authority).build(); + } catch (MalformedURLException e) { + throw new RedisEntraIDException("Failed to init EntraID client!", e); + } + clientParams = ClientCredentialParameters.builder(scopes).build(); + } + + @Override + public Token requestToken() { + try { + Future tokenRequest = app.acquireToken(clientParams); + return new JWToken(tokenRequest.get().accessToken()); + } catch (InterruptedException | ExecutionException e) { + throw new RedisEntraIDException("Failed to acquire token!", e); + } + } +} diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java new file mode 100644 index 0000000..cd39d53 --- /dev/null +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java @@ -0,0 +1,73 @@ +package redis.clients.authentication.entraid; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Set; + +import redis.clients.authentication.core.IdentityProvider; +import redis.clients.authentication.core.IdentityProviderConfig; + +public final class EntraIDIdentityProviderConfig implements IdentityProviderConfig, AutoCloseable { + + public enum EntraIDAccess { + WithSecret, + WithCert, + } + + private String clientId; + private EntraIDAccess accessWith; + private String secret; + private PrivateKey key; + private X509Certificate cert; + private String authority; + private Set scopes; + + public EntraIDIdentityProviderConfig(String clientId, + EntraIDAccess accessWith, + String secret, + PrivateKey key, + X509Certificate cert, + String authority, Set scopes) { + this.clientId = clientId; + this.accessWith = accessWith; + this.secret = secret; + this.key = key; + this.cert = cert; + this.authority = authority; + this.scopes = scopes; + } + + @Override + public IdentityProvider getProvider() { + IdentityProvider identityProvider = null; + switch (accessWith) { + case WithSecret: + identityProvider = new EntraIDIdentityProvider(clientId, authority, + secret, scopes); + break; + case WithCert: + identityProvider = new EntraIDIdentityProvider(clientId, authority, + key, cert, scopes); + break; + default: + throw new RedisEntraIDException("Access type and credentials must be set!"); + } + + clear(); + return identityProvider; + } + + @Override + public void close() throws Exception { + clear(); + } + + private void clear() { + clientId = null; + secret = null; + key = null; + cert = null; + authority = null; + scopes = null; + } +} diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfig.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfig.java new file mode 100644 index 0000000..a975049 --- /dev/null +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfig.java @@ -0,0 +1,85 @@ +package redis.clients.authentication.entraid; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Set; + +import redis.clients.authentication.core.TokenAuthConfig; + +public class EntraIDTokenAuthConfig { + + private EntraIDTokenAuthConfig() { + } + + public static class Builder extends TokenAuthConfig.Builder implements AutoCloseable { + public static final float DEFAULT_EXPIRATION_REFRESH_RATIO = 0.8F; + public static final int DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 2 * 60 * 1000; + public static final int DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS = 1000; + public static final int DEFAULT_MAX_ATTEMPTS_TO_RETRY = 5; + public static final int DEFAULT_DELAY_IN_MS_TO_RETRY = 100; + + private String clientId; + private String secret; + private PrivateKey key; + private X509Certificate cert; + private String authority; + private Set scopes; + private EntraIDIdentityProviderConfig.EntraIDAccess accessWith; + + public Builder() { + this.expirationRefreshRatio(DEFAULT_EXPIRATION_REFRESH_RATIO) + .lowerRefreshBoundMillis(DEFAULT_LOWER_REFRESH_BOUND_MILLIS) + .tokenRequestExecTimeoutInMs(DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS) + .maxAttemptsToRetry(DEFAULT_MAX_ATTEMPTS_TO_RETRY) + .delayInMsToRetry(DEFAULT_DELAY_IN_MS_TO_RETRY); + } + + public Builder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public Builder secret(String secret) { + this.secret = secret; + this.accessWith = EntraIDIdentityProviderConfig.EntraIDAccess.WithSecret; + return this; + } + + public Builder key(PrivateKey key, X509Certificate cert) { + this.key = key; + this.accessWith = EntraIDIdentityProviderConfig.EntraIDAccess.WithCert; + return this; + } + + public Builder authority(String authority) { + this.authority = authority; + return this; + } + + public Builder scopes(Set scopes) { + this.scopes = scopes; + return this; + } + + public TokenAuthConfig build() { + EntraIDIdentityProviderConfig idProviderConfig = new EntraIDIdentityProviderConfig(clientId, accessWith, + secret, key, cert, authority, scopes); + super.identityProviderConfig(idProviderConfig); + return super.build(); + } + + @Override + public void close() throws Exception { + clientId = null; + secret = null; + key = null; + cert = null; + authority = null; + scopes = null; + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java b/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java new file mode 100644 index 0000000..54a5392 --- /dev/null +++ b/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java @@ -0,0 +1,73 @@ +package redis.clients.authentication.entraid; + +import java.util.function.Function; + +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.JWT; + +import redis.clients.authentication.core.Token; + +public class JWToken implements Token { + private final String token; + private final long expiresAt; + private final long receivedAt; + private final Function claimQuery; + + public JWToken(String token) { + this.token = token; + DecodedJWT jwt = JWT.decode(token); + this.expiresAt = jwt.getExpiresAt().getTime(); + this.receivedAt = System.currentTimeMillis(); + this.claimQuery = key -> jwt.getClaim(key).asString(); + } + + @Override + public boolean isExpired() { + return System.currentTimeMillis() > expiresAt; + } + + @Override + public long ttl() { + return expiresAt - System.currentTimeMillis(); + } + + @Override + public String getValue() { + return token; + } + + @Override + public long getExpiresAt() { + return expiresAt; + } + + @Override + public long getReceivedAt() { + return receivedAt; + } + + @Override + public String toString() { + return token; + } + + @Override + public int hashCode() { + return token.hashCode(); + } + + @Override + public boolean equals(Object that) { + if (this == that) return true; + if (that == null) return false; + if (that instanceof Token) { + return token.equals(((Token) that).getValue()); + } + return token.equals(that); + } + + @Override + public String tryGet(String key) { + return claimQuery.apply(key); + } +} diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java b/entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java new file mode 100644 index 0000000..5ddb906 --- /dev/null +++ b/entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java @@ -0,0 +1,14 @@ +package redis.clients.authentication.entraid; + +import redis.clients.authentication.core.AuthXException; + +public class RedisEntraIDException extends AuthXException { + + public RedisEntraIDException(String message) { + super(message); + } + + public RedisEntraIDException(String message, Exception cause) { + super(message, cause); + } +} diff --git a/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java b/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java new file mode 100644 index 0000000..38d3e60 --- /dev/null +++ b/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java @@ -0,0 +1,140 @@ +package redis.clients.authentication; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.util.JedisURIHelper; + +import java.io.FileReader; +import java.net.URI; +import java.util.*; + +public class EndpointConfig { + + private final boolean tls; + private final String username; + private final String password; + private final int bdbId; + private final List endpoints; + + public EndpointConfig(HostAndPort hnp, String username, String password, boolean tls) { + this.tls = tls; + this.username = username; + this.password = password; + this.bdbId = 0; + this.endpoints = Collections.singletonList( + URI.create(getURISchema(tls) + hnp.getHost() + ":" + hnp.getPort())); + } + + public HostAndPort getHostAndPort() { + return JedisURIHelper.getHostAndPort(endpoints.get(0)); + } + + public HostAndPort getHostAndPort(int index) { + return JedisURIHelper.getHostAndPort(endpoints.get(index)); + } + + public String getPassword() { + return password; + } + + public String getUsername() { + return username == null? "default" : username; + } + + public String getHost() { + return getHostAndPort().getHost(); + } + + public int getPort() { + return getHostAndPort().getPort(); + } + + public int getBdbId() { return bdbId; } + + public URI getURI() { + return endpoints.get(0); + } + + public class EndpointURIBuilder { + private boolean tls; + + private String username; + + private String password; + + private String path; + + public EndpointURIBuilder() { + this.username = ""; + this.password = ""; + this.path = ""; + this.tls = EndpointConfig.this.tls; + } + + public EndpointURIBuilder defaultCredentials() { + this.username = EndpointConfig.this.username == null ? "" : getUsername(); + this.password = EndpointConfig.this.getPassword(); + return this; + } + + public EndpointURIBuilder tls(boolean v) { + this.tls = v; + return this; + } + + public EndpointURIBuilder path(String v) { + this.path = v; + return this; + } + + public EndpointURIBuilder credentials(String u, String p) { + this.username = u; + this.password = p; + return this; + } + + public URI build() { + String userInfo = !(this.username.isEmpty() && this.password.isEmpty()) ? + this.username + ':' + this.password + '@' : + ""; + return URI.create( + getURISchema(this.tls) + userInfo + getHost() + ":" + getPort() + this.path); + } + } + + public EndpointURIBuilder getURIBuilder() { + return new EndpointURIBuilder(); + } + + public DefaultJedisClientConfig.Builder getClientConfigBuilder() { + DefaultJedisClientConfig.Builder builder = DefaultJedisClientConfig.builder() + .password(password).ssl(tls); + + if (username != null) { + return builder.user(username); + } + + return builder; + } + + protected String getURISchema(boolean tls) { + return (tls ? "rediss" : "redis") + "://"; + } + + public static HashMap loadFromJSON(String filePath) throws Exception { + Gson gson = new GsonBuilder().setFieldNamingPolicy( + FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + + HashMap configs; + try (FileReader reader = new FileReader(filePath)) { + configs = gson.fromJson(reader, new TypeToken>() { + }.getType()); + } + return configs; + } +} diff --git a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java new file mode 100644 index 0000000..e6a8d4f --- /dev/null +++ b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java @@ -0,0 +1,32 @@ +package redis.clients.authentication; + +import static org.junit.Assert.assertNotNull; + +import java.net.MalformedURLException; +import org.junit.Test; + +import redis.clients.authentication.core.Token; +import redis.clients.authentication.entraid.EntraIDIdentityProvider; + +public class RedisEntraIDIntegrationTests { + + @Test + public void requestTokenWithSecret() throws MalformedURLException { + TestContext testCtx = TestContext.DEFAULT; + + Token token = new EntraIDIdentityProvider(testCtx.getClientId(), testCtx.getAuthority(), + testCtx.getClientSecret(), testCtx.getRedisScopes()).requestToken(); + + assertNotNull(token.getValue()); + } + + @Test + public void requestTokenWithCert() throws MalformedURLException { + TestContext testCtx = TestContext.DEFAULT; + + Token token = new EntraIDIdentityProvider(testCtx.getClientId(), testCtx.getAuthority(), + testCtx.getPrivateKey(), testCtx.getCert(), testCtx.getRedisScopes()).requestToken(); + + assertNotNull(token.getValue()); + } +} diff --git a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java new file mode 100644 index 0000000..066ec32 --- /dev/null +++ b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java @@ -0,0 +1,94 @@ +package redis.clients.authentication; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mockConstruction; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.MockedConstruction; + +import redis.clients.authentication.core.IdentityProviderConfig; +import redis.clients.authentication.core.SimpleToken; +import redis.clients.authentication.core.TokenAuthConfig; +import redis.clients.authentication.entraid.EntraIDIdentityProvider; +import redis.clients.authentication.entraid.EntraIDTokenAuthConfig; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisPooled; + +public class RedisEntraIDUnitTests { + + @Test + public void testConfigBuilder() { + String authority = "authority1"; + String clientId = "clientId1"; + String credential = "credential1"; + Set scopes = Collections.singleton("scope1"); + IdentityProviderConfig config = EntraIDTokenAuthConfig.builder().authority(authority).clientId(clientId) + .secret(credential).scopes(scopes).build().getIdentityProviderConfig(); + + assertNotNull(config); + + try (MockedConstruction mockedConstructor = mockConstruction( + EntraIDIdentityProvider.class, (mock, context) -> { + assertEquals(clientId, context.arguments().get(0)); + assertEquals(authority, context.arguments().get(1)); + assertEquals(credential, context.arguments().get(2)); + assertEquals(scopes, context.arguments().get(3)); + + })) { + config.getProvider(); + } + + try (MockedConstruction mockedConstructor = mockConstruction( + EntraIDIdentityProvider.class, (mock, context) -> { + assertNull(context.arguments().get(0)); + assertNull(context.arguments().get(1)); + assertNull(context.arguments().get(2)); + assertNull(context.arguments().get(3)); + + })) { + config.getProvider(); + } + } + + @Test + public void testJedisConfig() { + TestContext testCtx = TestContext.DEFAULT; + EndpointConfig endpointConfig = TestContext.getRedisEndpoint("standalone0"); + HostAndPort hnp = endpointConfig.getHostAndPort(); + + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfig.builder() + .authority(testCtx.getAuthority()).clientId(testCtx.getClientId()) + .secret(testCtx.getClientSecret()).scopes(testCtx.getRedisScopes()) + .build(); + + DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() + .tokenAuthConfig(tokenAuthConfig).build(); + + AtomicInteger counter = new AtomicInteger(0); + try (MockedConstruction mockedConstructor = mockConstruction( + EntraIDIdentityProvider.class, (mock, context) -> { + assertEquals(testCtx.getClientId(), context.arguments().get(0)); + assertEquals(testCtx.getAuthority(), context.arguments().get(1)); + assertEquals(testCtx.getClientSecret(), context.arguments().get(2)); + assertEquals(testCtx.getRedisScopes(), context.arguments().get(3)); + assertNotNull(mock); + doAnswer(invocation -> { + counter.incrementAndGet(); + return new SimpleToken("token1", System.currentTimeMillis() + 5 * 60 * 1000, + System.currentTimeMillis(), Collections.singletonMap("oid", "default")); + }).when(mock).requestToken(); + })) { + JedisPooled jedis = new JedisPooled(hnp, jedisConfig); + assertNotNull(jedis); + assertEquals(1, counter.get()); + } + } +} diff --git a/entraid/src/test/java/redis/clients/authentication/TestContext.java b/entraid/src/test/java/redis/clients/authentication/TestContext.java new file mode 100644 index 0000000..fff610a --- /dev/null +++ b/entraid/src/test/java/redis/clients/authentication/TestContext.java @@ -0,0 +1,187 @@ +package redis.clients.authentication; + +import java.util.Arrays; +import java.util.Base64; +import java.util.HashSet; +import java.util.Set; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Properties; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Protocol; + +public class TestContext { + + private static final String localContext = "./src/test/resources/local.context"; + private String clientId; + private String authority; + private String clientSecret; + private PrivateKey privateKey; + private X509Certificate cert; + private Set redisScopes; + + public static final TestContext DEFAULT = new TestContext(); + + private TestContext() { + if (Files.exists(Paths.get(localContext))) { + try { + Properties properties = new Properties(); + properties.load(Files.newBufferedReader(Paths.get(localContext))); + this.clientId = properties.getProperty("CLIENT_ID"); + this.authority = properties.getProperty("AUTHORITY"); + this.clientSecret = properties.getProperty("CLIENT_SECRET"); + this.privateKey = getPrivateKey(properties.getProperty("PRIVATE_KEY")); + this.cert = getCert(properties.getProperty("CERT")); + String redisScopesProp = properties.getProperty("REDIS_SCOPES"); + if (redisScopesProp != null && !redisScopesProp.isEmpty()) { + this.redisScopes = new HashSet<>(Arrays.asList(redisScopesProp.split(";"))); + } + } catch (IOException e) { + throw new RuntimeException("Failed to load local.context", e); + } + } else { + this.clientId = System.getenv("CLIENT_ID"); + this.authority = System.getenv("AUTHORITY"); + this.clientSecret = System.getenv("CLIENT_SECRET"); + String redisScopesEnv = System.getenv("REDIS_SCOPES"); + if (redisScopesEnv != null && !redisScopesEnv.isEmpty()) { + this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";"))); + } + } + } + + public TestContext(String clientId, String authority, String clientSecret, + Set redisScopes) { + this.clientId = clientId; + this.authority = authority; + this.clientSecret = clientSecret; + this.redisScopes = redisScopes; + } + + public String getClientId() { + return clientId; + } + + public String getAuthority() { + return authority; + } + + public String getClientSecret() { + return clientSecret; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + public X509Certificate getCert() { + return cert; + } + + public Set getRedisScopes() { + return redisScopes; + } + + private static HashMap endpointConfigs; + + private static List sentinelHostAndPortList = new ArrayList<>(); + private static List clusterHostAndPortList = new ArrayList<>(); + private static List stableClusterHostAndPortList = new ArrayList<>(); + + static { + String endpointsPath = System.getenv().getOrDefault("REDIS_ENDPOINTS_CONFIG_PATH", + "src/test/resources/endpoints.json"); + try { + endpointConfigs = EndpointConfig.loadFromJSON(endpointsPath); + } catch (Exception e) { + throw new RuntimeException(e); + } + + sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT)); + sentinelHostAndPortList + .add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 1)); + sentinelHostAndPortList + .add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 2)); + sentinelHostAndPortList + .add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 3)); + sentinelHostAndPortList + .add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 4)); + + clusterHostAndPortList.add(new HostAndPort("localhost", 7379)); + clusterHostAndPortList.add(new HostAndPort("localhost", 7380)); + clusterHostAndPortList.add(new HostAndPort("localhost", 7381)); + clusterHostAndPortList.add(new HostAndPort("localhost", 7382)); + clusterHostAndPortList.add(new HostAndPort("localhost", 7383)); + clusterHostAndPortList.add(new HostAndPort("localhost", 7384)); + + stableClusterHostAndPortList.add(new HostAndPort("localhost", 7479)); + stableClusterHostAndPortList.add(new HostAndPort("localhost", 7480)); + stableClusterHostAndPortList.add(new HostAndPort("localhost", 7481)); + } + + public static EndpointConfig getRedisEndpoint(String endpointName) { + if (!endpointConfigs.containsKey(endpointName)) { + throw new IllegalArgumentException("Unknown Redis endpoint: " + endpointName); + } + + return endpointConfigs.get(endpointName); + } + + public static List getSentinelServers() { + return sentinelHostAndPortList; + } + + public static List getClusterServers() { + return clusterHostAndPortList; + } + + public static List getStableClusterServers() { + return stableClusterHostAndPortList; + } + + private static PrivateKey getPrivateKey(String privateKey) { + try { + // Decode the base64 encoded key into a byte array + byte[] decodedKey = Base64.getDecoder().decode(privateKey); + + // Generate the private key from the decoded byte array using PKCS8EncodedKeySpec + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // Use the correct algorithm (e.g., "RSA", "EC", "DSA") + PrivateKey key = keyFactory.generatePrivate(keySpec); + return key; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + private static X509Certificate getCert(String cert) { + try { + // Convert the Base64 encoded string into a byte array + byte[] encoded = java.util.Base64.getDecoder().decode(cert); + + // Create a CertificateFactory for X.509 certificates + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + // Generate the certificate from the byte array + X509Certificate certificate = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(encoded)); + return certificate; + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } +} diff --git a/entraid/src/test/resources/endpoints.json b/entraid/src/test/resources/endpoints.json new file mode 100644 index 0000000..c1d905b --- /dev/null +++ b/entraid/src/test/resources/endpoints.json @@ -0,0 +1,107 @@ +{ + "standalone0": { + "password": "foobared", + "tls": false, + "endpoints": [ + "redis://localhost:6379" + ] + }, + "standalone0-tls": { + "username": "default", + "password": "foobared", + "tls": true, + "endpoints": [ + "rediss://localhost:6390" + ] + }, + "standalone0-acl": { + "username": "acljedis", + "password": "fizzbuzz", + "tls": false, + "endpoints": [ + "redis://localhost:6379" + ] + }, + "standalone0-acl-tls": { + "username": "acljedis", + "password": "fizzbuzz", + "tls": true, + "endpoints": [ + "rediss://localhost:6390" + ] + }, + "standalone1": { + "username": "default", + "password": "foobared", + "tls": false, + "endpoints": [ + "redis://localhost:6380" + ] + }, + "standalone2-primary": { + "username": "default", + "password": "foobared", + "tls": false, + "endpoints": [ + "redis://localhost:6381" + ] + }, + "standalone3-replica-of-standalone2": { + "username": "default", + "password": "foobared", + "tls": false, + "endpoints": [ + "redis://localhost:6382" + ] + }, + "standalone4-replica-of-standalone1": { + "username": "default", + "password": "foobared", + "tls": false, + "endpoints": [ + "redis://localhost:6383" + ] + }, + "standalone5-primary": { + "username": "default", + "password": "foobared", + "tls": false, + "endpoints": [ + "redis://localhost:6384" + ] + }, + "standalone6-replica-of-standalone5": { + "username": "default", + "password": "foobared", + "tls": false, + "endpoints": [ + "redis://localhost:6385" + ] + }, + "standalone7-with-lfu-policy": { + "username": "default", + "password": "foobared", + "tls": false, + "endpoints": [ + "redis://localhost:6386" + ] + }, + "standalone9": { + "tls": false, + "endpoints": [ + "redis://localhost:6388" + ] + }, + "standalone10-replica-of-standalone9": { + "tls": false, + "endpoints": [ + "redis://localhost:6389" + ] + }, + "modules-docker": { + "tls": false, + "endpoints": [ + "redis://localhost:6479" + ] + } +} \ No newline at end of file diff --git a/hbase-formatter.xml b/hbase-formatter.xml new file mode 100644 index 0000000..cf59372 --- /dev/null +++ b/hbase-formatter.xml @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0927c3f --- /dev/null +++ b/pom.xml @@ -0,0 +1,21 @@ + + + + org.sonatype.oss + oss-parent + 7 + + + 4.0.0 + pom + redis.clients.authentication + redis-authx + 0.1.0 + redis-authx + + + core + entraid + + + From 9583ad793b37ae1cf9e20c85bbb4ab7d38963212 Mon Sep 17 00:00:00 2001 From: atakavci Date: Mon, 18 Nov 2024 13:34:09 +0300 Subject: [PATCH 02/22] - deploy parent pom - core snapshot --- .github/workflows/core_snapshot.yml | 89 +++++++++++++++-------------- .github/workflows/snapshot.yml | 2 +- core/pom.xml | 8 +-- entraid/pom.xml | 2 +- 4 files changed, 51 insertions(+), 50 deletions(-) diff --git a/.github/workflows/core_snapshot.yml b/.github/workflows/core_snapshot.yml index 9e0e7fd..6e34cdc 100644 --- a/.github/workflows/core_snapshot.yml +++ b/.github/workflows/core_snapshot.yml @@ -1,46 +1,47 @@ --- -name: Publish Snapshot-Core - -on: - push: - branches: - - master - - '[0-9].x' - workflow_dispatch: - -jobs: - - snapshot: - name: Deploy Snapshot-Core - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./core - steps: - - uses: actions/checkout@v2 - - name: Set up publishing to maven central - uses: actions/setup-java@v2 - with: - java-version: '8' - distribution: 'temurin' - server-id: ossrh - server-username: MAVEN_USERNAME - server-password: MAVEN_PASSWORD - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: | - ~/.m2/repository - /var/cache/apt - key: core-${{hashFiles('**/pom.xml')}} - - name: mvn offline - run: | - mvn -q dependency:go-offline - - name: deploy - run: | - mvn --no-transfer-progress \ - -DskipTests deploy - env: - MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} - MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} + name: Publish Snapshot-Core + + on: + push: + branches: + - master + - '[0-9].x' + workflow_dispatch: + + jobs: + + snapshot: + name: Deploy Snapshot-Core + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./core + steps: + - uses: actions/checkout@v2 + - name: Set up publishing to maven central + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'temurin' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + /var/cache/apt + key: core-${{hashFiles('**/pom.xml')}} + - name: mvn offline + run: | + mvn -q dependency:go-offline + - name: deploy + run: | + mvn --no-transfer-progress \ + -DskipTests deploy + env: + MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} + MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} + \ No newline at end of file diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index e11b9a8..17eb87d 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -37,7 +37,7 @@ jobs: - name: deploy run: | mvn --no-transfer-progress \ - -DskipTests deploy + -DskipTests deploy -N env: MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} diff --git a/core/pom.xml b/core/pom.xml index c01c416..843b73d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -1,9 +1,9 @@ - - redis.clients.authentication - redis-authx - 0.1.0 + + org.sonatype.oss + oss-parent + 7 4.0.0 diff --git a/entraid/pom.xml b/entraid/pom.xml index 517efe8..5397ed9 100644 --- a/entraid/pom.xml +++ b/entraid/pom.xml @@ -60,7 +60,7 @@ redis.clients.authentication redis-authx-core - 0.1.0 + 0.1.0-SNAPSHOT com.microsoft.azure From 6d745462429ba339122c146c840452dbaf6219f3 Mon Sep 17 00:00:00 2001 From: atakavci Date: Mon, 18 Nov 2024 15:37:55 +0300 Subject: [PATCH 03/22] entra id integration and snapshot workflows --- .github/workflows/core_integration.yml | 2 +- .github/workflows/entraid_integration.yml | 68 +++++++++++++++++++++++ .github/workflows/entraid_snapshot.yml | 54 ++++++++++++++++++ entraid/pom.xml | 10 ++-- 4 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/entraid_integration.yml create mode 100644 .github/workflows/entraid_snapshot.yml diff --git a/.github/workflows/core_integration.yml b/.github/workflows/core_integration.yml index c49f61e..f4e97b7 100644 --- a/.github/workflows/core_integration.yml +++ b/.github/workflows/core_integration.yml @@ -40,7 +40,7 @@ jobs: path: | ~/.m2/repository /var/cache/apt - key: entraid-${{hashFiles('**/pom.xml')}} + key: core-${{hashFiles('**/pom.xml')}} - name: Maven offline run: | mvn -q dependency:go-offline diff --git a/.github/workflows/entraid_integration.yml b/.github/workflows/entraid_integration.yml new file mode 100644 index 0000000..d952b30 --- /dev/null +++ b/.github/workflows/entraid_integration.yml @@ -0,0 +1,68 @@ +--- + +name: Integration-EntraID + +on: + push: + paths-ignore: + - 'docs/**' + - '**/*.md' + - '**/*.rst' + branches: + - master + - '[0-9].*' + pull_request: + branches: + - master + - '[0-9].*' + schedule: + - cron: '0 1 * * *' # nightly build + workflow_dispatch: + +jobs: + + build: + name: Build and Test EntraID + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./entraid + steps: + - uses: actions/checkout@v2 + - name: Checkout Jedis repository (tba_draft branch) + uses: actions/checkout@v2 + with: + repository: atakavci/jedis # Replace with the actual jedis repository URL + ref: ali/authx2 + path: jedis # Check out into a subdirectory named `jedis` so it's isolated + + - name: Set up publishing to maven central + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'temurin' + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + /var/cache/apt + key: entraid-${{hashFiles('**/pom.xml')}} + - name: Maven offline + run: | + mvn -q dependency:go-offline + - name: Build and install Core into local repo + run: | + mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed + working-directory: ./core + - name: Build and install Jedis supports TBA into local repo + run: | + cd jedis + mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed + - name: Build docs + run: | + mvn javadoc:jar + - name: Build with Maven + run: mvn compile + - name: Test with Maven + run: mvn test diff --git a/.github/workflows/entraid_snapshot.yml b/.github/workflows/entraid_snapshot.yml new file mode 100644 index 0000000..2882eaf --- /dev/null +++ b/.github/workflows/entraid_snapshot.yml @@ -0,0 +1,54 @@ +--- + + name: Publish Snapshot-EntraID + + on: + push: + branches: + - master + - '[0-9].x' + workflow_dispatch: + + jobs: + + snapshot: + name: Deploy Snapshot-EntraID + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./entraid + steps: + - uses: actions/checkout@v2 + - name: Checkout Jedis repository (tba_draft branch) + uses: actions/checkout@v2 + with: + repository: atakavci/jedis # Replace with the actual jedis repository URL + ref: ali/authx2 + path: jedis # Check out into a subdirectory named `jedis` so it's isolated + + - name: Set up publishing to maven central + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'temurin' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: | + ~/.m2/repository + /var/cache/apt + key: entraid-${{hashFiles('**/pom.xml')}} + - name: Maven offline + run: | + mvn -q dependency:go-offline + - name: deploy + run: | + mvn --no-transfer-progress \ + -DskipTests deploy + env: + MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} + MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} + \ No newline at end of file diff --git a/entraid/pom.xml b/entraid/pom.xml index 5397ed9..70bf452 100644 --- a/entraid/pom.xml +++ b/entraid/pom.xml @@ -1,9 +1,9 @@ - - redis.clients.authentication - redis-authx - 0.1.0 + + org.sonatype.oss + oss-parent + 7 4.0.0 @@ -70,7 +70,7 @@ redis.clients jedis - 5.3.0-SNAPSHOT + 5.3.1-SNAPSHOT test From 47b39e6e19af71ca533045dc14f477846e09e58e Mon Sep 17 00:00:00 2001 From: atakavci Date: Mon, 18 Nov 2024 15:57:08 +0300 Subject: [PATCH 04/22] install local maven --- .github/workflows/entraid_integration.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/entraid_integration.yml b/.github/workflows/entraid_integration.yml index d952b30..a10ffa8 100644 --- a/.github/workflows/entraid_integration.yml +++ b/.github/workflows/entraid_integration.yml @@ -1,5 +1,3 @@ ---- - name: Integration-EntraID on: @@ -48,17 +46,25 @@ jobs: ~/.m2/repository /var/cache/apt key: entraid-${{hashFiles('**/pom.xml')}} - - name: Maven offline + + - name: Maven offline-core run: | mvn -q dependency:go-offline + working-directory: ./core - name: Build and install Core into local repo run: | mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed working-directory: ./core + + - name: Maven offline-jedis + run: | + mvn -q dependency:go-offline + working-directory: ./jedis - name: Build and install Jedis supports TBA into local repo run: | - cd jedis mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed + working-directory: ./jedis + - name: Build docs run: | mvn javadoc:jar From dd69b98a949168b5ad3d386321bab5c553561d62 Mon Sep 17 00:00:00 2001 From: atakavci Date: Mon, 18 Nov 2024 17:55:13 +0300 Subject: [PATCH 05/22] fix testcontext --- .../clients/authentication/TestContext.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/entraid/src/test/java/redis/clients/authentication/TestContext.java b/entraid/src/test/java/redis/clients/authentication/TestContext.java index fff610a..b8ce3da 100644 --- a/entraid/src/test/java/redis/clients/authentication/TestContext.java +++ b/entraid/src/test/java/redis/clients/authentication/TestContext.java @@ -24,6 +24,12 @@ public class TestContext { + private static final String AZURE_CLIENT_ID = "AZURE_CLIENT_ID"; + private static final String AZURE_AUTHORITY = "AZURE_AUTHORITY"; + private static final String AZURE_CLIENT_SECRET = "AZURE_CLIENT_SECRET"; + private static final String AZURE_PRIVATE_KEY = "AZURE_PRIVATE_KEY"; + private static final String AZURE_CERT = "AZURE_CERT"; + private static final String AZURE_REDIS_SCOPES = "AZURE_REDIS_SCOPES"; private static final String localContext = "./src/test/resources/local.context"; private String clientId; private String authority; @@ -39,12 +45,12 @@ private TestContext() { try { Properties properties = new Properties(); properties.load(Files.newBufferedReader(Paths.get(localContext))); - this.clientId = properties.getProperty("CLIENT_ID"); - this.authority = properties.getProperty("AUTHORITY"); - this.clientSecret = properties.getProperty("CLIENT_SECRET"); - this.privateKey = getPrivateKey(properties.getProperty("PRIVATE_KEY")); - this.cert = getCert(properties.getProperty("CERT")); - String redisScopesProp = properties.getProperty("REDIS_SCOPES"); + this.clientId = properties.getProperty(AZURE_CLIENT_ID); + this.authority = properties.getProperty(AZURE_AUTHORITY); + this.clientSecret = properties.getProperty(AZURE_CLIENT_SECRET); + this.privateKey = getPrivateKey(properties.getProperty(AZURE_PRIVATE_KEY)); + this.cert = getCert(properties.getProperty(AZURE_CERT)); + String redisScopesProp = properties.getProperty(AZURE_REDIS_SCOPES); if (redisScopesProp != null && !redisScopesProp.isEmpty()) { this.redisScopes = new HashSet<>(Arrays.asList(redisScopesProp.split(";"))); } @@ -52,10 +58,12 @@ private TestContext() { throw new RuntimeException("Failed to load local.context", e); } } else { - this.clientId = System.getenv("CLIENT_ID"); - this.authority = System.getenv("AUTHORITY"); - this.clientSecret = System.getenv("CLIENT_SECRET"); - String redisScopesEnv = System.getenv("REDIS_SCOPES"); + this.clientId = System.getenv(AZURE_CLIENT_ID); + this.authority = System.getenv(AZURE_AUTHORITY); + this.clientSecret = System.getenv(AZURE_CLIENT_SECRET); + this.privateKey = getPrivateKey(System.getenv(AZURE_PRIVATE_KEY)); + this.cert = getCert(System.getenv(AZURE_CERT)); + String redisScopesEnv = System.getenv(AZURE_REDIS_SCOPES); if (redisScopesEnv != null && !redisScopesEnv.isEmpty()) { this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";"))); } From 8a8332f46540b7c450027db511a16dfc327b43de Mon Sep 17 00:00:00 2001 From: atakavci Date: Mon, 18 Nov 2024 18:09:34 +0300 Subject: [PATCH 06/22] set azure params --- .github/workflows/entraid_integration.yml | 7 +++++++ .github/workflows/entraid_snapshot.yml | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/entraid_integration.yml b/.github/workflows/entraid_integration.yml index a10ffa8..b42c1d1 100644 --- a/.github/workflows/entraid_integration.yml +++ b/.github/workflows/entraid_integration.yml @@ -72,3 +72,10 @@ jobs: run: mvn compile - name: Test with Maven run: mvn test + env: + AZURE_CLIENT_ID: ${{secrets.AZURE_CLIENT_ID}} + AZURE_AUTHORITY: ${{secrets.AZURE_AUTHORITY}} + AZURE_CLIENT_SECRET: ${{secrets.AZURE_CLIENT_SECRET}} + AZURE_CERT: ${{secrets.AZURE_CERT}} + AZURE_PRIVATE_KEY: ${{secrets.AZURE_PRIVATE_KEY}} + AZURE_REDIS_SCOPES: ${{secrets.AZURE_REDIS_SCOPES}} diff --git a/.github/workflows/entraid_snapshot.yml b/.github/workflows/entraid_snapshot.yml index 2882eaf..ca465ac 100644 --- a/.github/workflows/entraid_snapshot.yml +++ b/.github/workflows/entraid_snapshot.yml @@ -51,4 +51,3 @@ env: MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} - \ No newline at end of file From 6b8dbeaadbffe99b26456506bbf40f91a6856bbb Mon Sep 17 00:00:00 2001 From: atakavci Date: Mon, 18 Nov 2024 18:30:24 +0300 Subject: [PATCH 07/22] build into local dependency --- .github/workflows/entraid_snapshot.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/entraid_snapshot.yml b/.github/workflows/entraid_snapshot.yml index ca465ac..f7e77ac 100644 --- a/.github/workflows/entraid_snapshot.yml +++ b/.github/workflows/entraid_snapshot.yml @@ -41,6 +41,25 @@ ~/.m2/repository /var/cache/apt key: entraid-${{hashFiles('**/pom.xml')}} + + - name: Maven offline-core + run: | + mvn -q dependency:go-offline + working-directory: ./core + - name: Build and install Core into local repo + run: | + mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed + working-directory: ./core + + - name: Maven offline-jedis + run: | + mvn -q dependency:go-offline + working-directory: ./jedis + - name: Build and install Jedis supports TBA into local repo + run: | + mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed + working-directory: ./jedis + - name: Maven offline run: | mvn -q dependency:go-offline From 7a5adbea0e7e792f0e44be74641d84f353836a67 Mon Sep 17 00:00:00 2001 From: atakavci Date: Tue, 19 Nov 2024 13:22:17 +0300 Subject: [PATCH 08/22] release workflow --- .github/workflows/spellcheck.yml | 2 +- .github/workflows/stale-issues.yml | 2 +- .github/workflows/version-and-release.yml | 38 +++++++++++++++++------ entraid/pom.xml | 2 +- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index e152841..bd88e08 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -1,4 +1,4 @@ -name: spellcheck +name: Spellcheck on: pull_request: jobs: diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml index 54bf059..d511f4a 100644 --- a/.github/workflows/stale-issues.yml +++ b/.github/workflows/stale-issues.yml @@ -1,4 +1,4 @@ -name: "Close stale issues" +name: Close stale issues on: schedule: - cron: "0 0 * * *" diff --git a/.github/workflows/version-and-release.yml b/.github/workflows/version-and-release.yml index 7c996e5..d007604 100644 --- a/.github/workflows/version-and-release.yml +++ b/.github/workflows/version-and-release.yml @@ -27,20 +27,40 @@ jobs: server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD - - name: mvn versions - run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }} - - name: Install gpg key run: | cat <(echo -e "${{ secrets.OSSH_GPG_SECRET_KEY }}") | gpg --batch --import gpg --list-secret-keys --keyid-format LONG - - name: Publish + - name: mvn versions - Core + run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }} + working-directory: ./core + + - name: Publish - Core run: | mvn --no-transfer-progress \ - --batch-mode \ - -Dgpg.passphrase='${{ secrets.OSSH_GPG_SECRET_KEY_PASSWORD }}' \ - -DskipTests deploy -P release + --batch-mode \ + -Dgpg.passphrase='${{ secrets.OSSH_GPG_SECRET_KEY_PASSWORD }}' \ + -DskipTests deploy -P release + env: + MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} + MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} + working-directory: ./core + + - name: mvn versions - EntraID + run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }} + working-directory: ./entraid + + - name: set release versions + run: mvn versions:use-releases -DallowSnapshots=false -DgenerateBackupPoms=false + working-directory: ./entraid + + - name: Publish - EntraID + run: | + mvn --no-transfer-progress \ + --batch-mode \ + -Dgpg.passphrase='${{ secrets.OSSH_GPG_SECRET_KEY_PASSWORD }}' \ + -DskipTests -Dmaven.test.skip=true deploy -P release env: - MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} - MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} + MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} + MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} diff --git a/entraid/pom.xml b/entraid/pom.xml index 70bf452..f7cac62 100644 --- a/entraid/pom.xml +++ b/entraid/pom.xml @@ -70,7 +70,7 @@ redis.clients jedis - 5.3.1-SNAPSHOT + 5.3.0-SNAPSHOT test From 49418e6c6efc4703df3e76a08587b4eb4db22680 Mon Sep 17 00:00:00 2001 From: atakavci Date: Wed, 20 Nov 2024 11:04:36 +0300 Subject: [PATCH 09/22] remove jedis build from enraid_snapshot --- .github/workflows/entraid_snapshot.yml | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/entraid_snapshot.yml b/.github/workflows/entraid_snapshot.yml index f7e77ac..503c2f7 100644 --- a/.github/workflows/entraid_snapshot.yml +++ b/.github/workflows/entraid_snapshot.yml @@ -19,12 +19,12 @@ working-directory: ./entraid steps: - uses: actions/checkout@v2 - - name: Checkout Jedis repository (tba_draft branch) - uses: actions/checkout@v2 - with: - repository: atakavci/jedis # Replace with the actual jedis repository URL - ref: ali/authx2 - path: jedis # Check out into a subdirectory named `jedis` so it's isolated + # - name: Checkout Jedis repository (tba_draft branch) + # uses: actions/checkout@v2 + # with: + # repository: atakavci/jedis # Replace with the actual jedis repository URL + # ref: ali/authx2 + # path: jedis # Check out into a subdirectory named `jedis` so it's isolated - name: Set up publishing to maven central uses: actions/setup-java@v2 @@ -51,14 +51,14 @@ mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed working-directory: ./core - - name: Maven offline-jedis - run: | - mvn -q dependency:go-offline - working-directory: ./jedis - - name: Build and install Jedis supports TBA into local repo - run: | - mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed - working-directory: ./jedis + # - name: Maven offline-jedis + # run: | + # mvn -q dependency:go-offline + # working-directory: ./jedis + # - name: Build and install Jedis supports TBA into local repo + # run: | + # mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed + # working-directory: ./jedis - name: Maven offline run: | @@ -66,7 +66,7 @@ - name: deploy run: | mvn --no-transfer-progress \ - -DskipTests deploy + -DskipTests -Dmaven.test.skip=true deploy env: MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} From 2c0bfecb36373ed3fe78995057ef9303e51afcb3 Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 28 Nov 2024 14:41:11 +0300 Subject: [PATCH 10/22] - add ManagedentityInfo - add ServicePrincipalInfo - unwrap ExecutionException - auth with managedId s - remove EntraIDTokenAuthConfig --- .../authentication/core/TokenManager.java | 34 ++++-- .../CoreAuthenticationUnitTests.java | 6 +- .../entraid/EntraIDIdentityProvider.java | 73 ++++++++---- .../EntraIDIdentityProviderConfig.java | 54 ++------- .../entraid/EntraIDTokenAuthConfig.java | 85 -------------- .../EntraIDTokenAuthConfigBuilder.java | 109 ++++++++++++++++++ .../entraid/ManagedIdentityInfo.java | 50 ++++++++ .../entraid/ServicePrincipalInfo.java | 58 ++++++++++ .../RedisEntraIDIntegrationTests.java | 32 ++--- .../authentication/RedisEntraIDUnitTests.java | 62 +++++----- 10 files changed, 362 insertions(+), 201 deletions(-) delete mode 100644 entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfig.java create mode 100644 entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java create mode 100644 entraid/src/main/java/redis/clients/authentication/entraid/ManagedIdentityInfo.java create mode 100644 entraid/src/main/java/redis/clients/authentication/entraid/ServicePrincipalInfo.java diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManager.java b/core/src/main/java/redis/clients/authentication/core/TokenManager.java index 5a95158..9d721a9 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenManager.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenManager.java @@ -8,6 +8,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,28 +18,38 @@ public class TokenManager { private TokenManagerConfig tokenManagerConfig; private IdentityProvider identityProvider; private TokenListener listener; - private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private ExecutorService executor = Executors.newSingleThreadExecutor(); private boolean stopped = false; private ScheduledFuture scheduledTask; private int numberOfRetries = 0; private Exception lastException; private Logger logger = LoggerFactory.getLogger(getClass()); + private Token currentToken = null; + private AtomicBoolean started = new AtomicBoolean(false); public TokenManager(IdentityProvider identityProvider, TokenManagerConfig tokenManagerConfig) { this.identityProvider = identityProvider; this.tokenManagerConfig = tokenManagerConfig; } - public void start(TokenListener listener, boolean blockForInitialToken) - throws InterruptedException, ExecutionException, TimeoutException { + public void start(TokenListener listener, boolean blockForInitialToken) { + if (!started.compareAndSet(false, true)) { + throw new AuthXException("Token manager already started!"); + } this.listener = listener; ScheduledFuture currentTask = scheduleNext(0); scheduledTask = currentTask; if (blockForInitialToken) { - while (currentTask.get() == null) { - currentTask = scheduledTask; + try { + while (currentTask.get() == null) { + currentTask = scheduledTask; + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new TokenRequestException(unwrap(e), lastException); } } } @@ -66,7 +77,8 @@ protected Token renewToken() { try { Future requestResult = executor.submit(() -> requestToken()); newToken = requestResult.get(tokenManagerConfig.getTokenRequestExecTimeoutInMs(), - TimeUnit.MILLISECONDS); + TimeUnit.MILLISECONDS); + currentToken = newToken; long delay = calculateRenewalDelay(newToken.getExpiresAt(), newToken.getReceivedAt()); scheduledTask = scheduleNext(delay); listener.onTokenRenewed(newToken); @@ -76,7 +88,7 @@ protected Token renewToken() { numberOfRetries++; scheduledTask = scheduleNext(tokenManagerConfig.getRetryPolicy().getdelayInMs()); } else { - TokenRequestException tre = new TokenRequestException(e, lastException); + TokenRequestException tre = new TokenRequestException(unwrap(e), lastException); listener.onError(tre); throw tre; } @@ -95,6 +107,14 @@ protected Token requestToken() { } } + private Throwable unwrap(Exception e) { + return (e instanceof ExecutionException) ? e.getCause() : e; + } + + public Token getCurrentToken() { + return currentToken; + } + public long calculateRenewalDelay(long expireDate, long issueDate) { long ttlLowerRefresh = ttlForLowerRefresh(expireDate); long ttlRatioRefresh = ttlForRatioRefresh(expireDate, issueDate); diff --git a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java index 327fac3..8c9dc64 100644 --- a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java +++ b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java @@ -28,6 +28,8 @@ import redis.clients.authentication.core.TokenListener; import redis.clients.authentication.core.TokenManager; import redis.clients.authentication.core.TokenManagerConfig; +import redis.clients.authentication.core.TokenRequestException; + import static org.awaitility.Awaitility.await; import java.util.concurrent.TimeUnit; @@ -157,10 +159,10 @@ public void testBlockForInitialToken() { TokenManager tokenManager = new TokenManager(identityProvider, new TokenManagerConfig(0.7F, 200, 2000, new TokenManagerConfig.RetryPolicy(5, 100))); - ExecutionException e = assertThrows(ExecutionException.class, + TokenRequestException e = assertThrows(TokenRequestException.class, () -> tokenManager.start(mock(TokenListener.class), true)); - assertEquals("java.lang.RuntimeException: Test exception from identity provider!", + assertEquals("Test exception from identity provider!", e.getCause().getCause().getMessage()); } diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java index 5fb01ca..3418fcc 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java @@ -1,54 +1,87 @@ package redis.clients.authentication.entraid; import java.net.MalformedURLException; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.function.Supplier; import com.microsoft.aad.msal4j.ClientCredentialFactory; import com.microsoft.aad.msal4j.ClientCredentialParameters; import com.microsoft.aad.msal4j.ConfidentialClientApplication; import com.microsoft.aad.msal4j.IAuthenticationResult; import com.microsoft.aad.msal4j.IClientCredential; +import com.microsoft.aad.msal4j.ManagedIdentityApplication; +import com.microsoft.aad.msal4j.ManagedIdentityParameters; import redis.clients.authentication.core.IdentityProvider; import redis.clients.authentication.core.Token; public final class EntraIDIdentityProvider implements IdentityProvider { - private ConfidentialClientApplication app; - private ClientCredentialParameters clientParams; + private Supplier resultSupplier; - public EntraIDIdentityProvider(String clientId, String authority, String secret, - Set scopes) { - IClientCredential credential = ClientCredentialFactory.createFromSecret(secret); - init(clientId, authority, credential, scopes); - } - - public EntraIDIdentityProvider(String clientId, String authority, PrivateKey key, X509Certificate cert, - Set scopes) { - IClientCredential credential = ClientCredentialFactory.createFromCertificate(key, cert); - init(clientId, authority, credential, scopes); - } + public EntraIDIdentityProvider(ServicePrincipalInfo servicePrincipalInfo, Set scopes) { + IClientCredential credential = getClientCredential(servicePrincipalInfo); + ConfidentialClientApplication app; - protected void init(String clientId, String authority, IClientCredential credential, Set scopes) { try { - app = ConfidentialClientApplication.builder(clientId, credential).authority(authority).build(); + String authority = servicePrincipalInfo.getAuthority(); + authority = authority == null ? ConfidentialClientApplication.DEFAULT_AUTHORITY + : authority; + app = ConfidentialClientApplication + .builder(servicePrincipalInfo.getClientId(), credential).authority(authority) + .build(); } catch (MalformedURLException e) { throw new RedisEntraIDException("Failed to init EntraID client!", e); } - clientParams = ClientCredentialParameters.builder(scopes).build(); + ClientCredentialParameters params = ClientCredentialParameters.builder(scopes).build(); + + resultSupplier = () -> supplierForConfidentialApp(app, params); + } + + public EntraIDIdentityProvider(ManagedIdentityInfo info, Set scopes) { + ManagedIdentityApplication app = ManagedIdentityApplication.builder(info.getId()).build(); + + ManagedIdentityParameters params = ManagedIdentityParameters + .builder(scopes.iterator().next()).build(); + resultSupplier = () -> supplierForManagedIdentityApp(app, params); + } + + private IClientCredential getClientCredential(ServicePrincipalInfo servicePrincipalInfo) { + switch (servicePrincipalInfo.getAccessWith()) { + case WithSecret: + return ClientCredentialFactory.createFromSecret(servicePrincipalInfo.getSecret()); + case WithCert: + return ClientCredentialFactory.createFromCertificate(servicePrincipalInfo.getKey(), + servicePrincipalInfo.getCert()); + default: + throw new RedisEntraIDException("Invalid ServicePrincipalAccess type!"); + } } @Override public Token requestToken() { + return new JWToken(resultSupplier.get().accessToken()); + } + + public IAuthenticationResult supplierForConfidentialApp(ConfidentialClientApplication app, + ClientCredentialParameters params) { try { - Future tokenRequest = app.acquireToken(clientParams); - return new JWToken(tokenRequest.get().accessToken()); + Future tokenRequest = app.acquireToken(params); + return tokenRequest.get(); } catch (InterruptedException | ExecutionException e) { throw new RedisEntraIDException("Failed to acquire token!", e); } } + + public IAuthenticationResult supplierForManagedIdentityApp(ManagedIdentityApplication app, + ManagedIdentityParameters params) { + try { + Future tokenRequest = app.acquireTokenForManagedIdentity(params); + return tokenRequest.get(); + } catch (Exception e) { + throw new RedisEntraIDException("Failed to acquire token!", e); + } + } } diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java index cd39d53..cdf30ab 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java @@ -1,7 +1,5 @@ package redis.clients.authentication.entraid; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; import java.util.Set; import redis.clients.authentication.core.IdentityProvider; @@ -9,50 +7,25 @@ public final class EntraIDIdentityProviderConfig implements IdentityProviderConfig, AutoCloseable { - public enum EntraIDAccess { - WithSecret, - WithCert, - } - - private String clientId; - private EntraIDAccess accessWith; - private String secret; - private PrivateKey key; - private X509Certificate cert; - private String authority; + private ServicePrincipalInfo servicePrincipalInfo; private Set scopes; + private ManagedIdentityInfo managedIdentityInfo; - public EntraIDIdentityProviderConfig(String clientId, - EntraIDAccess accessWith, - String secret, - PrivateKey key, - X509Certificate cert, - String authority, Set scopes) { - this.clientId = clientId; - this.accessWith = accessWith; - this.secret = secret; - this.key = key; - this.cert = cert; - this.authority = authority; + public EntraIDIdentityProviderConfig(ServicePrincipalInfo servicePrincipalInfo, + ManagedIdentityInfo info, Set scopes) { + this.servicePrincipalInfo = servicePrincipalInfo; this.scopes = scopes; + this.managedIdentityInfo = info; } @Override public IdentityProvider getProvider() { IdentityProvider identityProvider = null; - switch (accessWith) { - case WithSecret: - identityProvider = new EntraIDIdentityProvider(clientId, authority, - secret, scopes); - break; - case WithCert: - identityProvider = new EntraIDIdentityProvider(clientId, authority, - key, cert, scopes); - break; - default: - throw new RedisEntraIDException("Access type and credentials must be set!"); + if (managedIdentityInfo != null) { + identityProvider = new EntraIDIdentityProvider(managedIdentityInfo, scopes); + } else { + identityProvider = new EntraIDIdentityProvider(servicePrincipalInfo, scopes); } - clear(); return identityProvider; } @@ -63,11 +36,8 @@ public void close() throws Exception { } private void clear() { - clientId = null; - secret = null; - key = null; - cert = null; - authority = null; + servicePrincipalInfo = null; + managedIdentityInfo = null; scopes = null; } } diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfig.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfig.java deleted file mode 100644 index a975049..0000000 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfig.java +++ /dev/null @@ -1,85 +0,0 @@ -package redis.clients.authentication.entraid; - -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.Set; - -import redis.clients.authentication.core.TokenAuthConfig; - -public class EntraIDTokenAuthConfig { - - private EntraIDTokenAuthConfig() { - } - - public static class Builder extends TokenAuthConfig.Builder implements AutoCloseable { - public static final float DEFAULT_EXPIRATION_REFRESH_RATIO = 0.8F; - public static final int DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 2 * 60 * 1000; - public static final int DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS = 1000; - public static final int DEFAULT_MAX_ATTEMPTS_TO_RETRY = 5; - public static final int DEFAULT_DELAY_IN_MS_TO_RETRY = 100; - - private String clientId; - private String secret; - private PrivateKey key; - private X509Certificate cert; - private String authority; - private Set scopes; - private EntraIDIdentityProviderConfig.EntraIDAccess accessWith; - - public Builder() { - this.expirationRefreshRatio(DEFAULT_EXPIRATION_REFRESH_RATIO) - .lowerRefreshBoundMillis(DEFAULT_LOWER_REFRESH_BOUND_MILLIS) - .tokenRequestExecTimeoutInMs(DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS) - .maxAttemptsToRetry(DEFAULT_MAX_ATTEMPTS_TO_RETRY) - .delayInMsToRetry(DEFAULT_DELAY_IN_MS_TO_RETRY); - } - - public Builder clientId(String clientId) { - this.clientId = clientId; - return this; - } - - public Builder secret(String secret) { - this.secret = secret; - this.accessWith = EntraIDIdentityProviderConfig.EntraIDAccess.WithSecret; - return this; - } - - public Builder key(PrivateKey key, X509Certificate cert) { - this.key = key; - this.accessWith = EntraIDIdentityProviderConfig.EntraIDAccess.WithCert; - return this; - } - - public Builder authority(String authority) { - this.authority = authority; - return this; - } - - public Builder scopes(Set scopes) { - this.scopes = scopes; - return this; - } - - public TokenAuthConfig build() { - EntraIDIdentityProviderConfig idProviderConfig = new EntraIDIdentityProviderConfig(clientId, accessWith, - secret, key, cert, authority, scopes); - super.identityProviderConfig(idProviderConfig); - return super.build(); - } - - @Override - public void close() throws Exception { - clientId = null; - secret = null; - key = null; - cert = null; - authority = null; - scopes = null; - } - } - - public static Builder builder() { - return new Builder(); - } -} diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java new file mode 100644 index 0000000..67e1cd5 --- /dev/null +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java @@ -0,0 +1,109 @@ +package redis.clients.authentication.entraid; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Set; + +import redis.clients.authentication.core.TokenAuthConfig; +import redis.clients.authentication.entraid.ManagedIdentityInfo.UserManagedIdentityType; +import redis.clients.authentication.entraid.ServicePrincipalInfo.ServicePrincipalAccess; + +public class EntraIDTokenAuthConfigBuilder extends TokenAuthConfig.Builder + implements AutoCloseable { + public static final float DEFAULT_EXPIRATION_REFRESH_RATIO = 0.8F; + public static final int DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 2 * 60 * 1000; + public static final int DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS = 1000; + public static final int DEFAULT_MAX_ATTEMPTS_TO_RETRY = 5; + public static final int DEFAULT_DELAY_IN_MS_TO_RETRY = 100; + + private String clientId; + private String secret; + private PrivateKey key; + private X509Certificate cert; + private String authority; + private Set scopes; + private ServicePrincipalAccess accessWith; + private ManagedIdentityInfo mii; + + public EntraIDTokenAuthConfigBuilder() { + this.expirationRefreshRatio(DEFAULT_EXPIRATION_REFRESH_RATIO) + .lowerRefreshBoundMillis(DEFAULT_LOWER_REFRESH_BOUND_MILLIS) + .tokenRequestExecTimeoutInMs(DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS) + .maxAttemptsToRetry(DEFAULT_MAX_ATTEMPTS_TO_RETRY) + .delayInMsToRetry(DEFAULT_DELAY_IN_MS_TO_RETRY); + } + + public EntraIDTokenAuthConfigBuilder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public EntraIDTokenAuthConfigBuilder secret(String secret) { + this.secret = secret; + this.accessWith = ServicePrincipalAccess.WithSecret; + return this; + } + + public EntraIDTokenAuthConfigBuilder key(PrivateKey key, X509Certificate cert) { + this.key = key; + this.accessWith = ServicePrincipalAccess.WithCert; + return this; + } + + public EntraIDTokenAuthConfigBuilder authority(String authority) { + this.authority = authority; + return this; + } + + public EntraIDTokenAuthConfigBuilder systemAssignedManagedIdentity() { + mii = new ManagedIdentityInfo(); + return this; + } + + public EntraIDTokenAuthConfigBuilder userAssignedManagedIdentity( + UserManagedIdentityType userManagedType, String id) { + mii = new ManagedIdentityInfo(userManagedType, id); + return this; + } + + public EntraIDTokenAuthConfigBuilder scopes(Set scopes) { + this.scopes = scopes; + return this; + } + + public TokenAuthConfig build() { + ServicePrincipalInfo spi = null; + if (key != null || cert != null || secret != null) { + switch (accessWith) { + case WithCert: + spi = new ServicePrincipalInfo(clientId, key, cert, authority); + break; + case WithSecret: + spi = new ServicePrincipalInfo(clientId, secret, authority); + break; + } + } + if (spi != null && mii != null) { + throw new RedisEntraIDException( + "Cannot have both ServicePrincipal and ManagedIdentity"); + } + EntraIDIdentityProviderConfig idProviderConfig = new EntraIDIdentityProviderConfig(spi, mii, + scopes); + super.identityProviderConfig(idProviderConfig); + return super.build(); + } + + @Override + public void close() throws Exception { + clientId = null; + secret = null; + key = null; + cert = null; + authority = null; + scopes = null; + } + + public static EntraIDTokenAuthConfigBuilder builder() { + return new EntraIDTokenAuthConfigBuilder(); + } +} diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/ManagedIdentityInfo.java b/entraid/src/main/java/redis/clients/authentication/entraid/ManagedIdentityInfo.java new file mode 100644 index 0000000..c1bebc1 --- /dev/null +++ b/entraid/src/main/java/redis/clients/authentication/entraid/ManagedIdentityInfo.java @@ -0,0 +1,50 @@ +package redis.clients.authentication.entraid; + +import java.util.function.Function; + +import com.microsoft.aad.msal4j.ManagedIdentityId; + +public class ManagedIdentityInfo { + + public enum IdentityType { + SYSTEM_ASSIGNED, USER_ASSIGNED + } + + public enum UserManagedIdentityType { + CLIENT_ID(ManagedIdentityId::userAssignedClientId), + OBJECT_ID(ManagedIdentityId::userAssignedObjectId), + RESOURCE_ID(ManagedIdentityId::userAssignedResourceId); + + private final Function func; + + UserManagedIdentityType(Function func) { + this.func = func; + } + } + + private IdentityType type; + private UserManagedIdentityType userManagedIdentityType; + private String id; + + public ManagedIdentityInfo() { + type = IdentityType.SYSTEM_ASSIGNED; + } + + public ManagedIdentityInfo(UserManagedIdentityType userManagedType, String id) { + type = IdentityType.USER_ASSIGNED; + this.userManagedIdentityType = userManagedType; + this.id = id; + } + + public ManagedIdentityId getId() { + switch (type) { + case SYSTEM_ASSIGNED: + return ManagedIdentityId.systemAssigned(); + case USER_ASSIGNED: + return userManagedIdentityType.func.apply(id); + } + // this never happens + throw new UnsupportedOperationException( + "Operation not supported for the given identity type"); + } +} diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/ServicePrincipalInfo.java b/entraid/src/main/java/redis/clients/authentication/entraid/ServicePrincipalInfo.java new file mode 100644 index 0000000..96440ad --- /dev/null +++ b/entraid/src/main/java/redis/clients/authentication/entraid/ServicePrincipalInfo.java @@ -0,0 +1,58 @@ +package redis.clients.authentication.entraid; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +public class ServicePrincipalInfo { + + public enum ServicePrincipalAccess { + WithSecret, WithCert, + } + + private String clientId; + private String secret; + private PrivateKey key; + private X509Certificate cert; + private String authority; + private ServicePrincipalAccess accessWith; + + public ServicePrincipalInfo(String clientId, String secret, String authority) { + this.clientId = clientId; + this.secret = secret; + this.authority = authority; + accessWith = ServicePrincipalAccess.WithSecret; + } + + public ServicePrincipalInfo(String clientId, PrivateKey key, X509Certificate cert, + String authority) { + this.clientId = clientId; + this.key = key; + this.cert = cert; + this.authority = authority; + accessWith = ServicePrincipalAccess.WithCert; + } + + public String getClientId() { + return clientId; + } + + public String getSecret() { + return secret; + } + + public PrivateKey getKey() { + return key; + } + + public X509Certificate getCert() { + return cert; + } + + public String getAuthority() { + return authority; + } + + public ServicePrincipalAccess getAccessWith() { + return accessWith; + } +} diff --git a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java index e6a8d4f..059dba5 100644 --- a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java +++ b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java @@ -7,26 +7,30 @@ import redis.clients.authentication.core.Token; import redis.clients.authentication.entraid.EntraIDIdentityProvider; +import redis.clients.authentication.entraid.ServicePrincipalInfo; public class RedisEntraIDIntegrationTests { - @Test - public void requestTokenWithSecret() throws MalformedURLException { - TestContext testCtx = TestContext.DEFAULT; + @Test + public void requestTokenWithSecret() throws MalformedURLException { + TestContext testCtx = TestContext.DEFAULT; - Token token = new EntraIDIdentityProvider(testCtx.getClientId(), testCtx.getAuthority(), - testCtx.getClientSecret(), testCtx.getRedisScopes()).requestToken(); + Token token = new EntraIDIdentityProvider( + new ServicePrincipalInfo(testCtx.getClientId(), + testCtx.getClientSecret(), testCtx.getAuthority()), + testCtx.getRedisScopes()).requestToken(); - assertNotNull(token.getValue()); - } + assertNotNull(token.getValue()); + } - @Test - public void requestTokenWithCert() throws MalformedURLException { - TestContext testCtx = TestContext.DEFAULT; + @Test + public void requestTokenWithCert() throws MalformedURLException { + TestContext testCtx = TestContext.DEFAULT; - Token token = new EntraIDIdentityProvider(testCtx.getClientId(), testCtx.getAuthority(), - testCtx.getPrivateKey(), testCtx.getCert(), testCtx.getRedisScopes()).requestToken(); + Token token = new EntraIDIdentityProvider(new ServicePrincipalInfo( + testCtx.getClientId(), testCtx.getPrivateKey(), testCtx.getCert(), + testCtx.getAuthority()), testCtx.getRedisScopes()).requestToken(); - assertNotNull(token.getValue()); - } + assertNotNull(token.getValue()); + } } diff --git a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java index 066ec32..addf5ee 100644 --- a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java +++ b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java @@ -17,7 +17,8 @@ import redis.clients.authentication.core.SimpleToken; import redis.clients.authentication.core.TokenAuthConfig; import redis.clients.authentication.entraid.EntraIDIdentityProvider; -import redis.clients.authentication.entraid.EntraIDTokenAuthConfig; +import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder; +import redis.clients.authentication.entraid.ServicePrincipalInfo; import redis.clients.jedis.DefaultJedisClientConfig; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisPooled; @@ -30,30 +31,29 @@ public void testConfigBuilder() { String clientId = "clientId1"; String credential = "credential1"; Set scopes = Collections.singleton("scope1"); - IdentityProviderConfig config = EntraIDTokenAuthConfig.builder().authority(authority).clientId(clientId) - .secret(credential).scopes(scopes).build().getIdentityProviderConfig(); + IdentityProviderConfig config = EntraIDTokenAuthConfigBuilder.builder().authority(authority) + .clientId(clientId).secret(credential).scopes(scopes).build() + .getIdentityProviderConfig(); assertNotNull(config); try (MockedConstruction mockedConstructor = mockConstruction( - EntraIDIdentityProvider.class, (mock, context) -> { - assertEquals(clientId, context.arguments().get(0)); - assertEquals(authority, context.arguments().get(1)); - assertEquals(credential, context.arguments().get(2)); - assertEquals(scopes, context.arguments().get(3)); + EntraIDIdentityProvider.class, (mock, context) -> { + ServicePrincipalInfo info = (ServicePrincipalInfo) context.arguments().get(0); + assertEquals(clientId, info.getClientId()); + assertEquals(authority, info.getAuthority()); + assertEquals(credential, info.getSecret()); + assertEquals(scopes, context.arguments().get(1)); - })) { + })) { config.getProvider(); } try (MockedConstruction mockedConstructor = mockConstruction( - EntraIDIdentityProvider.class, (mock, context) -> { - assertNull(context.arguments().get(0)); - assertNull(context.arguments().get(1)); - assertNull(context.arguments().get(2)); - assertNull(context.arguments().get(3)); - - })) { + EntraIDIdentityProvider.class, (mock, context) -> { + assertNull(context.arguments().get(0)); + assertNull(context.arguments().get(1)); + })) { config.getProvider(); } } @@ -64,28 +64,28 @@ public void testJedisConfig() { EndpointConfig endpointConfig = TestContext.getRedisEndpoint("standalone0"); HostAndPort hnp = endpointConfig.getHostAndPort(); - TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfig.builder() + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() .authority(testCtx.getAuthority()).clientId(testCtx.getClientId()) - .secret(testCtx.getClientSecret()).scopes(testCtx.getRedisScopes()) - .build(); + .secret(testCtx.getClientSecret()).scopes(testCtx.getRedisScopes()).build(); DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() .tokenAuthConfig(tokenAuthConfig).build(); AtomicInteger counter = new AtomicInteger(0); try (MockedConstruction mockedConstructor = mockConstruction( - EntraIDIdentityProvider.class, (mock, context) -> { - assertEquals(testCtx.getClientId(), context.arguments().get(0)); - assertEquals(testCtx.getAuthority(), context.arguments().get(1)); - assertEquals(testCtx.getClientSecret(), context.arguments().get(2)); - assertEquals(testCtx.getRedisScopes(), context.arguments().get(3)); - assertNotNull(mock); - doAnswer(invocation -> { - counter.incrementAndGet(); - return new SimpleToken("token1", System.currentTimeMillis() + 5 * 60 * 1000, - System.currentTimeMillis(), Collections.singletonMap("oid", "default")); - }).when(mock).requestToken(); - })) { + EntraIDIdentityProvider.class, (mock, context) -> { + ServicePrincipalInfo info = (ServicePrincipalInfo) context.arguments().get(0); + assertEquals(testCtx.getClientId(), info.getClientId()); + assertEquals(testCtx.getAuthority(), info.getAuthority()); + assertEquals(testCtx.getClientSecret(), info.getSecret()); + assertEquals(testCtx.getRedisScopes(), context.arguments().get(1)); + assertNotNull(mock); + doAnswer(invocation -> { + counter.incrementAndGet(); + return new SimpleToken("token1", System.currentTimeMillis() + 5 * 60 * 1000, + System.currentTimeMillis(), Collections.singletonMap("oid", "default")); + }).when(mock).requestToken(); + })) { JedisPooled jedis = new JedisPooled(hnp, jedisConfig); assertNotNull(jedis); assertEquals(1, counter.get()); From ced3c820d2d1fbfa7fbc779a581ceca902cd27cb Mon Sep 17 00:00:00 2001 From: atakavci Date: Fri, 29 Nov 2024 15:50:34 +0300 Subject: [PATCH 11/22] - remove doctest - add unit and integration tests - add executor to shutdown in TokenManager (review from Ivo) --- .github/workflows/doctests.yml | 38 --- .../authentication/core/TokenManager.java | 2 +- .../CoreAuthenticationUnitTests.java | 18 +- .../EntraIDIntegrationTests.java | 37 ++ .../RedisEntraIDIntegrationTests.java | 120 ++++++- .../authentication/RedisEntraIDUnitTests.java | 322 +++++++++++++++++- .../clients/authentication/TestContext.java | 112 ++---- 7 files changed, 491 insertions(+), 158 deletions(-) delete mode 100644 .github/workflows/doctests.yml create mode 100644 entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java diff --git a/.github/workflows/doctests.yml b/.github/workflows/doctests.yml deleted file mode 100644 index ae06981..0000000 --- a/.github/workflows/doctests.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Documentation Tests - -on: - push: - tags-ignore: - - '*' - branches: [ master ] - pull_request: - workflow_dispatch: - -jobs: - doctests: - runs-on: ubuntu-latest - services: - redis-stack: - image: redis/redis-stack-server:latest - options: >- - --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - ports: - - 6379:6379 - - steps: - - uses: actions/checkout@v3 - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: | - ~/.m2/repository - /var/cache/apt - key: entraid-${{hashFiles('**/pom.xml')}} - - name: Set up Java - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'temurin' - - name: Run doctests - run: | - mvn -Pdoctests test diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManager.java b/core/src/main/java/redis/clients/authentication/core/TokenManager.java index 9d721a9..b1198e2 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenManager.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenManager.java @@ -7,7 +7,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import org.slf4j.Logger; @@ -58,6 +57,7 @@ public void stop() { stopped = true; scheduledTask.cancel(true); scheduler.shutdown(); + executor.shutdown(); } public TokenManagerConfig getConfig() { diff --git a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java index 8c9dc64..05b1897 100644 --- a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java +++ b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java @@ -162,8 +162,7 @@ public void testBlockForInitialToken() { TokenRequestException e = assertThrows(TokenRequestException.class, () -> tokenManager.start(mock(TokenListener.class), true)); - assertEquals("Test exception from identity provider!", - e.getCause().getCause().getMessage()); + assertEquals("Test exception from identity provider!", e.getCause().getCause().getMessage()); } @Test @@ -220,7 +219,7 @@ public void testTokenManagerWithFailingTokenRequest() @Test public void testTokenManagerWithHangingTokenRequest() throws InterruptedException, ExecutionException, TimeoutException { - int sleepDuration = 200; + int delayDuration = 200; int executionTimeout = 100; int tokenLifetime = 50 * 1000; int numberOfRetries = 5; @@ -229,11 +228,7 @@ public void testTokenManagerWithHangingTokenRequest() IdentityProvider identityProvider = () -> { requesLatch.countDown(); if (requesLatch.getCount() > 0) { - try { - Thread.sleep(sleepDuration); - } catch (InterruptedException e) { - } - return null; + delay(delayDuration); } return new SimpleToken("tokenValX", System.currentTimeMillis() + tokenLifetime, System.currentTimeMillis(), Collections.singletonMap("oid", "user1")); @@ -250,4 +245,11 @@ public void testTokenManagerWithHangingTokenRequest() verify(listener, times(1)).onTokenRenewed(any()); }); } + + private void delay(long durationInMs) { + try { + Thread.sleep(durationInMs); + } catch (InterruptedException e) { + } + } } diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java new file mode 100644 index 0000000..52c6901 --- /dev/null +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java @@ -0,0 +1,37 @@ +package redis.clients.authentication; + +import static org.junit.Assert.assertNotNull; + +import java.net.MalformedURLException; +import org.junit.Test; +import redis.clients.authentication.core.Token; +import redis.clients.authentication.entraid.EntraIDIdentityProvider; +import redis.clients.authentication.entraid.ServicePrincipalInfo; + +public class EntraIDIntegrationTests { + + + @Test + public void requestTokenWithSecret() throws MalformedURLException { + TestContext testCtx = TestContext.DEFAULT; + ServicePrincipalInfo servicePrincipalInfo = new ServicePrincipalInfo( + testCtx.getClientId(), testCtx.getClientSecret(), + testCtx.getAuthority()); + Token token = new EntraIDIdentityProvider(servicePrincipalInfo, + testCtx.getRedisScopes()).requestToken(); + + assertNotNull(token.getValue()); + } + + @Test + public void requestTokenWithCert() throws MalformedURLException { + TestContext testCtx = TestContext.DEFAULT; + ServicePrincipalInfo servicePrincipalInfo = new ServicePrincipalInfo( + testCtx.getClientId(), testCtx.getPrivateKey(), testCtx.getCert(), + testCtx.getAuthority()); + Token token = new EntraIDIdentityProvider(servicePrincipalInfo, + testCtx.getRedisScopes()).requestToken(); + assertNotNull(token.getValue()); + } + +} diff --git a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java index 059dba5..bcc4a9c 100644 --- a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java +++ b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java @@ -1,36 +1,120 @@ package redis.clients.authentication; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import java.util.UUID; -import java.net.MalformedURLException; +import org.junit.BeforeClass; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import redis.clients.authentication.core.Token; -import redis.clients.authentication.entraid.EntraIDIdentityProvider; -import redis.clients.authentication.entraid.ServicePrincipalInfo; +import redis.clients.authentication.core.TokenAuthConfig; +import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder; +import redis.clients.authentication.entraid.ManagedIdentityInfo.UserManagedIdentityType; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisPooled; public class RedisEntraIDIntegrationTests { + private static final Logger log = LoggerFactory + .getLogger(RedisEntraIDIntegrationTests.class); + private static TestContext testCtx; + private static EndpointConfig endpointConfig; + private static HostAndPort hnp; + + @BeforeClass + public static void before() { + try { + testCtx = TestContext.DEFAULT; + endpointConfig = testCtx.getRedisEndpoint("standalone-entraid-acl1"); + hnp = endpointConfig.getHostAndPort(); + } catch (IllegalArgumentException e) { + log.warn("Skipping test because no Redis endpoint is configured"); + org.junit.Assume.assumeTrue(false); + } + } + + // T.1.1 + // Verify authentication using Azure AD with managed identities + @Test + public void withUserAssignedId_azureManagedIdentityIntegrationTest() { + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() + .clientId(testCtx.getClientId()) + .userAssignedManagedIdentity(UserManagedIdentityType.CLIENT_ID, + "userManagedAuthxId") + .authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) + .build(); + + DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() + .tokenAuthConfig(tokenAuthConfig).build(); + + try (JedisPooled jedis = new JedisPooled(hnp, jedisConfig)) { + String key = UUID.randomUUID().toString(); + jedis.set(key, "value"); + assertEquals("value", jedis.get(key)); + jedis.del(key); + } + } + + // T.1.1 + // Verify authentication using Azure AD with managed identities @Test - public void requestTokenWithSecret() throws MalformedURLException { - TestContext testCtx = TestContext.DEFAULT; + public void withSystemAssignedId_azureManagedIdentityIntegrationTest() { + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() + .clientId(testCtx.getClientId()).systemAssignedManagedIdentity() + .authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) + .build(); - Token token = new EntraIDIdentityProvider( - new ServicePrincipalInfo(testCtx.getClientId(), - testCtx.getClientSecret(), testCtx.getAuthority()), - testCtx.getRedisScopes()).requestToken(); + DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() + .tokenAuthConfig(tokenAuthConfig).build(); - assertNotNull(token.getValue()); + try (JedisPooled jedis = new JedisPooled(hnp, jedisConfig)) { + String key = UUID.randomUUID().toString(); + jedis.set(key, "value"); + assertEquals("value", jedis.get(key)); + jedis.del(key); + } } + // T.1.1 + // Verify authentication using Azure AD with service principals @Test - public void requestTokenWithCert() throws MalformedURLException { - TestContext testCtx = TestContext.DEFAULT; + public void withSecret_azureServicePrincipalIntegrationTest() { + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() + .clientId(testCtx.getClientId()).secret(testCtx.getClientSecret()) + .authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) + .build(); - Token token = new EntraIDIdentityProvider(new ServicePrincipalInfo( - testCtx.getClientId(), testCtx.getPrivateKey(), testCtx.getCert(), - testCtx.getAuthority()), testCtx.getRedisScopes()).requestToken(); + DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() + .tokenAuthConfig(tokenAuthConfig).build(); - assertNotNull(token.getValue()); + try (JedisPooled jedis = new JedisPooled(hnp, jedisConfig)) { + String key = UUID.randomUUID().toString(); + jedis.set(key, "value"); + assertEquals("value", jedis.get(key)); + jedis.del(key); + } } + + // T.1.1 + // Verify authentication using Azure AD with service principals + @Test + public void withCertificate_azureServicePrincipalIntegrationTest() { + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() + .clientId(testCtx.getClientId()).secret(testCtx.getClientSecret()) + .authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) + .build(); + + DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() + .tokenAuthConfig(tokenAuthConfig).build(); + + try (JedisPooled jedis = new JedisPooled(hnp, jedisConfig)) { + String key = UUID.randomUUID().toString(); + jedis.set(key, "value"); + assertEquals("value", jedis.get(key)); + jedis.del(key); + } + } + } diff --git a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java index addf5ee..8e8b533 100644 --- a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java +++ b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java @@ -1,23 +1,48 @@ package redis.clients.authentication; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; +import static org.awaitility.Durations.*; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.both; +import static org.hamcrest.MatcherAssert.assertThat; import java.util.Collections; +import java.util.Date; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import org.awaitility.Awaitility; +import org.awaitility.Durations; import org.junit.Test; import org.mockito.MockedConstruction; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import redis.clients.authentication.core.IdentityProvider; import redis.clients.authentication.core.IdentityProviderConfig; import redis.clients.authentication.core.SimpleToken; +import redis.clients.authentication.core.Token; import redis.clients.authentication.core.TokenAuthConfig; +import redis.clients.authentication.core.TokenListener; +import redis.clients.authentication.core.TokenManager; +import redis.clients.authentication.core.TokenManagerConfig; +import redis.clients.authentication.core.TokenRequestException; import redis.clients.authentication.entraid.EntraIDIdentityProvider; import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder; +import redis.clients.authentication.entraid.JWToken; import redis.clients.authentication.entraid.ServicePrincipalInfo; import redis.clients.jedis.DefaultJedisClientConfig; import redis.clients.jedis.HostAndPort; @@ -25,6 +50,26 @@ public class RedisEntraIDUnitTests { + private static final float EXPIRATION_REFRESH_RATIO = 0.7F; + private static final int LOWER_REFRESH_BOUND_MILLIS = 200; + private static final int TOKEN_REQUEST_EXEC_TIMEOUT = 1000; + private static final int RETRY_POLICY_MAX_ATTEMPTS = 5; + private static final int RETRY_POLICY_DELAY = 100; + + private TokenManagerConfig tokenManagerConfig = new TokenManagerConfig(EXPIRATION_REFRESH_RATIO, + LOWER_REFRESH_BOUND_MILLIS, TOKEN_REQUEST_EXEC_TIMEOUT, + new TokenManagerConfig.RetryPolicy(RETRY_POLICY_MAX_ATTEMPTS, RETRY_POLICY_DELAY)); + + private static final String TOKEN_VALUE = "tokenVal"; + private static final long TOKEN_EXPIRATION_TIME = System.currentTimeMillis() + 60 * 60 * 1000; + private static final long TOKEN_ISSUE_TIME = System.currentTimeMillis(); + private static final String TOKEN_OID = "user1"; + + private Token simpleToken = new SimpleToken(TOKEN_VALUE, TOKEN_EXPIRATION_TIME, + TOKEN_ISSUE_TIME, Collections.singletonMap("oid", TOKEN_OID)); + + private TestContext testCtx = TestContext.DEFAULT; + @Test public void testConfigBuilder() { String authority = "authority1"; @@ -60,9 +105,6 @@ public void testConfigBuilder() { @Test public void testJedisConfig() { - TestContext testCtx = TestContext.DEFAULT; - EndpointConfig endpointConfig = TestContext.getRedisEndpoint("standalone0"); - HostAndPort hnp = endpointConfig.getHostAndPort(); TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() .authority(testCtx.getAuthority()).clientId(testCtx.getClientId()) @@ -75,6 +117,7 @@ public void testJedisConfig() { try (MockedConstruction mockedConstructor = mockConstruction( EntraIDIdentityProvider.class, (mock, context) -> { ServicePrincipalInfo info = (ServicePrincipalInfo) context.arguments().get(0); + assertEquals(testCtx.getClientId(), info.getClientId()); assertEquals(testCtx.getAuthority(), info.getAuthority()); assertEquals(testCtx.getClientSecret(), info.getSecret()); @@ -86,9 +129,280 @@ public void testJedisConfig() { System.currentTimeMillis(), Collections.singletonMap("oid", "default")); }).when(mock).requestToken(); })) { - JedisPooled jedis = new JedisPooled(hnp, jedisConfig); + JedisPooled jedis = new JedisPooled(new HostAndPort("localhost", 6379), jedisConfig); assertNotNull(jedis); assertEquals(1, counter.get()); } } + + // T.1.2 + // Implement a stubbed IdentityProvider and verify that the TokenManager works normally and handles: + // network errors or other exceptions thrown from the IdentityProvider + // token parser errors + // e.g missing ttl in IDPโ€™s response + // misformatted token + @Test + public void tokenRequestfailsWithException_fakeIdentityProviderTest() { + + IdentityProvider identityProvider = () -> { + throw new RuntimeException("Test exception from identity provider!"); + }; + + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + + TokenRequestException e = assertThrows(TokenRequestException.class, + () -> tokenManager.start(mock(TokenListener.class), true)); + + assertEquals("Test exception from identity provider!", + e.getCause().getCause().getMessage()); + } + + // T.2.1 + // Verify that the auth extension can obtain an initial token in a blocking manner from the identity provider. + @Test + public void initialTokenAcquisitionTest() { + CountDownLatch latch = new CountDownLatch(1); + AtomicBoolean isTokenManagerStarted = new AtomicBoolean(false); + IdentityProvider identityProvider = () -> { + try { + latch.await(); + } catch (InterruptedException e) { + } + return simpleToken; + }; + + TokenManagerConfig tokenManagerConfig = new TokenManagerConfig(EXPIRATION_REFRESH_RATIO, + LOWER_REFRESH_BOUND_MILLIS, 60 * 60 * 1000, + this.tokenManagerConfig.getRetryPolicy()); + + TokenListener listener = mock(TokenListener.class); + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + Thread thread = new Thread(() -> { + try { + tokenManager.start(listener, true); + isTokenManagerStarted.set(true); + } catch (Exception e) { + } + }); + thread.start(); + + Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(ONE_SECOND) + .until(() -> Thread.State.WAITING == thread.getState()); + + StackTraceElement[] stackTrace = thread.getStackTrace(); + assertEquals(false, isTokenManagerStarted.get()); + + for (int i = 0; i < stackTrace.length; i++) { + assertEquals(false, isTokenManagerStarted.get()); + + if (stackTrace[i].getMethodName().equals("get") + && stackTrace[i + 1].getClassName().equals(TokenManager.class.getName()) + && stackTrace[i + 1].getMethodName().equals("start")) { + latch.countDown(); + break; + } + } + assertEquals(0, latch.getCount()); + Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(ONE_SECOND) + .until(() -> isTokenManagerStarted.get()); + assertNotNull(tokenManager.getCurrentToken()); + } + + // T.2.1 + // Test the system's behavior when token acquisition fails initially but succeeds on retry. + @Test + public void tokenAcquisitionRetryTest() throws InterruptedException, TimeoutException { + AtomicInteger numberOfRetries = new AtomicInteger(0); + IdentityProvider identityProvider = () -> { + if (numberOfRetries.incrementAndGet() < 3) { + throw new RuntimeException("Test exception from identity provider!"); + } + return simpleToken; + + }; + + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + + tokenManager.start(mock(TokenListener.class), false); + + Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(ONE_SECOND) + .until(() -> tokenManager.getCurrentToken() != null); + assertEquals(3, numberOfRetries.get()); + } + + // T.2.1 + // Ensure the system handles timeouts during token acquisition gracefully. + @Test + public void tokenAcquisitionTimeoutTest() throws InterruptedException, TimeoutException { + AtomicInteger numberOfRetries = new AtomicInteger(0); + + IdentityProvider identityProvider = () -> { + if (numberOfRetries.getAndIncrement() < 1) { + delay(TOKEN_REQUEST_EXEC_TIMEOUT); + } + return simpleToken; + }; + + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + + tokenManager.start(mock(TokenListener.class), false); + + Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(Durations.FIVE_SECONDS) + .until(() -> tokenManager.getCurrentToken() != null); + assertEquals(2, numberOfRetries.get()); + } + + // T.2.2 + // Verify that tokens are automatically renewed in the background and listeners are notified asynchronously without user intervention. + @Test + public void backgroundTokenRenewalTest() throws InterruptedException, TimeoutException { + AtomicInteger numberOfTokens = new AtomicInteger(0); + + IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_VALUE, + System.currentTimeMillis() + 1000, System.currentTimeMillis(), + Collections.singletonMap("oid", TOKEN_OID)); + + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + TokenListener listener = new TokenListener() { + @Override + public void onTokenRenewed(Token token) { + numberOfTokens.incrementAndGet(); + } + + @Override + public void onError(Exception e) { + } + }; + + tokenManager.start(listener, false); + + Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(Durations.TWO_SECONDS) + .until(() -> numberOfTokens.get(), is(2)); + } + + // T.2.2 + // Ensure the system propagates error during renewal back to the user + @Test + public void failedRenewalTest() { + AtomicInteger numberOfErrors = new AtomicInteger(0); + + IdentityProvider identityProvider = () -> { + throw new RuntimeException("Test exception from identity provider!"); + }; + + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + TokenListener listener = new TokenListener() { + @Override + public void onTokenRenewed(Token token) { + } + + @Override + public void onError(Exception e) { + numberOfErrors.incrementAndGet(); + } + }; + + tokenManager.start(listener, false); + + Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(TWO_SECONDS) + .until(() -> numberOfErrors.get(), is(1)); + } + + // T.2.3 + // Test that token renewal can be triggered at a specified percentage of the token's lifetime. + @Test + public void customRenewalTimingTest() { + AtomicInteger numberOfTokens = new AtomicInteger(0); + AtomicInteger timeDiff = new AtomicInteger(0); + + IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_VALUE, + System.currentTimeMillis() + 1000, System.currentTimeMillis(), + Collections.singletonMap("oid", TOKEN_OID)); + + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + TokenListener listener = new TokenListener() { + Token lastToken = null; + + @Override + public void onTokenRenewed(Token token) { + numberOfTokens.incrementAndGet(); + if (lastToken != null) { + timeDiff.set((int) (token.getExpiresAt() - lastToken.getExpiresAt())); + } + lastToken = token; + } + + @Override + public void onError(Exception e) { + } + }; + + tokenManager.start(listener, false); + + Integer lower = (int) (tokenManagerConfig.getExpirationRefreshRatio() * 1000 - 10); + Integer upper = (int) (tokenManagerConfig.getExpirationRefreshRatio() * 1000 + 10); + Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(Durations.TWO_SECONDS) + .until(() -> numberOfTokens.get(), is(2)); + assertThat((Integer) timeDiff.get(), + both(greaterThanOrEqualTo(lower)).and(lessThanOrEqualTo(upper))); + } + + // T.2.3 + // Verify behavior with edge case renewal timing configurations (e.g., very low or high percentages). + @Test + public void edgeCaseRenewalTimingTest() { + } + + // T.2.4 + // Confirm that the system correctly identifies expired tokens. (isExpired works) + @Test + public void expiredTokenCheckTest() { + String token = JWT.create().withExpiresAt(new Date(System.currentTimeMillis() - 1000)) + .withClaim("oid", "user1").sign(Algorithm.none()); + assertTrue(new JWToken(token).isExpired()); + + token = JWT.create().withExpiresAt(new Date(System.currentTimeMillis() + 1000)) + .withClaim("oid", "user1").sign(Algorithm.none()); + assertFalse(new JWToken(token).isExpired()); + } + + // T.2.5 + // Verify that tokens are correctly parsed (e.g. with string value, expiresAt, and receivedAt attributes) + @Test + public void tokenParserTest() { + long aSecondBefore = (System.currentTimeMillis() / 1000) * 1000 - 1000; + + String token = JWT.create().withExpiresAt(new Date(aSecondBefore)).withClaim("oid", "user1") + .sign(Algorithm.none()); + Token actual = new JWToken(token); + + assertEquals(token, actual.getValue()); + assertEquals(aSecondBefore, actual.getExpiresAt()); + assertThat((Long) (System.currentTimeMillis() - actual.getReceivedAt()), + lessThanOrEqualTo((Long) 10L)); + } + + // T.3.1 + // Verify that the most recent valid token is correctly cached and that the cache is initially empty + @Test + public void tokenCachingTest() { + AtomicInteger numberOfRetries = new AtomicInteger(0); + IdentityProvider identityProvider = () -> { + if (numberOfRetries.getAndIncrement() < 1) { + delay(TOKEN_REQUEST_EXEC_TIMEOUT); + } + return simpleToken; + }; + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + assertNull(tokenManager.getCurrentToken()); + tokenManager.start(mock(TokenListener.class), true); + assertNotNull(tokenManager.getCurrentToken()); + } + + private void delay(long durationInMs) { + try { + Thread.sleep(durationInMs); + } catch (InterruptedException e) { + } + } } diff --git a/entraid/src/test/java/redis/clients/authentication/TestContext.java b/entraid/src/test/java/redis/clients/authentication/TestContext.java index b8ce3da..833e861 100644 --- a/entraid/src/test/java/redis/clients/authentication/TestContext.java +++ b/entraid/src/test/java/redis/clients/authentication/TestContext.java @@ -5,22 +5,12 @@ import java.util.HashSet; import java.util.Set; import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.PKCS8EncodedKeySpec; -import java.util.Properties; - -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; - -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.Protocol; public class TestContext { @@ -30,7 +20,9 @@ public class TestContext { private static final String AZURE_PRIVATE_KEY = "AZURE_PRIVATE_KEY"; private static final String AZURE_CERT = "AZURE_CERT"; private static final String AZURE_REDIS_SCOPES = "AZURE_REDIS_SCOPES"; - private static final String localContext = "./src/test/resources/local.context"; + + private static HashMap endpointConfigs; + private String clientId; private String authority; private String clientSecret; @@ -41,32 +33,23 @@ public class TestContext { public static final TestContext DEFAULT = new TestContext(); private TestContext() { - if (Files.exists(Paths.get(localContext))) { - try { - Properties properties = new Properties(); - properties.load(Files.newBufferedReader(Paths.get(localContext))); - this.clientId = properties.getProperty(AZURE_CLIENT_ID); - this.authority = properties.getProperty(AZURE_AUTHORITY); - this.clientSecret = properties.getProperty(AZURE_CLIENT_SECRET); - this.privateKey = getPrivateKey(properties.getProperty(AZURE_PRIVATE_KEY)); - this.cert = getCert(properties.getProperty(AZURE_CERT)); - String redisScopesProp = properties.getProperty(AZURE_REDIS_SCOPES); - if (redisScopesProp != null && !redisScopesProp.isEmpty()) { - this.redisScopes = new HashSet<>(Arrays.asList(redisScopesProp.split(";"))); - } - } catch (IOException e) { - throw new RuntimeException("Failed to load local.context", e); - } - } else { - this.clientId = System.getenv(AZURE_CLIENT_ID); - this.authority = System.getenv(AZURE_AUTHORITY); - this.clientSecret = System.getenv(AZURE_CLIENT_SECRET); - this.privateKey = getPrivateKey(System.getenv(AZURE_PRIVATE_KEY)); - this.cert = getCert(System.getenv(AZURE_CERT)); - String redisScopesEnv = System.getenv(AZURE_REDIS_SCOPES); - if (redisScopesEnv != null && !redisScopesEnv.isEmpty()) { - this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";"))); - } + + this.clientId = System.getenv(AZURE_CLIENT_ID); + this.authority = System.getenv(AZURE_AUTHORITY); + this.clientSecret = System.getenv(AZURE_CLIENT_SECRET); + this.privateKey = getPrivateKey(System.getenv(AZURE_PRIVATE_KEY)); + this.cert = getCert(System.getenv(AZURE_CERT)); + String redisScopesEnv = System.getenv(AZURE_REDIS_SCOPES); + if (redisScopesEnv != null && !redisScopesEnv.isEmpty()) { + this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";"))); + } + + String endpointsPath = System.getenv().getOrDefault("REDIS_ENDPOINTS_CONFIG_PATH", + "src/test/resources/endpoints.json"); + try { + endpointConfigs = EndpointConfig.loadFromJSON(endpointsPath); + } catch (Exception e) { + throw new RuntimeException(e); } } @@ -102,44 +85,7 @@ public Set getRedisScopes() { return redisScopes; } - private static HashMap endpointConfigs; - - private static List sentinelHostAndPortList = new ArrayList<>(); - private static List clusterHostAndPortList = new ArrayList<>(); - private static List stableClusterHostAndPortList = new ArrayList<>(); - - static { - String endpointsPath = System.getenv().getOrDefault("REDIS_ENDPOINTS_CONFIG_PATH", - "src/test/resources/endpoints.json"); - try { - endpointConfigs = EndpointConfig.loadFromJSON(endpointsPath); - } catch (Exception e) { - throw new RuntimeException(e); - } - - sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT)); - sentinelHostAndPortList - .add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 1)); - sentinelHostAndPortList - .add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 2)); - sentinelHostAndPortList - .add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 3)); - sentinelHostAndPortList - .add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 4)); - - clusterHostAndPortList.add(new HostAndPort("localhost", 7379)); - clusterHostAndPortList.add(new HostAndPort("localhost", 7380)); - clusterHostAndPortList.add(new HostAndPort("localhost", 7381)); - clusterHostAndPortList.add(new HostAndPort("localhost", 7382)); - clusterHostAndPortList.add(new HostAndPort("localhost", 7383)); - clusterHostAndPortList.add(new HostAndPort("localhost", 7384)); - - stableClusterHostAndPortList.add(new HostAndPort("localhost", 7479)); - stableClusterHostAndPortList.add(new HostAndPort("localhost", 7480)); - stableClusterHostAndPortList.add(new HostAndPort("localhost", 7481)); - } - - public static EndpointConfig getRedisEndpoint(String endpointName) { + public EndpointConfig getRedisEndpoint(String endpointName) { if (!endpointConfigs.containsKey(endpointName)) { throw new IllegalArgumentException("Unknown Redis endpoint: " + endpointName); } @@ -147,19 +93,7 @@ public static EndpointConfig getRedisEndpoint(String endpointName) { return endpointConfigs.get(endpointName); } - public static List getSentinelServers() { - return sentinelHostAndPortList; - } - - public static List getClusterServers() { - return clusterHostAndPortList; - } - - public static List getStableClusterServers() { - return stableClusterHostAndPortList; - } - - private static PrivateKey getPrivateKey(String privateKey) { + private PrivateKey getPrivateKey(String privateKey) { try { // Decode the base64 encoded key into a byte array byte[] decodedKey = Base64.getDecoder().decode(privateKey); @@ -175,7 +109,7 @@ private static PrivateKey getPrivateKey(String privateKey) { } } - private static X509Certificate getCert(String cert) { + private X509Certificate getCert(String cert) { try { // Convert the Base64 encoded string into a byte array byte[] encoded = java.util.Base64.getDecoder().decode(cert); From 17d6530dd19819f8fd122d345c53588c560b4890 Mon Sep 17 00:00:00 2001 From: atakavci Date: Fri, 29 Nov 2024 16:06:10 +0300 Subject: [PATCH 12/22] - experimental release with branch - remove snapshot --- .github/workflows/snapshot.yml | 43 ----------------------- .github/workflows/version-and-release.yml | 1 + 2 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 .github/workflows/snapshot.yml diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml deleted file mode 100644 index 17eb87d..0000000 --- a/.github/workflows/snapshot.yml +++ /dev/null @@ -1,43 +0,0 @@ ---- - -name: Publish Snapshot - -on: - push: - branches: - - master - - '[0-9].x' - workflow_dispatch: - -jobs: - - snapshot: - name: Deploy Snapshot - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up publishing to maven central - uses: actions/setup-java@v2 - with: - java-version: '8' - distribution: 'temurin' - server-id: ossrh - server-username: MAVEN_USERNAME - server-password: MAVEN_PASSWORD - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: | - ~/.m2/repository - /var/cache/apt - key: endtraid-${{hashFiles('**/pom.xml')}} - - name: mvn offline - run: | - mvn -q dependency:go-offline - - name: deploy - run: | - mvn --no-transfer-progress \ - -DskipTests deploy -N - env: - MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} - MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} diff --git a/.github/workflows/version-and-release.yml b/.github/workflows/version-and-release.yml index d007604..b3cca74 100644 --- a/.github/workflows/version-and-release.yml +++ b/.github/workflows/version-and-release.yml @@ -3,6 +3,7 @@ name: Release on: release: types: [published] + workflow_dispatch: jobs: build: From 0b1095f5d46f9160f9c8d70579aa8ca1044ce006 Mon Sep 17 00:00:00 2001 From: atakavci Date: Fri, 29 Nov 2024 18:02:46 +0300 Subject: [PATCH 13/22] - fix failed release --- .github/workflows/version-and-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/version-and-release.yml b/.github/workflows/version-and-release.yml index b3cca74..202fe8a 100644 --- a/.github/workflows/version-and-release.yml +++ b/.github/workflows/version-and-release.yml @@ -47,6 +47,7 @@ jobs: MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} working-directory: ./core + continue-on-error: true # This step will not stop the job even if it fails - name: mvn versions - EntraID run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }} @@ -65,3 +66,4 @@ jobs: env: MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} + working-directory: ./entraid From ee60a45c25ac4c11cf28930121e76011be7ee5eb Mon Sep 17 00:00:00 2001 From: atakavci Date: Sun, 1 Dec 2024 19:11:34 +0300 Subject: [PATCH 14/22] - support full customization of different MSAL application types and advanced configurations with EntraIDTokenAuthConfigBuilder - add more unit tests --- .../entraid/EntraIDIdentityProvider.java | 4 + .../EntraIDIdentityProviderConfig.java | 34 ++-- .../EntraIDTokenAuthConfigBuilder.java | 30 ++- .../authentication/RedisEntraIDUnitTests.java | 184 +++++++++++++++++- 4 files changed, 230 insertions(+), 22 deletions(-) diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java index 3418fcc..0f73ec5 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java @@ -48,6 +48,10 @@ public EntraIDIdentityProvider(ManagedIdentityInfo info, Set scopes) { resultSupplier = () -> supplierForManagedIdentityApp(app, params); } + public EntraIDIdentityProvider(Supplier customEntraIdAppSupplier) { + this.resultSupplier = customEntraIdAppSupplier; + } + private IClientCredential getClientCredential(ServicePrincipalInfo servicePrincipalInfo) { switch (servicePrincipalInfo.getAccessWith()) { case WithSecret: diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java index cdf30ab..7923be6 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java @@ -1,31 +1,33 @@ package redis.clients.authentication.entraid; import java.util.Set; +import java.util.function.Supplier; + +import com.microsoft.aad.msal4j.IAuthenticationResult; import redis.clients.authentication.core.IdentityProvider; import redis.clients.authentication.core.IdentityProviderConfig; public final class EntraIDIdentityProviderConfig implements IdentityProviderConfig, AutoCloseable { - private ServicePrincipalInfo servicePrincipalInfo; - private Set scopes; - private ManagedIdentityInfo managedIdentityInfo; + private Supplier providerSupplier; + + public EntraIDIdentityProviderConfig(ServicePrincipalInfo info, Set scopes) { + providerSupplier = () -> new EntraIDIdentityProvider(info, scopes); + } + + public EntraIDIdentityProviderConfig(ManagedIdentityInfo info, Set scopes) { + providerSupplier = () -> new EntraIDIdentityProvider(info, scopes); + } - public EntraIDIdentityProviderConfig(ServicePrincipalInfo servicePrincipalInfo, - ManagedIdentityInfo info, Set scopes) { - this.servicePrincipalInfo = servicePrincipalInfo; - this.scopes = scopes; - this.managedIdentityInfo = info; + public EntraIDIdentityProviderConfig( + Supplier customEntraIdAppSupplier) { + providerSupplier = () -> new EntraIDIdentityProvider(customEntraIdAppSupplier); } @Override public IdentityProvider getProvider() { - IdentityProvider identityProvider = null; - if (managedIdentityInfo != null) { - identityProvider = new EntraIDIdentityProvider(managedIdentityInfo, scopes); - } else { - identityProvider = new EntraIDIdentityProvider(servicePrincipalInfo, scopes); - } + IdentityProvider identityProvider = providerSupplier.get(); clear(); return identityProvider; } @@ -36,8 +38,6 @@ public void close() throws Exception { } private void clear() { - servicePrincipalInfo = null; - managedIdentityInfo = null; - scopes = null; + providerSupplier = null; } } diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java index 67e1cd5..237f30f 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java @@ -3,6 +3,9 @@ import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.Set; +import java.util.function.Supplier; + +import com.microsoft.aad.msal4j.IAuthenticationResult; import redis.clients.authentication.core.TokenAuthConfig; import redis.clients.authentication.entraid.ManagedIdentityInfo.UserManagedIdentityType; @@ -24,6 +27,7 @@ public class EntraIDTokenAuthConfigBuilder extends TokenAuthConfig.Builder private Set scopes; private ServicePrincipalAccess accessWith; private ManagedIdentityInfo mii; + Supplier customEntraIdAuthenticationSupplier; public EntraIDTokenAuthConfigBuilder() { this.expirationRefreshRatio(DEFAULT_EXPIRATION_REFRESH_RATIO) @@ -66,6 +70,12 @@ public EntraIDTokenAuthConfigBuilder userAssignedManagedIdentity( return this; } + public EntraIDTokenAuthConfigBuilder customEntraIdAuthenticationSupplier( + Supplier customEntraIdAuthenticationSupplier) { + + return this; + } + public EntraIDTokenAuthConfigBuilder scopes(Set scopes) { this.scopes = scopes; return this; @@ -85,11 +95,22 @@ public TokenAuthConfig build() { } if (spi != null && mii != null) { throw new RedisEntraIDException( - "Cannot have both ServicePrincipal and ManagedIdentity"); + "Cannot have both ServicePrincipal and ManagedIdentity!"); + } + if (this.customEntraIdAuthenticationSupplier != null && (spi != null || mii != null)) { + throw new RedisEntraIDException( + "Cannot have both customEntraIdAuthenticationSupplier and ServicePrincipal/ManagedIdentity!"); + } + if (spi != null) { + super.identityProviderConfig(new EntraIDIdentityProviderConfig(spi, scopes)); + } + if (mii != null) { + super.identityProviderConfig(new EntraIDIdentityProviderConfig(mii, scopes)); + } + if (customEntraIdAuthenticationSupplier != null) { + super.identityProviderConfig( + new EntraIDIdentityProviderConfig(customEntraIdAuthenticationSupplier)); } - EntraIDIdentityProviderConfig idProviderConfig = new EntraIDIdentityProviderConfig(spi, mii, - scopes); - super.identityProviderConfig(idProviderConfig); return super.build(); } @@ -101,6 +122,7 @@ public void close() throws Exception { cert = null; authority = null; scopes = null; + customEntraIdAuthenticationSupplier = null; } public static EntraIDTokenAuthConfigBuilder builder() { diff --git a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java index 8e8b533..3aab001 100644 --- a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java +++ b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java @@ -16,8 +16,11 @@ import static org.hamcrest.Matchers.both; import static org.hamcrest.MatcherAssert.assertThat; +import java.time.Duration; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeoutException; @@ -31,6 +34,7 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; + import redis.clients.authentication.core.IdentityProvider; import redis.clients.authentication.core.IdentityProviderConfig; import redis.clients.authentication.core.SimpleToken; @@ -280,6 +284,16 @@ public void onError(Exception e) { .until(() -> numberOfTokens.get(), is(2)); } + // T.2.2 + + // Test that the Redis client is not blocked/interrupted during token renewal. + @Test + public void renewalDuringOperationsTest() { + // set the stage with consecutive get/set operations with unique keys which takes at least for 2000 ms with a jedispooled instace, + // configure token manager to renew token every 500ms + // wait till all operations are completed and verify that token was renewed at least 3 times after initial token acquisition + } + // T.2.2 // Ensure the system propagates error during renewal back to the user @Test @@ -306,6 +320,7 @@ public void onError(Exception e) { Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(TWO_SECONDS) .until(() -> numberOfErrors.get(), is(1)); + } // T.2.3 @@ -330,6 +345,7 @@ public void onTokenRenewed(Token token) { timeDiff.set((int) (token.getExpiresAt() - lastToken.getExpiresAt())); } lastToken = token; + } @Override @@ -350,7 +366,89 @@ public void onError(Exception e) { // T.2.3 // Verify behavior with edge case renewal timing configurations (e.g., very low or high percentages). @Test - public void edgeCaseRenewalTimingTest() { + public void highPercentage_edgeCaseRenewalTimingTest() { + List tokens = new ArrayList(); + int validDurationInMs = 1000; + + IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_VALUE, + System.currentTimeMillis() + validDurationInMs, System.currentTimeMillis(), + Collections.singletonMap("oid", TOKEN_OID)); + + TokenManagerConfig tokenManagerConfig = new TokenManagerConfig(0.99F, 0, + TOKEN_REQUEST_EXEC_TIMEOUT, + new TokenManagerConfig.RetryPolicy(RETRY_POLICY_MAX_ATTEMPTS, RETRY_POLICY_DELAY)); + + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + TokenListener listener = new TokenListener() { + + @Override + public void onTokenRenewed(Token token) { + tokens.add(token); + } + + @Override + public void onError(Exception e) { + } + }; + + tokenManager.start(listener, false); + + Awaitility.await().pollInterval(Duration.ofMillis(10)).atMost(Durations.TWO_SECONDS) + .until(() -> tokens.size(), is(2)); + + Token initialToken = tokens.get(0); + Token secondToken = tokens.get(1); + Long renewalWindowStart = initialToken.getReceivedAt() + + (long) (validDurationInMs * tokenManagerConfig.getExpirationRefreshRatio()); + Long renewalWindowEnd = initialToken.getExpiresAt(); + assertThat((Long) secondToken.getReceivedAt(), + both(greaterThanOrEqualTo(renewalWindowStart)) + .and(lessThanOrEqualTo(renewalWindowEnd))); + + } + + // T.2.3 + // Verify behavior with edge case renewal timing configurations (e.g., very low or high percentages). + @Test + public void lowPercentage_edgeCaseRenewalTimingTest() { + List tokens = new ArrayList(); + int validDurationInMs = 1000; + + IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_VALUE, + System.currentTimeMillis() + validDurationInMs, System.currentTimeMillis(), + Collections.singletonMap("oid", TOKEN_OID)); + + TokenManagerConfig tokenManagerConfig = new TokenManagerConfig(0.01F, 0, + TOKEN_REQUEST_EXEC_TIMEOUT, + new TokenManagerConfig.RetryPolicy(RETRY_POLICY_MAX_ATTEMPTS, RETRY_POLICY_DELAY)); + + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + TokenListener listener = new TokenListener() { + + @Override + public void onTokenRenewed(Token token) { + tokens.add(token); + } + + @Override + public void onError(Exception e) { + } + }; + + tokenManager.start(listener, false); + + Awaitility.await().pollInterval(ONE_MILLISECOND).atMost(Durations.TWO_SECONDS) + .until(() -> tokens.size(), is(2)); + + Token initialToken = tokens.get(0); + Token secondToken = tokens.get(1); + Long renewalWindowStart = initialToken.getReceivedAt() + + (long) (validDurationInMs * tokenManagerConfig.getExpirationRefreshRatio()); + Long renewalWindowEnd = initialToken.getExpiresAt(); + assertThat((Long) secondToken.getReceivedAt(), + both(greaterThanOrEqualTo(renewalWindowStart)) + .and(lessThanOrEqualTo(renewalWindowEnd))); + } // T.2.4 @@ -363,6 +461,7 @@ public void expiredTokenCheckTest() { token = JWT.create().withExpiresAt(new Date(System.currentTimeMillis() + 1000)) .withClaim("oid", "user1").sign(Algorithm.none()); + assertFalse(new JWToken(token).isExpired()); } @@ -382,6 +481,13 @@ public void tokenParserTest() { lessThanOrEqualTo((Long) 10L)); } + // T.2.5 + // Ensure that token objects are immutable and cannot be modified after creation. + @Test + public void tokenImmutabilityTest() { + // ??? + } + // T.3.1 // Verify that the most recent valid token is correctly cached and that the cache is initially empty @Test @@ -399,6 +505,82 @@ public void tokenCachingTest() { assertNotNull(tokenManager.getCurrentToken()); } + // T.3.1 + // Ensure the token cache is updated when a new token is acquired or renewed. + @Test + public void cacheUpdateOnRenewalTest() { + + AtomicInteger numberOfTokens = new AtomicInteger(0); + IdentityProvider identityProvider = () -> { + return new SimpleToken("" + numberOfTokens.incrementAndGet(), + System.currentTimeMillis() + 500, System.currentTimeMillis(), + Collections.singletonMap("oid", "user1")); + }; + TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + assertNull(tokenManager.getCurrentToken()); + tokenManager.start(mock(TokenListener.class), true); + assertNotNull(tokenManager.getCurrentToken()); + assertEquals("1", tokenManager.getCurrentToken().getValue()); + Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(TWO_SECONDS) + .until(() -> tokenManager.getCurrentToken().getValue(), is("2")); + + } + + // T.3.2 + // Verify that all existing connections can be re-authenticated when a new token is received. + @Test + public void allConnectionsReauthTest() { + + } + + // T.3.2 + // Test system behavior when some connections fail to re-authenticate during bulk authentication. e.g when a network partition occurs for 1 or more of them + @Test + public void partialReauthFailureTest() { + + } + + // T.3.3 + // Test authentication of a single connection using the current valid token. + @Test + public void singleConnectionAuthTest() { + + } + + // T.3.3 + // Verify behavior when attempting to authenticate a single connection with an expired token. + @Test + public void connectionAuthWithExpiredTokenTest() { + + } + + // T.3.4 + // Verify handling of reconnection and re-authentication after a network partition. (use cached token) + @Test + public void networkPartitionEvictionTest() { + + } + + // T.4.1 + // Verify that token renewal timing can be configured correctly. + @Test + public void renewalTimingConfigTest() { + + } + + // T.4.2 + // Verify that Azure AD-specific parameters can be configured correctly. + @Test + public void azureADConfigTest() { + + } + + // T.4.2 + // Test configuration of custom identity provider parameters. + @Test + public void customProviderConfigTest() { + } + private void delay(long durationInMs) { try { Thread.sleep(durationInMs); From 10852a8b0f05714d407e85d69b05848dbb933b91 Mon Sep 17 00:00:00 2001 From: atakavci Date: Sun, 1 Dec 2024 19:17:56 +0300 Subject: [PATCH 15/22] - fix missing assignment --- .../authentication/entraid/EntraIDTokenAuthConfigBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java index 237f30f..8608970 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java @@ -27,7 +27,7 @@ public class EntraIDTokenAuthConfigBuilder extends TokenAuthConfig.Builder private Set scopes; private ServicePrincipalAccess accessWith; private ManagedIdentityInfo mii; - Supplier customEntraIdAuthenticationSupplier; + private Supplier customEntraIdAuthenticationSupplier; public EntraIDTokenAuthConfigBuilder() { this.expirationRefreshRatio(DEFAULT_EXPIRATION_REFRESH_RATIO) @@ -72,7 +72,7 @@ public EntraIDTokenAuthConfigBuilder userAssignedManagedIdentity( public EntraIDTokenAuthConfigBuilder customEntraIdAuthenticationSupplier( Supplier customEntraIdAuthenticationSupplier) { - + this.customEntraIdAuthenticationSupplier = customEntraIdAuthenticationSupplier; return this; } From b30f88f58404fbbc8f0ac05b78c3868e8a726d80 Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 5 Dec 2024 08:23:28 +0300 Subject: [PATCH 16/22] - cleanup - fix cert issue - drop jedis integration tests (move to jedis) - add unit tests - change textcontext to load demand --- .../authentication/core/TokenManager.java | 4 +- .../CoreAuthenticationIntegrationTests.java | 10 - .../entraid/EntraIDIdentityProvider.java | 6 +- .../EntraIDIdentityProviderConfig.java | 4 +- .../EntraIDTokenAuthConfigBuilder.java | 1 + .../EntraIDIntegrationTests.java | 1 - ...IDUnitTests.java => EntraIDUnitTests.java} | 227 ++++++++++-------- .../RedisEntraIDIntegrationTests.java | 120 --------- .../clients/authentication/TestContext.java | 35 +-- 9 files changed, 151 insertions(+), 257 deletions(-) delete mode 100644 core/src/test/java/redis/clients/authentication/CoreAuthenticationIntegrationTests.java rename entraid/src/test/java/redis/clients/authentication/{RedisEntraIDUnitTests.java => EntraIDUnitTests.java} (73%) delete mode 100644 entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManager.java b/core/src/main/java/redis/clients/authentication/core/TokenManager.java index b1198e2..b25ead4 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenManager.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenManager.java @@ -18,7 +18,9 @@ public class TokenManager { private IdentityProvider identityProvider; private TokenListener listener; private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - private ExecutorService executor = Executors.newSingleThreadExecutor(); + + // TODO: manage thread pool to avoid blocking on IDP hangs + private ExecutorService executor = Executors.newFixedThreadPool(2); private boolean stopped = false; private ScheduledFuture scheduledTask; private int numberOfRetries = 0; diff --git a/core/src/test/java/redis/clients/authentication/CoreAuthenticationIntegrationTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationIntegrationTests.java deleted file mode 100644 index 8858852..0000000 --- a/core/src/test/java/redis/clients/authentication/CoreAuthenticationIntegrationTests.java +++ /dev/null @@ -1,10 +0,0 @@ -package redis.clients.authentication; - -import org.junit.Test; - -public class CoreAuthenticationIntegrationTests { - @Test - public void testTokenManager() { - // Test code - } -} diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java index 0f73ec5..1089b8e 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java @@ -13,7 +13,6 @@ import com.microsoft.aad.msal4j.IClientCredential; import com.microsoft.aad.msal4j.ManagedIdentityApplication; import com.microsoft.aad.msal4j.ManagedIdentityParameters; - import redis.clients.authentication.core.IdentityProvider; import redis.clients.authentication.core.Token; @@ -48,8 +47,9 @@ public EntraIDIdentityProvider(ManagedIdentityInfo info, Set scopes) { resultSupplier = () -> supplierForManagedIdentityApp(app, params); } - public EntraIDIdentityProvider(Supplier customEntraIdAppSupplier) { - this.resultSupplier = customEntraIdAppSupplier; + public EntraIDIdentityProvider( + Supplier customEntraIdAuthenticationSupplier) { + this.resultSupplier = customEntraIdAuthenticationSupplier; } private IClientCredential getClientCredential(ServicePrincipalInfo servicePrincipalInfo) { diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java index 7923be6..669ef1f 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java @@ -21,8 +21,8 @@ public EntraIDIdentityProviderConfig(ManagedIdentityInfo info, Set scope } public EntraIDIdentityProviderConfig( - Supplier customEntraIdAppSupplier) { - providerSupplier = () -> new EntraIDIdentityProvider(customEntraIdAppSupplier); + Supplier customEntraIdAuthenticationSupplier) { + providerSupplier = () -> new EntraIDIdentityProvider(customEntraIdAuthenticationSupplier); } @Override diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java index 8608970..cd538e4 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java @@ -50,6 +50,7 @@ public EntraIDTokenAuthConfigBuilder secret(String secret) { public EntraIDTokenAuthConfigBuilder key(PrivateKey key, X509Certificate cert) { this.key = key; + this.cert = cert; this.accessWith = ServicePrincipalAccess.WithCert; return this; } diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java index 52c6901..82ffe37 100644 --- a/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java @@ -10,7 +10,6 @@ public class EntraIDIntegrationTests { - @Test public void requestTokenWithSecret() throws MalformedURLException { TestContext testCtx = TestContext.DEFAULT; diff --git a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java similarity index 73% rename from entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java rename to entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java index 3aab001..5920fc5 100644 --- a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDUnitTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java @@ -6,16 +6,18 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockConstruction; import static org.awaitility.Durations.*; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.both; import static org.hamcrest.MatcherAssert.assertThat; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; @@ -23,9 +25,11 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import org.awaitility.Awaitility; import org.awaitility.Durations; @@ -34,6 +38,12 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; +import com.microsoft.aad.msal4j.ClientCredentialFactory; +import com.microsoft.aad.msal4j.ClientCredentialParameters; +import com.microsoft.aad.msal4j.ConfidentialClientApplication; +import com.microsoft.aad.msal4j.IAuthenticationResult; +import com.microsoft.aad.msal4j.IClientSecret; +import com.microsoft.aad.msal4j.ManagedIdentityId; import redis.clients.authentication.core.IdentityProvider; import redis.clients.authentication.core.IdentityProviderConfig; @@ -47,12 +57,15 @@ import redis.clients.authentication.entraid.EntraIDIdentityProvider; import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder; import redis.clients.authentication.entraid.JWToken; +import redis.clients.authentication.entraid.ManagedIdentityInfo; import redis.clients.authentication.entraid.ServicePrincipalInfo; -import redis.clients.jedis.DefaultJedisClientConfig; -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.JedisPooled; +import redis.clients.authentication.entraid.ManagedIdentityInfo.UserManagedIdentityType; -public class RedisEntraIDUnitTests { +// import redis.clients.jedis.DefaultJedisClientConfig; +// import redis.clients.jedis.HostAndPort; +// import redis.clients.jedis.JedisPooled; + +public class EntraIDUnitTests { private static final float EXPIRATION_REFRESH_RATIO = 0.7F; private static final int LOWER_REFRESH_BOUND_MILLIS = 200; @@ -80,12 +93,10 @@ public void testConfigBuilder() { String clientId = "clientId1"; String credential = "credential1"; Set scopes = Collections.singleton("scope1"); - IdentityProviderConfig config = EntraIDTokenAuthConfigBuilder.builder().authority(authority) - .clientId(clientId).secret(credential).scopes(scopes).build() + IdentityProviderConfig configWithSecret = EntraIDTokenAuthConfigBuilder.builder() + .authority(authority).clientId(clientId).secret(credential).scopes(scopes).build() .getIdentityProviderConfig(); - - assertNotNull(config); - + assertNotNull(configWithSecret); try (MockedConstruction mockedConstructor = mockConstruction( EntraIDIdentityProvider.class, (mock, context) -> { ServicePrincipalInfo info = (ServicePrincipalInfo) context.arguments().get(0); @@ -95,47 +106,38 @@ public void testConfigBuilder() { assertEquals(scopes, context.arguments().get(1)); })) { - config.getProvider(); + configWithSecret.getProvider(); } + IdentityProviderConfig configWithCert = EntraIDTokenAuthConfigBuilder.builder() + .authority(authority).clientId(clientId) + .key(testCtx.getPrivateKey(), testCtx.getCert()).scopes(scopes).build() + .getIdentityProviderConfig(); + assertNotNull(configWithCert); try (MockedConstruction mockedConstructor = mockConstruction( EntraIDIdentityProvider.class, (mock, context) -> { - assertNull(context.arguments().get(0)); - assertNull(context.arguments().get(1)); + ServicePrincipalInfo info = (ServicePrincipalInfo) context.arguments().get(0); + assertEquals(clientId, info.getClientId()); + assertEquals(authority, info.getAuthority()); + assertEquals(testCtx.getPrivateKey(), info.getKey()); + assertEquals(testCtx.getCert(), info.getCert()); + assertEquals(scopes, context.arguments().get(1)); + })) { - config.getProvider(); + configWithCert.getProvider(); } - } - @Test - public void testJedisConfig() { - - TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() - .authority(testCtx.getAuthority()).clientId(testCtx.getClientId()) - .secret(testCtx.getClientSecret()).scopes(testCtx.getRedisScopes()).build(); - - DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() - .tokenAuthConfig(tokenAuthConfig).build(); - - AtomicInteger counter = new AtomicInteger(0); + IdentityProviderConfig configWithManagedId = EntraIDTokenAuthConfigBuilder.builder() + .systemAssignedManagedIdentity().scopes(scopes).build().getIdentityProviderConfig(); + assertNotNull(configWithManagedId); try (MockedConstruction mockedConstructor = mockConstruction( EntraIDIdentityProvider.class, (mock, context) -> { - ServicePrincipalInfo info = (ServicePrincipalInfo) context.arguments().get(0); - - assertEquals(testCtx.getClientId(), info.getClientId()); - assertEquals(testCtx.getAuthority(), info.getAuthority()); - assertEquals(testCtx.getClientSecret(), info.getSecret()); - assertEquals(testCtx.getRedisScopes(), context.arguments().get(1)); - assertNotNull(mock); - doAnswer(invocation -> { - counter.incrementAndGet(); - return new SimpleToken("token1", System.currentTimeMillis() + 5 * 60 * 1000, - System.currentTimeMillis(), Collections.singletonMap("oid", "default")); - }).when(mock).requestToken(); + ManagedIdentityInfo info = (ManagedIdentityInfo) context.arguments().get(0); + assertEquals(ManagedIdentityId.systemAssigned().getIdType(), + info.getId().getIdType()); + assertEquals(scopes, context.arguments().get(1)); })) { - JedisPooled jedis = new JedisPooled(new HostAndPort("localhost", 6379), jedisConfig); - assertNotNull(jedis); - assertEquals(1, counter.get()); + configWithManagedId.getProvider(); } } @@ -242,18 +244,21 @@ public void tokenAcquisitionTimeoutTest() throws InterruptedException, TimeoutEx IdentityProvider identityProvider = () -> { if (numberOfRetries.getAndIncrement() < 1) { - delay(TOKEN_REQUEST_EXEC_TIMEOUT); + delay(TOKEN_REQUEST_EXEC_TIMEOUT * 2); } return simpleToken; }; TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); + long startTime = System.currentTimeMillis(); tokenManager.start(mock(TokenListener.class), false); Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(Durations.FIVE_SECONDS) .until(() -> tokenManager.getCurrentToken() != null); assertEquals(2, numberOfRetries.get()); + long totalTime = System.currentTimeMillis() - startTime; + assertThat(totalTime, lessThan(TOKEN_REQUEST_EXEC_TIMEOUT * 2L)); } // T.2.2 @@ -284,16 +289,6 @@ public void onError(Exception e) { .until(() -> numberOfTokens.get(), is(2)); } - // T.2.2 - - // Test that the Redis client is not blocked/interrupted during token renewal. - @Test - public void renewalDuringOperationsTest() { - // set the stage with consecutive get/set operations with unique keys which takes at least for 2000 ms with a jedispooled instace, - // configure token manager to renew token every 500ms - // wait till all operations are completed and verify that token was renewed at least 3 times after initial token acquisition - } - // T.2.2 // Ensure the system propagates error during renewal back to the user @Test @@ -320,11 +315,12 @@ public void onError(Exception e) { Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(TWO_SECONDS) .until(() -> numberOfErrors.get(), is(1)); - } // T.2.3 // Test that token renewal can be triggered at a specified percentage of the token's lifetime. + // T.4.1 + // Verify that token renewal timing can be configured correctly. @Test public void customRenewalTimingTest() { AtomicInteger numberOfTokens = new AtomicInteger(0); @@ -365,6 +361,8 @@ public void onError(Exception e) { // T.2.3 // Verify behavior with edge case renewal timing configurations (e.g., very low or high percentages). + // T.4.1 + // Verify that token renewal timing can be configured correctly. @Test public void highPercentage_edgeCaseRenewalTimingTest() { List tokens = new ArrayList(); @@ -404,11 +402,12 @@ public void onError(Exception e) { assertThat((Long) secondToken.getReceivedAt(), both(greaterThanOrEqualTo(renewalWindowStart)) .and(lessThanOrEqualTo(renewalWindowEnd))); - } - // T.2.3 + // T.2.3 // Verify behavior with edge case renewal timing configurations (e.g., very low or high percentages). + // T.4.1 + // Verify that token renewal timing can be configured correctly. @Test public void lowPercentage_edgeCaseRenewalTimingTest() { List tokens = new ArrayList(); @@ -448,7 +447,6 @@ public void onError(Exception e) { assertThat((Long) secondToken.getReceivedAt(), both(greaterThanOrEqualTo(renewalWindowStart)) .and(lessThanOrEqualTo(renewalWindowEnd))); - } // T.2.4 @@ -485,7 +483,7 @@ public void tokenParserTest() { // Ensure that token objects are immutable and cannot be modified after creation. @Test public void tokenImmutabilityTest() { - // ??? + // TODO : what is expected exatcly ? } // T.3.1 @@ -523,62 +521,101 @@ public void cacheUpdateOnRenewalTest() { assertEquals("1", tokenManager.getCurrentToken().getValue()); Awaitility.await().pollInterval(ONE_HUNDRED_MILLISECONDS).atMost(TWO_SECONDS) .until(() -> tokenManager.getCurrentToken().getValue(), is("2")); - - } - - // T.3.2 - // Verify that all existing connections can be re-authenticated when a new token is received. - @Test - public void allConnectionsReauthTest() { - - } - - // T.3.2 - // Test system behavior when some connections fail to re-authenticate during bulk authentication. e.g when a network partition occurs for 1 or more of them - @Test - public void partialReauthFailureTest() { - - } - - // T.3.3 - // Test authentication of a single connection using the current valid token. - @Test - public void singleConnectionAuthTest() { - - } - - // T.3.3 - // Verify behavior when attempting to authenticate a single connection with an expired token. - @Test - public void connectionAuthWithExpiredTokenTest() { - - } - - // T.3.4 - // Verify handling of reconnection and re-authentication after a network partition. (use cached token) - @Test - public void networkPartitionEvictionTest() { - } // T.4.1 // Verify that token renewal timing can be configured correctly. @Test public void renewalTimingConfigTest() { - + float refreshRatio = 0.71F; + int delayInMsToRetry = 201; + int lowerRefreshBoundMillis = 301; + int maxAttemptsToRetry = 6; + int tokenRequestExecTimeoutInMs = 401; + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() + .expirationRefreshRatio(refreshRatio).delayInMsToRetry(delayInMsToRetry) + .lowerRefreshBoundMillis(lowerRefreshBoundMillis) + .maxAttemptsToRetry(maxAttemptsToRetry) + .tokenRequestExecTimeoutInMs(tokenRequestExecTimeoutInMs).build(); + TokenManagerConfig config = tokenAuthConfig.getTokenManagerConfig(); + assertEquals(refreshRatio, config.getExpirationRefreshRatio(), 0.00000001F); + assertEquals(delayInMsToRetry, config.getRetryPolicy().getdelayInMs()); + assertEquals(lowerRefreshBoundMillis, config.getLowerRefreshBoundMillis()); + assertEquals(maxAttemptsToRetry, config.getRetryPolicy().getMaxAttempts()); + assertEquals(tokenRequestExecTimeoutInMs, config.getTokenRequestExecTimeoutInMs()); } // T.4.2 // Verify that Azure AD-specific parameters can be configured correctly. @Test - public void azureADConfigTest() { + public void withKeyCert_azureADConfigTest() { + PrivateKey key = mock(PrivateKey.class); + X509Certificate cert = mock(X509Certificate.class); + Set scopes = Collections.singleton("testScope"); + try (MockedConstruction mockedConstructor = mockConstruction( + EntraIDIdentityProvider.class, (mock, context) -> { + ServicePrincipalInfo info = (ServicePrincipalInfo) (context.arguments().get(0)); + assertEquals("testClientId", info.getClientId()); + assertEquals("testAuthority", info.getAuthority()); + assertEquals(key, info.getKey()); + assertEquals(cert, info.getCert()); + assertEquals(scopes, context.arguments().get(1)); + })) { + TokenAuthConfig config = EntraIDTokenAuthConfigBuilder.builder() + .clientId("testClientId").authority("testAuthority").key(key, cert) + .scopes(scopes).build(); + config.getIdentityProviderConfig().getProvider(); + } + } + // T.4.2 + // Verify that Azure AD-specific parameters can be configured correctly. + @Test + public void withUserAssignedManagedId_azureADConfigTest() { + Set scopes = Collections.singleton("testScope"); + try (MockedConstruction mockedConstructor = mockConstruction( + EntraIDIdentityProvider.class, (mock, context) -> { + ManagedIdentityInfo info = (ManagedIdentityInfo) (context.arguments().get(0)); + assertEquals("CLIENT_ID", ((Object) info.getId().getIdType()).toString()); + assertEquals("testUserManagedId", info.getId().getUserAssignedId()); + assertEquals(scopes, context.arguments().get(1)); + })) { + TokenAuthConfig config = EntraIDTokenAuthConfigBuilder.builder() + .clientId("testClientId").authority("testAuthority") + .userAssignedManagedIdentity(UserManagedIdentityType.CLIENT_ID, + "testUserManagedId") + .scopes(scopes).build(); + config.getIdentityProviderConfig().getProvider(); + } } // T.4.2 // Test configuration of custom identity provider parameters. @Test public void customProviderConfigTest() { + IClientSecret secret = ClientCredentialFactory.createFromSecret(testCtx.getClientSecret()); + // Choose and configure any type of app with any parameters as needed + ConfidentialClientApplication app = ConfidentialClientApplication + .builder(testCtx.getClientId(), secret).build(); + // Customize credential parameters as needed + ClientCredentialParameters parameters = ClientCredentialParameters + .builder(Collections.singleton("testScope")).build(); + Supplier supplier = () -> { + try { + return app.acquireToken(parameters).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + }; + + try (MockedConstruction mockedConstructor = mockConstruction( + EntraIDIdentityProvider.class, (mock, context) -> { + assertEquals(supplier, context.arguments().get(0)); + })) { + TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() + .customEntraIdAuthenticationSupplier(supplier).build(); + tokenAuthConfig.getIdentityProviderConfig().getProvider(); + } } private void delay(long durationInMs) { diff --git a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java deleted file mode 100644 index bcc4a9c..0000000 --- a/entraid/src/test/java/redis/clients/authentication/RedisEntraIDIntegrationTests.java +++ /dev/null @@ -1,120 +0,0 @@ -package redis.clients.authentication; - -import static org.junit.Assert.assertEquals; -import java.util.UUID; - -import org.junit.BeforeClass; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import redis.clients.authentication.core.TokenAuthConfig; -import redis.clients.authentication.entraid.EntraIDTokenAuthConfigBuilder; -import redis.clients.authentication.entraid.ManagedIdentityInfo.UserManagedIdentityType; -import redis.clients.jedis.DefaultJedisClientConfig; -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.JedisPooled; - -public class RedisEntraIDIntegrationTests { - private static final Logger log = LoggerFactory - .getLogger(RedisEntraIDIntegrationTests.class); - - private static TestContext testCtx; - private static EndpointConfig endpointConfig; - private static HostAndPort hnp; - - @BeforeClass - public static void before() { - try { - testCtx = TestContext.DEFAULT; - endpointConfig = testCtx.getRedisEndpoint("standalone-entraid-acl1"); - hnp = endpointConfig.getHostAndPort(); - } catch (IllegalArgumentException e) { - log.warn("Skipping test because no Redis endpoint is configured"); - org.junit.Assume.assumeTrue(false); - } - } - - // T.1.1 - // Verify authentication using Azure AD with managed identities - @Test - public void withUserAssignedId_azureManagedIdentityIntegrationTest() { - TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() - .clientId(testCtx.getClientId()) - .userAssignedManagedIdentity(UserManagedIdentityType.CLIENT_ID, - "userManagedAuthxId") - .authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) - .build(); - - DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() - .tokenAuthConfig(tokenAuthConfig).build(); - - try (JedisPooled jedis = new JedisPooled(hnp, jedisConfig)) { - String key = UUID.randomUUID().toString(); - jedis.set(key, "value"); - assertEquals("value", jedis.get(key)); - jedis.del(key); - } - } - - // T.1.1 - // Verify authentication using Azure AD with managed identities - @Test - public void withSystemAssignedId_azureManagedIdentityIntegrationTest() { - TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() - .clientId(testCtx.getClientId()).systemAssignedManagedIdentity() - .authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) - .build(); - - DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() - .tokenAuthConfig(tokenAuthConfig).build(); - - try (JedisPooled jedis = new JedisPooled(hnp, jedisConfig)) { - String key = UUID.randomUUID().toString(); - jedis.set(key, "value"); - assertEquals("value", jedis.get(key)); - jedis.del(key); - } - } - - // T.1.1 - // Verify authentication using Azure AD with service principals - @Test - public void withSecret_azureServicePrincipalIntegrationTest() { - TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() - .clientId(testCtx.getClientId()).secret(testCtx.getClientSecret()) - .authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) - .build(); - - DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() - .tokenAuthConfig(tokenAuthConfig).build(); - - try (JedisPooled jedis = new JedisPooled(hnp, jedisConfig)) { - String key = UUID.randomUUID().toString(); - jedis.set(key, "value"); - assertEquals("value", jedis.get(key)); - jedis.del(key); - } - } - - // T.1.1 - // Verify authentication using Azure AD with service principals - @Test - public void withCertificate_azureServicePrincipalIntegrationTest() { - TokenAuthConfig tokenAuthConfig = EntraIDTokenAuthConfigBuilder.builder() - .clientId(testCtx.getClientId()).secret(testCtx.getClientSecret()) - .authority(testCtx.getAuthority()).scopes(testCtx.getRedisScopes()) - .build(); - - DefaultJedisClientConfig jedisConfig = DefaultJedisClientConfig.builder() - .tokenAuthConfig(tokenAuthConfig).build(); - - try (JedisPooled jedis = new JedisPooled(hnp, jedisConfig)) { - String key = UUID.randomUUID().toString(); - jedis.set(key, "value"); - assertEquals("value", jedis.get(key)); - jedis.del(key); - } - } - -} diff --git a/entraid/src/test/java/redis/clients/authentication/TestContext.java b/entraid/src/test/java/redis/clients/authentication/TestContext.java index 833e861..3ab6e2d 100644 --- a/entraid/src/test/java/redis/clients/authentication/TestContext.java +++ b/entraid/src/test/java/redis/clients/authentication/TestContext.java @@ -10,7 +10,6 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.PKCS8EncodedKeySpec; -import java.util.HashMap; public class TestContext { @@ -21,8 +20,6 @@ public class TestContext { private static final String AZURE_CERT = "AZURE_CERT"; private static final String AZURE_REDIS_SCOPES = "AZURE_REDIS_SCOPES"; - private static HashMap endpointConfigs; - private String clientId; private String authority; private String clientSecret; @@ -37,20 +34,6 @@ private TestContext() { this.clientId = System.getenv(AZURE_CLIENT_ID); this.authority = System.getenv(AZURE_AUTHORITY); this.clientSecret = System.getenv(AZURE_CLIENT_SECRET); - this.privateKey = getPrivateKey(System.getenv(AZURE_PRIVATE_KEY)); - this.cert = getCert(System.getenv(AZURE_CERT)); - String redisScopesEnv = System.getenv(AZURE_REDIS_SCOPES); - if (redisScopesEnv != null && !redisScopesEnv.isEmpty()) { - this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";"))); - } - - String endpointsPath = System.getenv().getOrDefault("REDIS_ENDPOINTS_CONFIG_PATH", - "src/test/resources/endpoints.json"); - try { - endpointConfigs = EndpointConfig.loadFromJSON(endpointsPath); - } catch (Exception e) { - throw new RuntimeException(e); - } } public TestContext(String clientId, String authority, String clientSecret, @@ -74,23 +57,25 @@ public String getClientSecret() { } public PrivateKey getPrivateKey() { + if (privateKey == null) { + this.privateKey = getPrivateKey(System.getenv(AZURE_PRIVATE_KEY)); + } return privateKey; } public X509Certificate getCert() { + if (cert == null) { + this.cert = getCert(System.getenv(AZURE_CERT)); + } return cert; } public Set getRedisScopes() { - return redisScopes; - } - - public EndpointConfig getRedisEndpoint(String endpointName) { - if (!endpointConfigs.containsKey(endpointName)) { - throw new IllegalArgumentException("Unknown Redis endpoint: " + endpointName); + if (redisScopes == null) { + String redisScopesEnv = System.getenv(AZURE_REDIS_SCOPES); + this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";"))); } - - return endpointConfigs.get(endpointName); + return redisScopes; } private PrivateKey getPrivateKey(String privateKey) { From dad80ac121d6461b174ba03a10733b795d25c70f Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 5 Dec 2024 16:01:55 +0300 Subject: [PATCH 17/22] - release drafter - make config builders generic - force refresh with managedidentity - skipcache with confidentialclientapp - add builder cloners --- .github/workflows/release-drafter.yml | 3 +- .../authentication/core/TokenAuthConfig.java | 35 +++++++++------ .../authentication/core/TokenManager.java | 9 ++-- .../entraid/EntraIDIdentityProvider.java | 15 ++++--- .../EntraIDIdentityProviderConfig.java | 22 +++------- .../EntraIDTokenAuthConfigBuilder.java | 44 +++++++++++++++++-- .../EntraIDIntegrationTests.java | 4 +- 7 files changed, 85 insertions(+), 47 deletions(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index caac3ca..052fdb6 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -5,7 +5,8 @@ on: # branches to consider in the event; optional, defaults to all branches: - master - + workflow_dispatch: + jobs: update_release_draft: runs-on: ubuntu-latest diff --git a/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java b/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java index 3d6a1a2..d8498ca 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java @@ -23,7 +23,7 @@ public static Builder builder() { return new Builder(); } - public static class Builder { + public static class Builder> { private IdentityProviderConfig identityProviderConfig; private int lowerRefreshBoundMillis; private float expirationRefreshRatio; @@ -31,34 +31,34 @@ public static class Builder { private int maxAttemptsToRetry; private int delayInMsToRetry; - public Builder expirationRefreshRatio(float expirationRefreshRatio) { + public T expirationRefreshRatio(float expirationRefreshRatio) { this.expirationRefreshRatio = expirationRefreshRatio; - return this; + return (T) this; } - public Builder lowerRefreshBoundMillis(int lowerRefreshBoundMillis) { + public T lowerRefreshBoundMillis(int lowerRefreshBoundMillis) { this.lowerRefreshBoundMillis = lowerRefreshBoundMillis; - return this; + return (T) this; } - public Builder tokenRequestExecTimeoutInMs(int tokenRequestExecTimeoutInMs) { + public T tokenRequestExecTimeoutInMs(int tokenRequestExecTimeoutInMs) { this.tokenRequestExecTimeoutInMs = tokenRequestExecTimeoutInMs; - return this; + return (T) this; } - public Builder maxAttemptsToRetry(int maxAttemptsToRetry) { + public T maxAttemptsToRetry(int maxAttemptsToRetry) { this.maxAttemptsToRetry = maxAttemptsToRetry; - return this; + return (T) this; } - public Builder delayInMsToRetry(int delayInMsToRetry) { + public T delayInMsToRetry(int delayInMsToRetry) { this.delayInMsToRetry = delayInMsToRetry; - return this; + return (T) this; } - public Builder identityProviderConfig(IdentityProviderConfig identityProviderConfig) { + public T identityProviderConfig(IdentityProviderConfig identityProviderConfig) { this.identityProviderConfig = identityProviderConfig; - return this; + return (T) this; } public TokenAuthConfig build() { @@ -67,5 +67,14 @@ public TokenAuthConfig build() { new TokenManagerConfig.RetryPolicy(maxAttemptsToRetry, delayInMsToRetry)), identityProviderConfig); } + + public static Builder from(Builder sample) { + return new Builder().expirationRefreshRatio(sample.expirationRefreshRatio) + .lowerRefreshBoundMillis(sample.lowerRefreshBoundMillis) + .tokenRequestExecTimeoutInMs(sample.tokenRequestExecTimeoutInMs) + .maxAttemptsToRetry(sample.maxAttemptsToRetry) + .delayInMsToRetry(sample.delayInMsToRetry) + .identityProviderConfig(sample.identityProviderConfig); + } } } diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManager.java b/core/src/main/java/redis/clients/authentication/core/TokenManager.java index b25ead4..ddda5d5 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenManager.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenManager.java @@ -8,6 +8,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,12 +19,10 @@ public class TokenManager { private IdentityProvider identityProvider; private TokenListener listener; private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - - // TODO: manage thread pool to avoid blocking on IDP hangs private ExecutorService executor = Executors.newFixedThreadPool(2); private boolean stopped = false; private ScheduledFuture scheduledTask; - private int numberOfRetries = 0; + private AtomicInteger numberOfRetries = new AtomicInteger(0); private Exception lastException; private Logger logger = LoggerFactory.getLogger(getClass()); private Token currentToken = null; @@ -86,8 +85,8 @@ protected Token renewToken() { listener.onTokenRenewed(newToken); return newToken; } catch (Exception e) { - if (numberOfRetries < tokenManagerConfig.getRetryPolicy().getMaxAttempts()) { - numberOfRetries++; + if (numberOfRetries.getAndIncrement() < tokenManagerConfig.getRetryPolicy() + .getMaxAttempts()) { scheduledTask = scheduleNext(tokenManagerConfig.getRetryPolicy().getdelayInMs()); } else { TokenRequestException tre = new TokenRequestException(unwrap(e), lastException); diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java index 1089b8e..170522e 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java @@ -20,7 +20,8 @@ public final class EntraIDIdentityProvider implements IdentityProvider { private Supplier resultSupplier; - public EntraIDIdentityProvider(ServicePrincipalInfo servicePrincipalInfo, Set scopes) { + public EntraIDIdentityProvider(ServicePrincipalInfo servicePrincipalInfo, Set scopes, + int timeout) { IClientCredential credential = getClientCredential(servicePrincipalInfo); ConfidentialClientApplication app; @@ -30,20 +31,22 @@ public EntraIDIdentityProvider(ServicePrincipalInfo servicePrincipalInfo, Set supplierForConfidentialApp(app, params); } - public EntraIDIdentityProvider(ManagedIdentityInfo info, Set scopes) { - ManagedIdentityApplication app = ManagedIdentityApplication.builder(info.getId()).build(); + public EntraIDIdentityProvider(ManagedIdentityInfo info, Set scopes, int timeout) { + ManagedIdentityApplication app = ManagedIdentityApplication.builder(info.getId()) + .readTimeoutForDefaultHttpClient(timeout).build(); ManagedIdentityParameters params = ManagedIdentityParameters - .builder(scopes.iterator().next()).build(); + .builder(scopes.iterator().next()).forceRefresh(true).build(); resultSupplier = () -> supplierForManagedIdentityApp(app, params); } diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java index 669ef1f..3782b15 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java @@ -8,16 +8,16 @@ import redis.clients.authentication.core.IdentityProvider; import redis.clients.authentication.core.IdentityProviderConfig; -public final class EntraIDIdentityProviderConfig implements IdentityProviderConfig, AutoCloseable { +public final class EntraIDIdentityProviderConfig implements IdentityProviderConfig { - private Supplier providerSupplier; + private final Supplier providerSupplier; - public EntraIDIdentityProviderConfig(ServicePrincipalInfo info, Set scopes) { - providerSupplier = () -> new EntraIDIdentityProvider(info, scopes); + public EntraIDIdentityProviderConfig(ServicePrincipalInfo info, Set scopes, int timeout) { + providerSupplier = () -> new EntraIDIdentityProvider(info, scopes, timeout); } - public EntraIDIdentityProviderConfig(ManagedIdentityInfo info, Set scopes) { - providerSupplier = () -> new EntraIDIdentityProvider(info, scopes); + public EntraIDIdentityProviderConfig(ManagedIdentityInfo info, Set scopes, int timeout) { + providerSupplier = () -> new EntraIDIdentityProvider(info, scopes, timeout); } public EntraIDIdentityProviderConfig( @@ -28,16 +28,6 @@ public EntraIDIdentityProviderConfig( @Override public IdentityProvider getProvider() { IdentityProvider identityProvider = providerSupplier.get(); - clear(); return identityProvider; } - - @Override - public void close() throws Exception { - clear(); - } - - private void clear() { - providerSupplier = null; - } } diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java index cd538e4..e77fe40 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java @@ -8,11 +8,12 @@ import com.microsoft.aad.msal4j.IAuthenticationResult; import redis.clients.authentication.core.TokenAuthConfig; +import redis.clients.authentication.core.TokenManagerConfig; import redis.clients.authentication.entraid.ManagedIdentityInfo.UserManagedIdentityType; import redis.clients.authentication.entraid.ServicePrincipalInfo.ServicePrincipalAccess; -public class EntraIDTokenAuthConfigBuilder extends TokenAuthConfig.Builder - implements AutoCloseable { +public class EntraIDTokenAuthConfigBuilder + extends TokenAuthConfig.Builder implements AutoCloseable { public static final float DEFAULT_EXPIRATION_REFRESH_RATIO = 0.8F; public static final int DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 2 * 60 * 1000; public static final int DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS = 1000; @@ -27,6 +28,7 @@ public class EntraIDTokenAuthConfigBuilder extends TokenAuthConfig.Builder private Set scopes; private ServicePrincipalAccess accessWith; private ManagedIdentityInfo mii; + private int tokenRequestExecTimeoutInMs; private Supplier customEntraIdAuthenticationSupplier; public EntraIDTokenAuthConfigBuilder() { @@ -82,6 +84,14 @@ public EntraIDTokenAuthConfigBuilder scopes(Set scopes) { return this; } + @Override + public EntraIDTokenAuthConfigBuilder tokenRequestExecTimeoutInMs( + int tokenRequestExecTimeoutInMs) { + super.tokenRequestExecTimeoutInMs(tokenRequestExecTimeoutInMs); + this.tokenRequestExecTimeoutInMs = tokenRequestExecTimeoutInMs; + return this; + } + public TokenAuthConfig build() { ServicePrincipalInfo spi = null; if (key != null || cert != null || secret != null) { @@ -103,10 +113,12 @@ public TokenAuthConfig build() { "Cannot have both customEntraIdAuthenticationSupplier and ServicePrincipal/ManagedIdentity!"); } if (spi != null) { - super.identityProviderConfig(new EntraIDIdentityProviderConfig(spi, scopes)); + super.identityProviderConfig( + new EntraIDIdentityProviderConfig(spi, scopes, tokenRequestExecTimeoutInMs)); } if (mii != null) { - super.identityProviderConfig(new EntraIDIdentityProviderConfig(mii, scopes)); + super.identityProviderConfig( + new EntraIDIdentityProviderConfig(mii, scopes, tokenRequestExecTimeoutInMs)); } if (customEntraIdAuthenticationSupplier != null) { super.identityProviderConfig( @@ -129,4 +141,28 @@ public void close() throws Exception { public static EntraIDTokenAuthConfigBuilder builder() { return new EntraIDTokenAuthConfigBuilder(); } + + public static EntraIDTokenAuthConfigBuilder from(EntraIDTokenAuthConfigBuilder sample) { + TokenAuthConfig tokenAuthConfig = TokenAuthConfig.Builder.from(sample).build(); + TokenManagerConfig tokenManagerConfig = tokenAuthConfig.getTokenManagerConfig(); + + EntraIDTokenAuthConfigBuilder builder = (EntraIDTokenAuthConfigBuilder) new EntraIDTokenAuthConfigBuilder() + .expirationRefreshRatio(tokenManagerConfig.getExpirationRefreshRatio()) + .lowerRefreshBoundMillis(tokenManagerConfig.getLowerRefreshBoundMillis()) + .tokenRequestExecTimeoutInMs(tokenManagerConfig.getTokenRequestExecTimeoutInMs()) + .maxAttemptsToRetry(tokenManagerConfig.getRetryPolicy().getMaxAttempts()) + .delayInMsToRetry(tokenManagerConfig.getRetryPolicy().getdelayInMs()) + .identityProviderConfig(tokenAuthConfig.getIdentityProviderConfig()); + + builder.accessWith = sample.accessWith; + builder.authority = sample.authority; + builder.cert = sample.cert; + builder.clientId = sample.clientId; + builder.customEntraIdAuthenticationSupplier = sample.customEntraIdAuthenticationSupplier; + builder.key = sample.key; + builder.mii = sample.mii; + builder.scopes = sample.scopes; + builder.secret = sample.secret; + return builder; + } } diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java index 82ffe37..b383c81 100644 --- a/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java @@ -17,7 +17,7 @@ public void requestTokenWithSecret() throws MalformedURLException { testCtx.getClientId(), testCtx.getClientSecret(), testCtx.getAuthority()); Token token = new EntraIDIdentityProvider(servicePrincipalInfo, - testCtx.getRedisScopes()).requestToken(); + testCtx.getRedisScopes(),1000).requestToken(); assertNotNull(token.getValue()); } @@ -29,7 +29,7 @@ public void requestTokenWithCert() throws MalformedURLException { testCtx.getClientId(), testCtx.getPrivateKey(), testCtx.getCert(), testCtx.getAuthority()); Token token = new EntraIDIdentityProvider(servicePrincipalInfo, - testCtx.getRedisScopes()).requestToken(); + testCtx.getRedisScopes(),1000).requestToken(); assertNotNull(token.getValue()); } From 39cacf39962639b4f84a49f54875febae27a268f Mon Sep 17 00:00:00 2001 From: atakavci Date: Sun, 8 Dec 2024 08:11:00 +0300 Subject: [PATCH 18/22] - change exception propogation/handling - fix units tests - set DEFAULT_EXPIRATION_REFRESH_RATIO in entraid 0.75 --- .../authentication/core/TokenManager.java | 19 +++++++++++++------ .../CoreAuthenticationUnitTests.java | 2 +- .../EntraIDTokenAuthConfigBuilder.java | 2 +- .../EntraIDIntegrationTests.java | 2 +- .../authentication/EntraIDUnitTests.java | 2 +- .../clients/authentication/TestContext.java | 8 ++++++++ 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManager.java b/core/src/main/java/redis/clients/authentication/core/TokenManager.java index ddda5d5..46e4b3e 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenManager.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenManager.java @@ -49,7 +49,7 @@ public void start(TokenListener listener, boolean blockForInitialToken) { } catch (RuntimeException e) { throw e; } catch (Exception e) { - throw new TokenRequestException(unwrap(e), lastException); + throw prepareToPropogate(e); } } } @@ -89,9 +89,9 @@ protected Token renewToken() { .getMaxAttempts()) { scheduledTask = scheduleNext(tokenManagerConfig.getRetryPolicy().getdelayInMs()); } else { - TokenRequestException tre = new TokenRequestException(unwrap(e), lastException); - listener.onError(tre); - throw tre; + RuntimeException propogateExc = prepareToPropogate(e); + listener.onError(propogateExc); + throw propogateExc; } } return null; @@ -108,8 +108,15 @@ protected Token requestToken() { } } - private Throwable unwrap(Exception e) { - return (e instanceof ExecutionException) ? e.getCause() : e; + private RuntimeException prepareToPropogate(Exception e) { + Throwable unwrapped = e; + if (unwrapped instanceof ExecutionException) { + unwrapped = e.getCause(); + } + if (unwrapped instanceof TokenRequestException) { + return (RuntimeException) unwrapped; + } + return new TokenRequestException(unwrapped, lastException); } public Token getCurrentToken() { diff --git a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java index 05b1897..c967343 100644 --- a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java +++ b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java @@ -162,7 +162,7 @@ public void testBlockForInitialToken() { TokenRequestException e = assertThrows(TokenRequestException.class, () -> tokenManager.start(mock(TokenListener.class), true)); - assertEquals("Test exception from identity provider!", e.getCause().getCause().getMessage()); + assertEquals("Test exception from identity provider!", e.getCause().getMessage()); } @Test diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java index e77fe40..3143660 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java @@ -14,7 +14,7 @@ public class EntraIDTokenAuthConfigBuilder extends TokenAuthConfig.Builder implements AutoCloseable { - public static final float DEFAULT_EXPIRATION_REFRESH_RATIO = 0.8F; + public static final float DEFAULT_EXPIRATION_REFRESH_RATIO = 0.75F; public static final int DEFAULT_LOWER_REFRESH_BOUND_MILLIS = 2 * 60 * 1000; public static final int DEFAULT_TOKEN_REQUEST_EXECUTION_TIMEOUT_IN_MS = 1000; public static final int DEFAULT_MAX_ATTEMPTS_TO_RETRY = 5; diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java index b383c81..207e691 100644 --- a/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java @@ -17,7 +17,7 @@ public void requestTokenWithSecret() throws MalformedURLException { testCtx.getClientId(), testCtx.getClientSecret(), testCtx.getAuthority()); Token token = new EntraIDIdentityProvider(servicePrincipalInfo, - testCtx.getRedisScopes(),1000).requestToken(); + testCtx.getRedisScopes(), 1000).requestToken(); assertNotNull(token.getValue()); } diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java index 5920fc5..5558db7 100644 --- a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java @@ -160,7 +160,7 @@ public void tokenRequestfailsWithException_fakeIdentityProviderTest() { () -> tokenManager.start(mock(TokenListener.class), true)); assertEquals("Test exception from identity provider!", - e.getCause().getCause().getMessage()); + e.getCause().getMessage()); } // T.2.1 diff --git a/entraid/src/test/java/redis/clients/authentication/TestContext.java b/entraid/src/test/java/redis/clients/authentication/TestContext.java index 3ab6e2d..be096ec 100644 --- a/entraid/src/test/java/redis/clients/authentication/TestContext.java +++ b/entraid/src/test/java/redis/clients/authentication/TestContext.java @@ -19,6 +19,7 @@ public class TestContext { private static final String AZURE_PRIVATE_KEY = "AZURE_PRIVATE_KEY"; private static final String AZURE_CERT = "AZURE_CERT"; private static final String AZURE_REDIS_SCOPES = "AZURE_REDIS_SCOPES"; + private static final String AZURE_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID = "AZURE_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID"; private String clientId; private String authority; @@ -26,6 +27,7 @@ public class TestContext { private PrivateKey privateKey; private X509Certificate cert; private Set redisScopes; + private String userAssignedManagedIdentityClientId; public static final TestContext DEFAULT = new TestContext(); @@ -34,6 +36,8 @@ private TestContext() { this.clientId = System.getenv(AZURE_CLIENT_ID); this.authority = System.getenv(AZURE_AUTHORITY); this.clientSecret = System.getenv(AZURE_CLIENT_SECRET); + this.userAssignedManagedIdentityClientId = System + .getenv(AZURE_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID); } public TestContext(String clientId, String authority, String clientSecret, @@ -78,6 +82,10 @@ public Set getRedisScopes() { return redisScopes; } + public String getUserAssignedManagedIdentityClientId() { + return userAssignedManagedIdentityClientId; + } + private PrivateKey getPrivateKey(String privateKey) { try { // Decode the base64 encoded key into a byte array From 457d4afcd150034310d853fb60404bddcebfa9ce Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 12 Dec 2024 14:18:03 +0300 Subject: [PATCH 19/22] - add getuser to Token interface - set user in JWToken --- .../authentication/core/SimpleToken.java | 31 +++++++++++------- .../clients/authentication/core/Token.java | 11 ++++--- .../CoreAuthenticationUnitTests.java | 14 ++++---- .../authentication/entraid/JWToken.java | 18 ++++++++--- .../authentication/EntraIDUnitTests.java | 32 ++++++++----------- 5 files changed, 59 insertions(+), 47 deletions(-) diff --git a/core/src/main/java/redis/clients/authentication/core/SimpleToken.java b/core/src/main/java/redis/clients/authentication/core/SimpleToken.java index 6579eaa..033abe9 100644 --- a/core/src/main/java/redis/clients/authentication/core/SimpleToken.java +++ b/core/src/main/java/redis/clients/authentication/core/SimpleToken.java @@ -4,12 +4,15 @@ public class SimpleToken implements Token { + private String user; private String value; private long expiresAt; private long receivedAt; - private Map claims; + private Map claims; - public SimpleToken(String value, long expiresAt, long receivedAt, Map claims) { + public SimpleToken(String user, String value, long expiresAt, long receivedAt, + Map claims) { + this.user = user; this.value = value; this.expiresAt = expiresAt; this.receivedAt = receivedAt; @@ -17,13 +20,8 @@ public SimpleToken(String value, long expiresAt, long receivedAt, Map expiresAt; - } - - @Override - public long ttl() { - return expiresAt - System.currentTimeMillis(); + public String getUser() { + return user; } @Override @@ -42,7 +40,18 @@ public long getReceivedAt() { } @Override - public String tryGet(String key) { - return claims.get(key); + public T tryGet(String key, Class clazz) { + return (T) claims.get(key); + } + + @Override + public boolean isExpired() { + return System.currentTimeMillis() > expiresAt; } + + @Override + public long ttl() { + return expiresAt - System.currentTimeMillis(); + } + } \ No newline at end of file diff --git a/core/src/main/java/redis/clients/authentication/core/Token.java b/core/src/main/java/redis/clients/authentication/core/Token.java index d8b7679..953950d 100644 --- a/core/src/main/java/redis/clients/authentication/core/Token.java +++ b/core/src/main/java/redis/clients/authentication/core/Token.java @@ -2,9 +2,7 @@ public interface Token { - public boolean isExpired(); - - public long ttl(); + public String getUser(); public String getValue(); @@ -12,5 +10,10 @@ public interface Token { public long getReceivedAt(); - public String tryGet(String key); + public boolean isExpired(); + + public long ttl(); + + public T tryGet(String key, Class clazz); + } \ No newline at end of file diff --git a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java index c967343..6fcf291 100644 --- a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java +++ b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java @@ -14,7 +14,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; @@ -131,9 +130,8 @@ public void testCalculateRenewalDelay() { public void testTokenManagerStart() throws InterruptedException, ExecutionException, TimeoutException { - IdentityProvider identityProvider = () -> new SimpleToken("tokenVal", - System.currentTimeMillis() + 5 * 1000, System.currentTimeMillis(), - Collections.singletonMap("oid", "user1")); + IdentityProvider identityProvider = () -> new SimpleToken("user1", "tokenVal", + System.currentTimeMillis() + 5 * 1000, System.currentTimeMillis(), null); TokenManager tokenManager = new TokenManager(identityProvider, new TokenManagerConfig(0.7F, 200, 2000, null)); @@ -198,8 +196,8 @@ public void testTokenManagerWithFailingTokenRequest() if (requesLatch.getCount() > 0) { throw new RuntimeException("Test exception from identity provider!"); } - return new SimpleToken("tokenValX", System.currentTimeMillis() + 50 * 1000, - System.currentTimeMillis(), Collections.singletonMap("oid", "user1")); + return new SimpleToken("user1", "tokenValX", System.currentTimeMillis() + 50 * 1000, + System.currentTimeMillis(), null); }); ArgumentCaptor argument = ArgumentCaptor.forClass(Token.class); @@ -230,8 +228,8 @@ public void testTokenManagerWithHangingTokenRequest() if (requesLatch.getCount() > 0) { delay(delayDuration); } - return new SimpleToken("tokenValX", System.currentTimeMillis() + tokenLifetime, - System.currentTimeMillis(), Collections.singletonMap("oid", "user1")); + return new SimpleToken("user1", "tokenValX", System.currentTimeMillis() + tokenLifetime, + System.currentTimeMillis(), null); }; TokenManager tokenManager = new TokenManager(identityProvider, new TokenManagerConfig(0.7F, 200, diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java b/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java index 54a5392..79ffc4c 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java @@ -1,6 +1,6 @@ package redis.clients.authentication.entraid; -import java.util.function.Function; +import java.util.function.BiFunction; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.JWT; @@ -8,17 +8,19 @@ import redis.clients.authentication.core.Token; public class JWToken implements Token { + private final String user; private final String token; private final long expiresAt; private final long receivedAt; - private final Function claimQuery; + private final BiFunction, ?> claimQuery; public JWToken(String token) { this.token = token; DecodedJWT jwt = JWT.decode(token); + this.user = jwt.getClaim("oid").asString(); this.expiresAt = jwt.getExpiresAt().getTime(); this.receivedAt = System.currentTimeMillis(); - this.claimQuery = key -> jwt.getClaim(key).asString(); + this.claimQuery = (key, clazz) -> jwt.getClaim(key).as(clazz); } @Override @@ -31,6 +33,11 @@ public long ttl() { return expiresAt - System.currentTimeMillis(); } + @Override + public String getUser() { + return user; + } + @Override public String getValue() { return token; @@ -67,7 +74,8 @@ public boolean equals(Object that) { } @Override - public String tryGet(String key) { - return claimQuery.apply(key); + public T tryGet(String key, Class clazz) { + return (T) claimQuery.apply(key, clazz); } + } diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java index 5558db7..dfb7a72 100644 --- a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java @@ -82,8 +82,8 @@ public class EntraIDUnitTests { private static final long TOKEN_ISSUE_TIME = System.currentTimeMillis(); private static final String TOKEN_OID = "user1"; - private Token simpleToken = new SimpleToken(TOKEN_VALUE, TOKEN_EXPIRATION_TIME, - TOKEN_ISSUE_TIME, Collections.singletonMap("oid", TOKEN_OID)); + private Token simpleToken = new SimpleToken(TOKEN_OID, TOKEN_VALUE, TOKEN_EXPIRATION_TIME, + TOKEN_ISSUE_TIME, null); private TestContext testCtx = TestContext.DEFAULT; @@ -159,8 +159,7 @@ public void tokenRequestfailsWithException_fakeIdentityProviderTest() { TokenRequestException e = assertThrows(TokenRequestException.class, () -> tokenManager.start(mock(TokenListener.class), true)); - assertEquals("Test exception from identity provider!", - e.getCause().getMessage()); + assertEquals("Test exception from identity provider!", e.getCause().getMessage()); } // T.2.1 @@ -267,9 +266,8 @@ public void tokenAcquisitionTimeoutTest() throws InterruptedException, TimeoutEx public void backgroundTokenRenewalTest() throws InterruptedException, TimeoutException { AtomicInteger numberOfTokens = new AtomicInteger(0); - IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_VALUE, - System.currentTimeMillis() + 1000, System.currentTimeMillis(), - Collections.singletonMap("oid", TOKEN_OID)); + IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_OID, TOKEN_VALUE, + System.currentTimeMillis() + 1000, System.currentTimeMillis(), null); TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); TokenListener listener = new TokenListener() { @@ -326,9 +324,8 @@ public void customRenewalTimingTest() { AtomicInteger numberOfTokens = new AtomicInteger(0); AtomicInteger timeDiff = new AtomicInteger(0); - IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_VALUE, - System.currentTimeMillis() + 1000, System.currentTimeMillis(), - Collections.singletonMap("oid", TOKEN_OID)); + IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_OID, TOKEN_VALUE, + System.currentTimeMillis() + 1000, System.currentTimeMillis(), null); TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); TokenListener listener = new TokenListener() { @@ -368,9 +365,8 @@ public void highPercentage_edgeCaseRenewalTimingTest() { List tokens = new ArrayList(); int validDurationInMs = 1000; - IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_VALUE, - System.currentTimeMillis() + validDurationInMs, System.currentTimeMillis(), - Collections.singletonMap("oid", TOKEN_OID)); + IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_OID, TOKEN_VALUE, + System.currentTimeMillis() + validDurationInMs, System.currentTimeMillis(), null); TokenManagerConfig tokenManagerConfig = new TokenManagerConfig(0.99F, 0, TOKEN_REQUEST_EXEC_TIMEOUT, @@ -413,9 +409,8 @@ public void lowPercentage_edgeCaseRenewalTimingTest() { List tokens = new ArrayList(); int validDurationInMs = 1000; - IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_VALUE, - System.currentTimeMillis() + validDurationInMs, System.currentTimeMillis(), - Collections.singletonMap("oid", TOKEN_OID)); + IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_OID, TOKEN_VALUE, + System.currentTimeMillis() + validDurationInMs, System.currentTimeMillis(), null); TokenManagerConfig tokenManagerConfig = new TokenManagerConfig(0.01F, 0, TOKEN_REQUEST_EXEC_TIMEOUT, @@ -510,9 +505,8 @@ public void cacheUpdateOnRenewalTest() { AtomicInteger numberOfTokens = new AtomicInteger(0); IdentityProvider identityProvider = () -> { - return new SimpleToken("" + numberOfTokens.incrementAndGet(), - System.currentTimeMillis() + 500, System.currentTimeMillis(), - Collections.singletonMap("oid", "user1")); + return new SimpleToken("user1", "" + numberOfTokens.incrementAndGet(), + System.currentTimeMillis() + 500, System.currentTimeMillis(), null); }; TokenManager tokenManager = new TokenManager(identityProvider, tokenManagerConfig); assertNull(tokenManager.getCurrentToken()); From 2ab345f875919b99642d7162871efd9ee155d3b5 Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 12 Dec 2024 14:31:53 +0300 Subject: [PATCH 20/22] remove all jedis config and dependency --- .github/workflows/entraid_integration.yml | 15 -- entraid/pom.xml | 8 +- .../authentication/EndpointConfig.java | 141 +----------------- .../authentication/EntraIDUnitTests.java | 4 - entraid/src/test/resources/endpoints.json | 107 ------------- 5 files changed, 2 insertions(+), 273 deletions(-) delete mode 100644 entraid/src/test/resources/endpoints.json diff --git a/.github/workflows/entraid_integration.yml b/.github/workflows/entraid_integration.yml index 5deb266..7e00b6c 100644 --- a/.github/workflows/entraid_integration.yml +++ b/.github/workflows/entraid_integration.yml @@ -27,12 +27,6 @@ jobs: working-directory: ./entraid steps: - uses: actions/checkout@v2 - - name: Checkout Jedis repository (tba_draft branch) - uses: actions/checkout@v2 - with: - repository: atakavci/jedis # Replace with the actual jedis repository URL - ref: ali/authx2 - path: jedis # Check out into a subdirectory named `jedis` so it's isolated - name: Set up publishing to maven central uses: actions/setup-java@v2 @@ -56,15 +50,6 @@ jobs: mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed working-directory: ./core - - name: Maven offline-jedis - run: | - mvn -q dependency:go-offline - working-directory: ./jedis - - name: Build and install Jedis supports TBA into local repo - run: | - mvn clean install -DskipTests # Skip tests for faster builds, but you can remove the flag if needed - working-directory: ./jedis - - name: Build docs run: | mvn javadoc:jar diff --git a/entraid/pom.xml b/entraid/pom.xml index f7cac62..d08b08c 100644 --- a/entraid/pom.xml +++ b/entraid/pom.xml @@ -67,13 +67,7 @@ msal4j 1.17.2 - - redis.clients - jedis - 5.3.0-SNAPSHOT - test - - + junit junit 4.13.2 diff --git a/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java b/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java index 38d3e60..078426a 100644 --- a/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java +++ b/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java @@ -1,140 +1 @@ -package redis.clients.authentication; - -import com.google.gson.FieldNamingPolicy; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; - -import redis.clients.jedis.DefaultJedisClientConfig; -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.util.JedisURIHelper; - -import java.io.FileReader; -import java.net.URI; -import java.util.*; - -public class EndpointConfig { - - private final boolean tls; - private final String username; - private final String password; - private final int bdbId; - private final List endpoints; - - public EndpointConfig(HostAndPort hnp, String username, String password, boolean tls) { - this.tls = tls; - this.username = username; - this.password = password; - this.bdbId = 0; - this.endpoints = Collections.singletonList( - URI.create(getURISchema(tls) + hnp.getHost() + ":" + hnp.getPort())); - } - - public HostAndPort getHostAndPort() { - return JedisURIHelper.getHostAndPort(endpoints.get(0)); - } - - public HostAndPort getHostAndPort(int index) { - return JedisURIHelper.getHostAndPort(endpoints.get(index)); - } - - public String getPassword() { - return password; - } - - public String getUsername() { - return username == null? "default" : username; - } - - public String getHost() { - return getHostAndPort().getHost(); - } - - public int getPort() { - return getHostAndPort().getPort(); - } - - public int getBdbId() { return bdbId; } - - public URI getURI() { - return endpoints.get(0); - } - - public class EndpointURIBuilder { - private boolean tls; - - private String username; - - private String password; - - private String path; - - public EndpointURIBuilder() { - this.username = ""; - this.password = ""; - this.path = ""; - this.tls = EndpointConfig.this.tls; - } - - public EndpointURIBuilder defaultCredentials() { - this.username = EndpointConfig.this.username == null ? "" : getUsername(); - this.password = EndpointConfig.this.getPassword(); - return this; - } - - public EndpointURIBuilder tls(boolean v) { - this.tls = v; - return this; - } - - public EndpointURIBuilder path(String v) { - this.path = v; - return this; - } - - public EndpointURIBuilder credentials(String u, String p) { - this.username = u; - this.password = p; - return this; - } - - public URI build() { - String userInfo = !(this.username.isEmpty() && this.password.isEmpty()) ? - this.username + ':' + this.password + '@' : - ""; - return URI.create( - getURISchema(this.tls) + userInfo + getHost() + ":" + getPort() + this.path); - } - } - - public EndpointURIBuilder getURIBuilder() { - return new EndpointURIBuilder(); - } - - public DefaultJedisClientConfig.Builder getClientConfigBuilder() { - DefaultJedisClientConfig.Builder builder = DefaultJedisClientConfig.builder() - .password(password).ssl(tls); - - if (username != null) { - return builder.user(username); - } - - return builder; - } - - protected String getURISchema(boolean tls) { - return (tls ? "rediss" : "redis") + "://"; - } - - public static HashMap loadFromJSON(String filePath) throws Exception { - Gson gson = new GsonBuilder().setFieldNamingPolicy( - FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); - - HashMap configs; - try (FileReader reader = new FileReader(filePath)) { - configs = gson.fromJson(reader, new TypeToken>() { - }.getType()); - } - return configs; - } -} +// \ No newline at end of file diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java index dfb7a72..9582eb6 100644 --- a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java @@ -61,10 +61,6 @@ import redis.clients.authentication.entraid.ServicePrincipalInfo; import redis.clients.authentication.entraid.ManagedIdentityInfo.UserManagedIdentityType; -// import redis.clients.jedis.DefaultJedisClientConfig; -// import redis.clients.jedis.HostAndPort; -// import redis.clients.jedis.JedisPooled; - public class EntraIDUnitTests { private static final float EXPIRATION_REFRESH_RATIO = 0.7F; diff --git a/entraid/src/test/resources/endpoints.json b/entraid/src/test/resources/endpoints.json deleted file mode 100644 index c1d905b..0000000 --- a/entraid/src/test/resources/endpoints.json +++ /dev/null @@ -1,107 +0,0 @@ -{ - "standalone0": { - "password": "foobared", - "tls": false, - "endpoints": [ - "redis://localhost:6379" - ] - }, - "standalone0-tls": { - "username": "default", - "password": "foobared", - "tls": true, - "endpoints": [ - "rediss://localhost:6390" - ] - }, - "standalone0-acl": { - "username": "acljedis", - "password": "fizzbuzz", - "tls": false, - "endpoints": [ - "redis://localhost:6379" - ] - }, - "standalone0-acl-tls": { - "username": "acljedis", - "password": "fizzbuzz", - "tls": true, - "endpoints": [ - "rediss://localhost:6390" - ] - }, - "standalone1": { - "username": "default", - "password": "foobared", - "tls": false, - "endpoints": [ - "redis://localhost:6380" - ] - }, - "standalone2-primary": { - "username": "default", - "password": "foobared", - "tls": false, - "endpoints": [ - "redis://localhost:6381" - ] - }, - "standalone3-replica-of-standalone2": { - "username": "default", - "password": "foobared", - "tls": false, - "endpoints": [ - "redis://localhost:6382" - ] - }, - "standalone4-replica-of-standalone1": { - "username": "default", - "password": "foobared", - "tls": false, - "endpoints": [ - "redis://localhost:6383" - ] - }, - "standalone5-primary": { - "username": "default", - "password": "foobared", - "tls": false, - "endpoints": [ - "redis://localhost:6384" - ] - }, - "standalone6-replica-of-standalone5": { - "username": "default", - "password": "foobared", - "tls": false, - "endpoints": [ - "redis://localhost:6385" - ] - }, - "standalone7-with-lfu-policy": { - "username": "default", - "password": "foobared", - "tls": false, - "endpoints": [ - "redis://localhost:6386" - ] - }, - "standalone9": { - "tls": false, - "endpoints": [ - "redis://localhost:6388" - ] - }, - "standalone10-replica-of-standalone9": { - "tls": false, - "endpoints": [ - "redis://localhost:6389" - ] - }, - "modules-docker": { - "tls": false, - "endpoints": [ - "redis://localhost:6479" - ] - } -} \ No newline at end of file From d786e23e5c0f1e5e060c0bd42bf4aeb87c244f6b Mon Sep 17 00:00:00 2001 From: atakavci Date: Thu, 19 Dec 2024 15:13:49 +0300 Subject: [PATCH 21/22] review from @tishun - licesing statement - checkout action version - drop useless file --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/spellcheck.yml | 2 +- .../redis/clients/authentication/core/AuthXException.java | 6 ++++++ .../redis/clients/authentication/core/IdentityProvider.java | 6 ++++++ .../clients/authentication/core/IdentityProviderConfig.java | 6 ++++++ .../java/redis/clients/authentication/core/SimpleToken.java | 6 ++++++ .../main/java/redis/clients/authentication/core/Token.java | 6 ++++++ .../redis/clients/authentication/core/TokenAuthConfig.java | 6 ++++++ .../redis/clients/authentication/core/TokenListener.java | 6 ++++++ .../redis/clients/authentication/core/TokenManager.java | 6 ++++++ .../clients/authentication/core/TokenManagerConfig.java | 6 ++++++ .../clients/authentication/core/TokenRequestException.java | 6 ++++++ .../clients/authentication/CoreAuthenticationUnitTests.java | 6 ++++++ .../authentication/entraid/EntraIDIdentityProvider.java | 6 ++++++ .../entraid/EntraIDIdentityProviderConfig.java | 6 ++++++ .../entraid/EntraIDTokenAuthConfigBuilder.java | 6 ++++++ .../java/redis/clients/authentication/entraid/JWToken.java | 6 ++++++ .../clients/authentication/entraid/ManagedIdentityInfo.java | 6 ++++++ .../authentication/entraid/RedisEntraIDException.java | 6 ++++++ .../authentication/entraid/ServicePrincipalInfo.java | 6 ++++++ .../java/redis/clients/authentication/EndpointConfig.java | 1 - .../clients/authentication/EntraIDIntegrationTests.java | 6 ++++++ .../java/redis/clients/authentication/EntraIDUnitTests.java | 6 ++++++ .../test/java/redis/clients/authentication/TestContext.java | 6 ++++++ 24 files changed, 128 insertions(+), 3 deletions(-) delete mode 100644 entraid/src/test/java/redis/clients/authentication/EndpointConfig.java diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3f472ee..d395145 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index bd88e08..4588835 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check Spelling uses: rojopolis/spellcheck-github-actions@0.33.1 with: diff --git a/core/src/main/java/redis/clients/authentication/core/AuthXException.java b/core/src/main/java/redis/clients/authentication/core/AuthXException.java index 7601b50..dc0981f 100644 --- a/core/src/main/java/redis/clients/authentication/core/AuthXException.java +++ b/core/src/main/java/redis/clients/authentication/core/AuthXException.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; public class AuthXException extends RuntimeException { diff --git a/core/src/main/java/redis/clients/authentication/core/IdentityProvider.java b/core/src/main/java/redis/clients/authentication/core/IdentityProvider.java index 7b9701d..be9717d 100644 --- a/core/src/main/java/redis/clients/authentication/core/IdentityProvider.java +++ b/core/src/main/java/redis/clients/authentication/core/IdentityProvider.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; public interface IdentityProvider { diff --git a/core/src/main/java/redis/clients/authentication/core/IdentityProviderConfig.java b/core/src/main/java/redis/clients/authentication/core/IdentityProviderConfig.java index 5fcffec..c155af1 100644 --- a/core/src/main/java/redis/clients/authentication/core/IdentityProviderConfig.java +++ b/core/src/main/java/redis/clients/authentication/core/IdentityProviderConfig.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; public interface IdentityProviderConfig { diff --git a/core/src/main/java/redis/clients/authentication/core/SimpleToken.java b/core/src/main/java/redis/clients/authentication/core/SimpleToken.java index 033abe9..e9ae58a 100644 --- a/core/src/main/java/redis/clients/authentication/core/SimpleToken.java +++ b/core/src/main/java/redis/clients/authentication/core/SimpleToken.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; import java.util.Map; diff --git a/core/src/main/java/redis/clients/authentication/core/Token.java b/core/src/main/java/redis/clients/authentication/core/Token.java index 953950d..fb6142f 100644 --- a/core/src/main/java/redis/clients/authentication/core/Token.java +++ b/core/src/main/java/redis/clients/authentication/core/Token.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; public interface Token { diff --git a/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java b/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java index d8498ca..9fed55c 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; public class TokenAuthConfig { diff --git a/core/src/main/java/redis/clients/authentication/core/TokenListener.java b/core/src/main/java/redis/clients/authentication/core/TokenListener.java index f4815ee..9ea8c75 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenListener.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenListener.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; public interface TokenListener { diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManager.java b/core/src/main/java/redis/clients/authentication/core/TokenManager.java index 46e4b3e..e87b829 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenManager.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenManager.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; import java.util.concurrent.ExecutionException; diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManagerConfig.java b/core/src/main/java/redis/clients/authentication/core/TokenManagerConfig.java index a8cb671..907a61a 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenManagerConfig.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenManagerConfig.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; /** diff --git a/core/src/main/java/redis/clients/authentication/core/TokenRequestException.java b/core/src/main/java/redis/clients/authentication/core/TokenRequestException.java index e095ada..14b9f13 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenRequestException.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenRequestException.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.core; public class TokenRequestException extends AuthXException { diff --git a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java index 6fcf291..bf44ae4 100644 --- a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java +++ b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication; import static org.mockito.Mockito.when; diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java index 170522e..b61167b 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.entraid; import java.net.MalformedURLException; diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java index 3782b15..a9b57f7 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.entraid; import java.util.Set; diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java index 3143660..d74c002 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.entraid; import java.security.PrivateKey; diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java b/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java index 79ffc4c..72cb576 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.entraid; import java.util.function.BiFunction; diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/ManagedIdentityInfo.java b/entraid/src/main/java/redis/clients/authentication/entraid/ManagedIdentityInfo.java index c1bebc1..c90173a 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/ManagedIdentityInfo.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/ManagedIdentityInfo.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.entraid; import java.util.function.Function; diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java b/entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java index 5ddb906..2248db1 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.entraid; import redis.clients.authentication.core.AuthXException; diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/ServicePrincipalInfo.java b/entraid/src/main/java/redis/clients/authentication/entraid/ServicePrincipalInfo.java index 96440ad..6840b01 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/ServicePrincipalInfo.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/ServicePrincipalInfo.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication.entraid; import java.security.PrivateKey; diff --git a/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java b/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java deleted file mode 100644 index 078426a..0000000 --- a/entraid/src/test/java/redis/clients/authentication/EndpointConfig.java +++ /dev/null @@ -1 +0,0 @@ -// \ No newline at end of file diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java index 207e691..ced17c3 100644 --- a/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication; import static org.junit.Assert.assertNotNull; diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java index 9582eb6..a1b8fba 100644 --- a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication; import static org.junit.Assert.assertEquals; diff --git a/entraid/src/test/java/redis/clients/authentication/TestContext.java b/entraid/src/test/java/redis/clients/authentication/TestContext.java index be096ec..79c7829 100644 --- a/entraid/src/test/java/redis/clients/authentication/TestContext.java +++ b/entraid/src/test/java/redis/clients/authentication/TestContext.java @@ -1,3 +1,9 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ package redis.clients.authentication; import java.util.Arrays; From 22d0f1efbf00339f2cae636ef8748d7d3ae9c1e7 Mon Sep 17 00:00:00 2001 From: atakavci Date: Fri, 20 Dec 2024 03:04:13 +0300 Subject: [PATCH 22/22] review from @tishun - attemp to increase readibility and establish a clear seperation of responsibilties via breaking tokenmanager into multiple classes and interfaces. - added some comments to explain the logic --- .../authentication/core/Dispatcher.java | 60 +++++++ .../authentication/core/RenewalScheduler.java | 66 ++++++++ .../authentication/core/RenewalTask.java | 27 +++ .../clients/authentication/core/Request.java | 15 ++ .../authentication/core/TokenManager.java | 154 ++++++++++-------- .../CoreAuthenticationUnitTests.java | 8 +- .../entraid/EntraIDIdentityProvider.java | 43 ++++- .../authentication/EntraIDUnitTests.java | 2 +- 8 files changed, 295 insertions(+), 80 deletions(-) create mode 100644 core/src/main/java/redis/clients/authentication/core/Dispatcher.java create mode 100644 core/src/main/java/redis/clients/authentication/core/RenewalScheduler.java create mode 100644 core/src/main/java/redis/clients/authentication/core/RenewalTask.java create mode 100644 core/src/main/java/redis/clients/authentication/core/Request.java diff --git a/core/src/main/java/redis/clients/authentication/core/Dispatcher.java b/core/src/main/java/redis/clients/authentication/core/Dispatcher.java new file mode 100644 index 0000000..58f6de4 --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/Dispatcher.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors All rights reserved. Licensed under the MIT License. + */ +package redis.clients.authentication.core; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dispatches requests to the identity provider asynchronously with a timeout for the request execution. + */ +class Dispatcher { + private ExecutorService executor = Executors.newFixedThreadPool(2); + private Exception error; + private long tokenRequestExecTimeoutInMs; + private IdentityProvider identityProvider; + private Logger logger = LoggerFactory.getLogger(getClass()); + + public Dispatcher(IdentityProvider provider, long tokenRequestExecTimeoutInMs) { + this.tokenRequestExecTimeoutInMs = tokenRequestExecTimeoutInMs; + this.identityProvider = provider; + } + + /** + * Dispatches a request to the identity provider asynchronously + * with a timeout for the request execution and returns the request object + * @return + */ + public Request requestTokenAsync() { + Future request = executor.submit(() -> requestToken()); + return () -> request.get(tokenRequestExecTimeoutInMs, TimeUnit.MILLISECONDS); + } + + public Exception getError() { + return error; + } + + public void stop() { + executor.shutdown(); + } + + /** + * Makes the actual request to the identity provider + * @return + */ + private Token requestToken() { + error = null; + try { + return identityProvider.requestToken(); + } catch (Exception e) { + error = e; + logger.error("Request to identity provider failed with message: " + e.getMessage(), e); + throw e; + } + } +} diff --git a/core/src/main/java/redis/clients/authentication/core/RenewalScheduler.java b/core/src/main/java/redis/clients/authentication/core/RenewalScheduler.java new file mode 100644 index 0000000..97d70d3 --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/RenewalScheduler.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors All rights reserved. Licensed under the MIT License. + */ +package redis.clients.authentication.core; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** + * Schedules a task for token renewal. + */ +class RenewalScheduler { + private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private RenewalTask lastTask; + private Supplier renewToken; + private boolean stopped = false; + + public RenewalScheduler(Supplier renewToken) { + this.renewToken = renewToken; + } + + /** + * Schedules a task to renew the token with a given delay + * Wraps the supplier function into RenewalTask + * @param delay + * @return + */ + public RenewalTask scheduleNext(long delay) { + // Schedule the task to run after the given delay + lastTask = new RenewalTask( + scheduler.schedule(() -> renewToken.get(), delay, TimeUnit.MILLISECONDS)); + return lastTask; + } + + /** + * Returns the last task that was scheduled + * @return + */ + public RenewalTask getLastTask() { + return lastTask; + } + + /** + * Waits for given task to complete + * If there is an execution error in the task, it throws the same exception + * It keeps following if there are consecutive tasks until a non-null result is returned or an exception occurs + * This makes the caller thread to wait until a first token is received with or after the pendingTask + * @param pendingTask + * @throws InterruptedException + * @throws ExecutionException + */ + public void waitFor(RenewalTask pendingTask) throws InterruptedException, ExecutionException { + while (!stopped && pendingTask.waitForResultOrError() == null) { + pendingTask = getLastTask(); + } + } + + public void stop() { + stopped = true; + lastTask.cancel(); + scheduler.shutdown(); + } +} diff --git a/core/src/main/java/redis/clients/authentication/core/RenewalTask.java b/core/src/main/java/redis/clients/authentication/core/RenewalTask.java new file mode 100644 index 0000000..4b6bf98 --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/RenewalTask.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package redis.clients.authentication.core; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; + +class RenewalTask { + + private ScheduledFuture future; + + public RenewalTask(ScheduledFuture future) { + this.future = future; + } + + public Token waitForResultOrError() throws InterruptedException, ExecutionException { + return future.get(); + } + + public void cancel() { + future.cancel(true); + } +} diff --git a/core/src/main/java/redis/clients/authentication/core/Request.java b/core/src/main/java/redis/clients/authentication/core/Request.java new file mode 100644 index 0000000..adebc44 --- /dev/null +++ b/core/src/main/java/redis/clients/authentication/core/Request.java @@ -0,0 +1,15 @@ +/* + * Copyright 2024, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + */ +package redis.clients.authentication.core; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +interface Request { + + public Token getResult() throws InterruptedException, ExecutionException, TimeoutException; +} diff --git a/core/src/main/java/redis/clients/authentication/core/TokenManager.java b/core/src/main/java/redis/clients/authentication/core/TokenManager.java index e87b829..30bc1b4 100644 --- a/core/src/main/java/redis/clients/authentication/core/TokenManager.java +++ b/core/src/main/java/redis/clients/authentication/core/TokenManager.java @@ -1,99 +1,79 @@ /* - * Copyright 2024, Redis Ltd. and Contributors - * All rights reserved. - * - * Licensed under the MIT License. + * Copyright 2024, Redis Ltd. and Contributors All rights reserved. Licensed under the MIT License. */ package redis.clients.authentication.core; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public class TokenManager { private TokenManagerConfig tokenManagerConfig; - private IdentityProvider identityProvider; private TokenListener listener; - private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); - private ExecutorService executor = Executors.newFixedThreadPool(2); private boolean stopped = false; - private ScheduledFuture scheduledTask; private AtomicInteger numberOfRetries = new AtomicInteger(0); - private Exception lastException; - private Logger logger = LoggerFactory.getLogger(getClass()); private Token currentToken = null; private AtomicBoolean started = new AtomicBoolean(false); + private Dispatcher dispatcher; + private RenewalScheduler renewalScheduler; + private int retryDelay; + private int maxRetries; public TokenManager(IdentityProvider identityProvider, TokenManagerConfig tokenManagerConfig) { - this.identityProvider = identityProvider; this.tokenManagerConfig = tokenManagerConfig; + maxRetries = tokenManagerConfig.getRetryPolicy().getMaxAttempts(); + retryDelay = tokenManagerConfig.getRetryPolicy().getdelayInMs(); + renewalScheduler = new RenewalScheduler(this::renewToken); + dispatcher = new Dispatcher(identityProvider, + tokenManagerConfig.getTokenRequestExecTimeoutInMs()); } + /** + * Starts the token manager with given listener, blocks if blockForInitialToken is true + * @param listener + * @param blockForInitialToken + */ public void start(TokenListener listener, boolean blockForInitialToken) { - if (!started.compareAndSet(false, true)) { throw new AuthXException("Token manager already started!"); } this.listener = listener; - ScheduledFuture currentTask = scheduleNext(0); - scheduledTask = currentTask; + RenewalTask currentTask = renewalScheduler.scheduleNext(0); if (blockForInitialToken) { try { - while (currentTask.get() == null) { - currentTask = scheduledTask; - } - } catch (RuntimeException e) { - throw e; + renewalScheduler.waitFor(currentTask); } catch (Exception e) { throw prepareToPropogate(e); } } } - public void stop() { - stopped = true; - scheduledTask.cancel(true); - scheduler.shutdown(); - executor.shutdown(); - } - - public TokenManagerConfig getConfig() { - return tokenManagerConfig; - } - - private ScheduledFuture scheduleNext(long delay) { - // Schedule the task to run after the calculated delay - return scheduler.schedule(() -> renewToken(), delay, TimeUnit.MILLISECONDS); - } - + /** + * This method is called by the renewal scheduler + * Dispatches a request to the identity provider asynchronously, with a timeout for execution, and returns the Token if successfully acquired. + * If the request fails, it retries until the max number of retries is reached + * If the request fails after max number of retries, it throws an exception + * When a new Token is received, it schedules the next renewal with calculating the delay in respect to the new token. + * Scheduling cycle only ends under two conditions: + * 1. TokenManager is stopped + * 2. Token renewal fails for max number of retries + * @return + */ protected Token renewToken() { if (stopped) { return null; } Token newToken = null; try { - Future requestResult = executor.submit(() -> requestToken()); - newToken = requestResult.get(tokenManagerConfig.getTokenRequestExecTimeoutInMs(), - TimeUnit.MILLISECONDS); - currentToken = newToken; + currentToken = newToken = dispatcher.requestTokenAsync().getResult(); long delay = calculateRenewalDelay(newToken.getExpiresAt(), newToken.getReceivedAt()); - scheduledTask = scheduleNext(delay); + renewalScheduler.scheduleNext(delay); listener.onTokenRenewed(newToken); return newToken; } catch (Exception e) { - if (numberOfRetries.getAndIncrement() < tokenManagerConfig.getRetryPolicy() - .getMaxAttempts()) { - scheduledTask = scheduleNext(tokenManagerConfig.getRetryPolicy().getdelayInMs()); + if (numberOfRetries.getAndIncrement() < maxRetries) { + renewalScheduler.scheduleNext(retryDelay); } else { RuntimeException propogateExc = prepareToPropogate(e); listener.onError(propogateExc); @@ -103,17 +83,6 @@ protected Token renewToken() { return null; } - protected Token requestToken() { - lastException = null; - try { - return identityProvider.requestToken(); - } catch (Exception e) { - lastException = e; - logger.error("Request to identity provider failed with message: " + e.getMessage(), e); - throw e; - } - } - private RuntimeException prepareToPropogate(Exception e) { Throwable unwrapped = e; if (unwrapped instanceof ExecutionException) { @@ -122,13 +91,35 @@ private RuntimeException prepareToPropogate(Exception e) { if (unwrapped instanceof TokenRequestException) { return (RuntimeException) unwrapped; } - return new TokenRequestException(unwrapped, lastException); + return new TokenRequestException(unwrapped, dispatcher.getError()); + } + + public TokenManagerConfig getConfig() { + return tokenManagerConfig; } public Token getCurrentToken() { return currentToken; } + public void stop() { + stopped = true; + renewalScheduler.stop(); + dispatcher.stop(); + } + + /** + * This method calculates the duration we need to wait for requesting the next token. + * Token acquisition and authentication with the new token should be completed before the current token expires. + * We define a time window between a point in time(T) and the token's expiration time. Let's call this the "renewal zone." + * The goal is to trigger a token renewal anytime soon within this renewal zone. + * This is necessary to avoid situations where connections are running on an AUTH where token has already expired. + * The method calculates the delay to the renewal zone based on two different strategies and returns the minimum of them. + * If the calculated delay is somehow negative, it returns 0 to trigger the renewal immediately. + * @param expireDate + * @param issueDate + * @return + */ public long calculateRenewalDelay(long expireDate, long issueDate) { long ttlLowerRefresh = ttlForLowerRefresh(expireDate); long ttlRatioRefresh = ttlForRatioRefresh(expireDate, issueDate); @@ -137,15 +128,36 @@ public long calculateRenewalDelay(long expireDate, long issueDate) { return delay < 0 ? 0 : delay; } - public long ttlForLowerRefresh(long expireDate) { - return expireDate - tokenManagerConfig.getLowerRefreshBoundMillis() - - System.currentTimeMillis(); + /** + * This method calculates TTL to renewal zone based on a minimum duration to token expiration. + * The suggested renewal zone here starts LowerRefreshBoundMillis(given in configuration) before the token expiration time. + * As example we have 1 hour left to token expiration and LowerRefreshBoundMillis is configured as 10 minutes, renewal zone will start in 50 minutes from now. + * This is the return value, 50 minutes TTL to renewal zone. + * @param expireDate + * @return + */ + protected long ttlForLowerRefresh(long expireDate) { + long startOfRenewalZone = expireDate - tokenManagerConfig.getLowerRefreshBoundMillis(); + return startOfRenewalZone - System.currentTimeMillis(); // TTL to renewal zone } + /** + * This method calculates TTL to renewal zone based on a ratio. + * The ExpirationRefreshRatio value in config, indicates the ratio of intended usage of token's total lifetime between receive/issue time and expiration time. + * The suggested renewal zone here starts right after the token completes the given ratio of its total valid duration starting from issue time till expiration. + * As example we have a token with 1 hour total valid time and it already reach to half life, which lefts 30 minutes to token expiration. + * ExpirationRefreshRatio is configured as 0.8, means token will be in use for first 48 minutes of its valid duration. It needs to renew 12 minutes before the expiration. + * This makes it is 30 minutes left to expiration and 18 minutes left to renewal zone. + * Return value is 18 minutes TTL to renewal zone. + * @param expireDate + * @param issueDate + * @return + */ protected long ttlForRatioRefresh(long expireDate, long issueDate) { - long validDuration = expireDate - issueDate; - long refreshBefore = validDuration - - (long) (validDuration * tokenManagerConfig.getExpirationRefreshRatio()); - return expireDate - refreshBefore - System.currentTimeMillis(); + long totalLifetime = expireDate - issueDate; + long intendedUsageDuration = (long) (totalLifetime + * tokenManagerConfig.getExpirationRefreshRatio()); + long startOfRenewalZone = issueDate + intendedUsageDuration; + return startOfRenewalZone - System.currentTimeMillis(); // TTL to renewal zone } } diff --git a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java index bf44ae4..aacc2f0 100644 --- a/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java +++ b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java @@ -33,6 +33,7 @@ import redis.clients.authentication.core.TokenListener; import redis.clients.authentication.core.TokenManager; import redis.clients.authentication.core.TokenManagerConfig; +import redis.clients.authentication.core.TokenManagerConfig.RetryPolicy; import redis.clients.authentication.core.TokenRequestException; import static org.awaitility.Awaitility.await; @@ -57,6 +58,11 @@ public int getLowerRefreshBoundMillis() { public float getExpirationRefreshRatio() { return ratio; } + + @Override + public RetryPolicy getRetryPolicy() { + return new RetryPolicy(1, 1); + } } @Test @@ -140,7 +146,7 @@ public void testTokenManagerStart() System.currentTimeMillis() + 5 * 1000, System.currentTimeMillis(), null); TokenManager tokenManager = new TokenManager(identityProvider, - new TokenManagerConfig(0.7F, 200, 2000, null)); + new TokenManagerConfig(0.7F, 200, 2000, new RetryPolicy(1, 1))); TokenListener listener = mock(TokenListener.class); final Token[] tokenHolder = new Token[1]; diff --git a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java index b61167b..74c2b4e 100644 --- a/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java +++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java @@ -24,10 +24,27 @@ public final class EntraIDIdentityProvider implements IdentityProvider { - private Supplier resultSupplier; + private interface ClientApp { + public IAuthenticationResult request(); + } + + private interface ClientAppFactory { + public ClientApp create(); + } + + private ClientAppFactory clientAppFactory; + private ClientApp clientApp; public EntraIDIdentityProvider(ServicePrincipalInfo servicePrincipalInfo, Set scopes, int timeout) { + + clientAppFactory = () -> { + return createConfidentialClientApp(servicePrincipalInfo, scopes, timeout); + }; + } + + private ClientApp createConfidentialClientApp(ServicePrincipalInfo servicePrincipalInfo, + Set scopes, int timeout) { IClientCredential credential = getClientCredential(servicePrincipalInfo); ConfidentialClientApplication app; @@ -44,21 +61,32 @@ public EntraIDIdentityProvider(ServicePrincipalInfo servicePrincipalInfo, Set supplierForConfidentialApp(app, params); + return () -> requestWithConfidentialClient(app, params); } public EntraIDIdentityProvider(ManagedIdentityInfo info, Set scopes, int timeout) { + + clientAppFactory = () -> { + return createManagedIdentityApp(info, scopes, timeout); + }; + } + + private ClientApp createManagedIdentityApp(ManagedIdentityInfo info, Set scopes, + int timeout) { ManagedIdentityApplication app = ManagedIdentityApplication.builder(info.getId()) .readTimeoutForDefaultHttpClient(timeout).build(); ManagedIdentityParameters params = ManagedIdentityParameters .builder(scopes.iterator().next()).forceRefresh(true).build(); - resultSupplier = () -> supplierForManagedIdentityApp(app, params); + return () -> requestWithManagedIdentity(app, params); } public EntraIDIdentityProvider( Supplier customEntraIdAuthenticationSupplier) { - this.resultSupplier = customEntraIdAuthenticationSupplier; + + clientAppFactory = () -> { + return () -> customEntraIdAuthenticationSupplier.get(); + }; } private IClientCredential getClientCredential(ServicePrincipalInfo servicePrincipalInfo) { @@ -75,10 +103,11 @@ private IClientCredential getClientCredential(ServicePrincipalInfo servicePrinci @Override public Token requestToken() { - return new JWToken(resultSupplier.get().accessToken()); + clientApp = clientApp == null ? clientAppFactory.create() : clientApp; + return new JWToken(clientApp.request().accessToken()); } - public IAuthenticationResult supplierForConfidentialApp(ConfidentialClientApplication app, + public IAuthenticationResult requestWithConfidentialClient(ConfidentialClientApplication app, ClientCredentialParameters params) { try { Future tokenRequest = app.acquireToken(params); @@ -88,7 +117,7 @@ public IAuthenticationResult supplierForConfidentialApp(ConfidentialClientApplic } } - public IAuthenticationResult supplierForManagedIdentityApp(ManagedIdentityApplication app, + public IAuthenticationResult requestWithManagedIdentity(ManagedIdentityApplication app, ManagedIdentityParameters params) { try { Future tokenRequest = app.acquireTokenForManagedIdentity(params); diff --git a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java index a1b8fba..923080d 100644 --- a/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java +++ b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java @@ -202,7 +202,7 @@ public void initialTokenAcquisitionTest() { for (int i = 0; i < stackTrace.length; i++) { assertEquals(false, isTokenManagerStarted.get()); - if (stackTrace[i].getMethodName().equals("get") + if (stackTrace[i].getMethodName().equals("waitFor") && stackTrace[i + 1].getClassName().equals(TokenManager.class.getName()) && stackTrace[i + 1].getMethodName().equals("start")) { latch.countDown();