Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Copilot Instructions for auth0-java-mvc-common

## Overview

This is an Auth0 SDK for Java Servlet applications that simplifies OAuth2/OpenID Connect authentication flows. The library provides secure cookie-based state/nonce management and handles both Authorization Code and Implicit Grant flows.

## Core Architecture

### Main Components

- **`AuthenticationController`**: Primary entry point with Builder pattern for configuration
- **`RequestProcessor`**: Internal handler for OAuth callbacks and token processing
- **`AuthorizeUrl`**: Fluent builder for constructing OAuth authorization URLs
- **Cookie Management**: Custom `AuthCookie`/`TransientCookieStore` for SameSite cookie support

### Key Design Patterns

- **Non-reusable builders**: `AuthenticationController.Builder` throws `IllegalStateException` if `build()` called twice
- **One-time URL builders**: `AuthorizeUrl` instances cannot be reused (throws on second `build()`)
- **Fallback authentication storage**: State/nonce stored in both cookies AND session for compatibility

## Critical Cookie Handling

The library implements sophisticated cookie management for browser compatibility:

### SameSite Cookie Strategy

- **Code flow**: Uses `SameSite=Lax` (single cookie)
- **ID token flows**: Uses `SameSite=None; Secure` with legacy fallback cookie (prefixed with `_`)
- **Legacy fallback**: Automatically creates fallback cookies for browsers that don't support `SameSite=None`

### Cookie Configuration

```java
// Configure cookie behavior
.withLegacySameSiteCookie(false) // Disable fallback cookies
.withSecureCookie(true) // Force Secure attribute
.withCookiePath("/custom") // Set cookie Path attribute
```

## Builder Pattern Usage

### Standard Authentication Controller Setup

```java
AuthenticationController controller = AuthenticationController.newBuilder(domain, clientId, clientSecret)
.withJwkProvider(jwkProvider) // Required for RS256
.withResponseType("code") // Default: "code"
.withClockSkew(120) // Default: 60 seconds
.withOrganization("org_id") // For organization login
.build();
```

### URL Building (Modern Pattern)

```java
// CORRECT: Use request + response for cookie storage
String url = controller.buildAuthorizeUrl(request, response, redirectUri)
.withState("custom-state")
.withAudience("https://api.example.com")
.withParameter("custom", "value")
.build();
```

## Response Type Behavior

- **`code`**: Authorization Code flow, uses `SameSite=Lax` cookies
- **`id_token`** or **`token`**: Implicit Grant, requires `SameSite=None; Secure` + fallback cookies
- **Mixed**: `id_token code` combinations follow implicit grant cookie rules

## Testing Patterns

### Mock Setup

```java
// Standard test setup pattern
@Mock private AuthAPI client;
@Mock private IdTokenVerifier.Options verificationOptions;
@Captor private ArgumentCaptor<SignatureVerifier> signatureVerifierCaptor;

AuthenticationController.Builder builderSpy = spy(AuthenticationController.newBuilder(...));
doReturn(client).when(builderSpy).createAPIClient(...);
```

### Cookie Assertions

```java
// Verify cookie headers in tests
List<String> headers = response.getHeaders("Set-Cookie");
assertThat(headers, hasItem("com.auth0.state=value; HttpOnly; Max-Age=600; SameSite=Lax"));
```

## Development Workflow

### Build & Test

```bash
./gradlew build # Build with Gradle wrapper
./gradlew test # Run tests
./gradlew jacocoTestReport # Generate coverage
```

### Key Dependencies

- **Auth0 Java SDK**: Core Auth0 API client (`com.auth0:auth0`)
- **java-jwt**: JWT token handling (`com.auth0:java-jwt`)
- **jwks-rsa**: RS256 signature verification (`com.auth0:jwks-rsa`)
- **Servlet API**: `javax.servlet-api` (compile-only)

## Migration Considerations

### Deprecated Methods

- `handle(HttpServletRequest)`: Session-based, incompatible with SameSite restrictions
- `buildAuthorizeUrl(HttpServletRequest, String)`: Session-only storage

### Modern Alternatives

- Use `handle(HttpServletRequest, HttpServletResponse)` for cookie-based auth
- Use `buildAuthorizeUrl(HttpServletRequest, HttpServletResponse, String)` for proper cookie storage

## Common Integration Points

- Organizations: Use `.withOrganization()` and validate `org_id` claims manually
- Custom parameters: Use `.withParameter()` on AuthorizeUrl (but not for `state`, `nonce`, `response_type`)
- Error handling: Catch `IdentityVerificationException` from `.handle()` calls
- HTTP customization: Use `.withHttpOptions()` for timeouts/proxy configuration
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM gradle:6.9.2-jdk8

