-
+
org.codehaus.mojo
build-helper-maven-plugin
@@ -95,7 +95,7 @@
-
+
org.apache.maven.plugins
maven-compiler-plugin
@@ -103,6 +103,12 @@
1.8
1.8
+
+ auth/**
+
+
+ auth/**
+
diff --git a/src/main/java/com/gophersecurity/orch/auth/AuthContext.java b/src/main/java/com/gophersecurity/orch/auth/AuthContext.java
new file mode 100644
index 00000000..a2a4039c
--- /dev/null
+++ b/src/main/java/com/gophersecurity/orch/auth/AuthContext.java
@@ -0,0 +1,131 @@
+package com.gophersecurity.orch.auth;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Authentication context from JWT token validation.
+ *
+ * Contains user information extracted from a validated token, including user ID, scopes,
+ * audience, and expiration time.
+ */
+public class AuthContext {
+
+ private final String userId;
+ private final String scopes;
+ private final String audience;
+ private final long tokenExpiry;
+ private final boolean authenticated;
+ private final Set scopeSet;
+
+ /**
+ * Create an authentication context.
+ *
+ * @param userId user identifier from token subject
+ * @param scopes space-separated list of scopes
+ * @param audience token audience
+ * @param tokenExpiry token expiration timestamp (Unix epoch seconds)
+ * @param authenticated whether the user is authenticated
+ */
+ public AuthContext(
+ String userId,
+ String scopes,
+ String audience,
+ long tokenExpiry,
+ boolean authenticated) {
+ this.userId = userId != null ? userId : "";
+ this.scopes = scopes != null ? scopes : "";
+ this.audience = audience != null ? audience : "";
+ this.tokenExpiry = tokenExpiry;
+ this.authenticated = authenticated;
+
+ // Pre-compute scope set for efficient lookups
+ if (this.scopes.isEmpty()) {
+ this.scopeSet = Collections.emptySet();
+ } else {
+ this.scopeSet = new HashSet<>(Arrays.asList(this.scopes.split("\\s+")));
+ }
+ }
+
+ /**
+ * Create an empty, unauthenticated context.
+ *
+ * @return empty auth context
+ */
+ public static AuthContext empty() {
+ return new AuthContext("", "", "", 0, false);
+ }
+
+ /**
+ * Create an anonymous authenticated context with specified scopes.
+ *
+ * Useful for development mode when authentication is disabled but scope checking is still
+ * active.
+ *
+ * @param scopes space-separated list of scopes
+ * @return anonymous auth context with given scopes
+ */
+ public static AuthContext anonymous(String scopes) {
+ return new AuthContext("anonymous", scopes, "", 0, true);
+ }
+
+ /**
+ * Check if the context has a specific scope.
+ *
+ * @param requiredScope the scope to check for
+ * @return true if the scope is present
+ */
+ public boolean hasScope(String requiredScope) {
+ if (requiredScope == null || requiredScope.isEmpty()) {
+ return true;
+ }
+ return scopeSet.contains(requiredScope);
+ }
+
+ /**
+ * Get the user identifier.
+ *
+ * @return user ID from token subject
+ */
+ public String getUserId() {
+ return userId;
+ }
+
+ /**
+ * Get the space-separated scopes string.
+ *
+ * @return scopes string
+ */
+ public String getScopes() {
+ return scopes;
+ }
+
+ /**
+ * Get the token audience.
+ *
+ * @return audience string
+ */
+ public String getAudience() {
+ return audience;
+ }
+
+ /**
+ * Get the token expiration timestamp.
+ *
+ * @return Unix epoch seconds
+ */
+ public long getTokenExpiry() {
+ return tokenExpiry;
+ }
+
+ /**
+ * Check if the context represents an authenticated user.
+ *
+ * @return true if authenticated
+ */
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+}
diff --git a/src/main/java/com/gophersecurity/orch/auth/GopherAuthClient.java b/src/main/java/com/gophersecurity/orch/auth/GopherAuthClient.java
new file mode 100644
index 00000000..df214685
--- /dev/null
+++ b/src/main/java/com/gophersecurity/orch/auth/GopherAuthClient.java
@@ -0,0 +1,34 @@
+package com.gophersecurity.orch.auth;
+
+/**
+ * Interface for JWT token validation using gopher-auth.
+ *
+ *
This interface abstracts the FFI calls to the gopher-auth native library.
+ */
+public interface GopherAuthClient {
+
+ /**
+ * Validate a JWT token.
+ *
+ * @param token JWT token string
+ * @param clockSkewSeconds allowed clock skew in seconds
+ * @return validation result
+ */
+ ValidationResult validateToken(String token, int clockSkewSeconds);
+
+ /**
+ * Extract payload from a JWT token.
+ *
+ * @param token JWT token string
+ * @return extracted payload
+ * @throws RuntimeException if payload extraction fails
+ */
+ TokenPayload extractPayload(String token);
+
+ /**
+ * Check if the client is initialized and ready.
+ *
+ * @return true if client is ready
+ */
+ boolean isReady();
+}
diff --git a/src/main/java/com/gophersecurity/orch/auth/TokenPayload.java b/src/main/java/com/gophersecurity/orch/auth/TokenPayload.java
new file mode 100644
index 00000000..a3c2f442
--- /dev/null
+++ b/src/main/java/com/gophersecurity/orch/auth/TokenPayload.java
@@ -0,0 +1,41 @@
+package com.gophersecurity.orch.auth;
+
+/** Extracted JWT token payload. */
+public class TokenPayload {
+
+ private final String subject;
+ private final String scopes;
+ private final String audience;
+ private final long expiration;
+
+ /**
+ * Create a token payload.
+ *
+ * @param subject user identifier (sub claim)
+ * @param scopes space-separated scopes (scope claim)
+ * @param audience token audience (aud claim)
+ * @param expiration expiration timestamp (exp claim)
+ */
+ public TokenPayload(String subject, String scopes, String audience, long expiration) {
+ this.subject = subject;
+ this.scopes = scopes;
+ this.audience = audience;
+ this.expiration = expiration;
+ }
+
+ public String getSubject() {
+ return subject;
+ }
+
+ public String getScopes() {
+ return scopes;
+ }
+
+ public String getAudience() {
+ return audience;
+ }
+
+ public long getExpiration() {
+ return expiration;
+ }
+}
diff --git a/src/main/java/com/gophersecurity/orch/auth/ValidationResult.java b/src/main/java/com/gophersecurity/orch/auth/ValidationResult.java
new file mode 100644
index 00000000..19a22cb8
--- /dev/null
+++ b/src/main/java/com/gophersecurity/orch/auth/ValidationResult.java
@@ -0,0 +1,46 @@
+package com.gophersecurity.orch.auth;
+
+/** Result of JWT token validation. */
+public class ValidationResult {
+
+ private final boolean valid;
+ private final String errorMessage;
+
+ /**
+ * Create a validation result.
+ *
+ * @param valid whether the token is valid
+ * @param errorMessage error message if invalid, null otherwise
+ */
+ public ValidationResult(boolean valid, String errorMessage) {
+ this.valid = valid;
+ this.errorMessage = errorMessage;
+ }
+
+ /**
+ * Create a successful validation result.
+ *
+ * @return successful result
+ */
+ public static ValidationResult success() {
+ return new ValidationResult(true, null);
+ }
+
+ /**
+ * Create a failed validation result.
+ *
+ * @param errorMessage error description
+ * @return failed result
+ */
+ public static ValidationResult failure(String errorMessage) {
+ return new ValidationResult(false, errorMessage);
+ }
+
+ public boolean isValid() {
+ return valid;
+ }
+
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+}
diff --git a/src/test/java/com/gophersecurity/orch/auth/AuthContextTest.java b/src/test/java/com/gophersecurity/orch/auth/AuthContextTest.java
new file mode 100644
index 00000000..a87a01f4
--- /dev/null
+++ b/src/test/java/com/gophersecurity/orch/auth/AuthContextTest.java
@@ -0,0 +1,94 @@
+package com.gophersecurity.orch.auth;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for AuthContext. */
+class AuthContextTest {
+
+ @Test
+ void testHasScopePresent() {
+ AuthContext ctx = new AuthContext("user1", "read write admin", "api", 12345, true);
+
+ assertTrue(ctx.hasScope("read"));
+ assertTrue(ctx.hasScope("write"));
+ assertTrue(ctx.hasScope("admin"));
+ }
+
+ @Test
+ void testHasScopeAbsent() {
+ AuthContext ctx = new AuthContext("user1", "read write", "api", 12345, true);
+
+ assertFalse(ctx.hasScope("admin"));
+ assertFalse(ctx.hasScope("delete"));
+ }
+
+ @Test
+ void testHasScopeEmptyScopes() {
+ AuthContext ctx = new AuthContext("user1", "", "api", 12345, true);
+
+ assertFalse(ctx.hasScope("read"));
+ assertFalse(ctx.hasScope("admin"));
+ }
+
+ @Test
+ void testHasScopeNullScopes() {
+ AuthContext ctx = new AuthContext("user1", null, "api", 12345, true);
+
+ assertFalse(ctx.hasScope("read"));
+ assertFalse(ctx.hasScope("admin"));
+ }
+
+ @Test
+ void testHasScopeEmptyRequired() {
+ AuthContext ctx = new AuthContext("user1", "read write", "api", 12345, true);
+
+ // Empty or null required scope should return true
+ assertTrue(ctx.hasScope(""));
+ assertTrue(ctx.hasScope(null));
+ }
+
+ @Test
+ void testEmpty() {
+ AuthContext ctx = AuthContext.empty();
+
+ assertEquals("", ctx.getUserId());
+ assertEquals("", ctx.getScopes());
+ assertEquals("", ctx.getAudience());
+ assertEquals(0, ctx.getTokenExpiry());
+ assertFalse(ctx.isAuthenticated());
+ }
+
+ @Test
+ void testAnonymous() {
+ AuthContext ctx = AuthContext.anonymous("mcp:read mcp:admin");
+
+ assertEquals("anonymous", ctx.getUserId());
+ assertEquals("mcp:read mcp:admin", ctx.getScopes());
+ assertTrue(ctx.isAuthenticated());
+ assertTrue(ctx.hasScope("mcp:read"));
+ assertTrue(ctx.hasScope("mcp:admin"));
+ }
+
+ @Test
+ void testGetters() {
+ AuthContext ctx = new AuthContext("user123", "scope1 scope2", "my-api", 9999999999L, true);
+
+ assertEquals("user123", ctx.getUserId());
+ assertEquals("scope1 scope2", ctx.getScopes());
+ assertEquals("my-api", ctx.getAudience());
+ assertEquals(9999999999L, ctx.getTokenExpiry());
+ assertTrue(ctx.isAuthenticated());
+ }
+
+ @Test
+ void testMultipleSpacesBetweenScopes() {
+ // Test that multiple spaces are handled correctly
+ AuthContext ctx = new AuthContext("user1", "read write admin", "api", 12345, true);
+
+ assertTrue(ctx.hasScope("read"));
+ assertTrue(ctx.hasScope("write"));
+ assertTrue(ctx.hasScope("admin"));
+ }
+}
diff --git a/third_party/gopher-orch b/third_party/gopher-orch
index 6b45ffbb..c8e7c406 160000
--- a/third_party/gopher-orch
+++ b/third_party/gopher-orch
@@ -1 +1 @@
-Subproject commit 6b45ffbbee74d5ae034008fc2cb2a927f3131992
+Subproject commit c8e7c40606db330142632ecf90aaa8777bc42a3a