diff --git a/.gitignore b/.gitignore index 1da6f15..53ab1b7 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,8 @@ hs_err_pid* # macOS .DS_Store +.vscode/ + # IntelliJ .idea/ *.iml @@ -25,4 +27,4 @@ out/ **/build/ # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar \ No newline at end of file +!gradle-wrapper.jar diff --git a/.version b/.version index 169f19b..32bd932 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -1.11.0 \ No newline at end of file +1.12.0 \ No newline at end of file diff --git a/README.md b/README.md index 1ee3030..3ce6e97 100644 --- a/README.md +++ b/README.md @@ -24,26 +24,49 @@ ### Requirements -Java 8 or above and `javax.servlet` version 3. +Java 8 or above and `javax.servlet` version 3, **or** `jakarta.servlet` version 5.0 or above for Jakarta EE environments. > If you are using Spring, we recommend leveraging Spring's OIDC and OAuth2 support, as demonstrated by the [Spring Boot Quickstart](https://auth0.com/docs/quickstart/webapp/java-spring-boot). ### Installation +#### For traditional Java EE / javax.servlet environments: + +Add the dependency via Maven: + +```xml + + com.auth0 + mvc-auth-commons + 1.12.0 + +``` + +or Gradle: + +```gradle +implementation 'com.auth0:mvc-auth-commons:1.12.0' +``` + +#### For Jakarta EE / jakarta.servlet environments: + +Starting from version 1.12.0, this library provides dual support for both javax.servlet and jakarta.servlet environments through bytecode transformation. For Jakarta EE environments (Tomcat 10+ etc.), use the Jakarta-compatible version: + Add the dependency via Maven: ```xml com.auth0 mvc-auth-commons - 1.11.0 + 1.12.0 + jakarta ``` or Gradle: ```gradle -implementation 'com.auth0:mvc-auth-commons:1.11.0' +implementation 'com.auth0:mvc-auth-commons:1.12.0:jakarta' ``` ### Configure Auth0 diff --git a/build.gradle b/build.gradle index 47632fa..8dabe94 100644 --- a/build.gradle +++ b/build.gradle @@ -26,8 +26,6 @@ version = getVersionFromFile() group = GROUP logger.lifecycle("Using version ${version} for ${name} group $group") -import me.champeau.gradle.japicmp.JapicmpTask - project.afterEvaluate { def versions = project.ext.testInJavaVersions for (pluginJavaTestVersion in versions) { @@ -49,7 +47,7 @@ project.afterEvaluate { project.configure(project) { def baselineVersion = project.ext.baselineCompareVersion - task('apiDiff', type: JapicmpTask, dependsOn: 'jar') { + task('apiDiff', type: me.champeau.gradle.japicmp.JapicmpTask, dependsOn: 'jar') { oldClasspath.from(files(getBaselineJar(project, baselineVersion))) newClasspath.from(files(jar.archiveFile)) onlyModified = true @@ -106,6 +104,20 @@ java { } } +// Jakarta test source set configuration +sourceSets { + testJakarta { + java { + srcDirs = ['src/testJakarta/java'] + } + resources { + srcDirs = ['src/testJakarta/resources'] + } + compileClasspath += sourceSets.main.output + configurations.testRuntimeClasspath + runtimeClasspath += output + compileClasspath + } +} + compileJava { sourceCompatibility '1.8' targetCompatibility '1.8' @@ -136,6 +148,146 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' testImplementation 'org.springframework:spring-test:4.3.14.RELEASE' testImplementation 'com.squareup.okhttp3:okhttp:4.11.0' + + // Jakarta test dependencies (Java 8 compatible) + testJakartaImplementation 'jakarta.servlet:jakarta.servlet-api:5.0.0' + testJakartaImplementation 'org.apache.commons:commons-lang3:3.12.0' + testJakartaImplementation 'com.google.guava:guava-annotations:r03' + testJakartaImplementation 'commons-codec:commons-codec:1.15' + testJakartaImplementation 'com.auth0:auth0:1.45.1' + testJakartaImplementation 'com.auth0:java-jwt:3.19.4' + testJakartaImplementation 'com.auth0:jwks-rsa:0.22.1' + testJakartaImplementation 'org.bouncycastle:bcprov-jdk15on:1.64' + testJakartaImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' + testJakartaImplementation 'org.hamcrest:hamcrest-core:1.3' + testJakartaImplementation 'org.mockito:mockito-core:2.8.9' + testJakartaImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' + testJakartaImplementation 'org.springframework:spring-test:4.3.14.RELEASE' + testJakartaImplementation 'com.squareup.okhttp3:okhttp:4.11.0' + testJakartaRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' +} + +// Task to perform javax to jakarta transformation using Eclipse Transformer +tasks.register('performJakartaTransformation') { + dependsOn tasks.named('classes') + + def transformedClassesDir = file("$buildDir/transformed-classes") + + outputs.dir transformedClassesDir + inputs.files sourceSets.main.output.classesDirs + + doLast { + // Eclipse Transformer CLI configuration + def transformerVersion = '0.5.0' + def transformerConfig = project.configurations.detachedConfiguration( + project.dependencies.create("org.eclipse.transformer:org.eclipse.transformer.cli:${transformerVersion}") + ) + + // Clean and create output directory + delete transformedClassesDir + transformedClassesDir.mkdirs() + + // Transform each classes directory directly to the root of transformedClassesDir + sourceSets.main.output.classesDirs.each { classDir -> + if (classDir.exists()) { + project.javaexec { + classpath = transformerConfig + main = 'org.eclipse.transformer.cli.JakartaTransformerCLI' + args = [ + classDir.absolutePath, + transformedClassesDir.absolutePath, + '-q' // quiet mode + ] + } + } + } + + // Also transform resources if needed + sourceSets.main.output.resourcesDir.with { resourceDir -> + if (resourceDir.exists()) { + copy { + from resourceDir + into transformedClassesDir + } + } + } + + println "Jakarta transformation completed: ${transformedClassesDir.absolutePath}" + } +} + +// Task to create Jakarta JAR from transformed classes +tasks.register('transformJarToJakarta', Jar) { + dependsOn tasks.named('performJakartaTransformation') + + archiveClassifier = 'jakarta' + + def transformedClassesDir = file("$buildDir/transformed-classes") + + from transformedClassesDir + + // Include original manifest with modifications + manifest { + from(tasks.jar.manifest) { + attributes.remove('Implementation-Title') + attributes.remove('Implementation-Version') + } + attributes( + 'Implementation-Title': "${project.name}-jakarta", + 'Implementation-Version': project.version, + 'Jakarta-Transformed': 'true' + ) + } + + doLast { + println "Jakarta JAR file generated: ${archiveFile.get().asFile.absolutePath}" + } +} + +// Jakarta test task (updated to use transformed JAR) +tasks.register('testJakarta', Test) { + description = "Runs Jakarta EE specific tests using transformed jakarta classes" + group = "verification" + testClassesDirs = sourceSets.testJakarta.output.classesDirs + + // Build classpath with Jakarta dependencies and transformed JAR + def jakartaClasspath = sourceSets.testJakarta.runtimeClasspath + + shouldRunAfter tasks.named('test') + dependsOn tasks.named('transformJarToJakarta') + + // Enabled for Jakarta testing + enabled = true + + useJUnitPlatform() + testLogging { + events "skipped", "failed" + exceptionFormat "short" + } + + doFirst { + def jakartaJar = tasks.transformJarToJakarta.archiveFile.get().asFile + if (jakartaJar.exists()) { + // Replace main classes with jakarta-transformed JAR in classpath + classpath = jakartaClasspath + files(jakartaJar) - sourceSets.main.output + } else { + throw new GradleException("Jakarta JAR not found: ${jakartaJar.absolutePath}") + } + } +} + +// Enable Jakarta tests in check task +tasks.named('check').configure { + dependsOn tasks.named('testJakarta') +} + +// Ensure Jakarta JAR is built when running build task +tasks.named('build').configure { + dependsOn tasks.named('transformJarToJakarta') } apply from: rootProject.file('gradle/maven-publish.gradle') + + + + diff --git a/gradle/maven-publish.gradle b/gradle/maven-publish.gradle index 206a581..4e6d31f 100644 --- a/gradle/maven-publish.gradle +++ b/gradle/maven-publish.gradle @@ -37,8 +37,18 @@ publishing { artifactId = POM_ARTIFACT_ID version = getVersionName() + // 1. Main artifact (javax version) - no classifier artifact("$buildDir/libs/${project.name}-${version}.jar") + + // 2. Jakarta EE version artifact - with 'jakarta' classifier + artifact("$buildDir/libs/${project.name}-${version}-jakarta.jar") { + classifier 'jakarta' + } + + // 3. Sources JAR artifact sourcesJar + + // 4. Javadoc JAR artifact javadocJar pom { @@ -94,6 +104,15 @@ publishing { } } +// Ensure Jakarta JAR is built before publishing +tasks.named('publishMavenJavaPublicationToSonatypeRepository').configure { + dependsOn tasks.named('transformJarToJakarta') +} + +tasks.named('publishToMavenLocal').configure { + dependsOn tasks.named('transformJarToJakarta') +} + signing { def signingKey = System.getenv("SIGNING_KEY") def signingPassword = System.getenv("SIGNING_PASSWORD") diff --git a/src/testJakarta/java/com/auth0/JakartaCompatibilityTest.java b/src/testJakarta/java/com/auth0/JakartaCompatibilityTest.java new file mode 100644 index 0000000..451e296 --- /dev/null +++ b/src/testJakarta/java/com/auth0/JakartaCompatibilityTest.java @@ -0,0 +1,85 @@ +package com.auth0; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.Cookie; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests to verify Jakarta EE compatibility. + * These tests ensure Jakarta servlet API is available and working. + */ +public class JakartaCompatibilityTest { + + @Test + public void testJakartaServletRequestAvailable() { + // Verify Jakarta HttpServletRequest can be mocked and used + HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); + assertNotNull(mockRequest, "Jakarta HttpServletRequest should be available"); + + // Verify it's from Jakarta package + String className = mockRequest.getClass().getName(); + assertTrue(className.contains("jakarta") || className.contains("HttpServletRequest"), + "Should be Jakarta servlet type"); + } + + @Test + public void testJakartaServletResponseAvailable() { + // Verify Jakarta HttpServletResponse can be mocked and used + HttpServletResponse mockResponse = Mockito.mock(HttpServletResponse.class); + assertNotNull(mockResponse, "Jakarta HttpServletResponse should be available"); + } + + @Test + public void testJakartaHttpSessionAvailable() { + // Test Jakarta HttpSession + HttpSession mockSession = Mockito.mock(HttpSession.class); + assertNotNull(mockSession, "Jakarta HttpSession should be available"); + + // Test basic session operations + Mockito.when(mockSession.getAttribute("test")).thenReturn("value"); + Mockito.doNothing().when(mockSession).setAttribute("test", "value"); + + mockSession.setAttribute("test", "value"); + Mockito.verify(mockSession).setAttribute("test", "value"); + } + + @Test + public void testJakartaCookieAvailable() { + // Test Jakarta Cookie + Cookie cookie = new Cookie("testCookie", "testValue"); + assertNotNull(cookie, "Jakarta Cookie should be available"); + + cookie.setPath("/"); + cookie.setSecure(true); + cookie.setHttpOnly(true); + + // Verify basic cookie properties + assertNotNull(cookie.getName()); + assertNotNull(cookie.getValue()); + } + + @Test + public void testJakartaServletIntegration() { + // Test integration between Jakarta servlet components + HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); + HttpServletResponse mockResponse = Mockito.mock(HttpServletResponse.class); + HttpSession mockSession = Mockito.mock(HttpSession.class); + + // Setup mock behavior + Mockito.when(mockRequest.getSession(true)).thenReturn(mockSession); + Mockito.when(mockRequest.getContextPath()).thenReturn("/test"); + + // Test session retrieval + HttpSession session = mockRequest.getSession(true); + assertNotNull(session, "Session should be retrieved successfully"); + + String contextPath = mockRequest.getContextPath(); + assertNotNull(contextPath, "Context path should be available"); + } +} \ No newline at end of file