WORKDIR /home/gradle
# Copy your project files
COPY . .

# Ensure the Gradle wrapper is executable
RUN chmod +x ./gradlew

# Expose both ports for your MCD test
EXPOSE 3000
EXPOSE 8080
EXPOSE 5005

# Use --no-daemon to keep the container process alive
# We use the wrapper (./gradlew) to ensure consistency
#CMD ["./gradlew", "appRun", "--no-daemon", "-Pgretty.managed=false"]
ENV GRADLE_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
CMD ["gradle", "appRun", "--no-daemon"]
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ plugins {
id 'jacoco'
id 'me.champeau.gradle.japicmp' version '0.4.6'
id 'io.github.gradle-nexus.publish-plugin' version '2.0.0'
id "war"
id "org.gretty" version "3.1.1"
}

gretty {
httpPort = 3000
host = '0.0.0.0' // Required for Docker to communicate
contextPath = '/'
servletContainer = 'tomcat9'
}

repositories {
Expand Down Expand Up @@ -125,6 +134,7 @@ dependencies {
implementation 'org.apache.commons:commons-lang3:3.18.0'
implementation 'com.google.guava:guava-annotations:r03'
implementation 'commons-codec:commons-codec:1.20.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'

api 'com.auth0:auth0:1.45.1'
api 'com.auth0:java-jwt:3.19.4'
Expand Down
127 changes: 102 additions & 25 deletions src/main/java/com/auth0/AuthenticationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,32 @@ RequestProcessor getRequestProcessor() {
* @return a new Builder instance ready to configure
*/
public static Builder newBuilder(String domain, String clientId, String clientSecret) {
return new Builder(domain, clientId, clientSecret);
Validate.notNull(domain, "domain must not be null");
return new Builder(clientId, clientSecret).withDomain(domain);
}

/**
* Create a new {@link Builder} instance to configure the {@link AuthenticationController} response type and algorithm used on the verification.
* By default it will request response type 'code' and later perform the Code Exchange, but if the response type is changed to 'token' it will handle
* the Implicit Grant using the HS256 algorithm with the Client Secret as secret.
*
* @param domainResolver the Auth0 domain resolver function
* @param clientId the Auth0 application's client id
* @param clientSecret the Auth0 application's client secret
* @return a new Builder instance ready to configure
*/
public static Builder newBuilder(DomainResolver domainResolver,
String clientId,
String clientSecret) {
Validate.notNull(domainResolver, "domainResolver must not be null");
return new Builder(clientId, clientSecret).withDomainResolver(domainResolver);
}


public static class Builder {
private static final String RESPONSE_TYPE_CODE = "code";

private final String domain;
private String domain;
private final String clientId;
private final String clientSecret;
private String responseType;
Expand All @@ -63,6 +81,7 @@ public static class Builder {
private String invitation;
private HttpOptions httpOptions;
private String cookiePath;
private DomainResolver domainResolver;

Builder(String domain, String clientId, String clientSecret) {
Validate.notNull(domain);
Expand All @@ -76,6 +95,54 @@ public static class Builder {
this.useLegacySameSiteCookie = true;
}

Builder(String clientId, String clientSecret) {
if (clientId == null) {
throw new IllegalArgumentException("clientId cannot be null");
}
if (clientSecret == null) {
throw new IllegalArgumentException("clientSecret cannot be null");
}

this.clientId = clientId;
this.clientSecret = clientSecret;
this.responseType = RESPONSE_TYPE_CODE;
this.useLegacySameSiteCookie = true;
}

/**
* Sets the Auth0 domain to use.
* Note: The `domainResolver` must be null when setting the `domain`.
*
* @param domain the Auth0 domain to use, a non-null value.
* @return this same builder instance.
* @throws IllegalStateException if `domainResolver` is already set.
*/
public Builder withDomain(String domain) {
if (this.domainResolver != null) {
throw new IllegalStateException("Cannot specify both 'domain' and 'domainResolver'.");
}
Validate.notNull(domain, "domain must not be null");
this.domain = domain;
return this;
}

/**
* Sets the Auth0 domain resolver function to use.
* Note: The `domain` must be null when setting the `domainResolver`.
*
* @param domainResolver the domain resolver function to use, a non-null value.
* @return this same builder instance.
* @throws IllegalStateException if `domain` is already set.
*/
public Builder withDomainResolver(DomainResolver domainResolver) {
if (this.domain != null) {
throw new IllegalStateException("Cannot specify both 'domain' and 'domainResolver'.");
}
Validate.notNull(domainResolver, "domainResolver must not be null");
this.domainResolver = domainResolver;
return this;
}

/**
* Customize certain aspects of the underlying HTTP client networking library, such as timeouts and proxy configuration.
*
Expand Down Expand Up @@ -196,29 +263,18 @@ public Builder withInvitation(String invitation) {
* @throws UnsupportedOperationException if the Implicit Grant is chosen and the environment doesn't support UTF-8 encoding.
*/
public AuthenticationController build() throws UnsupportedOperationException {
AuthAPI apiClient = createAPIClient(domain, clientId, clientSecret, httpOptions);
setupTelemetry(apiClient);

final boolean expectedAlgorithmIsExplicitlySetAndAsymmetric = jwkProvider != null;
final SignatureVerifier signatureVerifier;
if (expectedAlgorithmIsExplicitlySetAndAsymmetric) {
signatureVerifier = new AsymmetricSignatureVerifier(jwkProvider);
} else if (responseType.contains(RESPONSE_TYPE_CODE)) {
// Old behavior: To maintain backwards-compatibility when
// no explicit algorithm is set by the user, we
// must skip ID Token signature check.
signatureVerifier = new AlgorithmNameVerifier();
} else {
signatureVerifier = new SymmetricSignatureVerifier(clientSecret);
}
validateDomainConfiguration();

DomainProvider domainProvider =
domain != null
? new StaticDomainProvider(domain)
: new ResolverDomainProvider(domainResolver);

String issuer = getIssuer(domain);
IdTokenVerifier.Options verifyOptions = createIdTokenVerificationOptions(issuer, clientId, signatureVerifier);
verifyOptions.setClockSkew(clockSkew);
verifyOptions.setMaxAge(authenticationMaxAge);
verifyOptions.setOrganization(this.organization);
SignatureVerifier signatureVerifier = buildSignatureVerifier();

RequestProcessor processor = new RequestProcessor.Builder(apiClient, responseType, verifyOptions)
RequestProcessor processor = new RequestProcessor.Builder(domainProvider, responseType, clientId, clientSecret, httpOptions, signatureVerifier)
.withClockSkew(clockSkew)
.withAuthenticationMaxAge(authenticationMaxAge)
.withLegacySameSiteCookie(useLegacySameSiteCookie)
.withOrganization(organization)
.withInvitation(invitation)
Expand All @@ -228,6 +284,25 @@ public AuthenticationController build() throws UnsupportedOperationException {
return new AuthenticationController(processor);
}

private void validateDomainConfiguration() {
if (domain == null && domainResolver == null) {
throw new IllegalStateException("Either domain or domainResolver must be provided.");
}
if (domain != null && domainResolver != null) {
throw new IllegalStateException("Cannot specify both domain and domainResolver.");
}
}

private SignatureVerifier buildSignatureVerifier() {
if (jwkProvider != null) {
return new AsymmetricSignatureVerifier(jwkProvider);
}
if (responseType.contains(RESPONSE_TYPE_CODE)) {
return new AlgorithmNameVerifier(); // legacy behavior
}
return new SymmetricSignatureVerifier(clientSecret);
}

@VisibleForTesting
IdTokenVerifier.Options createIdTokenVerificationOptions(String issuer, String audience, SignatureVerifier signatureVerifier) {
return new IdTokenVerifier.Options(issuer, audience, signatureVerifier);
Expand All @@ -243,6 +318,7 @@ AuthAPI createAPIClient(String domain, String clientId, String clientSecret, Htt

@VisibleForTesting
void setupTelemetry(AuthAPI client) {
if (client == null) return;
Telemetry telemetry = new Telemetry("auth0-java-mvc-common", obtainPackageVersion());
client.setTelemetry(telemetry);
}
Expand Down Expand Up @@ -272,14 +348,15 @@ private String getIssuer(String domain) {
* @param enabled whether to enable the HTTP logger or not.
*/
public void setLoggingEnabled(boolean enabled) {
requestProcessor.getClient().setLoggingEnabled(enabled);
// No longer requestProcessor.getClient()... (which was null)
requestProcessor.setLoggingEnabled(enabled);
}

/**
* Disable sending the Telemetry header on every request to the Auth0 API
*/
public void doNotSendTelemetry() {
requestProcessor.getClient().doNotSendTelemetry();
requestProcessor.doNotSendTelemetry();
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/auth0/DomainProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.auth0;

import javax.servlet.http.HttpServletRequest;

public interface DomainProvider {
String getDomain(HttpServletRequest request);

}
12 changes: 12 additions & 0 deletions src/main/java/com/auth0/DomainResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.auth0;

import javax.servlet.http.HttpServletRequest;

public interface DomainResolver {
/**
* Resolves the domain to be used for the current request.
* @param request the current HttpServletRequest
* @return a single domain string (e.g., "tenant.auth0.com")
*/
String resolve(HttpServletRequest request);

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'request' is never used.

Copilot Autofix

AI 5 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

}
Loading
Loading