From aea7c90cc947e5c911262dd926618bf4592a81d3 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Thu, 26 Feb 2026 18:27:15 +0530 Subject: [PATCH 1/5] Added Publish Script --- auth0-api-java/build.gradle | 31 +++++++++++++++++++++---------- auth0-springboot-api/build.gradle | 10 +++++++++- gradle.properties | 6 +----- gradle/maven-publish.gradle | 20 ++++++++++++-------- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/auth0-api-java/build.gradle b/auth0-api-java/build.gradle index f647ebe..874da3a 100644 --- a/auth0-api-java/build.gradle +++ b/auth0-api-java/build.gradle @@ -2,27 +2,37 @@ plugins { id 'java-library' } -group = 'com.auth0' -version = '1.0.0-SNAPSHOT' - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 +ext { + POM_ARTIFACT_ID = 'auth0-api-java' + POM_NAME = 'auth0-api-java' + POM_DESCRIPTION = 'Auth0 Java Authentication and Authorization Library' + POM_PACKAGING = 'jar' } +// Disable javadoc for this module (Java 8 target, internal API) tasks.withType(Javadoc) { enabled = false } +apply from: rootProject.file('gradle/versioning.gradle') + +group = GROUP +version = getVersionFromFile() + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + dependencies { // Core dependencies implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' implementation 'org.apache.httpcomponents:httpclient:4.5.14' - + // JWT validation dependencies implementation 'com.auth0:java-jwt:4.5.1' implementation 'com.auth0:jwks-rsa:0.23.0' - + // Test dependencies testImplementation 'junit:junit:4.13.2' testImplementation 'org.hamcrest:hamcrest:2.2' @@ -41,5 +51,6 @@ test { } } -// This module is internal - no publishing configuration needed -// It will be bundled into auth0-springboot-api as a transitive dependency +logger.lifecycle("Using version ${version} for ${name} group ${group}") + +apply from: rootProject.file('gradle/maven-publish.gradle') diff --git a/auth0-springboot-api/build.gradle b/auth0-springboot-api/build.gradle index e55238e..5b0e21c 100644 --- a/auth0-springboot-api/build.gradle +++ b/auth0-springboot-api/build.gradle @@ -5,8 +5,14 @@ plugins { id "com.diffplug.spotless" version "6.25.0" } +ext { + POM_ARTIFACT_ID = 'auth0-springboot-api' + POM_NAME = 'auth0-springboot-api' + POM_DESCRIPTION = 'Auth0 Springboot Authentication Library with DPoP Support' + POM_PACKAGING = 'jar' +} + apply from: rootProject.file('gradle/versioning.gradle') -apply from: rootProject.file('gradle/maven-publish.gradle') group = GROUP version = getVersionFromFile() @@ -60,3 +66,5 @@ spotless { logger.lifecycle("Using version ${version} for ${name} group ${group}") +apply from: rootProject.file('gradle/maven-publish.gradle') + diff --git a/gradle.properties b/gradle.properties index 8adbcb9..b3603bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,7 @@ 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 - +# Shared POM metadata (module-specific properties are in each module's build.gradle) POM_URL=https://github.com/auth0/auth0-auth-java POM_SCM_URL=https://github.com/auth0/auth0-auth-java diff --git a/gradle/maven-publish.gradle b/gradle/maven-publish.gradle index 4fae07f..326e898 100644 --- a/gradle/maven-publish.gradle +++ b/gradle/maven-publish.gradle @@ -11,16 +11,20 @@ task('javadocJar', type: Jar, dependsOn: javadoc) { from javadoc.getDestinationDir() } -tasks.withType(Javadoc).configureEach { - javadocTool = javaToolchains.javadocToolFor { - // Use latest JDK for javadoc generation - languageVersion = JavaLanguageVersion.of(17) +// Configure javadoc toolchain only for modules where javadoc is enabled +if (tasks.findByName('javadoc')?.enabled != false) { + tasks.withType(Javadoc).configureEach { + javadocTool = javaToolchains.javadocToolFor { + languageVersion = JavaLanguageVersion.of(17) + } } -} -javadoc { - // Specify the Java version that the project will use - options.addStringOption('-release', "17") + javadoc { + options.addStringOption('-release', "17") + if(JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + } + } } artifacts { From 33d95fdf2d04aec69ee9ac324def346824c0943c Mon Sep 17 00:00:00 2001 From: tanya732 Date: Thu, 26 Feb 2026 18:29:39 +0530 Subject: [PATCH 2/5] Updated POM Desc --- auth0-api-java/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth0-api-java/build.gradle b/auth0-api-java/build.gradle index 874da3a..419080f 100644 --- a/auth0-api-java/build.gradle +++ b/auth0-api-java/build.gradle @@ -5,7 +5,7 @@ plugins { ext { POM_ARTIFACT_ID = 'auth0-api-java' POM_NAME = 'auth0-api-java' - POM_DESCRIPTION = 'Auth0 Java Authentication and Authorization Library' + POM_DESCRIPTION = 'Auth0 Java API Library' POM_PACKAGING = 'jar' } From 4788943a298ca9995219f8512e3b9dfad9ab743d Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 27 Feb 2026 11:28:48 +0530 Subject: [PATCH 3/5] Removed unused file --- .../spring/boot/Auth0SpringbootApiApplication.java | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0SpringbootApiApplication.java diff --git a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0SpringbootApiApplication.java b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0SpringbootApiApplication.java deleted file mode 100644 index 338fd8a..0000000 --- a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0SpringbootApiApplication.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.auth0.spring.boot; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Auth0SpringbootApiApplication { - - public static void main(String[] args) { - SpringApplication.run(Auth0SpringbootApiApplication.class, args); - } -} From 5cf47980807219d84beb207cb7f6a8c00cd7077c Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 27 Feb 2026 12:09:17 +0530 Subject: [PATCH 4/5] Fixed the build pipeline --- auth0-springboot-api/build.gradle | 5 ++++- .../com/auth0/spring/boot/Auth0AutoConfigurationTest.java | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/auth0-springboot-api/build.gradle b/auth0-springboot-api/build.gradle index 5b0e21c..4388f34 100644 --- a/auth0-springboot-api/build.gradle +++ b/auth0-springboot-api/build.gradle @@ -51,7 +51,10 @@ test { } } -// Don't build executable JAR for library +bootJar { + enabled = false +} + jar { enabled = true archiveClassifier = '' diff --git a/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AutoConfigurationTest.java b/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AutoConfigurationTest.java index a823fc6..df1f1cd 100644 --- a/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AutoConfigurationTest.java +++ b/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AutoConfigurationTest.java @@ -18,7 +18,7 @@ * *

