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/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..d395145
--- /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@v4
+
+ # 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/core_integration.yml b/.github/workflows/core_integration.yml
index ca52e91..603abc1 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/core_snapshot.yml b/.github/workflows/core_snapshot.yml
index ca7ad4f..fa44edd 100644
--- a/.github/workflows/core_snapshot.yml
+++ b/.github/workflows/core_snapshot.yml
@@ -1,46 +1,47 @@
---
-name: Publish Snapshot-Core
-
-on:
- push:
- branches:
- - main
- - '[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:
+ - main
+ - '[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/entraid_integration.yml b/.github/workflows/entraid_integration.yml
index ace1543..7e00b6c 100644
--- a/.github/workflows/entraid_integration.yml
+++ b/.github/workflows/entraid_integration.yml
@@ -1,69 +1,66 @@
----
+name: Integration-EntraID
- name: Integration-EntraID
-
- on:
- push:
- paths-ignore:
- - 'docs/**'
- - '**/*.md'
- - '**/*.rst'
- branches:
- - main
- - '[0-9].*'
- pull_request:
- branches:
- - main
- - '[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
-
\ No newline at end of file
+on:
+ push:
+ paths-ignore:
+ - 'docs/**'
+ - '**/*.md'
+ - '**/*.rst'
+ branches:
+ - main
+ - '[0-9].*'
+ pull_request:
+ branches:
+ - main
+ - '[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: 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-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: Build docs
+ run: |
+ mvn javadoc:jar
+ - name: Build with Maven
+ 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 81c19e2..aec0f3a 100644
--- a/.github/workflows/entraid_snapshot.yml
+++ b/.github/workflows/entraid_snapshot.yml
@@ -19,12 +19,6 @@
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
@@ -41,14 +35,23 @@
~/.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
run: |
mvn -q dependency:go-offline
- 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}}
-
\ No newline at end of file
diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml
new file mode 100644
index 0000000..4588835
--- /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@v4
+ - 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..d511f4a
--- /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
index 09623d6..202fe8a 100644
--- a/.github/workflows/version-and-release.yml
+++ b/.github/workflows/version-and-release.yml
@@ -11,3 +11,59 @@ jobs:
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: Install gpg key
+ run: |
+ cat <(echo -e "${{ secrets.OSSH_GPG_SECRET_KEY }}") | gpg --batch --import
+ gpg --list-secret-keys --keyid-format LONG
+
+ - 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
+ env:
+ 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 }}
+ 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}}
+ working-directory: ./entraid
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..843b73d
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,274 @@
+
+
+
+ org.sonatype.oss
+ oss-parent
+ 7
+
+
+ 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
+
+
+
+ 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
+
+
+ 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..dc0981f
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/AuthXException.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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/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/IdentityProvider.java b/core/src/main/java/redis/clients/authentication/core/IdentityProvider.java
new file mode 100644
index 0000000..be9717d
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/IdentityProvider.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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..c155af1
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/IdentityProviderConfig.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication.core;
+
+public interface IdentityProviderConfig {
+
+ public IdentityProvider getProvider();
+}
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/SimpleToken.java b/core/src/main/java/redis/clients/authentication/core/SimpleToken.java
new file mode 100644
index 0000000..e9ae58a
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/SimpleToken.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication.core;
+
+import java.util.Map;
+
+public class SimpleToken implements Token {
+
+ private String user;
+ private String value;
+ private long expiresAt;
+ private long receivedAt;
+ private 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;
+ this.claims = claims;
+ }
+
+ @Override
+ public String getUser() {
+ return user;
+ }
+
+ @Override
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public long getExpiresAt() {
+ return expiresAt;
+ }
+
+ @Override
+ public long getReceivedAt() {
+ return receivedAt;
+ }
+
+ @Override
+ 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
new file mode 100644
index 0000000..fb6142f
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/Token.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication.core;
+
+public interface Token {
+
+ public String getUser();
+
+ public String getValue();
+
+ public long getExpiresAt();
+
+ public long getReceivedAt();
+
+ public boolean isExpired();
+
+ public long ttl();
+
+ public T tryGet(String key, Class clazz);
+
+}
\ 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..9fed55c
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/TokenAuthConfig.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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 T expirationRefreshRatio(float expirationRefreshRatio) {
+ this.expirationRefreshRatio = expirationRefreshRatio;
+ return (T) this;
+ }
+
+ public T lowerRefreshBoundMillis(int lowerRefreshBoundMillis) {
+ this.lowerRefreshBoundMillis = lowerRefreshBoundMillis;
+ return (T) this;
+ }
+
+ public T tokenRequestExecTimeoutInMs(int tokenRequestExecTimeoutInMs) {
+ this.tokenRequestExecTimeoutInMs = tokenRequestExecTimeoutInMs;
+ return (T) this;
+ }
+
+ public T maxAttemptsToRetry(int maxAttemptsToRetry) {
+ this.maxAttemptsToRetry = maxAttemptsToRetry;
+ return (T) this;
+ }
+
+ public T delayInMsToRetry(int delayInMsToRetry) {
+ this.delayInMsToRetry = delayInMsToRetry;
+ return (T) this;
+ }
+
+ public T identityProviderConfig(IdentityProviderConfig identityProviderConfig) {
+ this.identityProviderConfig = identityProviderConfig;
+ return (T) this;
+ }
+
+ public TokenAuthConfig build() {
+ return new TokenAuthConfig(new TokenManagerConfig(expirationRefreshRatio,
+ lowerRefreshBoundMillis, tokenRequestExecTimeoutInMs,
+ 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/TokenListener.java b/core/src/main/java/redis/clients/authentication/core/TokenListener.java
new file mode 100644
index 0000000..9ea8c75
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/TokenListener.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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..30bc1b4
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/TokenManager.java
@@ -0,0 +1,163 @@
+/*
+ * 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.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class TokenManager {
+
+ private TokenManagerConfig tokenManagerConfig;
+ private TokenListener listener;
+ private boolean stopped = false;
+ private AtomicInteger numberOfRetries = new AtomicInteger(0);
+ 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.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;
+ RenewalTask currentTask = renewalScheduler.scheduleNext(0);
+ if (blockForInitialToken) {
+ try {
+ renewalScheduler.waitFor(currentTask);
+ } catch (Exception e) {
+ throw prepareToPropogate(e);
+ }
+ }
+ }
+
+ /**
+ * 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 {
+ currentToken = newToken = dispatcher.requestTokenAsync().getResult();
+ long delay = calculateRenewalDelay(newToken.getExpiresAt(), newToken.getReceivedAt());
+ renewalScheduler.scheduleNext(delay);
+ listener.onTokenRenewed(newToken);
+ return newToken;
+ } catch (Exception e) {
+ if (numberOfRetries.getAndIncrement() < maxRetries) {
+ renewalScheduler.scheduleNext(retryDelay);
+ } else {
+ RuntimeException propogateExc = prepareToPropogate(e);
+ listener.onError(propogateExc);
+ throw propogateExc;
+ }
+ }
+ return null;
+ }
+
+ 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, 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);
+ long delay = Math.min(ttlLowerRefresh, ttlRatioRefresh);
+
+ return delay < 0 ? 0 : delay;
+ }
+
+ /**
+ * 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 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/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..907a61a
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/TokenManagerConfig.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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..14b9f13
--- /dev/null
+++ b/core/src/main/java/redis/clients/authentication/core/TokenRequestException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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/CoreAuthenticationUnitTests.java b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java
new file mode 100644
index 0000000..aacc2f0
--- /dev/null
+++ b/core/src/test/java/redis/clients/authentication/CoreAuthenticationUnitTests.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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.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 redis.clients.authentication.core.TokenManagerConfig.RetryPolicy;
+import redis.clients.authentication.core.TokenRequestException;
+
+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;
+ }
+
+ @Override
+ public RetryPolicy getRetryPolicy() {
+ return new RetryPolicy(1, 1);
+ }
+ }
+
+ @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("user1", "tokenVal",
+ System.currentTimeMillis() + 5 * 1000, System.currentTimeMillis(), null);
+
+ TokenManager tokenManager = new TokenManager(identityProvider,
+ new TokenManagerConfig(0.7F, 200, 2000, new RetryPolicy(1, 1)));
+
+ 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)));
+
+ TokenRequestException e = assertThrows(TokenRequestException.class,
+ () -> tokenManager.start(mock(TokenListener.class), true));
+
+ assertEquals("Test exception from identity provider!", e.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("user1", "tokenValX", System.currentTimeMillis() + 50 * 1000,
+ System.currentTimeMillis(), null);
+ });
+
+ 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 delayDuration = 200;
+ int executionTimeout = 100;
+ int tokenLifetime = 50 * 1000;
+ int numberOfRetries = 5;
+ CountDownLatch requesLatch = new CountDownLatch(numberOfRetries);
+
+ IdentityProvider identityProvider = () -> {
+ requesLatch.countDown();
+ if (requesLatch.getCount() > 0) {
+ delay(delayDuration);
+ }
+ return new SimpleToken("user1", "tokenValX", System.currentTimeMillis() + tokenLifetime,
+ System.currentTimeMillis(), null);
+ };
+
+ 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());
+ });
+ }
+
+ private void delay(long durationInMs) {
+ try {
+ Thread.sleep(durationInMs);
+ } catch (InterruptedException e) {
+ }
+ }
+}
diff --git a/entraid/pom.xml b/entraid/pom.xml
new file mode 100644
index 0000000..d08b08c
--- /dev/null
+++ b/entraid/pom.xml
@@ -0,0 +1,282 @@
+
+
+
+ org.sonatype.oss
+ oss-parent
+ 7
+
+
+ 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-SNAPSHOT
+
+
+ com.microsoft.azure
+ msal4j
+ 1.17.2
+
+
+ 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
+
+
+
+ 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
+
+
+ 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..74c2b4e
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProvider.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication.entraid;
+
+import java.net.MalformedURLException;
+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 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;
+
+ try {
+ String authority = servicePrincipalInfo.getAuthority();
+ authority = authority == null ? ConfidentialClientApplication.DEFAULT_AUTHORITY
+ : authority;
+ app = ConfidentialClientApplication
+ .builder(servicePrincipalInfo.getClientId(), credential).authority(authority)
+ .readTimeoutForDefaultHttpClient(timeout).build();
+ } catch (MalformedURLException e) {
+ throw new RedisEntraIDException("Failed to init EntraID client!", e);
+ }
+ ClientCredentialParameters params = ClientCredentialParameters.builder(scopes)
+ .skipCache(true).build();
+
+ 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();
+ return () -> requestWithManagedIdentity(app, params);
+ }
+
+ public EntraIDIdentityProvider(
+ Supplier customEntraIdAuthenticationSupplier) {
+
+ clientAppFactory = () -> {
+ return () -> customEntraIdAuthenticationSupplier.get();
+ };
+ }
+
+ 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() {
+ clientApp = clientApp == null ? clientAppFactory.create() : clientApp;
+ return new JWToken(clientApp.request().accessToken());
+ }
+
+ public IAuthenticationResult requestWithConfidentialClient(ConfidentialClientApplication app,
+ ClientCredentialParameters params) {
+ try {
+ Future tokenRequest = app.acquireToken(params);
+ return tokenRequest.get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RedisEntraIDException("Failed to acquire token!", e);
+ }
+ }
+
+ public IAuthenticationResult requestWithManagedIdentity(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
new file mode 100644
index 0000000..a9b57f7
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDIdentityProviderConfig.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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 {
+
+ private final Supplier providerSupplier;
+
+ public EntraIDIdentityProviderConfig(ServicePrincipalInfo info, Set scopes, int timeout) {
+ providerSupplier = () -> new EntraIDIdentityProvider(info, scopes, timeout);
+ }
+
+ public EntraIDIdentityProviderConfig(ManagedIdentityInfo info, Set scopes, int timeout) {
+ providerSupplier = () -> new EntraIDIdentityProvider(info, scopes, timeout);
+ }
+
+ public EntraIDIdentityProviderConfig(
+ Supplier customEntraIdAuthenticationSupplier) {
+ providerSupplier = () -> new EntraIDIdentityProvider(customEntraIdAuthenticationSupplier);
+ }
+
+ @Override
+ public IdentityProvider getProvider() {
+ IdentityProvider identityProvider = providerSupplier.get();
+ return identityProvider;
+ }
+}
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..d74c002
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/EntraIDTokenAuthConfigBuilder.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication.entraid;
+
+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.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 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;
+ 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;
+ private int tokenRequestExecTimeoutInMs;
+ private Supplier customEntraIdAuthenticationSupplier;
+
+ 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.cert = cert;
+ 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 customEntraIdAuthenticationSupplier(
+ Supplier customEntraIdAuthenticationSupplier) {
+ this.customEntraIdAuthenticationSupplier = customEntraIdAuthenticationSupplier;
+ return this;
+ }
+
+ public EntraIDTokenAuthConfigBuilder scopes(Set scopes) {
+ this.scopes = 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) {
+ 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!");
+ }
+ 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, tokenRequestExecTimeoutInMs));
+ }
+ if (mii != null) {
+ super.identityProviderConfig(
+ new EntraIDIdentityProviderConfig(mii, scopes, tokenRequestExecTimeoutInMs));
+ }
+ if (customEntraIdAuthenticationSupplier != null) {
+ super.identityProviderConfig(
+ new EntraIDIdentityProviderConfig(customEntraIdAuthenticationSupplier));
+ }
+ return super.build();
+ }
+
+ @Override
+ public void close() throws Exception {
+ clientId = null;
+ secret = null;
+ key = null;
+ cert = null;
+ authority = null;
+ scopes = null;
+ customEntraIdAuthenticationSupplier = null;
+ }
+
+ 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/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..72cb576
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/JWToken.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+package redis.clients.authentication.entraid;
+
+import java.util.function.BiFunction;
+
+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 user;
+ private final String token;
+ private final long expiresAt;
+ private final long receivedAt;
+ 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, clazz) -> jwt.getClaim(key).as(clazz);
+ }
+
+ @Override
+ public boolean isExpired() {
+ return System.currentTimeMillis() > expiresAt;
+ }
+
+ @Override
+ public long ttl() {
+ return expiresAt - System.currentTimeMillis();
+ }
+
+ @Override
+ public String getUser() {
+ return user;
+ }
+
+ @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 T tryGet(String key, Class clazz) {
+ return (T) claimQuery.apply(key, clazz);
+ }
+
+}
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..c90173a
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/ManagedIdentityInfo.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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/RedisEntraIDException.java b/entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java
new file mode 100644
index 0000000..2248db1
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/RedisEntraIDException.java
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+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/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..6840b01
--- /dev/null
+++ b/entraid/src/main/java/redis/clients/authentication/entraid/ServicePrincipalInfo.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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/EntraIDIntegrationTests.java b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java
new file mode 100644
index 0000000..ced17c3
--- /dev/null
+++ b/entraid/src/test/java/redis/clients/authentication/EntraIDIntegrationTests.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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(), 1000).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(),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
new file mode 100644
index 0000000..923080d
--- /dev/null
+++ b/entraid/src/test/java/redis/clients/authentication/EntraIDUnitTests.java
@@ -0,0 +1,623 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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.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;
+import java.util.Date;
+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;
+import org.junit.Test;
+import org.mockito.MockedConstruction;
+
+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;
+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.ManagedIdentityInfo;
+import redis.clients.authentication.entraid.ServicePrincipalInfo;
+import redis.clients.authentication.entraid.ManagedIdentityInfo.UserManagedIdentityType;
+
+public class EntraIDUnitTests {
+
+ 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_OID, TOKEN_VALUE, TOKEN_EXPIRATION_TIME,
+ TOKEN_ISSUE_TIME, null);
+
+ private TestContext testCtx = TestContext.DEFAULT;
+
+ @Test
+ public void testConfigBuilder() {
+ String authority = "authority1";
+ String clientId = "clientId1";
+ String credential = "credential1";
+ Set scopes = Collections.singleton("scope1");
+ IdentityProviderConfig configWithSecret = EntraIDTokenAuthConfigBuilder.builder()
+ .authority(authority).clientId(clientId).secret(credential).scopes(scopes).build()
+ .getIdentityProviderConfig();
+ assertNotNull(configWithSecret);
+ try (MockedConstruction mockedConstructor = mockConstruction(
+ 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));
+
+ })) {
+ 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) -> {
+ 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));
+
+ })) {
+ configWithCert.getProvider();
+ }
+
+ IdentityProviderConfig configWithManagedId = EntraIDTokenAuthConfigBuilder.builder()
+ .systemAssignedManagedIdentity().scopes(scopes).build().getIdentityProviderConfig();
+ assertNotNull(configWithManagedId);
+ try (MockedConstruction mockedConstructor = mockConstruction(
+ EntraIDIdentityProvider.class, (mock, context) -> {
+ ManagedIdentityInfo info = (ManagedIdentityInfo) context.arguments().get(0);
+ assertEquals(ManagedIdentityId.systemAssigned().getIdType(),
+ info.getId().getIdType());
+ assertEquals(scopes, context.arguments().get(1));
+ })) {
+ configWithManagedId.getProvider();
+ }
+ }
+
+ // 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().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("waitFor")
+ && 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 * 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
+ // 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_OID, TOKEN_VALUE,
+ System.currentTimeMillis() + 1000, System.currentTimeMillis(), null);
+
+ 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.
+ // T.4.1
+ // Verify that token renewal timing can be configured correctly.
+ @Test
+ public void customRenewalTimingTest() {
+ AtomicInteger numberOfTokens = new AtomicInteger(0);
+ AtomicInteger timeDiff = new AtomicInteger(0);
+
+ IdentityProvider identityProvider = () -> new SimpleToken(TOKEN_OID, TOKEN_VALUE,
+ System.currentTimeMillis() + 1000, System.currentTimeMillis(), null);
+
+ 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).
+ // T.4.1
+ // Verify that token renewal timing can be configured correctly.
+ @Test
+ public void highPercentage_edgeCaseRenewalTimingTest() {
+ List tokens = new ArrayList();
+ int validDurationInMs = 1000;
+
+ 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,
+ 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).
+ // T.4.1
+ // Verify that token renewal timing can be configured correctly.
+ @Test
+ public void lowPercentage_edgeCaseRenewalTimingTest() {
+ List tokens = new ArrayList();
+ int validDurationInMs = 1000;
+
+ 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,
+ 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
+ // 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.2.5
+ // Ensure that token objects are immutable and cannot be modified after creation.
+ @Test
+ public void tokenImmutabilityTest() {
+ // TODO : what is expected exatcly ?
+ }
+
+ // 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());
+ }
+
+ // 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("user1", "" + numberOfTokens.incrementAndGet(),
+ System.currentTimeMillis() + 500, System.currentTimeMillis(), null);
+ };
+ 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.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 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) {
+ 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
new file mode 100644
index 0000000..79c7829
--- /dev/null
+++ b/entraid/src/test/java/redis/clients/authentication/TestContext.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2024, Redis Ltd. and Contributors
+ * All rights reserved.
+ *
+ * Licensed under the MIT License.
+ */
+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.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+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 AZURE_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID = "AZURE_USER_ASSIGNED_MANAGED_IDENTITY_CLIENT_ID";
+
+ private String clientId;
+ private String authority;
+ private String clientSecret;
+ private PrivateKey privateKey;
+ private X509Certificate cert;
+ private Set redisScopes;
+ private String userAssignedManagedIdentityClientId;
+
+ public static final TestContext DEFAULT = new TestContext();
+
+ 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,
+ 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() {
+ 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() {
+ if (redisScopes == null) {
+ String redisScopesEnv = System.getenv(AZURE_REDIS_SCOPES);
+ this.redisScopes = new HashSet<>(Arrays.asList(redisScopesEnv.split(";")));
+ }
+ return redisScopes;
+ }
+
+ public String getUserAssignedManagedIdentityClientId() {
+ return userAssignedManagedIdentityClientId;
+ }
+
+ private 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 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/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
+
+
+