diff --git a/.github/actions/get-prerelease/action.yml b/.github/actions/get-prerelease/action.yml new file mode 100644 index 0000000..7755024 --- /dev/null +++ b/.github/actions/get-prerelease/action.yml @@ -0,0 +1,24 @@ +name: Return a boolean indicating if the version contains prerelease identifiers + +inputs: + version: + required: true + +outputs: + prerelease: + value: ${{ steps.get_prerelease.outputs.PRERELEASE }} + +runs: + using: composite + + steps: + - id: get_prerelease + shell: bash + run: | + if [[ "${VERSION}" == *"beta"* || "${VERSION}" == *"alpha"* ]]; then + echo "PRERELEASE=true" >> $GITHUB_OUTPUT + else + echo "PRERELEASE=false" >> $GITHUB_OUTPUT + fi + env: + VERSION: ${{ inputs.version }} diff --git a/.github/actions/get-release-notes/action.yml b/.github/actions/get-release-notes/action.yml new file mode 100644 index 0000000..a62a19d --- /dev/null +++ b/.github/actions/get-release-notes/action.yml @@ -0,0 +1,37 @@ +name: Return the release notes extracted from the body of the PR associated with the release. + +inputs: + version: + required: true + repo_name: + required: false + repo_owner: + required: true + token: + required: true + +outputs: + release-notes: + value: ${{ steps.get_release_notes.outputs.RELEASE_NOTES }} + +runs: + using: composite + + steps: + - uses: actions/github-script@v7 + id: get_release_notes + with: + result-encoding: string + script: | + const { data: pulls } = await github.rest.pulls.list({ + owner: process.env.REPO_OWNER, + repo: process.env.REPO_NAME, + state: 'all', + head: `${process.env.REPO_OWNER}:release/${process.env.VERSION}`, + }); + core.setOutput('RELEASE_NOTES', pulls[0].body); + env: + GITHUB_TOKEN: ${{ inputs.token }} + REPO_OWNER: ${{ inputs.repo_owner }} + REPO_NAME: ${{ inputs.repo_name }} + VERSION: ${{ inputs.version }} diff --git a/.github/actions/get-version/action.yml b/.github/actions/get-version/action.yml new file mode 100644 index 0000000..a1ac9d4 --- /dev/null +++ b/.github/actions/get-version/action.yml @@ -0,0 +1,15 @@ +name: Return the version extracted from the branch name + +outputs: + version: + value: ${{ steps.get_version.outputs.VERSION }} + +runs: + using: composite + + steps: + - id: get_version + shell: bash + run: | + VERSION=$(head -1 .version) + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT diff --git a/.github/actions/maven-publish/action.yml b/.github/actions/maven-publish/action.yml new file mode 100644 index 0000000..4c8614b --- /dev/null +++ b/.github/actions/maven-publish/action.yml @@ -0,0 +1,39 @@ +name: Publish release to Java + +inputs: + java-version: + required: true + ossr-username: + required: true + ossr-token: + required: true + signing-key: + required: true + signing-password: + required: true + +runs: + using: composite + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Java + shell: bash + run: | + curl -s "https://get.sdkman.io" | bash + source "/home/runner/.sdkman/bin/sdkman-init.sh" + sdk list java + sdk install java ${{ inputs.java-version }} && sdk default java ${{ inputs.java-version }} + + - uses: gradle/actions/wrapper-validation@v5 + + - name: Publish Packages to Maven + shell: bash + run: ./gradlew publishToSonatype closeSonatypeStagingRepository -PisSnapshot=false --stacktrace + env: + MAVEN_USERNAME: ${{ inputs.ossr-username }} + MAVEN_PASSWORD: ${{ inputs.ossr-token }} + SIGNING_KEY: ${{ inputs.signing-key }} + SIGNING_PASSWORD: ${{ inputs.signing-password }} diff --git a/.github/actions/release-create/action.yml b/.github/actions/release-create/action.yml new file mode 100644 index 0000000..2f96679 --- /dev/null +++ b/.github/actions/release-create/action.yml @@ -0,0 +1,41 @@ +name: Create a GitHub release + +inputs: + token: + required: true + files: + required: false + name: + required: true + body: + required: true + tag: + required: true + commit: + required: true + draft: + default: false + required: false + prerelease: + default: false + required: false + fail_on_unmatched_files: + default: true + required: false + +runs: + using: composite + + steps: + - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + with: + body: ${{ inputs.body }} + name: ${{ inputs.name }} + tag_name: ${{ inputs.tag }} + target_commitish: ${{ inputs.commit }} + draft: ${{ inputs.draft }} + prerelease: ${{ inputs.prerelease }} + fail_on_unmatched_files: ${{ inputs.fail_on_unmatched_files }} + files: ${{ inputs.files }} + env: + GITHUB_TOKEN: ${{ inputs.token }} diff --git a/.github/actions/rl-scanner/action.yml b/.github/actions/rl-scanner/action.yml new file mode 100644 index 0000000..03c378a --- /dev/null +++ b/.github/actions/rl-scanner/action.yml @@ -0,0 +1,71 @@ +name: "Reversing Labs Scanner" +description: "Runs the Reversing Labs scanner on a specified artifact." +inputs: + artifact-path: + description: "Path to the artifact to be scanned." + required: true + version: + description: "Version of the artifact." + required: true + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install Python dependencies + shell: bash + run: | + pip install boto3 requests + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: ${{ env.PRODSEC_TOOLS_ARN }} + aws-region: us-east-1 + mask-aws-account-id: true + + - name: Install RL Wrapper + shell: bash + run: | + pip install rl-wrapper>=1.0.0 --index-url "https://${{ env.PRODSEC_TOOLS_USER }}:${{ env.PRODSEC_TOOLS_TOKEN }}@a0us.jfrog.io/artifactory/api/pypi/python-local/simple" + + - name: Run RL Scanner + shell: bash + env: + RLSECURE_LICENSE: ${{ env.RLSECURE_LICENSE }} + RLSECURE_SITE_KEY: ${{ env.RLSECURE_SITE_KEY }} + SIGNAL_HANDLER_TOKEN: ${{ env.SIGNAL_HANDLER_TOKEN }} + PYTHONUNBUFFERED: 1 + run: | + if [ ! -f "${{ inputs.artifact-path }}" ]; then + echo "Artifact not found: ${{ inputs.artifact-path }}" + exit 1 + fi + + rl-wrapper \ + --artifact "${{ inputs.artifact-path }}" \ + --name "${{ github.event.repository.name }}" \ + --version "${{ inputs.version }}" \ + --repository "${{ github.repository }}" \ + --commit "${{ github.sha }}" \ + --build-env "github_actions" \ + --suppress_output + + # Check the outcome of the scanner + if [ $? -ne 0 ]; then + echo "RL Scanner failed." + echo "scan-status=failed" >> $GITHUB_ENV + exit 1 + else + echo "RL Scanner passed." + echo "scan-status=success" >> $GITHUB_ENV + fi + +outputs: + scan-status: + description: "The outcome of the scan process." + value: ${{ env.scan-status }} diff --git a/.github/actions/tag-exists/action.yml b/.github/actions/tag-exists/action.yml new file mode 100644 index 0000000..a319dc4 --- /dev/null +++ b/.github/actions/tag-exists/action.yml @@ -0,0 +1,30 @@ +name: Return a boolean indicating if a tag already exists for the repository + +inputs: + token: + required: true + tag: + required: true + +outputs: + exists: + description: "Whether the tag exists or not" + value: ${{ steps.tag-exists.outputs.EXISTS }} + +runs: + using: composite + + steps: + - id: tag-exists + shell: bash + run: | + GET_API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG_NAME}" + http_status_code=$(curl -LI $GET_API_URL -o /dev/null -w '%{http_code}\n' -s -H "Authorization: token ${GITHUB_TOKEN}") + if [ "$http_status_code" -ne "404" ] ; then + echo "EXISTS=true" >> $GITHUB_OUTPUT + else + echo "EXISTS=false" >> $GITHUB_OUTPUT + fi + env: + TAG_NAME: ${{ inputs.tag }} + GITHUB_TOKEN: ${{ inputs.token }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d885879 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index fbc6433..87cc7e9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -10,21 +10,25 @@ jobs: gradle: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 - name: Set up Gradle - uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1 + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 - name: Test and Assemble with Gradle run: ./gradlew assemble check --continue --console=plain - - uses: actions/upload-artifact@v4 + - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + with: + flags: unittests + + - uses: actions/upload-artifact@v5 with: name: Reports path: | - packages/auth0-api-java/build/reports/ - packages/auth0-springboot-api/build/reports/ + auth0-api-java/build/reports/ + auth0-springboot-api/build/reports/ diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 0000000..6f97702 --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,10 @@ +name: "Validate Gradle Wrapper" +on: [pull_request] + +jobs: + validation: + name: "validation/gradlew" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: gradle/actions/wrapper-validation@v5 diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml new file mode 100644 index 0000000..132f104 --- /dev/null +++ b/.github/workflows/java-release.yml @@ -0,0 +1,86 @@ +name: Create Java and GitHub Release + +on: + workflow_call: + inputs: + java-version: + required: true + type: string + + secrets: + ossr-username: + required: true + ossr-token: + required: true + signing-key: + required: true + signing-password: + required: true + github-token: + required: true + +### TODO: Replace instances of './.github/actions/' w/ `auth0/dx-sdk-actions/` and append `@latest` after the +### common `dx-sdk-actions` repo is made public. +### TODO: Also remove `get-prerelease`, `get-version`, `release-create`, `tag-create` and `tag-exists` actions from +### this repo's .github/actions folder once the repo is public. + +jobs: + release: + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) + runs-on: ubuntu-latest + environment: release + + steps: + # Checkout the code + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + # Get the version from the branch name + - id: get_version + uses: ./.github/actions/get-version + + # Get the prerelease flag from the branch name + - id: get_prerelease + uses: ./.github/actions/get-prerelease + with: + version: ${{ steps.get_version.outputs.version }} + + # Get the release notes + - id: get_release_notes + uses: ./.github/actions/get-release-notes + with: + token: ${{ secrets.github-token }} + version: ${{ steps.get_version.outputs.version }} + repo_owner: ${{ github.repository_owner }} + repo_name: ${{ github.event.repository.name }} + + # Check if the tag already exists + - id: tag_exists + uses: ./.github/actions/tag-exists + with: + tag: ${{ steps.get_version.outputs.version }} + token: ${{ secrets.github-token }} + + # If the tag already exists, exit with an error + - if: steps.tag_exists.outputs.exists == 'true' + run: exit 1 + + # Publish the release to Maven + - uses: ./.github/actions/maven-publish + with: + java-version: ${{ inputs.java-version }} + ossr-username: ${{ secrets.ossr-username }} + ossr-token: ${{ secrets.ossr-token }} + signing-key: ${{ secrets.signing-key }} + signing-password: ${{ secrets.signing-password }} + + # Create a release for the tag + - uses: ./.github/actions/release-create + with: + token: ${{ secrets.github-token }} + name: ${{ steps.get_version.outputs.version }} + body: ${{ steps.get_release_notes.outputs.release-notes }} + tag: ${{ steps.get_version.outputs.version }} + commit: ${{ github.sha }} + prerelease: ${{ steps.get_prerelease.outputs.prerelease }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b145ab1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Create GitHub Release + +on: + pull_request: + types: + - closed + workflow_dispatch: + +permissions: + contents: write + id-token: write # This is required for requesting the JWT + +### TODO: Replace instances of './.github/workflows/' w/ `auth0/dx-sdk-actions/workflows/` and append `@latest` after the common +### `dx-sdk-actions` repo is made public. +### TODO: Also remove `get-prerelease`, `get-release-notes`, `get-version`, `maven-publish`, `release-create`, and +### `tag-exists` actions from this repo's .github/actions folder once the repo is public. +### TODO: Also remove `java-release` workflow from this repo's .github/workflows +### folder once the repo is public. + +jobs: + rl-scanner: + uses: ./.github/workflows/rl-scanner.yml + with: + java-version: "17" + artifact-name: "auth0-springboot-api.tgz" + secrets: + RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} + RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} + SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} + PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} + PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} + PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} + + release: + uses: ./.github/workflows/java-release.yml + needs: rl-scanner + with: + java-version: "17.0.9-tem" + secrets: + ossr-username: ${{ secrets.OSSR_USERNAME }} + ossr-token: ${{ secrets.OSSR_TOKEN }} + signing-key: ${{ secrets.SIGNING_KEY }} + signing-password: ${{ secrets.SIGNING_PASSWORD }} + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/rl-scanner.yml b/.github/workflows/rl-scanner.yml new file mode 100644 index 0000000..7088395 --- /dev/null +++ b/.github/workflows/rl-scanner.yml @@ -0,0 +1,71 @@ +name: RL-Secure Workflow + +on: + workflow_call: + inputs: + java-version: + required: true + type: string + artifact-name: + required: true + type: string + secrets: + RLSECURE_LICENSE: + required: true + RLSECURE_SITE_KEY: + required: true + SIGNAL_HANDLER_TOKEN: + required: true + PRODSEC_TOOLS_USER: + required: true + PRODSEC_TOOLS_TOKEN: + required: true + PRODSEC_TOOLS_ARN: + required: true + +jobs: + checkout-build-scan-only: + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) + runs-on: ubuntu-latest + outputs: + scan-status: ${{ steps.rl-scan-conclusion.outcome }} + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ inputs.java-version }} + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 + + - name: Test and Assemble with Gradle + run: ./gradlew assemble check --continue --console=plain + + - id: get_version + uses: ./.github/actions/get-version + + - name: Create tgz build artifact + run: | + tar -czvf ${{ inputs.artifact-name }} * + + - name: Run RL Scanner + id: rl-scan-conclusion + uses: ./.github/actions/rl-scanner + with: + artifact-path: "$(pwd)/${{ inputs.artifact-name }}" + version: "${{ steps.get_version.outputs.version }}" + env: + RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} + RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} + SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} + PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} + PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} + PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} + + - name: Output scan result + run: echo "scan-status=${{ steps.rl-scan-conclusion.outcome }}" >> $GITHUB_ENV diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 0000000..d818e89 --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,39 @@ +name: Snyk + +on: + merge_group: + workflow_dispatch: + pull_request: + types: + - opened + - synchronize + push: + branches: + - master + - main + schedule: + - cron: "30 0 1,15 * *" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/main' }} + +jobs: + check: + name: Check for Vulnerabilities + runs-on: ubuntu-latest + + steps: + - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' + run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artificially flag as successful, as this is a required check for branch protection. + + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + + - uses: snyk/actions/gradle-jdk17@9adf32b1121593767fc3c057af55b55db032dc04 # pin@1.0.0 + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} diff --git a/README.md b/README.md index 3649f1e..0a95c31 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,10 @@ This repository contains multiple modules designed for different use cases: ### Core Modules -| Module | Description | Java Version | Status | -| ------------------------------------------------------------------------- | --------------------------------------------- | ------------ | ---------------- | -| **[auth0-api-java](./auth0-api-java/)** | Core JWT validation library with DPoP support | Java 8+ | 🔒 **Internal** | -| **[auth0-springboot-api](./auth0-springboot-api/)** | Spring Boot auto-configuration and filters | Java 17+ | 📦 **Published** | -| **[auth0-springboot-api-playground](./auth0-springboot-api-playground/)** | Working example application | Java 17+ | Example | +| Module | Description | Java Version | +| ------------------------------------------------------------------------- | --------------------------------------------- | ------------ | +| **[auth0-springboot-api](./auth0-springboot-api/)** | Spring Boot auto-configuration and filters | Java 17+ | +| **[auth0-springboot-api-playground](./auth0-springboot-api-playground/)** | Working example application | Java 17+ | ### Module Relationship @@ -67,9 +66,8 @@ This project uses Gradle with a multi-module setup: # Build all modules ./gradlew build -# Build specific module +# Build module ./gradlew :auth0-springboot-api:build -./gradlew :auth0-api-java:build # Run tests ./gradlew test @@ -85,7 +83,6 @@ Only the Spring Boot integration module is published as a public artifact: | Module | Group ID | Artifact ID | Version | Status | | ---------------------- | ----------- | ---------------------- | ---------------- | ---------------- | | `auth0-springboot-api` | `com.auth0` | `auth0-springboot-api` | `1.0.0-SNAPSHOT` | 📦 **Published** | -| `auth0-api-java` | `com.auth0` | `auth0-api-java` | `1.0.0-SNAPSHOT` | 🔒 **Internal** | The core library (`auth0-api-java`) is bundled as an internal dependency within the Spring Boot module and is not published separately. diff --git a/auth0-api-java/JWT_VALIDATION_GUIDE.md b/auth0-api-java/JWT_VALIDATION_GUIDE.md deleted file mode 100644 index 3da1290..0000000 --- a/auth0-api-java/JWT_VALIDATION_GUIDE.md +++ /dev/null @@ -1,247 +0,0 @@ -# JWT Validation with Auth0 Java SDK - -This document demonstrates how to use the JWT validation functionality in the auth0-api-java module. - -## Overview - -The `JWTValidator` class provides comprehensive JWT token validation using: - -- **java-jwt**: For creating, decoding, and verifying JWTs -- **jwks-rsa**: For retrieving RSA public keys from JWKS endpoints - -## Quick Start - -### 1. Basic JWT Validation - -```java -import com.auth0.jwt.JWTValidator; -import com.auth0.jwt.interfaces.DecodedJWT; -import com.auth0.exception.JWTValidationException; - -// Initialize validator -JWTValidator validator = new JWTValidator("your-tenant.auth0.com", "https://your-api.example.com"); - -// Validate a JWT token -try { - DecodedJWT jwt = validator.validateToken(jwtToken); - System.out.println("Token valid! User: " + jwt.getSubject()); -} catch (JWTValidationException e) { - System.err.println("Invalid token: " + e.getMessage()); -} -``` - -### 2. Validation with Scope Checking - -```java -import java.util.Arrays; -import java.util.List; - -List requiredScopes = Arrays.asList("read:users", "write:users"); - -try { - DecodedJWT jwt = validator.validateTokenWithScopes(jwtToken, requiredScopes); - System.out.println("Token valid with required scopes!"); -} catch (JWTValidationException e) { - System.err.println("Token missing required scopes: " + e.getMessage()); -} -``` - -### 3. Token Decoding Without Verification - -```java -// Useful for inspecting token contents without verification -try { - DecodedJWT jwt = validator.decodeToken(jwtToken); - System.out.println("Token issuer: " + jwt.getIssuer()); - System.out.println("Token subject: " + jwt.getSubject()); - System.out.println("Token expires: " + jwt.getExpiresAt()); -} catch (JWTValidationException e) { - System.err.println("Cannot decode token: " + e.getMessage()); -} -``` - -## Complete Validation Flow - -The JWT validation follows this process: - -1. **Decode token header** to extract the Key ID (`kid`) -2. **Fetch public key** from JWKS endpoint using the `kid` -3. **Create RSA256 algorithm** instance with the public key -4. **Build JWT verifier** with expected issuer and audience -5. **Verify signature and claims** of the token - -```java -// This is what happens internally in validateToken(): - -// 1. Decode the token header and payload without verifying the signature -DecodedJWT decodedJWT = JWT.decode(token); -String kid = decodedJWT.getKeyId(); - -// 2. Fetch the public key using the 'kid' from the JWKS endpoint -Jwk jwk = jwkProvider.get(kid); -RSAPublicKey publicKey = (RSAPublicKey) jwk.getPublicKey(); - -// 3. Create an RSA256 algorithm instance using the public key -Algorithm algorithm = Algorithm.RSA256(publicKey, null); - -// 4. Build verifier with the expected issuer -JWTVerifier verifier = JWT.require(algorithm) - .withIssuer(issuer) - .withAudience(audience) - .build(); - -// 5. Verify the token's signature and claims -DecodedJWT jwt = verifier.verify(token); -``` - -## Web Service Integration Example - -### Middleware Pattern - -```java -public class JWTAuthMiddleware { - private final JWTValidator jwtValidator; - - public JWTAuthMiddleware(String domain, String audience) throws MalformedURLException { - this.jwtValidator = new JWTValidator(domain, audience); - } - - public DecodedJWT validateRequest(String authorizationHeader) throws JWTValidationException { - // Extract JWT from "Bearer " format - String token = extractJWTFromAuthorizationHeader(authorizationHeader); - if (token == null) { - throw new JWTValidationException("Missing or invalid Authorization header"); - } - - return jwtValidator.validateToken(token); - } - - private String extractJWTFromAuthorizationHeader(String authHeader) { - if (authHeader != null && authHeader.startsWith("Bearer ")) { - return authHeader.substring(7); // Remove "Bearer " prefix - } - return null; - } -} -``` - -### Usage in API Endpoints - -```java -// In your API controller/service -JWTAuthMiddleware authMiddleware = new JWTAuthMiddleware("your-tenant.auth0.com", "https://your-api.com"); - -public void handleApiRequest(HttpServletRequest request) { - try { - String authHeader = request.getHeader("Authorization"); - DecodedJWT jwt = authMiddleware.validateRequest(authHeader); - - // Extract user information - String userId = jwt.getSubject(); - String userEmail = jwt.getClaim("email").asString(); - - // Proceed with business logic - processRequest(userId, userEmail); - - } catch (JWTValidationException e) { - // Return 401 Unauthorized - response.sendError(401, "Invalid or expired token"); - } -} -``` - -## Error Handling - -The `JWTValidator` throws `JWTValidationException` for various scenarios: - -- **Missing Key ID**: Token header doesn't contain `kid` claim -- **Invalid Signature**: Token signature doesn't match JWKS public key -- **Expired Token**: Token `exp` claim is in the past -- **Invalid Issuer**: Token `iss` claim doesn't match expected issuer -- **Invalid Audience**: Token `aud` claim doesn't match expected audience -- **Missing Scopes**: Token doesn't contain required scopes in `scope` claim -- **JWKS Fetch Error**: Unable to retrieve public key from JWKS endpoint - -## Testing - -### Unit Tests - -The project includes comprehensive unit tests in `JWTValidatorTest.java` that demonstrate: - -- ✅ Successful token validation -- ✅ Scope-based validation -- ✅ Token decoding without verification -- ❌ Expired token handling -- ❌ Missing scope validation -- ❌ Invalid signature detection - -### Running Tests - -```bash -./gradlew test -``` - -### Test Output Examples - -``` -✅ Example 1: Successfully validated JWT token - Token subject: test-user - Token issuer: https://test-domain.auth0.com/ - Token audience: [https://api.example.com] - -✅ Example 2: Successfully validated JWT token with scopes - Token scopes: read:users write:users admin:system - Required scopes: [read:users, write:users] - -❌ Example 4: Correctly rejected expired token - Error message: JWT verification failed: The Token has expired on... -``` - -## Configuration - -### Environment Setup - -1. **Add Dependencies** (already added to `build.gradle`): - - ```gradle - implementation 'com.auth0:java-jwt:4.4.0' - implementation 'com.auth0:jwks-rsa:0.22.1' - ``` - -2. **Configure Auth0 Settings**: - - - Domain: Your Auth0 tenant domain (e.g., `your-tenant.auth0.com`) - - Audience: Your API identifier (configured in Auth0 Dashboard) - -3. **Initialize Validator**: - ```java - JWTValidator validator = new JWTValidator(domain, audience); - ``` - -### Custom JWKS Provider - -For advanced scenarios, you can provide a custom JWKS provider: - -```java -JwkProvider customProvider = new UrlJwkProvider(new URL("https://custom-jwks-url/.well-known/jwks.json")); -JWTValidator validator = new JWTValidator(issuer, audience, customProvider); -``` - -## Best Practices - -1. **Cache Validator Instance**: Create validator once and reuse it -2. **Handle Exceptions**: Always wrap validation in try-catch blocks -3. **Validate Scopes**: Check required permissions for each API endpoint -4. **Log Validation Events**: Log both successful and failed validations -5. **Token Extraction**: Always validate Authorization header format -6. **Performance**: The JWKS provider caches public keys automatically - -## Security Considerations - -- ✅ Always validate tokens on the server side -- ✅ Use HTTPS for all JWKS endpoint communications -- ✅ Verify issuer and audience claims match your configuration -- ✅ Check token expiration times -- ✅ Validate required scopes for API access -- ❌ Never trust client-side token validation alone -- ❌ Don't expose sensitive information in JWT claims diff --git a/auth0-api-java/build.gradle b/auth0-api-java/build.gradle index b0ca294..020d940 100644 --- a/auth0-api-java/build.gradle +++ b/auth0-api-java/build.gradle @@ -6,11 +6,8 @@ group = 'com.auth0' version = '1.0.0-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(8) - } - withSourcesJar() - withJavadocJar() + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } tasks.withType(Javadoc) { diff --git a/auth0-springboot-api/build.gradle b/auth0-springboot-api/build.gradle index 4cd4e07..87fa857 100644 --- a/auth0-springboot-api/build.gradle +++ b/auth0-springboot-api/build.gradle @@ -2,18 +2,18 @@ plugins { id 'java-library' id 'org.springframework.boot' version '3.2.0' id 'io.spring.dependency-management' version '1.1.4' - id 'maven-publish' } -group = 'com.auth0' -version = '1.0.0-SNAPSHOT' +apply from: rootProject.file('gradle/versioning.gradle') +apply from: rootProject.file('gradle/maven-publish.gradle') + +group = GROUP +version = getVersionFromFile() java { toolchain { languageVersion = JavaLanguageVersion.of(17) } - withSourcesJar() - withJavadocJar() } dependencies { @@ -50,37 +50,5 @@ jar { archiveClassifier = '' } -publishing { - publications { - maven(MavenPublication) { - from components.java - - pom { - name = 'Auth0 Spring Boot API' - description = 'Spring Boot integration for Auth0 Java SDK' - url = 'https://github.com/atko-cic/auth0-auth-java' - - licenses { - license { - name = 'MIT License' - url = 'https://opensource.org/licenses/MIT' - } - } - - developers { - developer { - id = 'auth0' - name = 'Auth0 Team' - email = 'support@auth0.com' - } - } - - scm { - connection = 'scm:git:git://github.com/atko-cic/auth0-auth-java.git' - developerConnection = 'scm:git:ssh://github.com/atko-cic/auth0-auth-java.git' - url = 'https://github.com/atko-cic/auth0-auth-java' - } - } - } - } -} + +logger.lifecycle("Using version ${version} for ${name} group ${group}") diff --git a/build.gradle b/build.gradle index b90dd8d..7975bf9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ -//plugins { -// id 'java' -//} +plugins { + id 'io.github.gradle-nexus.publish-plugin' version '2.0.0' +} allprojects { repositories { @@ -8,19 +8,15 @@ allprojects { } } -//subprojects { -// apply plugin: 'java' -// apply plugin: 'maven-publish' -// -// group = 'com.auth0' -// version = '1.0.0-SNAPSHOT' -// -// java { -// sourceCompatibility = JavaVersion.VERSION_1_8 -// targetCompatibility = JavaVersion.VERSION_1_8 -// } -// -// repositories { -// mavenCentral() -// } -//} +apply from: rootProject.file('gradle/versioning.gradle') + +nexusPublishing { + repositories { + sonatype { + nexusUrl.set(uri('https://ossrh-staging-api.central.sonatype.com/service/local/')) + snapshotRepositoryUrl.set(uri('https://central.sonatype.com/repository/maven-snapshots/')) + username.set(System.getenv("MAVEN_USERNAME")) + password.set(System.getenv("MAVEN_PASSWORD")) + } + } +} diff --git a/gradle.properties b/gradle.properties index e69de29..8adbcb9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -0,0 +1,21 @@ +GROUP=com.auth0 +POM_ARTIFACT_ID=auth0-springboot-api +VERSION_NAME=1.0.0-beta.1 + +POM_NAME=auth0-springboot-api +POM_DESCRIPTION=Auth0 Springboot Authentication Library with DPoP Support +POM_PACKAGING=jar + +POM_URL=https://github.com/auth0/auth0-auth-java +POM_SCM_URL=https://github.com/auth0/auth0-auth-java + +POM_SCM_CONNECTION=scm:git:https://github.com/auth0/auth0-auth-java.git +POM_SCM_DEV_CONNECTION=scm:git:https://github.com/auth0/auth0-auth-java.git + +POM_LICENCE_NAME=The MIT License (MIT) +POM_LICENCE_URL=https://raw.githubusercontent.com/auth0/auth0-auth-java/main/LICENSE +POM_LICENCE_DIST=repo + +POM_DEVELOPER_ID=auth0 +POM_DEVELOPER_NAME=Auth0 +POM_DEVELOPER_EMAIL=oss@auth0.com diff --git a/gradle/maven-publish.gradle b/gradle/maven-publish.gradle new file mode 100644 index 0000000..4fae07f --- /dev/null +++ b/gradle/maven-publish.gradle @@ -0,0 +1,90 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' + +task('sourcesJar', type: Jar, dependsOn: classes) { + archiveClassifier = 'sources' + from sourceSets.main.allSource +} + +task('javadocJar', type: Jar, dependsOn: javadoc) { + archiveClassifier = 'javadoc' + from javadoc.getDestinationDir() +} + +tasks.withType(Javadoc).configureEach { + javadocTool = javaToolchains.javadocToolFor { + // Use latest JDK for javadoc generation + languageVersion = JavaLanguageVersion.of(17) + } +} + +javadoc { + // Specify the Java version that the project will use + options.addStringOption('-release', "17") +} + +artifacts { + archives sourcesJar, javadocJar +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + groupId = GROUP + artifactId = POM_ARTIFACT_ID + version = project.version + + pom { + name = POM_NAME + packaging = POM_PACKAGING + description = POM_DESCRIPTION + url = POM_URL + + licenses { + license { + name = POM_LICENCE_NAME + url = POM_LICENCE_URL + distribution = POM_LICENCE_DIST + } + } + + developers { + developer { + id = POM_DEVELOPER_ID + name = POM_DEVELOPER_NAME + email = POM_DEVELOPER_EMAIL + } + } + + scm { + url = POM_SCM_URL + connection = POM_SCM_CONNECTION + developerConnection = POM_SCM_DEV_CONNECTION + } + } + } + } +} + +signing { + def signingKey = System.getenv("SIGNING_KEY") + def signingPassword = System.getenv("SIGNING_PASSWORD") + useInMemoryPgpKeys(signingKey, signingPassword) + + sign publishing.publications.mavenJava +} + +javadoc { + if(JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + } +} + +tasks.named('publish').configure { + dependsOn tasks.named('assemble') +} diff --git a/gradle/versioning.gradle b/gradle/versioning.gradle new file mode 100644 index 0000000..9a8dae5 --- /dev/null +++ b/gradle/versioning.gradle @@ -0,0 +1,17 @@ +def getVersionFromFile() { + def versionFile = rootProject.file('.version') + return versionFile.text.readLines().first().trim() +} + +def isSnapshot() { + return hasProperty('isSnapshot') ? isSnapshot.toBoolean() : true +} + +def getVersionName() { + return isSnapshot() ? project.version+"-SNAPSHOT" : project.version +} + +ext { + getVersionName = this.&getVersionName + getVersionFromFile = this.&getVersionFromFile +}