*/ -@SpringBootTest +@SpringBootTest(classes = {Auth0AutoConfiguration.class, Auth0SecurityAutoConfiguration.class}) @TestPropertySource( properties = {"auth0.domain=test-domain.auth0.com", "auth0.audience=https://api.example.com"}) class Auth0AutoConfigurationTest { @@ -52,7 +52,7 @@ void shouldRegisterAllBeansInContext() { } @Nested - @SpringBootTest + @SpringBootTest(classes = {Auth0AutoConfiguration.class, Auth0SecurityAutoConfiguration.class}) @TestPropertySource( properties = { "auth0.domain=dpop-test.auth0.com", @@ -88,7 +88,7 @@ void shouldConfigureDPoPIatOffsetSeconds() { } @Nested - @SpringBootTest + @SpringBootTest(classes = {Auth0AutoConfiguration.class, Auth0SecurityAutoConfiguration.class}) @TestPropertySource( properties = { "auth0.domain=minimal-test.auth0.com", @@ -110,7 +110,7 @@ void shouldUseDefaultDPoPSettings() { } @Nested - @SpringBootTest + @SpringBootTest(classes = {Auth0AutoConfiguration.class, Auth0SecurityAutoConfiguration.class}) @TestPropertySource( properties = { "auth0.domain=partial-dpop.auth0.com", From 5d1cc54bc828a326737bb95d871c23af73010949 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 27 Feb 2026 14:42:47 +0530 Subject: [PATCH 5/5] Updated Examples file --- .../auth0/playground/ProfileController.java | 18 +- .../com/auth0/playground/SecurityConfig.java | 2 + auth0-springboot-api/EXAMPLES.md | 195 +++++++++--------- 3 files changed, 115 insertions(+), 100 deletions(-) diff --git a/auth0-springboot-api-playground/src/main/java/com/auth0/playground/ProfileController.java b/auth0-springboot-api-playground/src/main/java/com/auth0/playground/ProfileController.java index 356f9a7..02576ab 100644 --- a/auth0-springboot-api-playground/src/main/java/com/auth0/playground/ProfileController.java +++ b/auth0-springboot-api-playground/src/main/java/com/auth0/playground/ProfileController.java @@ -1,5 +1,6 @@ package com.auth0.playground; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -11,13 +12,20 @@ public class ProfileController { @GetMapping("/protected") - public String protectedEndpoint(Authentication authentication) { - System.out.println("🔐 Received request for protected resource: "+ authentication.getPrincipal().toString()); - return "Hello " + authentication.getName() + ", access granted!"; + public ResponseEntity> protectedEndpoint(Authentication authentication) { + String userId = authentication.getName(); // Returns the 'sub' claim + + return ResponseEntity.ok(Map.of( + "message", "Access granted!", + "user", userId, + "authenticated", true + )); } @GetMapping("/public") - public Map pub() { - return Map.of("message", "Public endpoint — no token required"); + public ResponseEntity> publicEndpoint() { + return ResponseEntity.ok(Map.of( + "message", "Public endpoint - no token required" + )); } } \ No newline at end of file diff --git a/auth0-springboot-api-playground/src/main/java/com/auth0/playground/SecurityConfig.java b/auth0-springboot-api-playground/src/main/java/com/auth0/playground/SecurityConfig.java index 88298de..df91a95 100644 --- a/auth0-springboot-api-playground/src/main/java/com/auth0/playground/SecurityConfig.java +++ b/auth0-springboot-api-playground/src/main/java/com/auth0/playground/SecurityConfig.java @@ -3,12 +3,14 @@ import com.auth0.spring.boot.Auth0AuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration +@EnableMethodSecurity public class SecurityConfig { @Bean SecurityFilterChain apiSecurity( diff --git a/auth0-springboot-api/EXAMPLES.md b/auth0-springboot-api/EXAMPLES.md index dacec30..3effcc1 100644 --- a/auth0-springboot-api/EXAMPLES.md +++ b/auth0-springboot-api/EXAMPLES.md @@ -40,8 +40,8 @@ public class SecurityConfig { session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/protected").authenticated() .requestMatchers("/api/public").permitAll() + .requestMatchers("/api/protected").authenticated() .anyRequest().authenticated() ) .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class) @@ -86,16 +86,12 @@ public class UserController { @GetMapping("/profile") public ResponseEntity> getUserProfile(Authentication authentication) { - if (authentication instanceof Auth0AuthenticationToken) { - Auth0AuthenticationToken auth0Token = (Auth0AuthenticationToken) authentication; - DecodedJWT jwt = auth0Token.getJwt(); - + if (authentication instanceof Auth0AuthenticationToken auth0Token) { return ResponseEntity.ok(Map.of( - "sub", jwt.getSubject(), - "email", jwt.getClaim("email").asString(), - "scope", jwt.getClaim("scope").asString(), - "exp", jwt.getExpiresAt(), - "iat", jwt.getIssuedAt() + "sub", String.valueOf(auth0Token.getClaim("sub")), + "email", String.valueOf(auth0Token.getClaim("email")), + "scope", String.valueOf(auth0Token.getClaim("scope")), + "scopes", auth0Token.getScopes() )); } @@ -118,7 +114,7 @@ Accepts both Bearer and DPoP tokens: auth0: domain: "your-tenant.auth0.com" audience: "https://api.example.com" - dpopMode: ALLOWED # Default value + dpop-mode: ALLOWED ``` #### 2. Required Mode @@ -129,7 +125,7 @@ Only accepts DPoP tokens: auth0: domain: "your-tenant.auth0.com" audience: "https://api.example.com" - dpopMode: REQUIRED + dpop-mode: REQUIRED ``` #### 3. Disabled Mode @@ -140,7 +136,7 @@ Only accepts Bearer tokens: auth0: domain: "your-tenant.auth0.com" audience: "https://api.example.com" - dpopMode: DISABLED + dpop-mode: DISABLED ``` ### Advanced DPoP Configuration @@ -149,40 +145,30 @@ auth0: auth0: domain: "your-tenant.auth0.com" audience: "https://api.example.com" - dpopMode: ALLOWED - dpopIatOffsetSeconds: 300 # DPoP proof time window (default: 300) - dpopIatLeewaySeconds: 30 # DPoP proof time leeway (default: 30) + dpop-mode: ALLOWED + dpop-iat-offset-seconds: 300 # DPoP proof time window (default: 300) + dpop-iat-leeway-seconds: 30 # DPoP proof time leeway (default: 30) ``` -### DPoP-Token Controller +### How DPoP Works in Your Controllers + +DPoP validation is handled entirely by the library at the filter level. Your controllers don't need any DPoP-specific code — the library validates the DPoP proof automatically before the request reaches your controller. A validated DPoP request produces the same `Auth0AuthenticationToken` as a Bearer request: ```java @RestController @RequestMapping("/api") -public class DPoPController { +public class SensitiveDataController { @GetMapping("/sensitive") - public ResponseEntity> sensitiveEndpoint( - Authentication authentication, - HttpServletRequest request - ) { - if (authentication instanceof Auth0AuthenticationToken) { - Auth0AuthenticationToken auth0Token = (Auth0AuthenticationToken) authentication; - AuthenticationContext context = auth0Token.getAuthenticationContext(); - - Map response = new HashMap<>(); - response.put("user", authentication.getName()); - response.put("scheme", context.getScheme().toString()); - - if (context.getScheme() == AuthScheme.DPOP) { - response.put("message", "Access granted with DPoP proof"); - response.put("dpop_bound", true); - } else { - response.put("message", "Access granted with Bearer token"); - response.put("dpop_bound", false); - } - - return ResponseEntity.ok(response); + public ResponseEntity> sensitiveEndpoint(Authentication authentication) { + // This works the same whether the client used Bearer or DPoP. + // DPoP proof validation already happened in the filter. + if (authentication instanceof Auth0AuthenticationToken auth0Token) { + return ResponseEntity.ok(Map.of( + "user", authentication.getName(), + "scopes", auth0Token.getScopes(), + "message", "Access granted" + )); } return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); @@ -190,79 +176,106 @@ public class DPoPController { } ``` +The difference is in what the library **rejects**: +- `ALLOWED` mode: Accepts both `Authorization: Bearer ` and `Authorization: DPoP ` + `DPoP: ` +- `REQUIRED` mode: Rejects Bearer tokens — only `DPoP` tokens with a valid proof are accepted +- `DISABLED` mode: Rejects DPoP tokens — only `Bearer` tokens are accepted + ## Scope-Based Authorization -### Method-Level Security +The library maps JWT scopes to Spring Security authorities with a `SCOPE_` prefix. For example, a token with `scope: "read:messages write:messages"` produces authorities `SCOPE_read:messages` and `SCOPE_write:messages`. + +### Option 1: Security Filter Chain (Recommended) + +The simplest approach — define scope requirements in your security configuration: + +```java +@Configuration +public class SecurityConfig { + + @Bean + SecurityFilterChain apiSecurity( + HttpSecurity http, + Auth0AuthenticationFilter authFilter + ) throws Exception { + return http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/public").permitAll() + .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin") + .requestMatchers("/api/users/**").hasAuthority("SCOPE_read:users") + .anyRequest().authenticated() + ) + .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } +} +``` + +### Option 2: Method-Level Security with @PreAuthorize + +For fine-grained control per method. Requires `@EnableMethodSecurity` on a configuration class: + +```java +@Configuration +@EnableMethodSecurity +public class MethodSecurityConfig { + // Enables @PreAuthorize annotations +} +``` ```java @RestController -@RequestMapping("/api") -@PreAuthorize("hasAuthority('SCOPE_read:users')") +@RequestMapping("/api/users") public class UserManagementController { - @GetMapping("/users") + @GetMapping @PreAuthorize("hasAuthority('SCOPE_read:users')") public ResponseEntity> getUsers() { - // Only accessible with 'read:users' scope return ResponseEntity.ok(userService.getAllUsers()); } - @PostMapping("/users") + @PostMapping @PreAuthorize("hasAuthority('SCOPE_write:users')") public ResponseEntity createUser(@RequestBody User user) { - // Only accessible with 'write:users' scope return ResponseEntity.ok(userService.createUser(user)); } - @DeleteMapping("/users/{id}") + @DeleteMapping("/{id}") @PreAuthorize("hasAuthority('SCOPE_delete:users')") public ResponseEntity deleteUser(@PathVariable String id) { - // Only accessible with 'delete:users' scope userService.deleteUser(id); return ResponseEntity.noContent().build(); } } ``` -### Custom Scope Validation - -```java -@Component -public class ScopeValidator { - - public boolean hasRequiredScopes(Authentication authentication, String... requiredScopes) { - if (!(authentication instanceof Auth0AuthenticationToken)) { - return false; - } - - Auth0AuthenticationToken auth0Token = (Auth0AuthenticationToken) authentication; - DecodedJWT jwt = auth0Token.getJwt(); - String scopeClaim = jwt.getClaim("scope").asString(); - - if (scopeClaim == null) { - return false; - } +### Option 3: Programmatic Scope Check - Set tokenScopes = Set.of(scopeClaim.split(" ")); - return tokenScopes.containsAll(Arrays.asList(requiredScopes)); - } -} +Use `getScopes()` on the token directly when you need custom logic: +```java @RestController @RequestMapping("/api") -public class CustomScopeController { - - @Autowired - private ScopeValidator scopeValidator; +public class AdminController { @GetMapping("/admin") public ResponseEntity> adminEndpoint(Authentication authentication) { - if (!scopeValidator.hasRequiredScopes(authentication, "admin", "read:admin")) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(Map.of("error", "insufficient_scope")); + if (authentication instanceof Auth0AuthenticationToken auth0Token) { + Set scopes = auth0Token.getScopes(); + + if (!scopes.contains("admin") || !scopes.contains("read:admin")) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(Map.of("error", "insufficient_scope")); + } + + return ResponseEntity.ok(Map.of("message", "Admin access granted")); } - return ResponseEntity.ok(Map.of("message", "Admin access granted")); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } ``` @@ -281,15 +294,15 @@ auth0: # Optional: DPoP mode (DISABLED, ALLOWED, REQUIRED) # Default: ALLOWED - dpopMode: ALLOWED + dpop-mode: ALLOWED # Optional: DPoP proof time window in seconds # Default: 300 (5 minutes) - dpopIatOffsetSeconds: 300 + dpop-iat-offset-seconds: 300 # Optional: DPoP proof time leeway in seconds # Default: 30 (30 seconds) - dpopIatLeewaySeconds: 30 + dpop-iat-leeway-seconds: 30 ``` ### Environment Variables @@ -299,22 +312,23 @@ You can also configure using environment variables: ```bash AUTH0_DOMAIN=your-tenant.auth0.com AUTH0_AUDIENCE=https://api.example.com -AUTH0_DPOP_MODE=ALLOWED -AUTH0_DPOP_IAT_OFFSET_SECONDS=300 -AUTH0_DPOP_IAT_LEEWAY_SECONDS=30 +AUTH0_DPOPMODE=ALLOWED +AUTH0_DPOPIATOFFSETSECONDS=300 +AUTH0_DPOPIATLEEWAYSSECONDS=30 ``` +> **Note:** Spring Boot environment variable binding removes dashes and is case-insensitive. Do not use underscores to separate words within a property name (e.g., use `AUTH0_DPOPMODE`, not `AUTH0_DPOP_MODE`). + ## Error Handling ### Common HTTP Status Codes - **401 Unauthorized**: Missing or invalid token - **403 Forbidden**: Valid token but insufficient permissions -- **400 Bad Request**: Invalid DPoP proof or malformed request ### WWW-Authenticate Headers -The library automatically sets appropriate `WWW-Authenticate` headers: +The library automatically sets appropriate `WWW-Authenticate` headers on authentication failures: ``` # ALLOWED mode (default) @@ -326,12 +340,3 @@ WWW-Authenticate: DPoP algs="ES256" # DPoP-specific errors WWW-Authenticate: DPoP error="invalid_dpop_proof", error_description="DPoP proof validation failed" ``` - -## Best Practices - -1. **Environment-Specific Configuration**: Use different Auth0 domains and audiences for different environments -2. **Scope Validation**: Always validate scopes for sensitive operations -3. **Error Handling**: Implement comprehensive error handling for auth failures -4. **Testing**: Use mocked authentication for unit tests and real tokens for integration tests -5. **Security Headers**: Ensure proper CORS and security headers are configured -6. **DPoP Mode**: Use `REQUIRED` mode for high-security APIs, `ALLOWED` for gradual adoption