diff --git a/.version b/.version index ebb5fbf..42e8a3e 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -1.0.0-beta.1 \ No newline at end of file +1.0.0-beta.0 \ No newline at end of file diff --git a/README.md b/README.md index 0a95c31..c5f4c68 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![Java Version](https://img.shields.io/badge/java-8%2B-blue) ![License](https://img.shields.io/badge/license-MIT-green) -A comprehensive Java library for Auth0 JWT authentication with built-in **DPoP (Demonstration of Proof-of-Possession)** support. This multi-module project provides both a core authentication library and Spring Boot integration for secure API development. +A comprehensive Java library for Auth0 JWT authentication with built-in **DPoP (Demonstration of Proof-of-Possession)** support. This project provides Spring Boot integration for secure API development. ## 🏗️ Architecture Overview @@ -37,7 +37,7 @@ If you're building a Spring Boot application, use the Spring Boot integration: com.auth0 auth0-springboot-api - 1.0.0-SNAPSHOT + 1.0.0-beta.0 ``` @@ -45,7 +45,7 @@ If you're building a Spring Boot application, use the Spring Boot integration: ### For Core Java Applications -The core library (`auth0-api-java`) is currently an internal module used by the Spring Boot integration. It provides: +It provides: - JWT validation with Auth0 JWKS integration - DPoP proof validation per [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449) @@ -78,11 +78,11 @@ This project uses Gradle with a multi-module setup: ## 📦 Publishing -Only the Spring Boot integration module is published as a public artifact: +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** | +| Module | Group ID | Artifact ID | Version | Status | +| ---------------------- | ----------- | ---------------------- |----------------| ---------------- | +| `auth0-springboot-api` | `com.auth0` | `auth0-springboot-api` | `1.0.0-beta.0` | 📦 **Published** | 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-springboot-api/README.md b/auth0-springboot-api/README.md index a7f470c..009615f 100644 --- a/auth0-springboot-api/README.md +++ b/auth0-springboot-api/README.md @@ -11,7 +11,6 @@ This library builds on top of the standard Spring Security JWT authentication, p ## Requirements -- This library currently supports **Java 8+** for core functionality - **Spring Boot 3.2+** (requires Java 17+) for Spring Boot integration ## Getting Started @@ -24,7 +23,7 @@ Add the dependency via Maven: com.auth0 auth0-springboot-api - 1.0.0 + 1.0.0-beta.0 ``` @@ -32,7 +31,7 @@ or Gradle: ```gradle dependencies { - implementation 'com.auth0:auth0-springboot-api:1.0.0' + implementation 'com.auth0:auth0-springboot-api:1.0.0-beta.0' } ``` @@ -176,49 +175,6 @@ curl -H "Authorization: DPoP " \ ## Advanced Features -### Manual JWT Validation - -For scenarios requiring manual token validation, inject and use the `AuthClient`: - -```java -@RestController -public class CustomController { - - @Autowired - private AuthClient authClient; - - @PostMapping("/api/custom-validation") - public ResponseEntity customValidation(HttpServletRequest request) { - try { - // Extract headers and request info - Map headers = extractHeaders(request); - HttpRequestInfo requestInfo = new HttpRequestInfo( - request.getMethod(), - request.getRequestURL().toString(), - null - ); - - // Manual validation - AuthenticationContext context = authClient.verifyRequest(headers, requestInfo); - String userId = (String) context.getClaims().get("sub"); - - return ResponseEntity.ok("Token valid for user: " + userId); - - } catch (BaseAuthException e) { - return ResponseEntity.status(401).body("Authentication failed: " + e.getMessage()); - } - } - - private Map extractHeaders(HttpServletRequest request) { - Map headers = new HashMap<>(); - Collections.list(request.getHeaderNames()).forEach(headerName -> - headers.put(headerName, request.getHeader(headerName)) - ); - return headers; - } -} -``` - ### Custom Claim Validation Access JWT claims directly through `Auth0AuthenticationToken`'s clean API: diff --git a/auth0-springboot-api/build.gradle b/auth0-springboot-api/build.gradle index 87fa857..8812a0d 100644 --- a/auth0-springboot-api/build.gradle +++ b/auth0-springboot-api/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java-library' id 'org.springframework.boot' version '3.2.0' id 'io.spring.dependency-management' version '1.1.4' + id "com.diffplug.spotless" version "6.25.0" } apply from: rootProject.file('gradle/versioning.gradle') @@ -50,5 +51,12 @@ jar { archiveClassifier = '' } +spotless { + java { + googleJavaFormat() + target 'src/**/*.java' + } +} + logger.lifecycle("Using version ${version} for ${name} group ${group}") diff --git a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AuthenticationFilter.java b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AuthenticationFilter.java index 7be2917..447c4ec 100644 --- a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AuthenticationFilter.java +++ b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AuthenticationFilter.java @@ -11,134 +11,127 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; -import java.io.IOException; -import java.util.*; - public class Auth0AuthenticationFilter extends OncePerRequestFilter { - private final AuthClient authClient; + private final AuthClient authClient; - private final Auth0Properties auth0Properties; + private final Auth0Properties auth0Properties; - public Auth0AuthenticationFilter(AuthClient authClient, Auth0Properties auth0Properties) { - this.authClient = authClient; - this.auth0Properties = auth0Properties; - } + public Auth0AuthenticationFilter(AuthClient authClient, Auth0Properties auth0Properties) { + this.authClient = authClient; + this.auth0Properties = auth0Properties; + } - @Override - protected void doFilterInternal( - HttpServletRequest request, - HttpServletResponse response, - FilterChain chain - ) throws ServletException, IOException { + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { - try { + try { - Map headers = extractHeaders(request); + Map headers = extractHeaders(request); - String authorizationHeader = headers.get("authorization"); - if (authorizationHeader == null || authorizationHeader.trim().isEmpty()) { - chain.doFilter(request, response); - return; - } + String authorizationHeader = headers.get("authorization"); + if (authorizationHeader == null || authorizationHeader.trim().isEmpty()) { + chain.doFilter(request, response); + return; + } - HttpRequestInfo requestInfo = extractRequestInfo(request); + HttpRequestInfo requestInfo = extractRequestInfo(request); - AuthenticationContext ctx = authClient.verifyRequest(headers, requestInfo); + AuthenticationContext ctx = authClient.verifyRequest(headers, requestInfo); - Auth0AuthenticationToken authentication = new Auth0AuthenticationToken(ctx); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + Auth0AuthenticationToken authentication = new Auth0AuthenticationToken(ctx); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); - chain.doFilter(request, response); + chain.doFilter(request, response); - } catch (BaseAuthException ex) { - response.setStatus(ex.getStatusCode()); + } catch (BaseAuthException ex) { + response.setStatus(ex.getStatusCode()); - Map exceptionHeaders = ex.getHeaders(); - String wwwAuthenticate = exceptionHeaders.get("WWW-Authenticate"); + Map exceptionHeaders = ex.getHeaders(); + String wwwAuthenticate = exceptionHeaders.get("WWW-Authenticate"); - if (wwwAuthenticate != null) { - response.addHeader("WWW-Authenticate", wwwAuthenticate); - } - SecurityContextHolder.clearContext(); - } + if (wwwAuthenticate != null) { + response.addHeader("WWW-Authenticate", wwwAuthenticate); + } + SecurityContextHolder.clearContext(); } + } - Map extractHeaders(HttpServletRequest request) - throws MissingAuthorizationException { + Map extractHeaders(HttpServletRequest request) + throws MissingAuthorizationException { - List authHeaders = Collections.list(request.getHeaders("Authorization")); - if (authHeaders != null && authHeaders.size() > 1) { - String firstValue = authHeaders.get(0); + List authHeaders = Collections.list(request.getHeaders("Authorization")); + if (authHeaders != null && authHeaders.size() > 1) { + String firstValue = authHeaders.get(0); - MissingAuthorizationException ex = new MissingAuthorizationException(); + MissingAuthorizationException ex = new MissingAuthorizationException(); - String[] parts = firstValue.trim().split("\\s+", 2); + String[] parts = firstValue.trim().split("\\s+", 2); - DPoPMode dpopMode = auth0Properties.getDpopMode(); - if (dpopMode == null) { - dpopMode = DPoPMode.ALLOWED; // default fallback - } + DPoPMode dpopMode = auth0Properties.getDpopMode(); + if (dpopMode == null) { + dpopMode = DPoPMode.ALLOWED; // default fallback + } - List challenges = WWWAuthenticateBuilder.buildChallenges( - ex.getErrorCode(), - ex.getErrorDescription(), - dpopMode, - parts[0].toLowerCase(Locale.ROOT) - ); + List challenges = + WWWAuthenticateBuilder.buildChallenges( + ex.getErrorCode(), + ex.getErrorDescription(), + dpopMode, + parts[0].toLowerCase(Locale.ROOT)); - if (!challenges.isEmpty()) { - ex.addHeader("WWW-Authenticate", String.join(", ", challenges)); - } + if (!challenges.isEmpty()) { + ex.addHeader("WWW-Authenticate", String.join(", ", challenges)); + } - throw ex; - } - - Map headers = new HashMap<>(); - Enumeration names = request.getHeaderNames(); - - if (names != null) { - while (names.hasMoreElements()) { - String name = names.nextElement(); - headers.put( - name.toLowerCase(Locale.ROOT), - request.getHeader(name) - ); - } - } - - return headers; + throw ex; } - HttpRequestInfo extractRequestInfo(HttpServletRequest request) { - String htu = buildHtu(request); - return new HttpRequestInfo(request.getMethod(), htu, null); + Map headers = new HashMap<>(); + Enumeration names = request.getHeaderNames(); + + if (names != null) { + while (names.hasMoreElements()) { + String name = names.nextElement(); + headers.put(name.toLowerCase(Locale.ROOT), request.getHeader(name)); + } } - static String buildHtu(HttpServletRequest request) { - String scheme = request.getScheme().toLowerCase(Locale.ROOT); - String host = request.getServerName().toLowerCase(Locale.ROOT); + return headers; + } - int port = request.getServerPort(); - boolean defaultPort = - (scheme.equals("http") && port == 80) || - (scheme.equals("https") && port == 443); + HttpRequestInfo extractRequestInfo(HttpServletRequest request) { + String htu = buildHtu(request); + return new HttpRequestInfo(request.getMethod(), htu, null); + } - StringBuilder htu = new StringBuilder(); - htu.append(scheme).append("://").append(host); + static String buildHtu(HttpServletRequest request) { + String scheme = request.getScheme().toLowerCase(Locale.ROOT); + String host = request.getServerName().toLowerCase(Locale.ROOT); - if (!defaultPort) { - htu.append(":").append(port); - } + int port = request.getServerPort(); + boolean defaultPort = + (scheme.equals("http") && port == 80) || (scheme.equals("https") && port == 443); - htu.append(request.getRequestURI()); + StringBuilder htu = new StringBuilder(); + htu.append(scheme).append("://").append(host); - return htu.toString(); + if (!defaultPort) { + htu.append(":").append(port); } + + htu.append(request.getRequestURI()); + + return htu.toString(); + } } diff --git a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AuthenticationToken.java b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AuthenticationToken.java index ed41b0b..8e62a8b 100644 --- a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AuthenticationToken.java +++ b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AuthenticationToken.java @@ -1,120 +1,117 @@ package com.auth0.spring.boot; import com.auth0.models.AuthenticationContext; -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; /** * Spring Security Authentication object representing a successfully validated Auth0 JWT. - *

- * Authorities are derived from the "scope" claim in the JWT, if present, and mapped - * to {@code SCOPE_} prefixed {@link SimpleGrantedAuthority} instances. If no scopes - * are present, a default {@code ROLE_USER} authority is assigned. + * + *

Authorities are derived from the "scope" claim in the JWT, if present, and mapped to {@code + * SCOPE_} prefixed {@link SimpleGrantedAuthority} instances. If no scopes are present, a default + * {@code ROLE_USER} authority is assigned. */ public class Auth0AuthenticationToken extends AbstractAuthenticationToken { - private final AuthenticationContext authenticationContext; - private final String principal; - - /** - * Constructs a new {@code Auth0AuthenticationToken} from the given {@link AuthenticationContext}. - *

- * Extracts authorities from the "scope" claim and sets the principal to the "sub" claim. - * - * @param authenticationContext the validated Auth0 authentication context - */ - public Auth0AuthenticationToken(AuthenticationContext authenticationContext) { - super(createAuthorities(authenticationContext)); - this.authenticationContext = authenticationContext; - this.principal = (String) authenticationContext.getClaims().get("sub"); - setAuthenticated(true); - } + private final AuthenticationContext authenticationContext; + private final String principal; - static Collection createAuthorities(AuthenticationContext ctx) { - Object scopeClaim = ctx.getClaims().get("scope"); + /** + * Constructs a new {@code Auth0AuthenticationToken} from the given {@link AuthenticationContext}. + * + *

Extracts authorities from the "scope" claim and sets the principal to the "sub" claim. + * + * @param authenticationContext the validated Auth0 authentication context + */ + public Auth0AuthenticationToken(AuthenticationContext authenticationContext) { + super(createAuthorities(authenticationContext)); + this.authenticationContext = authenticationContext; + this.principal = (String) authenticationContext.getClaims().get("sub"); + setAuthenticated(true); + } - if (scopeClaim instanceof String && !((String) scopeClaim).isBlank()) { - String scopes = (String) scopeClaim; - List authorities = List.of(scopes.trim().split("\\s+")); + static Collection createAuthorities(AuthenticationContext ctx) { + Object scopeClaim = ctx.getClaims().get("scope"); - return authorities.stream() - .map(scope -> "SCOPE_" + scope) - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - } + if (scopeClaim instanceof String && !((String) scopeClaim).isBlank()) { + String scopes = (String) scopeClaim; + List authorities = List.of(scopes.trim().split("\\s+")); - return AuthorityUtils.createAuthorityList("ROLE_USER"); + return authorities.stream() + .map(scope -> "SCOPE_" + scope) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); } - /** - * Returns the credentials for this authentication token. - *

- * Always returns {@code null} as credentials are not exposed. - * - * @return {@code null} - */ - @Override - public Object getCredentials() { - return null; - } + return AuthorityUtils.createAuthorityList("ROLE_USER"); + } - /** - * Returns the principal identifier for this authentication token. - *

- * Typically the "sub" claim from the JWT. - * - * @return the principal identifier - */ - @Override - public Object getPrincipal() { - return this.principal; - } + /** + * Returns the credentials for this authentication token. + * + *

Always returns {@code null} as credentials are not exposed. + * + * @return {@code null} + */ + @Override + public Object getCredentials() { + return null; + } - /** - * Returns the JWT claims from the authenticated token. - *

- * Provides access to all JWT claims without exposing internal authentication - * context. - * - * @return a map containing all JWT claims - */ - public Map getClaims() { - return authenticationContext.getClaims(); - } + /** + * Returns the principal identifier for this authentication token. + * + *

Typically the "sub" claim from the JWT. + * + * @return the principal identifier + */ + @Override + public Object getPrincipal() { + return this.principal; + } - /** - * Returns the scopes from the JWT as a set of strings. - *

- * Extracts and parses the "scope" claim into individual scope strings. - * - * @return a set of scope strings, or empty set if no scopes present - */ - public Set getScopes() { - Object scopeClaim = authenticationContext.getClaims().get("scope"); - if (scopeClaim instanceof String && !((String) scopeClaim).isBlank()) { - String scopes = (String) scopeClaim; - return Set.of(scopes.trim().split("\\s+")); - } - return Set.of(); - } + /** + * Returns the JWT claims from the authenticated token. + * + *

Provides access to all JWT claims without exposing internal authentication context. + * + * @return a map containing all JWT claims + */ + public Map getClaims() { + return authenticationContext.getClaims(); + } - /** - * Returns a specific claim value from the JWT. - *

- * Convenience method for accessing individual claims without getting the full - * claims map. - * - * @param claimName the name of the claim to retrieve - * @return the claim value, or null if not present - */ - public Object getClaim(String claimName) { - return authenticationContext.getClaims().get(claimName); + /** + * Returns the scopes from the JWT as a set of strings. + * + *

Extracts and parses the "scope" claim into individual scope strings. + * + * @return a set of scope strings, or empty set if no scopes present + */ + public Set getScopes() { + Object scopeClaim = authenticationContext.getClaims().get("scope"); + if (scopeClaim instanceof String && !((String) scopeClaim).isBlank()) { + String scopes = (String) scopeClaim; + return Set.of(scopes.trim().split("\\s+")); } + return Set.of(); + } + + /** + * Returns a specific claim value from the JWT. + * + *

Convenience method for accessing individual claims without getting the full claims map. + * + * @param claimName the name of the claim to retrieve + * @return the claim value, or null if not present + */ + public Object getClaim(String claimName) { + return authenticationContext.getClaims().get(claimName); + } } diff --git a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AutoConfiguration.java b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AutoConfiguration.java index e634fdc..d859a87 100644 --- a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AutoConfiguration.java +++ b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0AutoConfiguration.java @@ -7,55 +7,53 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -/** - * Autoconfiguration for Auth0 authentication and JWT validation. - */ +/** Autoconfiguration for Auth0 authentication and JWT validation. */ @AutoConfiguration @EnableConfigurationProperties(Auth0Properties.class) public class Auth0AutoConfiguration { - /** - * Creates an {@link AuthOptions} bean from {@link Auth0Properties}. - *

- * Builds the authentication options configuration. - * @param properties the Auth0 configuration properties from application configuration - * @return configured AuthOptions instance for creating AuthClient - * @see AuthOptions.Builder - * @see Auth0Properties - */ - @Bean - public AuthOptions authOptions(Auth0Properties properties) { + /** + * Creates an {@link AuthOptions} bean from {@link Auth0Properties}. + * + *

Builds the authentication options configuration. + * + * @param properties the Auth0 configuration properties from application configuration + * @return configured AuthOptions instance for creating AuthClient + * @see AuthOptions.Builder + * @see Auth0Properties + */ + @Bean + public AuthOptions authOptions(Auth0Properties properties) { - AuthOptions.Builder builder = new AuthOptions.Builder() - .domain(properties.getDomain()) - .audience(properties.getAudience()); + AuthOptions.Builder builder = + new AuthOptions.Builder().domain(properties.getDomain()).audience(properties.getAudience()); - if (properties.getDpopMode() != null) { - builder.dpopMode(properties.getDpopMode()); - } - - if (properties.getDpopIatLeewaySeconds() != null) { - builder.dpopIatLeewaySeconds(properties.getDpopIatLeewaySeconds()); - } - if (properties.getDpopIatOffsetSeconds() != null) { - builder.dpopIatOffsetSeconds(properties.getDpopIatOffsetSeconds()); - } - - return builder.build(); + if (properties.getDpopMode() != null) { + builder.dpopMode(properties.getDpopMode()); } - /** - * Creates an {@link AuthClient} bean for request authentication and JWT validation. - *

- * Serves as the main entry point for verifying HTTP requests containing - * access tokens. - * @param options the AuthOptions configuration for creating the client - * @return AuthClient instance configured with the specified options - * @see AuthClient#from(AuthOptions) - * @see AuthClient#verifyRequest(java.util.Map, com.auth0.models.HttpRequestInfo) - */ - @Bean - @ConditionalOnMissingBean - public AuthClient authClient(AuthOptions options) { - return AuthClient.from(options); + if (properties.getDpopIatLeewaySeconds() != null) { + builder.dpopIatLeewaySeconds(properties.getDpopIatLeewaySeconds()); + } + if (properties.getDpopIatOffsetSeconds() != null) { + builder.dpopIatOffsetSeconds(properties.getDpopIatOffsetSeconds()); } + + return builder.build(); + } + + /** + * Creates an {@link AuthClient} bean for request authentication and JWT validation. + * + *

Serves as the main entry point for verifying HTTP requests containing access tokens. + * + * @param options the AuthOptions configuration for creating the client + * @return AuthClient instance configured with the specified options + * @see AuthClient#from(AuthOptions) + * @see AuthClient#verifyRequest(java.util.Map, com.auth0.models.HttpRequestInfo) + */ + @Bean + @ConditionalOnMissingBean + public AuthClient authClient(AuthOptions options) { + return AuthClient.from(options); + } } diff --git a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0Properties.java b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0Properties.java index 82896b5..e1018fb 100644 --- a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0Properties.java +++ b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0Properties.java @@ -5,11 +5,12 @@ /** * Configuration properties for Auth0 authentication and token validation. - *

- * This class binds Spring Boot configuration properties prefixed with {@code auth0} to provide + * + *

This class binds Spring Boot configuration properties prefixed with {@code auth0} to provide * configuration for JWT validation, DPoP support, and API access control. - *

- * Example configuration in {@code application.yml}: + * + *

Example configuration in {@code application.yml}: + * *

  * auth0:
  *   domain: "random-test.us.auth0.com"
@@ -18,94 +19,115 @@
  *   dpopIatOffsetSeconds: 300
  *   dpopIatLeewaySeconds: 60
  * 
+ * * @see com.auth0.enums.DPoPMode */ @ConfigurationProperties(prefix = "auth0") public class Auth0Properties { - private String domain; - private String audience; - private DPoPMode dpopMode; + private String domain; + private String audience; + private DPoPMode dpopMode; - private Long dpopIatOffsetSeconds; - private Long dpopIatLeewaySeconds; + private Long dpopIatOffsetSeconds; + private Long dpopIatLeewaySeconds; - /** - * Gets the Auth0 domain configured for this application. - * @return the Auth0 domain, or {@code null} if not configured - */ - public String getDomain() { return domain; } + /** + * Gets the Auth0 domain configured for this application. + * + * @return the Auth0 domain, or {@code null} if not configured + */ + public String getDomain() { + return domain; + } - /** - * Sets the Auth0 domain for this application. - * @param domain the Auth0 domain to configure - */ - public void setDomain(String domain) { this.domain = domain; } + /** + * Sets the Auth0 domain for this application. + * + * @param domain the Auth0 domain to configure + */ + public void setDomain(String domain) { + this.domain = domain; + } - /** - * Gets the audience (API identifier) for token validation. - * @return the configured audience, or {@code null} if not set - */ - public String getAudience() { return audience; } + /** + * Gets the audience (API identifier) for token validation. + * + * @return the configured audience, or {@code null} if not set + */ + public String getAudience() { + return audience; + } - /** - * Sets the audience (API identifier). - * @param audience the audience to configure - */ - public void setAudience(String audience) { this.audience = audience; } + /** + * Sets the audience (API identifier). + * + * @param audience the audience to configure + */ + public void setAudience(String audience) { + this.audience = audience; + } - /** - * Gets the DPoP mode for token validation. - * @return the configured DPoP mode ({@code DISABLED}, {@code ALLOWED}, or {@code REQUIRED}), or {@code null} if not set - */ - public DPoPMode getDpopMode() { - return dpopMode; - } + /** + * Gets the DPoP mode for token validation. + * + * @return the configured DPoP mode ({@code DISABLED}, {@code ALLOWED}, or {@code REQUIRED}), or + * {@code null} if not set + */ + public DPoPMode getDpopMode() { + return dpopMode; + } - /** - * Sets the DPoP mode for token validation. - * @param dpopMode the DPoP mode to configure ({@code DISABLED}, {@code ALLOWED}, or {@code REQUIRED}) - */ - public void setDpopMode(DPoPMode dpopMode) { - this.dpopMode = dpopMode; - } + /** + * Sets the DPoP mode for token validation. + * + * @param dpopMode the DPoP mode to configure ({@code DISABLED}, {@code ALLOWED}, or {@code + * REQUIRED}) + */ + public void setDpopMode(DPoPMode dpopMode) { + this.dpopMode = dpopMode; + } - /** - * Gets the DPoP proof iat (issued-at) offset in seconds. - * @return the configured offset in seconds, or {@code null} if not set - */ - public Long getDpopIatOffsetSeconds() { - return dpopIatOffsetSeconds; - } + /** + * Gets the DPoP proof iat (issued-at) offset in seconds. + * + * @return the configured offset in seconds, or {@code null} if not set + */ + public Long getDpopIatOffsetSeconds() { + return dpopIatOffsetSeconds; + } - /** - * Sets the DPoP proof iat (issued-at) offset in seconds. - * @param dpopIatOffsetSeconds the offset in seconds to configure (must be non-negative) - * @throws IllegalArgumentException if the value is negative - */ - public void setDpopIatOffsetSeconds(Long dpopIatOffsetSeconds) { - if (dpopIatOffsetSeconds != null && dpopIatOffsetSeconds < 0) { - throw new IllegalArgumentException("DPoP iat offset seconds must be non-negative"); - } - this.dpopIatOffsetSeconds = dpopIatOffsetSeconds; + /** + * Sets the DPoP proof iat (issued-at) offset in seconds. + * + * @param dpopIatOffsetSeconds the offset in seconds to configure (must be non-negative) + * @throws IllegalArgumentException if the value is negative + */ + public void setDpopIatOffsetSeconds(Long dpopIatOffsetSeconds) { + if (dpopIatOffsetSeconds != null && dpopIatOffsetSeconds < 0) { + throw new IllegalArgumentException("DPoP iat offset seconds must be non-negative"); } + this.dpopIatOffsetSeconds = dpopIatOffsetSeconds; + } - /** - * Gets the DPoP proof iat (issued-at) leeway in seconds. - * @return the configured leeway in seconds, or {@code null} if not set - */ - public Long getDpopIatLeewaySeconds() { - return dpopIatLeewaySeconds; - } + /** + * Gets the DPoP proof iat (issued-at) leeway in seconds. + * + * @return the configured leeway in seconds, or {@code null} if not set + */ + public Long getDpopIatLeewaySeconds() { + return dpopIatLeewaySeconds; + } - /** - * Sets the DPoP proof iat (issued-at) leeway in seconds. - * @param dpopIatLeewaySeconds the leeway in seconds to configure (must be non-negative) - * @throws IllegalArgumentException if the value is negative - */ - public void setDpopIatLeewaySeconds(Long dpopIatLeewaySeconds) { - if (dpopIatLeewaySeconds != null && dpopIatLeewaySeconds < 0) { - throw new IllegalArgumentException("DPoP iat leeway seconds must be non-negative"); - } - this.dpopIatLeewaySeconds = dpopIatLeewaySeconds; + /** + * Sets the DPoP proof iat (issued-at) leeway in seconds. + * + * @param dpopIatLeewaySeconds the leeway in seconds to configure (must be non-negative) + * @throws IllegalArgumentException if the value is negative + */ + public void setDpopIatLeewaySeconds(Long dpopIatLeewaySeconds) { + if (dpopIatLeewaySeconds != null && dpopIatLeewaySeconds < 0) { + throw new IllegalArgumentException("DPoP iat leeway seconds must be non-negative"); } + this.dpopIatLeewaySeconds = dpopIatLeewaySeconds; + } } diff --git a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0SecurityAutoConfiguration.java b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0SecurityAutoConfiguration.java index 4ce3ddb..49685c5 100644 --- a/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0SecurityAutoConfiguration.java +++ b/auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0SecurityAutoConfiguration.java @@ -10,9 +10,10 @@ @ConditionalOnClass(AuthClient.class) public class Auth0SecurityAutoConfiguration { - @Bean - @ConditionalOnMissingBean - public Auth0AuthenticationFilter authAuthenticationFilter(AuthClient authClient, Auth0Properties auth0Properties) { - return new Auth0AuthenticationFilter(authClient, auth0Properties); - } + @Bean + @ConditionalOnMissingBean + public Auth0AuthenticationFilter authAuthenticationFilter( + AuthClient authClient, Auth0Properties auth0Properties) { + return new Auth0AuthenticationFilter(authClient, auth0Properties); + } } 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 index 5bac0ec..338fd8a 100644 --- 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 @@ -6,8 +6,7 @@ @SpringBootApplication public class Auth0SpringbootApiApplication { - public static void main(String[] args) { - SpringApplication.run(Auth0SpringbootApiApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(Auth0SpringbootApiApplication.class, args); + } } diff --git a/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AuthenticationFilterTest.java b/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AuthenticationFilterTest.java index 96471cf..7a8c584 100644 --- a/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AuthenticationFilterTest.java +++ b/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AuthenticationFilterTest.java @@ -1,10 +1,16 @@ package com.auth0.spring.boot; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + import com.auth0.AuthClient; import com.auth0.enums.DPoPMode; import com.auth0.exception.MissingAuthorizationException; import com.auth0.models.AuthenticationContext; import com.auth0.models.HttpRequestInfo; +import jakarta.servlet.FilterChain; +import java.util.Enumeration; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,417 +19,412 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; - -import jakarta.servlet.FilterChain; import org.springframework.security.core.context.SecurityContextHolder; -import java.util.Enumeration; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.when; - -/** - * Test cases for Auth0AuthenticationFilter - */ +/** Test cases for Auth0AuthenticationFilter */ @ExtendWith(MockitoExtension.class) class Auth0AuthenticationFilterTest { - @Mock - private Auth0Properties auth0Properties; - - @Mock - private AuthClient authClient; - - @Mock - private FilterChain filterChain; - - private Auth0AuthenticationFilter filter; - private MockHttpServletRequest request; - private MockHttpServletResponse response; - - @BeforeEach - void setUp() { - filter = new Auth0AuthenticationFilter(authClient, auth0Properties); - request = new MockHttpServletRequest(); - response = new MockHttpServletResponse(); - } - - @Test - @DisplayName("Should return empty map when request has no headers") - void extractHeaders_shouldReturnEmptyMap_whenNoHeadersPresent() throws MissingAuthorizationException { - Map headers = filter.extractHeaders(request); - - assertNotNull(headers); - assertTrue(headers.isEmpty()); - } - - @Test - @DisplayName("Should extract single header with normalized lowercase name") - void extractHeaders_shouldExtractSingleHeader_withLowercaseName() throws MissingAuthorizationException { - request.addHeader("Authorization", "Bearer token123"); - - Map headers = filter.extractHeaders(request); - - assertEquals(1, headers.size()); - assertEquals("Bearer token123", headers.get("authorization")); - } - - @Test - @DisplayName("Should extract multiple headers with all names normalized to lowercase") - void extractHeaders_shouldExtractMultipleHeaders_withNormalizedNames() throws MissingAuthorizationException { - String bearerToken = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuYXV0aDAuY29tLyJ9.signature"; - String dpopProof = "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0.eyJqdGkiOiIxMjM0NSJ9.proof"; - - request.addHeader("AuThOrIzAtIoN", bearerToken); - request.addHeader("Content-Type", "application/json"); - request.addHeader("DPoP", dpopProof); - - Map headers = filter.extractHeaders(request); - - assertEquals(3, headers.size()); - assertEquals(bearerToken, headers.get("authorization")); - assertEquals("application/json", headers.get("content-type")); - assertEquals(dpopProof, headers.get("dpop")); - } - - @Test - @DisplayName("Should fail when multiple authorization headers are present") - void extractHeaders_shouldThrowExceptionWhenMultipleAuthorizationHeadersArePresent(){ - - when(auth0Properties.getDpopMode()).thenReturn(DPoPMode.REQUIRED); - String dpopToken = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuYXV0aDAuY29tLyJ9.signature"; - String dpopProof = "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0.eyJqdGkiOiIxMjM0NSJ9.proof"; - - request.addHeader("authorization", dpopToken); - request.addHeader("Content-Type", "application/json"); - request.addHeader("DPoP", dpopProof); - request.addHeader("Authorization", dpopToken); - - MissingAuthorizationException ex = assertThrows( - MissingAuthorizationException.class, - () -> filter.extractHeaders(request) - ); - - assertEquals(400, ex.getStatusCode()); - } - - @Test - @DisplayName("Should return empty map when header enumeration is null") - void extractHeaders_shouldReturnEmptyMap_whenHeaderEnumerationIsNull() throws MissingAuthorizationException { - MockHttpServletRequest nullHeaderRequest = new MockHttpServletRequest() { - @Override - public Enumeration getHeaderNames() { - return null; - } + @Mock private Auth0Properties auth0Properties; + + @Mock private AuthClient authClient; + + @Mock private FilterChain filterChain; + + private Auth0AuthenticationFilter filter; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @BeforeEach + void setUp() { + filter = new Auth0AuthenticationFilter(authClient, auth0Properties); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + } + + @Test + @DisplayName("Should return empty map when request has no headers") + void extractHeaders_shouldReturnEmptyMap_whenNoHeadersPresent() + throws MissingAuthorizationException { + Map headers = filter.extractHeaders(request); + + assertNotNull(headers); + assertTrue(headers.isEmpty()); + } + + @Test + @DisplayName("Should extract single header with normalized lowercase name") + void extractHeaders_shouldExtractSingleHeader_withLowercaseName() + throws MissingAuthorizationException { + request.addHeader("Authorization", "Bearer token123"); + + Map headers = filter.extractHeaders(request); + + assertEquals(1, headers.size()); + assertEquals("Bearer token123", headers.get("authorization")); + } + + @Test + @DisplayName("Should extract multiple headers with all names normalized to lowercase") + void extractHeaders_shouldExtractMultipleHeaders_withNormalizedNames() + throws MissingAuthorizationException { + String bearerToken = + "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuYXV0aDAuY29tLyJ9.signature"; + String dpopProof = "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0.eyJqdGkiOiIxMjM0NSJ9.proof"; + + request.addHeader("AuThOrIzAtIoN", bearerToken); + request.addHeader("Content-Type", "application/json"); + request.addHeader("DPoP", dpopProof); + + Map headers = filter.extractHeaders(request); + + assertEquals(3, headers.size()); + assertEquals(bearerToken, headers.get("authorization")); + assertEquals("application/json", headers.get("content-type")); + assertEquals(dpopProof, headers.get("dpop")); + } + + @Test + @DisplayName("Should fail when multiple authorization headers are present") + void extractHeaders_shouldThrowExceptionWhenMultipleAuthorizationHeadersArePresent() { + + when(auth0Properties.getDpopMode()).thenReturn(DPoPMode.REQUIRED); + String dpopToken = + "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuYXV0aDAuY29tLyJ9.signature"; + String dpopProof = "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0.eyJqdGkiOiIxMjM0NSJ9.proof"; + + request.addHeader("authorization", dpopToken); + request.addHeader("Content-Type", "application/json"); + request.addHeader("DPoP", dpopProof); + request.addHeader("Authorization", dpopToken); + + MissingAuthorizationException ex = + assertThrows(MissingAuthorizationException.class, () -> filter.extractHeaders(request)); + + assertEquals(400, ex.getStatusCode()); + } + + @Test + @DisplayName("Should return empty map when header enumeration is null") + void extractHeaders_shouldReturnEmptyMap_whenHeaderEnumerationIsNull() + throws MissingAuthorizationException { + MockHttpServletRequest nullHeaderRequest = + new MockHttpServletRequest() { + @Override + public Enumeration getHeaderNames() { + return null; + } }; - Map headers = filter.extractHeaders(nullHeaderRequest); - - assertNotNull(headers); - assertTrue(headers.isEmpty()); - } - - @Test - @DisplayName("Should build HTTP URL with default port 80") - void buildHtu_shouldBuildHttpUrl_withDefaultPort() { - request.setScheme("http"); - request.setServerName("example.com"); - request.setServerPort(80); - request.setRequestURI("/api/users"); - - String htu = Auth0AuthenticationFilter.buildHtu(request); - - assertEquals("http://example.com/api/users", htu); - } - - @Test - @DisplayName("Should build HTTPS URL with default port 443") - void buildHtu_shouldBuildHttpsUrl_withDefaultPort() { - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/v2/resource"); - - String htu = Auth0AuthenticationFilter.buildHtu(request); - - assertEquals("https://api.example.com/v2/resource", htu); - } - - @Test - @DisplayName("Should build HTTP URL with non-default port") - void buildHtu_shouldBuildHttpUrl_withNonDefaultPort() { - request.setScheme("HtTp"); - request.setServerName("localhost"); - request.setServerPort(8080); - request.setRequestURI("/test"); - - String htu = Auth0AuthenticationFilter.buildHtu(request); - - assertEquals("http://localhost:8080/test", htu); - } - - @Test - @DisplayName("Should build HTTPS URL with non-default port") - void buildHtu_shouldBuildHttpsUrl_withNonDefaultPort() { - request.setScheme("https"); - request.setServerName("secure.example.com"); - request.setServerPort(8443); - request.setRequestURI("/api/data"); - - String htu = Auth0AuthenticationFilter.buildHtu(request); - - assertEquals("https://secure.example.com:8443/api/data", htu); - } - - @Test - @DisplayName("Should normalize scheme and host to lowercase") - void buildHtu_shouldNormalizeSchemeAndHost_toLowerCase() { - request.setScheme("HTTPS"); - request.setServerName("API.EXAMPLE.COM"); - request.setServerPort(443); - request.setRequestURI("/Resource"); - - String htu = Auth0AuthenticationFilter.buildHtu(request); - - assertEquals("https://api.example.com/Resource", htu); - } - - @Test - @DisplayName("Should create HttpRequestInfo with GET method and built HTU") - void extractRequestInfo_shouldCreateHttpRequestInfo_withGetMethod() { - request.setMethod("GET"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/users"); - - HttpRequestInfo requestInfo = filter.extractRequestInfo(request); - - assertNotNull(requestInfo); - assertEquals("GET", requestInfo.getHttpMethod()); - assertEquals("https://api.example.com/api/users", requestInfo.getHttpUrl()); - } - - @Test - @DisplayName("Should authenticate successfully with valid Bearer token and set security context") - void doFilterInternal_shouldAuthenticateSuccessfully_withValidBearerToken() throws Exception { - request.setMethod("GET"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/users"); - request.addHeader("Authorization", "Bearer valid_token"); - - AuthenticationContext mockContext = org.mockito.Mockito.mock(AuthenticationContext.class); - when(authClient.verifyRequest( - org.mockito.ArgumentMatchers.anyMap(), - org.mockito.ArgumentMatchers.any(HttpRequestInfo.class) - )).thenReturn(mockContext); - - filter.doFilterInternal(request, response, filterChain); - - org.mockito.Mockito.verify(authClient).verifyRequest( - org.mockito.ArgumentMatchers.anyMap(), - org.mockito.ArgumentMatchers.any(HttpRequestInfo.class) - ); - org.mockito.Mockito.verify(filterChain).doFilter(request, response); - assertNotNull(SecurityContextHolder.getContext().getAuthentication()); - assertTrue(SecurityContextHolder.getContext().getAuthentication() instanceof Auth0AuthenticationToken); - } - - @Test - @DisplayName("Should authenticate successfully with valid DPoP token and proof") - void doFilterInternal_shouldAuthenticateSuccessfully_withValidDpopToken() throws Exception { - request.setMethod("POST"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/resource"); - request.addHeader("Authorization", "DPoP dpop_token"); - request.addHeader("DPoP", "dpop_proof_jwt"); - - AuthenticationContext mockContext = org.mockito.Mockito.mock(AuthenticationContext.class); - when(authClient.verifyRequest( - org.mockito.ArgumentMatchers.anyMap(), - org.mockito.ArgumentMatchers.any(HttpRequestInfo.class) - )).thenReturn(mockContext); - - filter.doFilterInternal(request, response, filterChain); - - org.mockito.Mockito.verify(filterChain).doFilter(request, response); - assertNotNull(SecurityContextHolder.getContext().getAuthentication()); - } - - @Test - @DisplayName("Should handle missing authorization header by returning 200 status") - void doFilterInternal_shouldReturn200_whenAuthorizationHeaderMissing() throws Exception { - request.setMethod("GET"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/users"); - - filter.doFilterInternal(request, response, filterChain); - - assertEquals(200, response.getStatus()); - org.mockito.Mockito.verify(filterChain).doFilter(request, response); - assertNull(SecurityContextHolder.getContext().getAuthentication()); - } - - @Test - @DisplayName("Should handle invalid token by returning 401 status and clearing context") - void doFilterInternal_shouldReturn401AndClearContext_withInvalidToken() throws Exception { - request.setMethod("GET"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/users"); - request.addHeader("Authorization", "Bearer invalid_token"); - - when(authClient.verifyRequest( - org.mockito.ArgumentMatchers.anyMap(), - org.mockito.ArgumentMatchers.any(HttpRequestInfo.class) - )).thenThrow(new com.auth0.exception.VerifyAccessTokenException("Invalid JWT signature")); - - filter.doFilterInternal(request, response, filterChain); - - assertEquals(401, response.getStatus()); - org.mockito.Mockito.verify(filterChain, org.mockito.Mockito.never()).doFilter(request, response); - assertNull(SecurityContextHolder.getContext().getAuthentication()); - } - - @Test - @DisplayName("Should handle insufficient scope by returning 403 status") - void doFilterInternal_shouldReturn403_withInsufficientScope() throws Exception { - request.setMethod("POST"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/admin"); - request.addHeader("Authorization", "Bearer valid_token"); - - when(authClient.verifyRequest( - org.mockito.ArgumentMatchers.anyMap(), - org.mockito.ArgumentMatchers.any(HttpRequestInfo.class) - )).thenThrow(new com.auth0.exception.InsufficientScopeException("Insufficient scope")); - - filter.doFilterInternal(request, response, filterChain); - - assertEquals(403, response.getStatus()); - org.mockito.Mockito.verify(filterChain, org.mockito.Mockito.never()).doFilter(request, response); - assertNull(SecurityContextHolder.getContext().getAuthentication()); - } - - @Test - @DisplayName("Should add WWW-Authenticate header when present in exception") - void doFilterInternal_shouldAddWwwAuthenticateHeader_whenPresentInException() throws Exception { - request.setMethod("GET"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/users"); - request.addHeader("Authorization", "Bearer expired_token"); - - Map exceptionHeaders = new java.util.HashMap<>(); - exceptionHeaders.put("WWW-Authenticate", "Bearer realm=\"api\", error=\"invalid_token\""); - - // Simulating an exception that would be thrown by the authClient - com.auth0.exception.VerifyAccessTokenException exception = - new com.auth0.exception.VerifyAccessTokenException("Token expired"); - exception.addHeader("WWW-Authenticate", "Bearer realm=\"api\", error=\"invalid_token\""); - - when(authClient.verifyRequest( - org.mockito.ArgumentMatchers.anyMap(), - org.mockito.ArgumentMatchers.any(HttpRequestInfo.class) - )).thenThrow(exception); - - filter.doFilterInternal(request, response, filterChain); - - assertEquals(401, response.getStatus()); - assertEquals("Bearer realm=\"api\", error=\"invalid_token\"", - response.getHeader("WWW-Authenticate")); - assertNull(SecurityContextHolder.getContext().getAuthentication()); - } - - @Test - @DisplayName("Should not add WWW-Authenticate header when not present in exception") - void doFilterInternal_shouldNotAddWwwAuthenticateHeader_whenNotPresentInException() throws Exception { - request.setMethod("GET"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/users"); - request.addHeader("Authorization", "Bearer malformed_token"); - - com.auth0.exception.VerifyAccessTokenException exception = - new com.auth0.exception.VerifyAccessTokenException("Malformed token"); - - when(authClient.verifyRequest( - org.mockito.ArgumentMatchers.anyMap(), - org.mockito.ArgumentMatchers.any(HttpRequestInfo.class) - )).thenThrow(exception); - - filter.doFilterInternal(request, response, filterChain); - - assertEquals(401, response.getStatus()); - assertNull(response.getHeader("WWW-Authenticate")); - assertNull(SecurityContextHolder.getContext().getAuthentication()); - } - - @Test - @DisplayName("Should set authentication details from request") - void doFilterInternal_shouldSetAuthenticationDetails_fromRequest() throws Exception { - request.setMethod("GET"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/users"); - request.setRemoteAddr("192.168.1.100"); - request.addHeader("Authorization", "Bearer valid_token"); - - AuthenticationContext mockContext = org.mockito.Mockito.mock(AuthenticationContext.class); - when(authClient.verifyRequest( - org.mockito.ArgumentMatchers.anyMap(), - org.mockito.ArgumentMatchers.any(HttpRequestInfo.class) - )).thenReturn(mockContext); - - filter.doFilterInternal(request, response, filterChain); - - Auth0AuthenticationToken auth = - (Auth0AuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); - assertNotNull(auth.getDetails()); - } - - @Test - @DisplayName("Should handle DPoP validation exception and return appropriate status") - void doFilterInternal_shouldHandleDpopValidationException_withProperStatus() throws Exception { - request.setMethod("POST"); - request.setScheme("https"); - request.setServerName("api.example.com"); - request.setServerPort(443); - request.setRequestURI("/api/resource"); - request.addHeader("Authorization", "DPoP dpop_token"); - request.addHeader("DPoP", "invalid_proof"); - - Map exceptionHeaders = new java.util.HashMap<>(); - exceptionHeaders.put("WWW-Authenticate", "DPoP error=\"invalid_dpop_proof\""); - - // Simulating an exception that would be thrown by the authClient - com.auth0.exception.InvalidDpopProofException exception = - new com.auth0.exception.InvalidDpopProofException("Invalid DPoP proof"); - exception.addHeader("WWW-Authenticate", "DPoP error=\"invalid_dpop_proof\""); - - when(authClient.verifyRequest( - org.mockito.ArgumentMatchers.anyMap(), - org.mockito.ArgumentMatchers.any(HttpRequestInfo.class) - )).thenThrow(exception); - - filter.doFilterInternal(request, response, filterChain); - - assertEquals(400, response.getStatus()); - assertEquals("DPoP error=\"invalid_dpop_proof\"", response.getHeader("WWW-Authenticate")); - assertNull(SecurityContextHolder.getContext().getAuthentication()); - } + Map headers = filter.extractHeaders(nullHeaderRequest); + + assertNotNull(headers); + assertTrue(headers.isEmpty()); + } + + @Test + @DisplayName("Should build HTTP URL with default port 80") + void buildHtu_shouldBuildHttpUrl_withDefaultPort() { + request.setScheme("http"); + request.setServerName("example.com"); + request.setServerPort(80); + request.setRequestURI("/api/users"); + + String htu = Auth0AuthenticationFilter.buildHtu(request); + + assertEquals("http://example.com/api/users", htu); + } + + @Test + @DisplayName("Should build HTTPS URL with default port 443") + void buildHtu_shouldBuildHttpsUrl_withDefaultPort() { + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/v2/resource"); + + String htu = Auth0AuthenticationFilter.buildHtu(request); + + assertEquals("https://api.example.com/v2/resource", htu); + } + + @Test + @DisplayName("Should build HTTP URL with non-default port") + void buildHtu_shouldBuildHttpUrl_withNonDefaultPort() { + request.setScheme("HtTp"); + request.setServerName("localhost"); + request.setServerPort(8080); + request.setRequestURI("/test"); + + String htu = Auth0AuthenticationFilter.buildHtu(request); + + assertEquals("http://localhost:8080/test", htu); + } + + @Test + @DisplayName("Should build HTTPS URL with non-default port") + void buildHtu_shouldBuildHttpsUrl_withNonDefaultPort() { + request.setScheme("https"); + request.setServerName("secure.example.com"); + request.setServerPort(8443); + request.setRequestURI("/api/data"); + + String htu = Auth0AuthenticationFilter.buildHtu(request); + + assertEquals("https://secure.example.com:8443/api/data", htu); + } + + @Test + @DisplayName("Should normalize scheme and host to lowercase") + void buildHtu_shouldNormalizeSchemeAndHost_toLowerCase() { + request.setScheme("HTTPS"); + request.setServerName("API.EXAMPLE.COM"); + request.setServerPort(443); + request.setRequestURI("/Resource"); + + String htu = Auth0AuthenticationFilter.buildHtu(request); + + assertEquals("https://api.example.com/Resource", htu); + } + + @Test + @DisplayName("Should create HttpRequestInfo with GET method and built HTU") + void extractRequestInfo_shouldCreateHttpRequestInfo_withGetMethod() { + request.setMethod("GET"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/users"); + + HttpRequestInfo requestInfo = filter.extractRequestInfo(request); + + assertNotNull(requestInfo); + assertEquals("GET", requestInfo.getHttpMethod()); + assertEquals("https://api.example.com/api/users", requestInfo.getHttpUrl()); + } + + @Test + @DisplayName("Should authenticate successfully with valid Bearer token and set security context") + void doFilterInternal_shouldAuthenticateSuccessfully_withValidBearerToken() throws Exception { + request.setMethod("GET"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/users"); + request.addHeader("Authorization", "Bearer valid_token"); + + AuthenticationContext mockContext = org.mockito.Mockito.mock(AuthenticationContext.class); + when(authClient.verifyRequest( + org.mockito.ArgumentMatchers.anyMap(), + org.mockito.ArgumentMatchers.any(HttpRequestInfo.class))) + .thenReturn(mockContext); + + filter.doFilterInternal(request, response, filterChain); + + org.mockito.Mockito.verify(authClient) + .verifyRequest( + org.mockito.ArgumentMatchers.anyMap(), + org.mockito.ArgumentMatchers.any(HttpRequestInfo.class)); + org.mockito.Mockito.verify(filterChain).doFilter(request, response); + assertNotNull(SecurityContextHolder.getContext().getAuthentication()); + assertTrue( + SecurityContextHolder.getContext().getAuthentication() instanceof Auth0AuthenticationToken); + } + + @Test + @DisplayName("Should authenticate successfully with valid DPoP token and proof") + void doFilterInternal_shouldAuthenticateSuccessfully_withValidDpopToken() throws Exception { + request.setMethod("POST"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/resource"); + request.addHeader("Authorization", "DPoP dpop_token"); + request.addHeader("DPoP", "dpop_proof_jwt"); + + AuthenticationContext mockContext = org.mockito.Mockito.mock(AuthenticationContext.class); + when(authClient.verifyRequest( + org.mockito.ArgumentMatchers.anyMap(), + org.mockito.ArgumentMatchers.any(HttpRequestInfo.class))) + .thenReturn(mockContext); + + filter.doFilterInternal(request, response, filterChain); + + org.mockito.Mockito.verify(filterChain).doFilter(request, response); + assertNotNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("Should handle missing authorization header by returning 200 status") + void doFilterInternal_shouldReturn200_whenAuthorizationHeaderMissing() throws Exception { + request.setMethod("GET"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/users"); + + filter.doFilterInternal(request, response, filterChain); + + assertEquals(200, response.getStatus()); + org.mockito.Mockito.verify(filterChain).doFilter(request, response); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("Should handle invalid token by returning 401 status and clearing context") + void doFilterInternal_shouldReturn401AndClearContext_withInvalidToken() throws Exception { + request.setMethod("GET"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/users"); + request.addHeader("Authorization", "Bearer invalid_token"); + + when(authClient.verifyRequest( + org.mockito.ArgumentMatchers.anyMap(), + org.mockito.ArgumentMatchers.any(HttpRequestInfo.class))) + .thenThrow(new com.auth0.exception.VerifyAccessTokenException("Invalid JWT signature")); + + filter.doFilterInternal(request, response, filterChain); + + assertEquals(401, response.getStatus()); + org.mockito.Mockito.verify(filterChain, org.mockito.Mockito.never()) + .doFilter(request, response); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("Should handle insufficient scope by returning 403 status") + void doFilterInternal_shouldReturn403_withInsufficientScope() throws Exception { + request.setMethod("POST"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/admin"); + request.addHeader("Authorization", "Bearer valid_token"); + + when(authClient.verifyRequest( + org.mockito.ArgumentMatchers.anyMap(), + org.mockito.ArgumentMatchers.any(HttpRequestInfo.class))) + .thenThrow(new com.auth0.exception.InsufficientScopeException("Insufficient scope")); + + filter.doFilterInternal(request, response, filterChain); + + assertEquals(403, response.getStatus()); + org.mockito.Mockito.verify(filterChain, org.mockito.Mockito.never()) + .doFilter(request, response); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("Should add WWW-Authenticate header when present in exception") + void doFilterInternal_shouldAddWwwAuthenticateHeader_whenPresentInException() throws Exception { + request.setMethod("GET"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/users"); + request.addHeader("Authorization", "Bearer expired_token"); + + Map exceptionHeaders = new java.util.HashMap<>(); + exceptionHeaders.put("WWW-Authenticate", "Bearer realm=\"api\", error=\"invalid_token\""); + + // Simulating an exception that would be thrown by the authClient + com.auth0.exception.VerifyAccessTokenException exception = + new com.auth0.exception.VerifyAccessTokenException("Token expired"); + exception.addHeader("WWW-Authenticate", "Bearer realm=\"api\", error=\"invalid_token\""); + + when(authClient.verifyRequest( + org.mockito.ArgumentMatchers.anyMap(), + org.mockito.ArgumentMatchers.any(HttpRequestInfo.class))) + .thenThrow(exception); + + filter.doFilterInternal(request, response, filterChain); + + assertEquals(401, response.getStatus()); + assertEquals( + "Bearer realm=\"api\", error=\"invalid_token\"", response.getHeader("WWW-Authenticate")); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("Should not add WWW-Authenticate header when not present in exception") + void doFilterInternal_shouldNotAddWwwAuthenticateHeader_whenNotPresentInException() + throws Exception { + request.setMethod("GET"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/users"); + request.addHeader("Authorization", "Bearer malformed_token"); + + com.auth0.exception.VerifyAccessTokenException exception = + new com.auth0.exception.VerifyAccessTokenException("Malformed token"); + + when(authClient.verifyRequest( + org.mockito.ArgumentMatchers.anyMap(), + org.mockito.ArgumentMatchers.any(HttpRequestInfo.class))) + .thenThrow(exception); + + filter.doFilterInternal(request, response, filterChain); + + assertEquals(401, response.getStatus()); + assertNull(response.getHeader("WWW-Authenticate")); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @Test + @DisplayName("Should set authentication details from request") + void doFilterInternal_shouldSetAuthenticationDetails_fromRequest() throws Exception { + request.setMethod("GET"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/users"); + request.setRemoteAddr("192.168.1.100"); + request.addHeader("Authorization", "Bearer valid_token"); + + AuthenticationContext mockContext = org.mockito.Mockito.mock(AuthenticationContext.class); + when(authClient.verifyRequest( + org.mockito.ArgumentMatchers.anyMap(), + org.mockito.ArgumentMatchers.any(HttpRequestInfo.class))) + .thenReturn(mockContext); + + filter.doFilterInternal(request, response, filterChain); + + Auth0AuthenticationToken auth = + (Auth0AuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + assertNotNull(auth.getDetails()); + } + + @Test + @DisplayName("Should handle DPoP validation exception and return appropriate status") + void doFilterInternal_shouldHandleDpopValidationException_withProperStatus() throws Exception { + request.setMethod("POST"); + request.setScheme("https"); + request.setServerName("api.example.com"); + request.setServerPort(443); + request.setRequestURI("/api/resource"); + request.addHeader("Authorization", "DPoP dpop_token"); + request.addHeader("DPoP", "invalid_proof"); + + Map exceptionHeaders = new java.util.HashMap<>(); + exceptionHeaders.put("WWW-Authenticate", "DPoP error=\"invalid_dpop_proof\""); + + // Simulating an exception that would be thrown by the authClient + com.auth0.exception.InvalidDpopProofException exception = + new com.auth0.exception.InvalidDpopProofException("Invalid DPoP proof"); + exception.addHeader("WWW-Authenticate", "DPoP error=\"invalid_dpop_proof\""); + + when(authClient.verifyRequest( + org.mockito.ArgumentMatchers.anyMap(), + org.mockito.ArgumentMatchers.any(HttpRequestInfo.class))) + .thenThrow(exception); + + filter.doFilterInternal(request, response, filterChain); + + assertEquals(400, response.getStatus()); + assertEquals("DPoP error=\"invalid_dpop_proof\"", response.getHeader("WWW-Authenticate")); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } } - diff --git a/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AuthenticationTokenTest.java b/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AuthenticationTokenTest.java index 06bcb22..eec1072 100644 --- a/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AuthenticationTokenTest.java +++ b/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0AuthenticationTokenTest.java @@ -1,192 +1,198 @@ package com.auth0.spring.boot; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.auth0.models.AuthenticationContext; +import java.util.*; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Test cases for Auth0AuthenticationToken - */ +/** Test cases for Auth0AuthenticationToken */ class Auth0AuthenticationTokenTest { - @Test - @DisplayName("Should create SCOPE_ prefixed authorities from single scope in scope claim") - void createAuthorities_shouldCreateScopePrefixedAuthorities_withSingleScope() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("scope", "read:users"); - when(context.getClaims()).thenReturn(claims); - - Collection authorities = Auth0AuthenticationToken.createAuthorities(context); - - assertEquals(1, authorities.size()); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); - } - - @Test - @DisplayName("Should create multiple SCOPE_ prefixed authorities from space-separated scopes") - void createAuthorities_shouldCreateMultipleAuthorities_withSpaceSeparatedScopes() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("scope", "read:users write:users delete:users"); - when(context.getClaims()).thenReturn(claims); - - Collection authorities = Auth0AuthenticationToken.createAuthorities(context); - - assertEquals(3, authorities.size()); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_write:users"))); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_delete:users"))); - } - - @Test - @DisplayName("Should return ROLE_USER when scope claim is missing") - void createAuthorities_shouldReturnRoleUser_whenScopeClaimMissing() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - when(context.getClaims()).thenReturn(claims); - - Collection authorities = Auth0AuthenticationToken.createAuthorities(context); - - assertEquals(1, authorities.size()); - assertTrue(authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))); - } - - @Test - @DisplayName("Should return ROLE_USER when scope claim is null") - void createAuthorities_shouldReturnRoleUser_whenScopeClaimIsNull() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("scope", null); - when(context.getClaims()).thenReturn(claims); - - Collection authorities = Auth0AuthenticationToken.createAuthorities(context); - - assertEquals(1, authorities.size()); - assertTrue(authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))); - } - - @Test - @DisplayName("Should return ROLE_USER when scope claim is not a String") - void createAuthorities_shouldReturnRoleUser_whenScopeClaimIsNotString() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("scope", Arrays.asList("read:users", "write:users")); - when(context.getClaims()).thenReturn(claims); - - Collection authorities = Auth0AuthenticationToken.createAuthorities(context); - - assertEquals(1, authorities.size()); - assertTrue(authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))); - } - - @Test - @DisplayName("Should return ROLE_USER when scope claim is empty string") - void createAuthorities_shouldReturnRoleUser_whenScopeClaimIsEmptyString() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("scope", ""); - when(context.getClaims()).thenReturn(claims); - - Collection authorities = Auth0AuthenticationToken.createAuthorities(context); - - assertEquals(1, authorities.size()); - assertTrue(authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))); - } - - @Test - @DisplayName("Should handle multiple consecutive spaces between scopes") - void createAuthorities_shouldHandleMultipleSpaces_betweenScopes() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("scope", "read:users write:users delete:users"); - when(context.getClaims()).thenReturn(claims); - - Collection authorities = Auth0AuthenticationToken.createAuthorities(context); - - assertEquals(3, authorities.size()); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_write:users"))); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_delete:users"))); - } - - @Test - @DisplayName("Should set principal to sub claim value from authentication context") - void constructor_shouldSetPrincipal_fromSubClaim() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("sub", "auth0|123456789"); - claims.put("scope", "read:users"); - when(context.getClaims()).thenReturn(claims); - - Auth0AuthenticationToken token = new Auth0AuthenticationToken(context); - - assertEquals("auth0|123456789", token.getPrincipal()); - } - - @Test - @DisplayName("Should set authentication as authenticated on construction") - void constructor_shouldSetAuthenticated_toTrue() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("sub", "auth0|123456789"); - claims.put("scope", "read:users"); - when(context.getClaims()).thenReturn(claims); - - Auth0AuthenticationToken token = new Auth0AuthenticationToken(context); - - assertTrue(token.isAuthenticated()); - } - - @Test - @DisplayName("Should return null for credentials") - void getCredentials_shouldReturnNull() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("sub", "auth0|123456789"); - when(context.getClaims()).thenReturn(claims); - - Auth0AuthenticationToken token = new Auth0AuthenticationToken(context); - - assertNull(token.getCredentials()); - } - - @Test - @DisplayName("Should create authorities with scopes containing special characters") - void createAuthorities_shouldHandleSpecialCharacters_inScopes() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("scope", "read:users write:admin-panel delete:resource/123"); - when(context.getClaims()).thenReturn(claims); - - Collection authorities = Auth0AuthenticationToken.createAuthorities(context); - - assertEquals(3, authorities.size()); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_write:admin-panel"))); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_delete:resource/123"))); - } - - @Test - @DisplayName("Should handle scope claim with leading and trailing whitespace") - void createAuthorities_shouldHandleWhitespace_aroundScopes() { - AuthenticationContext context = mock(AuthenticationContext.class); - Map claims = new HashMap<>(); - claims.put("scope", " read:users write:users "); - when(context.getClaims()).thenReturn(claims); - - Collection authorities = Auth0AuthenticationToken.createAuthorities(context); - - assertEquals(2, authorities.size()); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); - assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_write:users"))); - } -} \ No newline at end of file + @Test + @DisplayName("Should create SCOPE_ prefixed authorities from single scope in scope claim") + void createAuthorities_shouldCreateScopePrefixedAuthorities_withSingleScope() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("scope", "read:users"); + when(context.getClaims()).thenReturn(claims); + + Collection authorities = + Auth0AuthenticationToken.createAuthorities(context); + + assertEquals(1, authorities.size()); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); + } + + @Test + @DisplayName("Should create multiple SCOPE_ prefixed authorities from space-separated scopes") + void createAuthorities_shouldCreateMultipleAuthorities_withSpaceSeparatedScopes() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("scope", "read:users write:users delete:users"); + when(context.getClaims()).thenReturn(claims); + + Collection authorities = + Auth0AuthenticationToken.createAuthorities(context); + + assertEquals(3, authorities.size()); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_write:users"))); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_delete:users"))); + } + + @Test + @DisplayName("Should return ROLE_USER when scope claim is missing") + void createAuthorities_shouldReturnRoleUser_whenScopeClaimMissing() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + when(context.getClaims()).thenReturn(claims); + + Collection authorities = + Auth0AuthenticationToken.createAuthorities(context); + + assertEquals(1, authorities.size()); + assertTrue(authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))); + } + + @Test + @DisplayName("Should return ROLE_USER when scope claim is null") + void createAuthorities_shouldReturnRoleUser_whenScopeClaimIsNull() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("scope", null); + when(context.getClaims()).thenReturn(claims); + + Collection authorities = + Auth0AuthenticationToken.createAuthorities(context); + + assertEquals(1, authorities.size()); + assertTrue(authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))); + } + + @Test + @DisplayName("Should return ROLE_USER when scope claim is not a String") + void createAuthorities_shouldReturnRoleUser_whenScopeClaimIsNotString() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("scope", Arrays.asList("read:users", "write:users")); + when(context.getClaims()).thenReturn(claims); + + Collection authorities = + Auth0AuthenticationToken.createAuthorities(context); + + assertEquals(1, authorities.size()); + assertTrue(authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))); + } + + @Test + @DisplayName("Should return ROLE_USER when scope claim is empty string") + void createAuthorities_shouldReturnRoleUser_whenScopeClaimIsEmptyString() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("scope", ""); + when(context.getClaims()).thenReturn(claims); + + Collection authorities = + Auth0AuthenticationToken.createAuthorities(context); + + assertEquals(1, authorities.size()); + assertTrue(authorities.contains(new SimpleGrantedAuthority("ROLE_USER"))); + } + + @Test + @DisplayName("Should handle multiple consecutive spaces between scopes") + void createAuthorities_shouldHandleMultipleSpaces_betweenScopes() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("scope", "read:users write:users delete:users"); + when(context.getClaims()).thenReturn(claims); + + Collection authorities = + Auth0AuthenticationToken.createAuthorities(context); + + assertEquals(3, authorities.size()); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_write:users"))); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_delete:users"))); + } + + @Test + @DisplayName("Should set principal to sub claim value from authentication context") + void constructor_shouldSetPrincipal_fromSubClaim() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("sub", "auth0|123456789"); + claims.put("scope", "read:users"); + when(context.getClaims()).thenReturn(claims); + + Auth0AuthenticationToken token = new Auth0AuthenticationToken(context); + + assertEquals("auth0|123456789", token.getPrincipal()); + } + + @Test + @DisplayName("Should set authentication as authenticated on construction") + void constructor_shouldSetAuthenticated_toTrue() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("sub", "auth0|123456789"); + claims.put("scope", "read:users"); + when(context.getClaims()).thenReturn(claims); + + Auth0AuthenticationToken token = new Auth0AuthenticationToken(context); + + assertTrue(token.isAuthenticated()); + } + + @Test + @DisplayName("Should return null for credentials") + void getCredentials_shouldReturnNull() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("sub", "auth0|123456789"); + when(context.getClaims()).thenReturn(claims); + + Auth0AuthenticationToken token = new Auth0AuthenticationToken(context); + + assertNull(token.getCredentials()); + } + + @Test + @DisplayName("Should create authorities with scopes containing special characters") + void createAuthorities_shouldHandleSpecialCharacters_inScopes() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("scope", "read:users write:admin-panel delete:resource/123"); + when(context.getClaims()).thenReturn(claims); + + Collection authorities = + Auth0AuthenticationToken.createAuthorities(context); + + assertEquals(3, authorities.size()); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_write:admin-panel"))); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_delete:resource/123"))); + } + + @Test + @DisplayName("Should handle scope claim with leading and trailing whitespace") + void createAuthorities_shouldHandleWhitespace_aroundScopes() { + AuthenticationContext context = mock(AuthenticationContext.class); + Map claims = new HashMap<>(); + claims.put("scope", " read:users write:users "); + when(context.getClaims()).thenReturn(claims); + + Collection authorities = + Auth0AuthenticationToken.createAuthorities(context); + + assertEquals(2, authorities.size()); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_read:users"))); + assertTrue(authorities.contains(new SimpleGrantedAuthority("SCOPE_write:users"))); + } +} 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 d13fe08..a823fc6 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 @@ -1,8 +1,10 @@ package com.auth0.spring.boot; +import static org.junit.jupiter.api.Assertions.*; + import com.auth0.AuthClient; -import com.auth0.models.AuthOptions; import com.auth0.enums.DPoPMode; +import com.auth0.models.AuthOptions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -11,129 +13,124 @@ import org.springframework.context.ApplicationContext; import org.springframework.test.context.TestPropertySource; -import static org.junit.jupiter.api.Assertions.*; - /** * Test cases for Auth0AutoConfiguration + * *

*/ @SpringBootTest -@TestPropertySource(properties = { - "auth0.domain=test-domain.auth0.com", - "auth0.audience=https://api.example.com" -}) +@TestPropertySource( + properties = {"auth0.domain=test-domain.auth0.com", "auth0.audience=https://api.example.com"}) class Auth0AutoConfigurationTest { - @Autowired - private ApplicationContext context; - - @Autowired - private AuthOptions authOptions; - - @Autowired - private AuthClient authClient; + @Autowired private ApplicationContext context; + + @Autowired private AuthOptions authOptions; + + @Autowired private AuthClient authClient; + + @Test + @DisplayName("Should create AuthOptions bean with required domain and audience properties") + void shouldCreateAuthOptionsBean() { + assertNotNull(authOptions); + assertEquals("test-domain.auth0.com", authOptions.getDomain()); + assertEquals("https://api.example.com", authOptions.getAudience()); + } + + @Test + @DisplayName("Should create AuthClient bean configured with AuthOptions") + void shouldCreateAuthClientBean() { + assertNotNull(authClient); + assertTrue(context.containsBean("authClient")); + } + + @Test + @DisplayName("Should register all auto-configuration beans in application context") + void shouldRegisterAllBeansInContext() { + assertTrue(context.containsBean("authOptions")); + assertTrue(context.containsBean("authClient")); + } + + @Nested + @SpringBootTest + @TestPropertySource( + properties = { + "auth0.domain=dpop-test.auth0.com", + "auth0.audience=https://api.dpop.com", + "auth0.dpop-mode=REQUIRED", + "auth0.dpop-iat-leeway-seconds=10", + "auth0.dpop-iat-offset-seconds=300" + }) + class DPoPConfigurationTest { + + @Autowired private AuthOptions authOptions; @Test - @DisplayName("Should create AuthOptions bean with required domain and audience properties") - void shouldCreateAuthOptionsBean() { - assertNotNull(authOptions); - assertEquals("test-domain.auth0.com", authOptions.getDomain()); - assertEquals("https://api.example.com", authOptions.getAudience()); + @DisplayName("Should configure AuthOptions with DPoP mode when dpop-mode property is set") + void shouldConfigureDPoPMode() { + assertNotNull(authOptions); + assertEquals(DPoPMode.REQUIRED, authOptions.getDpopMode()); } @Test - @DisplayName("Should create AuthClient bean configured with AuthOptions") - void shouldCreateAuthClientBean() { - assertNotNull(authClient); - assertTrue(context.containsBean("authClient")); + @DisplayName("Should configure AuthOptions with DPoP IAT leeway seconds when property is set") + void shouldConfigureDPoPIatLeewaySeconds() { + assertNotNull(authOptions); + assertEquals(10, authOptions.getDpopIatLeewaySeconds()); } @Test - @DisplayName("Should register all auto-configuration beans in application context") - void shouldRegisterAllBeansInContext() { - assertTrue(context.containsBean("authOptions")); - assertTrue(context.containsBean("authClient")); + @DisplayName("Should configure AuthOptions with DPoP IAT offset seconds when property is set") + void shouldConfigureDPoPIatOffsetSeconds() { + assertNotNull(authOptions); + assertEquals(300, authOptions.getDpopIatOffsetSeconds()); } + } - @Nested - @SpringBootTest - @TestPropertySource(properties = { - "auth0.domain=dpop-test.auth0.com", - "auth0.audience=https://api.dpop.com", - "auth0.dpop-mode=REQUIRED", - "auth0.dpop-iat-leeway-seconds=10", - "auth0.dpop-iat-offset-seconds=300" - }) - class DPoPConfigurationTest { - - @Autowired - private AuthOptions authOptions; - - @Test - @DisplayName("Should configure AuthOptions with DPoP mode when dpop-mode property is set") - void shouldConfigureDPoPMode() { - assertNotNull(authOptions); - assertEquals(DPoPMode.REQUIRED, authOptions.getDpopMode()); - } - - @Test - @DisplayName("Should configure AuthOptions with DPoP IAT leeway seconds when property is set") - void shouldConfigureDPoPIatLeewaySeconds() { - assertNotNull(authOptions); - assertEquals(10, authOptions.getDpopIatLeewaySeconds()); - } - - @Test - @DisplayName("Should configure AuthOptions with DPoP IAT offset seconds when property is set") - void shouldConfigureDPoPIatOffsetSeconds() { - assertNotNull(authOptions); - assertEquals(300, authOptions.getDpopIatOffsetSeconds()); - } - } + @Nested + @SpringBootTest + @TestPropertySource( + properties = { + "auth0.domain=minimal-test.auth0.com", + "auth0.audience=https://api.minimal.com" + }) + class MinimalConfigurationTest { + @Autowired private AuthOptions authOptions; - @Nested - @SpringBootTest - @TestPropertySource(properties = { - "auth0.domain=minimal-test.auth0.com", - "auth0.audience=https://api.minimal.com" - }) - class MinimalConfigurationTest { - - @Autowired - private AuthOptions authOptions; - - @Test - @DisplayName("Should configure AuthOptions with default DPoP settings when no DPoP properties are set") - void shouldUseDefaultDPoPSettings() { - assertNotNull(authOptions); - assertEquals(DPoPMode.ALLOWED, authOptions.getDpopMode()); - assertEquals(30, authOptions.getDpopIatLeewaySeconds()); - assertEquals(300, authOptions.getDpopIatOffsetSeconds()); - } + @Test + @DisplayName( + "Should configure AuthOptions with default DPoP settings when no DPoP properties are set") + void shouldUseDefaultDPoPSettings() { + assertNotNull(authOptions); + assertEquals(DPoPMode.ALLOWED, authOptions.getDpopMode()); + assertEquals(30, authOptions.getDpopIatLeewaySeconds()); + assertEquals(300, authOptions.getDpopIatOffsetSeconds()); } + } - @Nested - @SpringBootTest - @TestPropertySource(properties = { - "auth0.domain=partial-dpop.auth0.com", - "auth0.audience=https://api.partial.com", - "auth0.dpop-mode=ALLOWED" - }) - class PartialDPoPConfigurationTest { - - @Autowired - private AuthOptions authOptions; - - @Test - @DisplayName("Should configure AuthOptions with only DPoP mode when other DPoP properties are not set") - void shouldConfigureOnlyDPoPMode() { - assertNotNull(authOptions); - assertEquals(DPoPMode.ALLOWED, authOptions.getDpopMode()); - - // Others should be set to their respective defaults - assertEquals(30, authOptions.getDpopIatLeewaySeconds()); - assertEquals(300, authOptions.getDpopIatOffsetSeconds()); - } + @Nested + @SpringBootTest + @TestPropertySource( + properties = { + "auth0.domain=partial-dpop.auth0.com", + "auth0.audience=https://api.partial.com", + "auth0.dpop-mode=ALLOWED" + }) + class PartialDPoPConfigurationTest { + + @Autowired private AuthOptions authOptions; + + @Test + @DisplayName( + "Should configure AuthOptions with only DPoP mode when other DPoP properties are not set") + void shouldConfigureOnlyDPoPMode() { + assertNotNull(authOptions); + assertEquals(DPoPMode.ALLOWED, authOptions.getDpopMode()); + + // Others should be set to their respective defaults + assertEquals(30, authOptions.getDpopIatLeewaySeconds()); + assertEquals(300, authOptions.getDpopIatOffsetSeconds()); } -} \ No newline at end of file + } +} diff --git a/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0PropertiesTest.java b/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0PropertiesTest.java index bdca5f1..01687b1 100644 --- a/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0PropertiesTest.java +++ b/auth0-springboot-api/src/test/java/com/auth0/spring/boot/Auth0PropertiesTest.java @@ -1,126 +1,120 @@ package com.auth0.spring.boot; +import static org.junit.jupiter.api.Assertions.*; + import com.auth0.enums.DPoPMode; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -/** - * Test cases for Auth0Properties - */ +/** Test cases for Auth0Properties */ class Auth0PropertiesTest { - private Auth0Properties properties; - - @BeforeEach - void setUp() { - properties = new Auth0Properties(); - } - - @Test - @DisplayName("Should set and get domain property") - void shouldSetAndGetDomain() { - String domain = "dev-tenant.us.auth0.com"; - properties.setDomain(domain); - - assertEquals(domain, properties.getDomain()); - } - - @Test - @DisplayName("Should set and get audience property") - void shouldSetAndGetAudience() { - String audience = "https://api.example.com/v2/"; - properties.setAudience(audience); - - assertEquals(audience, properties.getAudience()); - } - - @Test - @DisplayName("Should set and get dpopMode property") - void shouldSetAndGetDpopMode() { - properties.setDpopMode(DPoPMode.REQUIRED); - assertEquals(DPoPMode.REQUIRED, properties.getDpopMode()); - - properties.setDpopMode(DPoPMode.ALLOWED); - assertEquals(DPoPMode.ALLOWED, properties.getDpopMode()); - - properties.setDpopMode(DPoPMode.DISABLED); - assertEquals(DPoPMode.DISABLED, properties.getDpopMode()); - } - - @Test - @DisplayName("Should set and get dpopIatOffsetSeconds property") - void shouldSetAndGetDpopIatOffsetSeconds() { - Long offsetSeconds = 300L; - properties.setDpopIatOffsetSeconds(offsetSeconds); - - assertEquals(offsetSeconds, properties.getDpopIatOffsetSeconds()); - } - - @Test - @DisplayName("Should set and get dpopIatLeewaySeconds property") - void shouldSetAndGetDpopIatLeewaySeconds() { - Long leewaySeconds = 60L; - properties.setDpopIatLeewaySeconds(leewaySeconds); - - assertEquals(leewaySeconds, properties.getDpopIatLeewaySeconds()); - } - - @Test - @DisplayName("Should have null default values for all properties") - void shouldHaveNullDefaultValues() { - assertNull(properties.getDomain()); - assertNull(properties.getAudience()); - assertNull(properties.getDpopMode()); - assertNull(properties.getDpopIatOffsetSeconds()); - assertNull(properties.getDpopIatLeewaySeconds()); - } - - @Test - @DisplayName("Should handle null values for all properties") - void shouldHandleNullValues() { - properties.setDomain("test.com"); - properties.setDomain(null); - assertNull(properties.getDomain()); - - properties.setAudience("https://api.test.com"); - properties.setAudience(null); - assertNull(properties.getAudience()); - - properties.setDpopMode(DPoPMode.REQUIRED); - properties.setDpopMode(null); - assertNull(properties.getDpopMode()); - - properties.setDpopIatOffsetSeconds(100L); - properties.setDpopIatOffsetSeconds(null); - assertNull(properties.getDpopIatOffsetSeconds()); - - properties.setDpopIatLeewaySeconds(50L); - properties.setDpopIatLeewaySeconds(null); - assertNull(properties.getDpopIatLeewaySeconds()); - } - - @Test - @DisplayName("Should allow zero values for DPoP timing properties") - void shouldAllowZeroValuesForDpopTimingProperties() { - properties.setDpopIatOffsetSeconds(0L); - assertEquals(0L, properties.getDpopIatOffsetSeconds()); - - properties.setDpopIatLeewaySeconds(0L); - assertEquals(0L, properties.getDpopIatLeewaySeconds()); - } - - @Test - @DisplayName("Should reject negative values for DPoP timing properties") - void shouldRejectNegativeValuesForDpopTimingProperties() { - assertThrows(IllegalArgumentException.class, () -> - properties.setDpopIatOffsetSeconds(-100L) - ); - - assertThrows(IllegalArgumentException.class, () -> - properties.setDpopIatLeewaySeconds(-50L) - ); - } -} \ No newline at end of file + private Auth0Properties properties; + + @BeforeEach + void setUp() { + properties = new Auth0Properties(); + } + + @Test + @DisplayName("Should set and get domain property") + void shouldSetAndGetDomain() { + String domain = "dev-tenant.us.auth0.com"; + properties.setDomain(domain); + + assertEquals(domain, properties.getDomain()); + } + + @Test + @DisplayName("Should set and get audience property") + void shouldSetAndGetAudience() { + String audience = "https://api.example.com/v2/"; + properties.setAudience(audience); + + assertEquals(audience, properties.getAudience()); + } + + @Test + @DisplayName("Should set and get dpopMode property") + void shouldSetAndGetDpopMode() { + properties.setDpopMode(DPoPMode.REQUIRED); + assertEquals(DPoPMode.REQUIRED, properties.getDpopMode()); + + properties.setDpopMode(DPoPMode.ALLOWED); + assertEquals(DPoPMode.ALLOWED, properties.getDpopMode()); + + properties.setDpopMode(DPoPMode.DISABLED); + assertEquals(DPoPMode.DISABLED, properties.getDpopMode()); + } + + @Test + @DisplayName("Should set and get dpopIatOffsetSeconds property") + void shouldSetAndGetDpopIatOffsetSeconds() { + Long offsetSeconds = 300L; + properties.setDpopIatOffsetSeconds(offsetSeconds); + + assertEquals(offsetSeconds, properties.getDpopIatOffsetSeconds()); + } + + @Test + @DisplayName("Should set and get dpopIatLeewaySeconds property") + void shouldSetAndGetDpopIatLeewaySeconds() { + Long leewaySeconds = 60L; + properties.setDpopIatLeewaySeconds(leewaySeconds); + + assertEquals(leewaySeconds, properties.getDpopIatLeewaySeconds()); + } + + @Test + @DisplayName("Should have null default values for all properties") + void shouldHaveNullDefaultValues() { + assertNull(properties.getDomain()); + assertNull(properties.getAudience()); + assertNull(properties.getDpopMode()); + assertNull(properties.getDpopIatOffsetSeconds()); + assertNull(properties.getDpopIatLeewaySeconds()); + } + + @Test + @DisplayName("Should handle null values for all properties") + void shouldHandleNullValues() { + properties.setDomain("test.com"); + properties.setDomain(null); + assertNull(properties.getDomain()); + + properties.setAudience("https://api.test.com"); + properties.setAudience(null); + assertNull(properties.getAudience()); + + properties.setDpopMode(DPoPMode.REQUIRED); + properties.setDpopMode(null); + assertNull(properties.getDpopMode()); + + properties.setDpopIatOffsetSeconds(100L); + properties.setDpopIatOffsetSeconds(null); + assertNull(properties.getDpopIatOffsetSeconds()); + + properties.setDpopIatLeewaySeconds(50L); + properties.setDpopIatLeewaySeconds(null); + assertNull(properties.getDpopIatLeewaySeconds()); + } + + @Test + @DisplayName("Should allow zero values for DPoP timing properties") + void shouldAllowZeroValuesForDpopTimingProperties() { + properties.setDpopIatOffsetSeconds(0L); + assertEquals(0L, properties.getDpopIatOffsetSeconds()); + + properties.setDpopIatLeewaySeconds(0L); + assertEquals(0L, properties.getDpopIatLeewaySeconds()); + } + + @Test + @DisplayName("Should reject negative values for DPoP timing properties") + void shouldRejectNegativeValuesForDpopTimingProperties() { + assertThrows(IllegalArgumentException.class, () -> properties.setDpopIatOffsetSeconds(-100L)); + + assertThrows(IllegalArgumentException.class, () -> properties.setDpopIatLeewaySeconds(-50L)); + } +}