diff --git a/authority/pom.xml b/authority/pom.xml
index ce2cce2..c127f6f 100644
--- a/authority/pom.xml
+++ b/authority/pom.xml
@@ -10,7 +10,7 @@
- org.prole
+ dev.knoe
authority
0.0.1-SNAPSHOT
knoe-authority
@@ -20,6 +20,7 @@
21
+
org.springframework.boot
@@ -41,7 +42,7 @@
- org.springframework.security
+ org.springframework.security.kerberos
spring-security-kerberos-client
2.0.1
@@ -103,6 +104,41 @@
org.springframework.boot
spring-boot-maven-plugin
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ @{argLine} -Dnet.bytebuddy.experimental=true
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.12
+
+
+ prepare-agent
+
+
+ java/**
+ javax/**
+ jdk/**
+ sun/**
+ com/sun/**
+ org/ietf/**
+ org/omg/**
+ org/w3c/**
+ org/xml/**
+
+
+
+
+ report
+ test
+ report
+
+
+
diff --git a/authority/src/main/java/org/prole/authority/HealthController.java b/authority/src/main/java/dev/knoe/authority/HealthController.java
similarity index 89%
rename from authority/src/main/java/org/prole/authority/HealthController.java
rename to authority/src/main/java/dev/knoe/authority/HealthController.java
index e6eb896..7724b1f 100644
--- a/authority/src/main/java/org/prole/authority/HealthController.java
+++ b/authority/src/main/java/dev/knoe/authority/HealthController.java
@@ -1,4 +1,4 @@
-package org.prole.authority;
+package dev.knoe.authority;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
diff --git a/authority/src/main/java/org/prole/authority/KnoeAuthApplication.java b/authority/src/main/java/dev/knoe/authority/KnoeAuthApplication.java
similarity index 93%
rename from authority/src/main/java/org/prole/authority/KnoeAuthApplication.java
rename to authority/src/main/java/dev/knoe/authority/KnoeAuthApplication.java
index 2c764b4..d903db4 100644
--- a/authority/src/main/java/org/prole/authority/KnoeAuthApplication.java
+++ b/authority/src/main/java/dev/knoe/authority/KnoeAuthApplication.java
@@ -1,4 +1,4 @@
-package org.prole.authority;
+package dev.knoe.authority;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
diff --git a/authority/src/main/java/org/prole/authority/admin/AdminController.java b/authority/src/main/java/dev/knoe/authority/admin/AdminController.java
similarity index 97%
rename from authority/src/main/java/org/prole/authority/admin/AdminController.java
rename to authority/src/main/java/dev/knoe/authority/admin/AdminController.java
index 701f5a1..1586fb6 100644
--- a/authority/src/main/java/org/prole/authority/admin/AdminController.java
+++ b/authority/src/main/java/dev/knoe/authority/admin/AdminController.java
@@ -1,7 +1,7 @@
-package org.prole.authority.admin;
+package dev.knoe.authority.admin;
-import org.prole.authority.enroll.InviteService;
-import org.prole.authority.enroll.InviteService.InviteResult;
+import dev.knoe.authority.enroll.InviteService;
+import dev.knoe.authority.enroll.InviteService.InviteResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
diff --git a/authority/src/main/java/org/prole/authority/admin/KnobjectService.java b/authority/src/main/java/dev/knoe/authority/admin/KnobjectService.java
similarity index 99%
rename from authority/src/main/java/org/prole/authority/admin/KnobjectService.java
rename to authority/src/main/java/dev/knoe/authority/admin/KnobjectService.java
index 5f882e6..5e5c996 100644
--- a/authority/src/main/java/org/prole/authority/admin/KnobjectService.java
+++ b/authority/src/main/java/dev/knoe/authority/admin/KnobjectService.java
@@ -1,4 +1,4 @@
-package org.prole.authority.admin;
+package dev.knoe.authority.admin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/authority/src/main/java/org/prole/authority/config/AuthProperties.java b/authority/src/main/java/dev/knoe/authority/config/AuthProperties.java
similarity index 98%
rename from authority/src/main/java/org/prole/authority/config/AuthProperties.java
rename to authority/src/main/java/dev/knoe/authority/config/AuthProperties.java
index a55a92a..61e8987 100644
--- a/authority/src/main/java/org/prole/authority/config/AuthProperties.java
+++ b/authority/src/main/java/dev/knoe/authority/config/AuthProperties.java
@@ -1,4 +1,4 @@
-package org.prole.authority.config;
+package dev.knoe.authority.config;
import java.time.Duration;
import java.util.ArrayList;
diff --git a/authority/src/main/java/org/prole/authority/config/KerberosProperties.java b/authority/src/main/java/dev/knoe/authority/config/KerberosProperties.java
similarity index 95%
rename from authority/src/main/java/org/prole/authority/config/KerberosProperties.java
rename to authority/src/main/java/dev/knoe/authority/config/KerberosProperties.java
index d1b4eae..26496ff 100644
--- a/authority/src/main/java/org/prole/authority/config/KerberosProperties.java
+++ b/authority/src/main/java/dev/knoe/authority/config/KerberosProperties.java
@@ -1,4 +1,4 @@
-package org.prole.authority.config;
+package dev.knoe.authority.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
diff --git a/authority/src/main/java/org/prole/authority/enroll/EnrollmentController.java b/authority/src/main/java/dev/knoe/authority/enroll/EnrollmentController.java
similarity index 99%
rename from authority/src/main/java/org/prole/authority/enroll/EnrollmentController.java
rename to authority/src/main/java/dev/knoe/authority/enroll/EnrollmentController.java
index f4e84cb..c5851c7 100644
--- a/authority/src/main/java/org/prole/authority/enroll/EnrollmentController.java
+++ b/authority/src/main/java/dev/knoe/authority/enroll/EnrollmentController.java
@@ -1,4 +1,4 @@
-package org.prole.authority.enroll;
+package dev.knoe.authority.enroll;
import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger;
diff --git a/authority/src/main/java/org/prole/authority/enroll/GoogleOAuthService.java b/authority/src/main/java/dev/knoe/authority/enroll/GoogleOAuthService.java
similarity index 99%
rename from authority/src/main/java/org/prole/authority/enroll/GoogleOAuthService.java
rename to authority/src/main/java/dev/knoe/authority/enroll/GoogleOAuthService.java
index 9682d57..f98560d 100644
--- a/authority/src/main/java/org/prole/authority/enroll/GoogleOAuthService.java
+++ b/authority/src/main/java/dev/knoe/authority/enroll/GoogleOAuthService.java
@@ -1,4 +1,4 @@
-package org.prole.authority.enroll;
+package dev.knoe.authority.enroll;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
diff --git a/authority/src/main/java/org/prole/authority/enroll/InviteService.java b/authority/src/main/java/dev/knoe/authority/enroll/InviteService.java
similarity index 99%
rename from authority/src/main/java/org/prole/authority/enroll/InviteService.java
rename to authority/src/main/java/dev/knoe/authority/enroll/InviteService.java
index db764e6..b7ab7ae 100644
--- a/authority/src/main/java/org/prole/authority/enroll/InviteService.java
+++ b/authority/src/main/java/dev/knoe/authority/enroll/InviteService.java
@@ -1,4 +1,4 @@
-package org.prole.authority.enroll;
+package dev.knoe.authority.enroll;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/authority/src/main/java/org/prole/authority/enroll/TotpService.java b/authority/src/main/java/dev/knoe/authority/enroll/TotpService.java
similarity index 98%
rename from authority/src/main/java/org/prole/authority/enroll/TotpService.java
rename to authority/src/main/java/dev/knoe/authority/enroll/TotpService.java
index 56686fd..a71be95 100644
--- a/authority/src/main/java/org/prole/authority/enroll/TotpService.java
+++ b/authority/src/main/java/dev/knoe/authority/enroll/TotpService.java
@@ -1,4 +1,4 @@
-package org.prole.authority.enroll;
+package dev.knoe.authority.enroll;
import dev.samstevens.totp.code.CodeGenerator;
import dev.samstevens.totp.code.CodeVerifier;
diff --git a/authority/src/main/java/org/prole/authority/enroll/UserProvisioningService.java b/authority/src/main/java/dev/knoe/authority/enroll/UserProvisioningService.java
similarity index 98%
rename from authority/src/main/java/org/prole/authority/enroll/UserProvisioningService.java
rename to authority/src/main/java/dev/knoe/authority/enroll/UserProvisioningService.java
index 42772a1..33f1615 100644
--- a/authority/src/main/java/org/prole/authority/enroll/UserProvisioningService.java
+++ b/authority/src/main/java/dev/knoe/authority/enroll/UserProvisioningService.java
@@ -1,6 +1,6 @@
-package org.prole.authority.enroll;
+package dev.knoe.authority.enroll;
-import org.prole.authority.kerberos.KadminClient;
+import dev.knoe.authority.kerberos.KadminClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
diff --git a/authority/src/main/java/org/prole/authority/kerberos/KadminClient.java b/authority/src/main/java/dev/knoe/authority/kerberos/KadminClient.java
similarity index 99%
rename from authority/src/main/java/org/prole/authority/kerberos/KadminClient.java
rename to authority/src/main/java/dev/knoe/authority/kerberos/KadminClient.java
index 494768a..1005352 100644
--- a/authority/src/main/java/org/prole/authority/kerberos/KadminClient.java
+++ b/authority/src/main/java/dev/knoe/authority/kerberos/KadminClient.java
@@ -1,4 +1,4 @@
-package org.prole.authority.kerberos;
+package dev.knoe.authority.kerberos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/authority/src/main/java/org/prole/authority/kerberos/KerberosPasswordService.java b/authority/src/main/java/dev/knoe/authority/kerberos/KerberosPasswordService.java
similarity index 98%
rename from authority/src/main/java/org/prole/authority/kerberos/KerberosPasswordService.java
rename to authority/src/main/java/dev/knoe/authority/kerberos/KerberosPasswordService.java
index cb89c15..99bef60 100644
--- a/authority/src/main/java/org/prole/authority/kerberos/KerberosPasswordService.java
+++ b/authority/src/main/java/dev/knoe/authority/kerberos/KerberosPasswordService.java
@@ -1,4 +1,4 @@
-package org.prole.authority.kerberos;
+package dev.knoe.authority.kerberos;
import java.io.IOException;
import java.util.Map;
diff --git a/authority/src/main/java/org/prole/authority/kerberos/KerberosSpnegoService.java b/authority/src/main/java/dev/knoe/authority/kerberos/KerberosSpnegoService.java
similarity index 98%
rename from authority/src/main/java/org/prole/authority/kerberos/KerberosSpnegoService.java
rename to authority/src/main/java/dev/knoe/authority/kerberos/KerberosSpnegoService.java
index 51f3781..548f174 100644
--- a/authority/src/main/java/org/prole/authority/kerberos/KerberosSpnegoService.java
+++ b/authority/src/main/java/dev/knoe/authority/kerberos/KerberosSpnegoService.java
@@ -1,4 +1,4 @@
-package org.prole.authority.kerberos;
+package dev.knoe.authority.kerberos;
import java.security.PrivilegedExceptionAction;
import java.util.Map;
diff --git a/authority/src/main/java/org/prole/authority/provisioning/ProvisioningWorker.java b/authority/src/main/java/dev/knoe/authority/provisioning/ProvisioningWorker.java
similarity index 98%
rename from authority/src/main/java/org/prole/authority/provisioning/ProvisioningWorker.java
rename to authority/src/main/java/dev/knoe/authority/provisioning/ProvisioningWorker.java
index 676113b..efe5cf1 100644
--- a/authority/src/main/java/org/prole/authority/provisioning/ProvisioningWorker.java
+++ b/authority/src/main/java/dev/knoe/authority/provisioning/ProvisioningWorker.java
@@ -1,6 +1,6 @@
-package org.prole.authority.provisioning;
+package dev.knoe.authority.provisioning;
-import org.prole.authority.admin.KnobjectService;
+import dev.knoe.authority.admin.KnobjectService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
diff --git a/authority/src/main/java/org/prole/authority/session/OidcCodeService.java b/authority/src/main/java/dev/knoe/authority/session/OidcCodeService.java
similarity index 95%
rename from authority/src/main/java/org/prole/authority/session/OidcCodeService.java
rename to authority/src/main/java/dev/knoe/authority/session/OidcCodeService.java
index e7c98e8..bed5d43 100644
--- a/authority/src/main/java/org/prole/authority/session/OidcCodeService.java
+++ b/authority/src/main/java/dev/knoe/authority/session/OidcCodeService.java
@@ -1,4 +1,4 @@
-package org.prole.authority.session;
+package dev.knoe.authority.session;
import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;
diff --git a/authority/src/main/java/org/prole/authority/session/OidcTokenService.java b/authority/src/main/java/dev/knoe/authority/session/OidcTokenService.java
similarity index 93%
rename from authority/src/main/java/org/prole/authority/session/OidcTokenService.java
rename to authority/src/main/java/dev/knoe/authority/session/OidcTokenService.java
index 5c9ae92..d65622b 100644
--- a/authority/src/main/java/org/prole/authority/session/OidcTokenService.java
+++ b/authority/src/main/java/dev/knoe/authority/session/OidcTokenService.java
@@ -1,13 +1,13 @@
-package org.prole.authority.session;
+package dev.knoe.authority.session;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
-import org.prole.authority.config.AuthProperties;
-import org.prole.authority.config.KerberosProperties;
+import dev.knoe.authority.config.AuthProperties;
+import dev.knoe.authority.config.KerberosProperties;
import org.springframework.stereotype.Service;
-import javax.annotation.PostConstruct;
+import jakarta.annotation.PostConstruct;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@@ -44,7 +44,7 @@ public class OidcTokenService {
byte[] keyBytes = Base64.getDecoder().decode(signingKey);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
- PrivateKey privateKey = kf.generatePrivateKey(spec);
+ PrivateKey privateKey = kf.generatePrivate(spec);
// Derive public key from private key (simplified for RSA)
RSAPublicKey publicKey = (RSAPublicKey) kf.generatePublic(new java.security.spec.RSAPublicKeySpec(
diff --git a/authority/src/main/java/org/prole/authority/session/SessionService.java b/authority/src/main/java/dev/knoe/authority/session/SessionService.java
similarity index 91%
rename from authority/src/main/java/org/prole/authority/session/SessionService.java
rename to authority/src/main/java/dev/knoe/authority/session/SessionService.java
index 6997e0e..c438a7c 100644
--- a/authority/src/main/java/org/prole/authority/session/SessionService.java
+++ b/authority/src/main/java/dev/knoe/authority/session/SessionService.java
@@ -1,6 +1,6 @@
-package org.prole.authority.session;
+package dev.knoe.authority.session;
-import org.prole.authority.config.AuthProperties;
+import dev.knoe.authority.config.AuthProperties;
import org.springframework.stereotype.Service;
import org.springframework.web.util.WebUtils;
diff --git a/authority/src/main/java/org/prole/authority/session/SessionTokenService.java b/authority/src/main/java/dev/knoe/authority/session/SessionTokenService.java
similarity index 99%
rename from authority/src/main/java/org/prole/authority/session/SessionTokenService.java
rename to authority/src/main/java/dev/knoe/authority/session/SessionTokenService.java
index 769a1ad..53632e6 100644
--- a/authority/src/main/java/org/prole/authority/session/SessionTokenService.java
+++ b/authority/src/main/java/dev/knoe/authority/session/SessionTokenService.java
@@ -1,4 +1,4 @@
-package org.prole.authority.session;
+package dev.knoe.authority.session;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
diff --git a/authority/src/main/java/org/prole/authority/session/SessionUser.java b/authority/src/main/java/dev/knoe/authority/session/SessionUser.java
similarity index 74%
rename from authority/src/main/java/org/prole/authority/session/SessionUser.java
rename to authority/src/main/java/dev/knoe/authority/session/SessionUser.java
index d730be7..7ab6d25 100644
--- a/authority/src/main/java/org/prole/authority/session/SessionUser.java
+++ b/authority/src/main/java/dev/knoe/authority/session/SessionUser.java
@@ -1,4 +1,4 @@
-package org.prole.authority.session;
+package dev.knoe.authority.session;
import java.util.List;
diff --git a/authority/src/main/java/org/prole/authority/user/PrincipalNormalizer.java b/authority/src/main/java/dev/knoe/authority/user/PrincipalNormalizer.java
similarity index 95%
rename from authority/src/main/java/org/prole/authority/user/PrincipalNormalizer.java
rename to authority/src/main/java/dev/knoe/authority/user/PrincipalNormalizer.java
index 5f87587..ab2ef6e 100644
--- a/authority/src/main/java/org/prole/authority/user/PrincipalNormalizer.java
+++ b/authority/src/main/java/dev/knoe/authority/user/PrincipalNormalizer.java
@@ -1,4 +1,4 @@
-package org.prole.authority.user;
+package dev.knoe.authority.user;
import java.util.Locale;
import java.util.Optional;
diff --git a/authority/src/main/java/org/prole/authority/web/JwksController.java b/authority/src/main/java/dev/knoe/authority/web/JwksController.java
similarity index 85%
rename from authority/src/main/java/org/prole/authority/web/JwksController.java
rename to authority/src/main/java/dev/knoe/authority/web/JwksController.java
index 8bba4b9..1e5db80 100644
--- a/authority/src/main/java/org/prole/authority/web/JwksController.java
+++ b/authority/src/main/java/dev/knoe/authority/web/JwksController.java
@@ -1,7 +1,7 @@
-package org.prole.authority.web;
+package dev.knoe.authority.web;
-import org.prole.authority.config.AuthProperties;
-import org.prole.authority.session.OidcTokenService;
+import dev.knoe.authority.config.AuthProperties;
+import dev.knoe.authority.session.OidcTokenService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
diff --git a/authority/src/main/java/org/prole/authority/web/LoginController.java b/authority/src/main/java/dev/knoe/authority/web/LoginController.java
similarity index 95%
rename from authority/src/main/java/org/prole/authority/web/LoginController.java
rename to authority/src/main/java/dev/knoe/authority/web/LoginController.java
index 755de0f..1b8c1e6 100644
--- a/authority/src/main/java/org/prole/authority/web/LoginController.java
+++ b/authority/src/main/java/dev/knoe/authority/web/LoginController.java
@@ -1,4 +1,4 @@
-package org.prole.authority.web;
+package dev.knoe.authority.web;
import java.net.URI;
import java.util.ArrayList;
@@ -10,14 +10,14 @@ import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import org.prole.authority.config.AuthProperties;
-import org.prole.authority.config.KerberosProperties;
-import org.prole.authority.enroll.GoogleOAuthService;
-import org.prole.authority.kerberos.KerberosPasswordService;
-import org.prole.authority.kerberos.KerberosSpnegoService;
-import org.prole.authority.session.SessionTokenService;
-import org.prole.authority.session.SessionUser;
-import org.prole.authority.user.PrincipalNormalizer;
+import dev.knoe.authority.config.AuthProperties;
+import dev.knoe.authority.config.KerberosProperties;
+import dev.knoe.authority.enroll.GoogleOAuthService;
+import dev.knoe.authority.kerberos.KerberosPasswordService;
+import dev.knoe.authority.kerberos.KerberosSpnegoService;
+import dev.knoe.authority.session.SessionTokenService;
+import dev.knoe.authority.session.SessionUser;
+import dev.knoe.authority.user.PrincipalNormalizer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -83,7 +83,7 @@ public class LoginController {
code { background: #f3f3f3; padding: 2px 4px; }
.box { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin: 16px 0; }
.row { margin: 8px 0; }
- input { width: 100%; padding: 8px; }
+ input { width: 100%%; padding: 8px; }
button { padding: 10px 14px; }
diff --git a/authority/src/main/java/org/prole/authority/web/OidcAuthorizeController.java b/authority/src/main/java/dev/knoe/authority/web/OidcAuthorizeController.java
similarity index 92%
rename from authority/src/main/java/org/prole/authority/web/OidcAuthorizeController.java
rename to authority/src/main/java/dev/knoe/authority/web/OidcAuthorizeController.java
index 7408535..330008f 100644
--- a/authority/src/main/java/org/prole/authority/web/OidcAuthorizeController.java
+++ b/authority/src/main/java/dev/knoe/authority/web/OidcAuthorizeController.java
@@ -1,9 +1,9 @@
-package org.prole.authority.web;
+package dev.knoe.authority.web;
-import org.prole.authority.config.AuthProperties;
-import org.prole.authority.session.OidcCodeService;
-import org.prole.authority.session.SessionService;
-import org.prole.authority.session.SessionUser;
+import dev.knoe.authority.config.AuthProperties;
+import dev.knoe.authority.session.OidcCodeService;
+import dev.knoe.authority.session.SessionService;
+import dev.knoe.authority.session.SessionUser;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
diff --git a/authority/src/main/java/org/prole/authority/web/OidcDiscoveryController.java b/authority/src/main/java/dev/knoe/authority/web/OidcDiscoveryController.java
similarity index 95%
rename from authority/src/main/java/org/prole/authority/web/OidcDiscoveryController.java
rename to authority/src/main/java/dev/knoe/authority/web/OidcDiscoveryController.java
index 6f9b8a3..6ceecfd 100644
--- a/authority/src/main/java/org/prole/authority/web/OidcDiscoveryController.java
+++ b/authority/src/main/java/dev/knoe/authority/web/OidcDiscoveryController.java
@@ -1,6 +1,6 @@
-package org.prole.authority.web;
+package dev.knoe.authority.web;
-import org.prole.authority.config.AuthProperties;
+import dev.knoe.authority.config.AuthProperties;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
diff --git a/authority/src/main/java/org/prole/authority/web/OidcTokenController.java b/authority/src/main/java/dev/knoe/authority/web/OidcTokenController.java
similarity index 94%
rename from authority/src/main/java/org/prole/authority/web/OidcTokenController.java
rename to authority/src/main/java/dev/knoe/authority/web/OidcTokenController.java
index e9a3ada..1fdf746 100644
--- a/authority/src/main/java/org/prole/authority/web/OidcTokenController.java
+++ b/authority/src/main/java/dev/knoe/authority/web/OidcTokenController.java
@@ -1,8 +1,8 @@
-package org.prole.authority.web;
+package dev.knoe.authority.web;
-import org.prole.authority.config.AuthProperties;
-import org.prole.authority.session.OidcCodeService;
-import org.prole.authority.session.OidcTokenService;
+import dev.knoe.authority.config.AuthProperties;
+import dev.knoe.authority.session.OidcCodeService;
+import dev.knoe.authority.session.OidcTokenService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
diff --git a/authority/src/main/java/org/prole/authority/web/OidcUserInfoController.java b/authority/src/main/java/dev/knoe/authority/web/OidcUserInfoController.java
similarity index 88%
rename from authority/src/main/java/org/prole/authority/web/OidcUserInfoController.java
rename to authority/src/main/java/dev/knoe/authority/web/OidcUserInfoController.java
index e940423..33976f7 100644
--- a/authority/src/main/java/org/prole/authority/web/OidcUserInfoController.java
+++ b/authority/src/main/java/dev/knoe/authority/web/OidcUserInfoController.java
@@ -1,8 +1,8 @@
-package org.prole.authority.web;
+package dev.knoe.authority.web;
-import org.prole.authority.config.AuthProperties;
-import org.prole.authority.session.SessionService;
-import org.prole.authority.session.SessionUser;
+import dev.knoe.authority.config.AuthProperties;
+import dev.knoe.authority.session.SessionService;
+import dev.knoe.authority.session.SessionUser;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
diff --git a/authority/src/main/java/org/prole/authority/web/VerifyController.java b/authority/src/main/java/dev/knoe/authority/web/VerifyController.java
similarity index 93%
rename from authority/src/main/java/org/prole/authority/web/VerifyController.java
rename to authority/src/main/java/dev/knoe/authority/web/VerifyController.java
index af7b815..c9ed7e0 100644
--- a/authority/src/main/java/org/prole/authority/web/VerifyController.java
+++ b/authority/src/main/java/dev/knoe/authority/web/VerifyController.java
@@ -1,13 +1,13 @@
-package org.prole.authority.web;
+package dev.knoe.authority.web;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Optional;
-import org.prole.authority.config.AuthProperties;
-import org.prole.authority.session.SessionTokenService;
-import org.prole.authority.session.SessionUser;
+import dev.knoe.authority.config.AuthProperties;
+import dev.knoe.authority.session.SessionTokenService;
+import dev.knoe.authority.session.SessionUser;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
diff --git a/authority/src/test/java/dev/knoe/authority/HealthControllerTest.java b/authority/src/test/java/dev/knoe/authority/HealthControllerTest.java
new file mode 100644
index 0000000..07b9885
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/HealthControllerTest.java
@@ -0,0 +1,14 @@
+package dev.knoe.authority;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class HealthControllerTest {
+
+ private final HealthController controller = new HealthController();
+
+ @Test
+ void healthReturnsOk() {
+ assertEquals("ok", controller.health());
+ }
+}
diff --git a/authority/src/test/java/dev/knoe/authority/admin/AdminControllerTest.java b/authority/src/test/java/dev/knoe/authority/admin/AdminControllerTest.java
new file mode 100644
index 0000000..75c8d45
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/admin/AdminControllerTest.java
@@ -0,0 +1,115 @@
+package dev.knoe.authority.admin;
+
+import dev.knoe.authority.enroll.InviteService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest(properties = {
+ "knoe.auth.enabled=true",
+ "knoe.auth.sessionSecret=test-secret",
+ "knoe.auth.cookieName=knoe_session",
+ "knoe.auth.sessionTtl=1h",
+ "knoe.kerberos.servicePrincipal=HTTP/api.knoe.org@EXAMPLE.TEST",
+ "knoe.kerberos.keytabPath=/tmp/does-not-matter"
+})
+@AutoConfigureMockMvc
+class AdminControllerTest {
+
+ @Autowired private MockMvc mvc;
+
+ @MockBean private InviteService inviteService;
+ @MockBean private KnobjectService knobjectService;
+
+ // ── POST /auth/admin/invites ──────────────────────────────────────────────
+
+ @Test
+ void createInviteReturnsTokenAndOtp() throws Exception {
+ when(inviteService.createInvite(anyString(), anyString(), anyString(), anyString()))
+ .thenReturn(new InviteService.InviteResult(
+ "tok-abc", "https://auth.knoe.dev/enroll?token=tok-abc", "654321"));
+
+ mvc.perform(post("/auth/admin/invites")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"contact\":\"alice@corp.com\",\"contactType\":\"email\",\"nameHint\":\"Alice\"}"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.token").value("tok-abc"))
+ .andExpect(jsonPath("$.otp").value("654321"))
+ .andExpect(jsonPath("$.enrollUrl").exists());
+ }
+
+ @Test
+ void createInviteWithAdminHeader() throws Exception {
+ when(inviteService.createInvite(anyString(), anyString(), anyString(), anyString()))
+ .thenReturn(new InviteService.InviteResult("tok-xyz", "https://auth.knoe.dev/enroll?token=tok-xyz", "111222"));
+
+ mvc.perform(post("/auth/admin/invites")
+ .contentType(MediaType.APPLICATION_JSON)
+ .header("X-Knoe-Admin", "chrisfu")
+ .content("{\"contact\":\"bob@corp.com\",\"contactType\":\"email\",\"nameHint\":\"Bob\"}"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.token").value("tok-xyz"));
+ }
+
+ @Test
+ void createInviteWithNullContactType() throws Exception {
+ when(inviteService.createInvite(anyString(), anyString(), any(), anyString()))
+ .thenReturn(new InviteService.InviteResult("tok-1", "https://auth.knoe.dev/enroll?token=tok-1", "000001"));
+
+ mvc.perform(post("/auth/admin/invites")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"contact\":\"carol@corp.com\",\"nameHint\":\"Carol\"}"))
+ .andExpect(status().isOk());
+ }
+
+ // ── GET /auth/admin/users ─────────────────────────────────────────────────
+
+ @Test
+ void listUsersReturnsJsonArray() throws Exception {
+ when(knobjectService.listUsers()).thenReturn(List.of(
+ Map.of("id", 1, "username", "alice", "email", "alice@knoe.org"),
+ Map.of("id", 2, "username", "bob", "email", "bob@knoe.org")
+ ));
+
+ mvc.perform(get("/auth/admin/users"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$[0].username").value("alice"))
+ .andExpect(jsonPath("$[1].username").value("bob"));
+ }
+
+ @Test
+ void listUsersReturnsEmptyArray() throws Exception {
+ when(knobjectService.listUsers()).thenReturn(List.of());
+ mvc.perform(get("/auth/admin/users"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$").isArray());
+ }
+
+ // ── POST /auth/admin/grants ───────────────────────────────────────────────
+
+ @Test
+ void createGrantReturnsGrantId() throws Exception {
+ when(knobjectService.grantAccess(1, 10, "developer", "admin")).thenReturn(42);
+
+ mvc.perform(post("/auth/admin/grants")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"userId\":1,\"knobjectId\":10,\"role\":\"developer\",\"grantedBy\":\"admin\"}"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.grantId").value(42))
+ .andExpect(jsonPath("$.status").value("granted"));
+ }
+}
diff --git a/authority/src/test/java/dev/knoe/authority/enroll/EnrollValueTypesTest.java b/authority/src/test/java/dev/knoe/authority/enroll/EnrollValueTypesTest.java
new file mode 100644
index 0000000..94891d1
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/enroll/EnrollValueTypesTest.java
@@ -0,0 +1,125 @@
+package dev.knoe.authority.enroll;
+
+import dev.knoe.authority.admin.AdminController;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for pure value types (records, enums, exceptions) in the enroll/admin packages.
+ * No Spring context or DB required.
+ */
+class EnrollValueTypesTest {
+
+ // ── InviteService.InviteResult ────────────────────────────────────────────
+
+ @Test
+ void inviteResultFields() {
+ InviteService.InviteResult r = new InviteService.InviteResult("tok-1", "https://auth.knoe.dev/enroll?token=tok-1", "123456");
+ assertEquals("tok-1", r.token());
+ assertEquals("https://auth.knoe.dev/enroll?token=tok-1", r.enrollUrl());
+ assertEquals("123456", r.rawOtp());
+ }
+
+ // ── InviteService.OtpResult ───────────────────────────────────────────────
+
+ @Test
+ void otpResultEnumValues() {
+ assertNotNull(InviteService.OtpResult.OK);
+ assertNotNull(InviteService.OtpResult.WRONG_OTP);
+ assertNotNull(InviteService.OtpResult.OTP_EXPIRED);
+ assertNotNull(InviteService.OtpResult.LOCKED);
+ assertNotNull(InviteService.OtpResult.INVALID_TOKEN);
+ assertEquals(5, InviteService.OtpResult.values().length);
+ }
+
+ @Test
+ void otpResultValueOf() {
+ assertEquals(InviteService.OtpResult.OK, InviteService.OtpResult.valueOf("OK"));
+ assertEquals(InviteService.OtpResult.LOCKED, InviteService.OtpResult.valueOf("LOCKED"));
+ }
+
+ // ── GoogleOAuthService.GoogleIdentity ─────────────────────────────────────
+
+ @Test
+ void googleIdentityFields() {
+ GoogleOAuthService.GoogleIdentity id = new GoogleOAuthService.GoogleIdentity(
+ "sub-123", "alice@gmail.com", "Alice Smith", null);
+ assertEquals("sub-123", id.sub());
+ assertEquals("alice@gmail.com", id.email());
+ assertEquals("Alice Smith", id.name());
+ assertNull(id.hd());
+ }
+
+ @Test
+ void googleIdentityWithHostedDomain() {
+ GoogleOAuthService.GoogleIdentity id = new GoogleOAuthService.GoogleIdentity(
+ "sub-456", "bob@corp.com", "Bob", "corp.com");
+ assertEquals("corp.com", id.hd());
+ }
+
+ // ── GoogleOAuthService.GoogleOAuthException ───────────────────────────────
+
+ @Test
+ void googleOAuthExceptionMessageOnly() {
+ GoogleOAuthService.GoogleOAuthException ex = new GoogleOAuthService.GoogleOAuthException("oauth failed");
+ assertEquals("oauth failed", ex.getMessage());
+ assertNull(ex.getCause());
+ assertInstanceOf(RuntimeException.class, ex);
+ }
+
+ @Test
+ void googleOAuthExceptionWithCause() {
+ RuntimeException cause = new RuntimeException("network error");
+ GoogleOAuthService.GoogleOAuthException ex = new GoogleOAuthService.GoogleOAuthException("wrapped", cause);
+ assertEquals("wrapped", ex.getMessage());
+ assertSame(cause, ex.getCause());
+ }
+
+ // ── UserProvisioningService.ProvisionResult ───────────────────────────────
+
+ @Test
+ void provisionResultFields() {
+ UserProvisioningService.ProvisionResult r = new UserProvisioningService.ProvisionResult(42, "alice", "alice@knoe.org");
+ assertEquals(42, r.userId());
+ assertEquals("alice", r.username());
+ assertEquals("alice@knoe.org", r.email());
+ }
+
+ // ── UserProvisioningService.ProvisioningException ─────────────────────────
+
+ @Test
+ void provisioningExceptionMessageOnly() {
+ UserProvisioningService.ProvisioningException ex = new UserProvisioningService.ProvisioningException("provision failed");
+ assertEquals("provision failed", ex.getMessage());
+ assertNull(ex.getCause());
+ assertInstanceOf(RuntimeException.class, ex);
+ }
+
+ @Test
+ void provisioningExceptionWithCause() {
+ RuntimeException cause = new RuntimeException("db error");
+ UserProvisioningService.ProvisioningException ex = new UserProvisioningService.ProvisioningException("wrapped", cause);
+ assertSame(cause, ex.getCause());
+ }
+
+ // ── AdminController.CreateInviteRequest ───────────────────────────────────
+
+ @Test
+ void createInviteRequestFields() {
+ AdminController.CreateInviteRequest req = new AdminController.CreateInviteRequest("alice@corp.com", "email", "Alice");
+ assertEquals("alice@corp.com", req.contact());
+ assertEquals("email", req.contactType());
+ assertEquals("Alice", req.nameHint());
+ }
+
+ // ── AdminController.GrantRequest ──────────────────────────────────────────
+
+ @Test
+ void grantRequestFields() {
+ AdminController.GrantRequest req = new AdminController.GrantRequest(1, 10, "read", "admin");
+ assertEquals(1, req.userId());
+ assertEquals(10, req.knobjectId());
+ assertEquals("read", req.role());
+ assertEquals("admin", req.grantedBy());
+ }
+}
diff --git a/authority/src/test/java/dev/knoe/authority/enroll/EnrollmentControllerTest.java b/authority/src/test/java/dev/knoe/authority/enroll/EnrollmentControllerTest.java
new file mode 100644
index 0000000..fcf0faf
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/enroll/EnrollmentControllerTest.java
@@ -0,0 +1,217 @@
+package dev.knoe.authority.enroll;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest(properties = {
+ "knoe.auth.enabled=true",
+ "knoe.auth.sessionSecret=test-secret",
+ "knoe.auth.cookieName=knoe_session",
+ "knoe.auth.sessionTtl=1h",
+ "knoe.kerberos.servicePrincipal=HTTP/api.knoe.org@EXAMPLE.TEST",
+ "knoe.kerberos.keytabPath=/tmp/does-not-matter"
+})
+@AutoConfigureMockMvc
+class EnrollmentControllerTest {
+
+ @Autowired private MockMvc mvc;
+
+ @MockBean private InviteService inviteService;
+ @MockBean private GoogleOAuthService googleOAuth;
+ @MockBean private TotpService totpService;
+ @MockBean private UserProvisioningService provisioning;
+
+ // ── GET /auth/enroll?token=... ────────────────────────────────────────────
+
+ @Test
+ void landingWithInvalidTokenReturnsBadRequest() throws Exception {
+ when(inviteService.findValidInvite(anyString())).thenReturn(null);
+ mvc.perform(get("/auth/enroll").param("token", "bad-token"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void landingWithValidTokenReturnsOk() throws Exception {
+ when(inviteService.findValidInvite("good-token"))
+ .thenReturn(Map.of("contact", "alice@corp.com", "name_hint", "Alice"));
+ mvc.perform(get("/auth/enroll").param("token", "good-token"))
+ .andExpect(status().isOk())
+ .andExpect(content().string(org.hamcrest.Matchers.containsString("invited to knoe.dev")));
+ }
+
+ // ── POST /auth/enroll/verify-otp ─────────────────────────────────────────
+
+ @Test
+ void verifyOtpOkRedirects() throws Exception {
+ when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.OK);
+ mvc.perform(post("/auth/enroll/verify-otp")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("token", "tok").param("otp", "123456"))
+ .andExpect(status().is3xxRedirection());
+ }
+
+ @Test
+ void verifyOtpWrongCodeReturnsBadRequest() throws Exception {
+ when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.WRONG_OTP);
+ mvc.perform(post("/auth/enroll/verify-otp")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("token", "tok").param("otp", "000000"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void verifyOtpExpiredReturnsBadRequest() throws Exception {
+ when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.OTP_EXPIRED);
+ mvc.perform(post("/auth/enroll/verify-otp")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("token", "tok").param("otp", "000000"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void verifyOtpLockedReturnsForbidden() throws Exception {
+ when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.LOCKED);
+ mvc.perform(post("/auth/enroll/verify-otp")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("token", "tok").param("otp", "000000"))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ void verifyOtpInvalidTokenReturnsBadRequest() throws Exception {
+ when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.INVALID_TOKEN);
+ mvc.perform(post("/auth/enroll/verify-otp")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("token", "tok").param("otp", "000000"))
+ .andExpect(status().isBadRequest());
+ }
+
+ // ── GET /auth/enroll/identity (session-gated) ─────────────────────────────
+
+ @Test
+ void identityFormWithoutOtpVerifiedReturnsForbidden() throws Exception {
+ mvc.perform(get("/auth/enroll/identity"))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ void identityFormWithOtpVerifiedReturnsOk() throws Exception {
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("enroll.otpVerified", Boolean.TRUE);
+ mvc.perform(get("/auth/enroll/identity").session(session))
+ .andExpect(status().isOk())
+ .andExpect(content().string(org.hamcrest.Matchers.containsString("Your details")));
+ }
+
+ // ── POST /auth/enroll/identity/start ──────────────────────────────────────
+
+ @Test
+ void identityStartWithoutOtpVerifiedReturnsForbidden() throws Exception {
+ mvc.perform(post("/auth/enroll/identity/start")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("username", "alice").param("displayName", "Alice"))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ void identityStartWithInvalidUsernameReturnsBadRequest() throws Exception {
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("enroll.otpVerified", Boolean.TRUE);
+ mvc.perform(post("/auth/enroll/identity/start")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("username", "A!").param("displayName", "Alice")
+ .session(session))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void identityStartWithValidUsernameRedirects() throws Exception {
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("enroll.otpVerified", Boolean.TRUE);
+ when(googleOAuth.buildAuthorizationUrl(anyString(), anyString()))
+ .thenReturn("https://accounts.google.com/o/oauth2/auth?state=x");
+ mvc.perform(post("/auth/enroll/identity/start")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("username", "alice").param("displayName", "Alice")
+ .session(session))
+ .andExpect(status().is3xxRedirection());
+ }
+
+ // ── GET /auth/enroll/google-callback ──────────────────────────────────────
+
+ @Test
+ void googleCallbackStateMismatchReturnsBadRequest() throws Exception {
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("enroll.googleState", "expected-state");
+ mvc.perform(get("/auth/enroll/google-callback")
+ .param("code", "auth-code").param("state", "wrong-state")
+ .session(session))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void googleCallbackNoSessionStateReturnsBadRequest() throws Exception {
+ mvc.perform(get("/auth/enroll/google-callback")
+ .param("code", "auth-code").param("state", "some-state"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void googleCallbackSuccessRedirects() throws Exception {
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("enroll.googleState", "state-123");
+ when(googleOAuth.exchangeCode(anyString()))
+ .thenReturn(new GoogleOAuthService.GoogleIdentity("sub", "alice@gmail.com", "Alice", null));
+ mvc.perform(get("/auth/enroll/google-callback")
+ .param("code", "auth-code").param("state", "state-123")
+ .session(session))
+ .andExpect(status().is3xxRedirection());
+ }
+
+ @Test
+ void googleCallbackOAuthExceptionReturnsBadRequest() throws Exception {
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("enroll.googleState", "state-123");
+ when(googleOAuth.exchangeCode(anyString()))
+ .thenThrow(new GoogleOAuthService.GoogleOAuthException("token exchange failed"));
+ mvc.perform(get("/auth/enroll/google-callback")
+ .param("code", "auth-code").param("state", "state-123")
+ .session(session))
+ .andExpect(status().isBadRequest());
+ }
+
+ // ── GET /auth/enroll/totp (session-gated) ─────────────────────────────────
+
+ @Test
+ void totpSetupWithoutGoogleIdReturnsForbidden() throws Exception {
+ mvc.perform(get("/auth/enroll/totp"))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ void totpSetupWithGoogleIdReturnsOk() throws Exception {
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("enroll.googleId",
+ new GoogleOAuthService.GoogleIdentity("sub", "alice@gmail.com", "Alice", null));
+ session.setAttribute("enroll.username", "alice");
+ when(totpService.generateSecret()).thenReturn("JBSWY3DPEHPK3PXP");
+ when(totpService.buildQrUri(anyString(), anyString())).thenReturn("otpauth://totp/alice");
+ mvc.perform(get("/auth/enroll/totp").session(session))
+ .andExpect(status().isOk());
+ }
+}
diff --git a/authority/src/test/java/dev/knoe/authority/enroll/TotpServiceTest.java b/authority/src/test/java/dev/knoe/authority/enroll/TotpServiceTest.java
new file mode 100644
index 0000000..4710d40
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/enroll/TotpServiceTest.java
@@ -0,0 +1,74 @@
+package dev.knoe.authority.enroll;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class TotpServiceTest {
+
+ private TotpService totpService;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ totpService = new TotpService();
+ // inject issuer via reflection since @Value is not wired in plain unit tests
+ var field = TotpService.class.getDeclaredField("issuer");
+ field.setAccessible(true);
+ field.set(totpService, "Knoe.DEV");
+ }
+
+ @Test
+ void generateSecretIsNotNull() {
+ String secret = totpService.generateSecret();
+ assertNotNull(secret);
+ assertFalse(secret.isBlank());
+ }
+
+ @Test
+ void generateSecretIsBase32Like() {
+ String secret = totpService.generateSecret();
+ // Base32 alphabet: A-Z and 2-7
+ assertTrue(secret.matches("[A-Z2-7]+"), "Expected Base32 string, got: " + secret);
+ }
+
+ @Test
+ void generateSecretIsDifferentEachTime() {
+ String s1 = totpService.generateSecret();
+ String s2 = totpService.generateSecret();
+ assertNotEquals(s1, s2);
+ }
+
+ @Test
+ void buildQrUriContainsExpectedParts() {
+ String secret = totpService.generateSecret();
+ String uri = totpService.buildQrUri("alice", secret);
+ assertNotNull(uri);
+ assertTrue(uri.startsWith("otpauth://totp/"), "URI should start with otpauth://totp/");
+ assertTrue(uri.contains("alice"), "URI should contain the username");
+ assertTrue(uri.contains("Knoe.DEV"), "URI should contain the issuer");
+ assertTrue(uri.contains(secret), "URI should contain the secret");
+ }
+
+ @Test
+ void verifyReturnsFalseForNullSecret() {
+ assertFalse(totpService.verify(null, "123456"));
+ }
+
+ @Test
+ void verifyReturnsFalseForNullCode() {
+ assertFalse(totpService.verify(totpService.generateSecret(), null));
+ }
+
+ @Test
+ void verifyReturnsFalseForWrongCode() {
+ String secret = totpService.generateSecret();
+ // "000000" is almost certainly wrong for any real secret at any time
+ assertFalse(totpService.verify(secret, "000000"));
+ }
+
+ @Test
+ void verifyReturnsFalseForInvalidSecret() {
+ // Garbage secret — library should throw internally, service should return false
+ assertFalse(totpService.verify("not-valid-base32!!!", "123456"));
+ }
+}
diff --git a/authority/src/test/java/dev/knoe/authority/kerberos/KadminClientTest.java b/authority/src/test/java/dev/knoe/authority/kerberos/KadminClientTest.java
new file mode 100644
index 0000000..49116bb
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/kerberos/KadminClientTest.java
@@ -0,0 +1,72 @@
+package dev.knoe.authority.kerberos;
+
+import org.junit.jupiter.api.Test;
+import java.lang.reflect.Method;
+import static org.junit.jupiter.api.Assertions.*;
+
+class KadminClientTest {
+
+ // ── KadminException constructors ──────────────────────────────────────────
+
+ @Test
+ void exceptionMessageOnly() {
+ KadminClient.KadminException ex = new KadminClient.KadminException("something went wrong");
+ assertEquals("something went wrong", ex.getMessage());
+ assertNull(ex.getCause());
+ }
+
+ @Test
+ void exceptionWithCause() {
+ RuntimeException cause = new RuntimeException("root cause");
+ KadminClient.KadminException ex = new KadminClient.KadminException("wrapped", cause);
+ assertEquals("wrapped", ex.getMessage());
+ assertSame(cause, ex.getCause());
+ }
+
+ @Test
+ void exceptionIsRuntimeException() {
+ assertInstanceOf(RuntimeException.class, new KadminClient.KadminException("err"));
+ }
+
+ // ── qualified() — private method tested via reflection ───────────────────
+
+ @Test
+ void qualifiedAppendsRealmWhenMissing() throws Exception {
+ KadminClient client = new KadminClient();
+ // inject realm field
+ var realmField = KadminClient.class.getDeclaredField("realm");
+ realmField.setAccessible(true);
+ realmField.set(client, "KNOE.DEV");
+
+ Method qualified = KadminClient.class.getDeclaredMethod("qualified", String.class);
+ qualified.setAccessible(true);
+
+ assertEquals("alice@KNOE.DEV", qualified.invoke(client, "alice"));
+ }
+
+ @Test
+ void qualifiedLeavesFullyQualifiedPrincipalUnchanged() throws Exception {
+ KadminClient client = new KadminClient();
+ var realmField = KadminClient.class.getDeclaredField("realm");
+ realmField.setAccessible(true);
+ realmField.set(client, "KNOE.DEV");
+
+ Method qualified = KadminClient.class.getDeclaredMethod("qualified", String.class);
+ qualified.setAccessible(true);
+
+ assertEquals("alice@KNOE.DEV", qualified.invoke(client, "alice@KNOE.DEV"));
+ }
+
+ @Test
+ void qualifiedWithServicePrincipal() throws Exception {
+ KadminClient client = new KadminClient();
+ var realmField = KadminClient.class.getDeclaredField("realm");
+ realmField.setAccessible(true);
+ realmField.set(client, "KNOE.DEV");
+
+ Method qualified = KadminClient.class.getDeclaredMethod("qualified", String.class);
+ qualified.setAccessible(true);
+
+ assertEquals("HTTP/api.knoe.dev@KNOE.DEV", qualified.invoke(client, "HTTP/api.knoe.dev@KNOE.DEV"));
+ }
+}
diff --git a/authority/src/test/java/dev/knoe/authority/kerberos/KerberosSpnegoResultTest.java b/authority/src/test/java/dev/knoe/authority/kerberos/KerberosSpnegoResultTest.java
new file mode 100644
index 0000000..e9e3b3e
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/kerberos/KerberosSpnegoResultTest.java
@@ -0,0 +1,24 @@
+package dev.knoe.authority.kerberos;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class KerberosSpnegoResultTest {
+
+ @Test
+ void establishedResult() {
+ byte[] token = new byte[]{1, 2, 3};
+ KerberosSpnegoService.Result r = new KerberosSpnegoService.Result(true, "alice@KNOE.DEV", token);
+ assertTrue(r.established());
+ assertEquals("alice@KNOE.DEV", r.clientPrincipal());
+ assertArrayEquals(token, r.responseToken());
+ }
+
+ @Test
+ void notEstablishedResult() {
+ KerberosSpnegoService.Result r = new KerberosSpnegoService.Result(false, null, new byte[0]);
+ assertFalse(r.established());
+ assertNull(r.clientPrincipal());
+ assertEquals(0, r.responseToken().length);
+ }
+}
diff --git a/authority/src/test/java/org/prole/authority/regression/IdentityRegressionTest.java b/authority/src/test/java/dev/knoe/authority/regression/IdentityRegressionTest.java
similarity index 98%
rename from authority/src/test/java/org/prole/authority/regression/IdentityRegressionTest.java
rename to authority/src/test/java/dev/knoe/authority/regression/IdentityRegressionTest.java
index 3edfd29..00623a8 100644
--- a/authority/src/test/java/org/prole/authority/regression/IdentityRegressionTest.java
+++ b/authority/src/test/java/dev/knoe/authority/regression/IdentityRegressionTest.java
@@ -1,4 +1,4 @@
-package org.prole.authority.regression;
+package dev.knoe.authority.regression;
import org.junit.jupiter.api.Test;
import java.io.File;
diff --git a/authority/src/test/java/dev/knoe/authority/session/OidcCodeServiceTest.java b/authority/src/test/java/dev/knoe/authority/session/OidcCodeServiceTest.java
new file mode 100644
index 0000000..294883f
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/session/OidcCodeServiceTest.java
@@ -0,0 +1,61 @@
+package dev.knoe.authority.session;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class OidcCodeServiceTest {
+
+ private final OidcCodeService service = new OidcCodeService();
+
+ private OidcCodeService.AuthorizationRequest sampleRequest() {
+ return new OidcCodeService.AuthorizationRequest(
+ "client-id", "https://app.knoe.dev/callback", "state-xyz", "nonce-abc",
+ new SessionUser("alice", "alice@knoe.org", java.util.List.of()));
+ }
+
+ @Test
+ void createCodeReturnsNonNullString() {
+ String code = service.createCode(sampleRequest());
+ assertNotNull(code);
+ assertFalse(code.isBlank());
+ }
+
+ @Test
+ void createCodeReturnsDifferentCodesEachTime() {
+ String c1 = service.createCode(sampleRequest());
+ String c2 = service.createCode(sampleRequest());
+ assertNotEquals(c1, c2);
+ }
+
+ @Test
+ void consumeCodeReturnsRequest() {
+ OidcCodeService.AuthorizationRequest req = sampleRequest();
+ String code = service.createCode(req);
+ OidcCodeService.AuthorizationRequest consumed = service.consumeCode(code);
+ assertNotNull(consumed);
+ assertEquals("client-id", consumed.clientId());
+ assertEquals("alice", consumed.user().username());
+ }
+
+ @Test
+ void consumeCodeRemovesIt() {
+ String code = service.createCode(sampleRequest());
+ service.consumeCode(code);
+ assertNull(service.consumeCode(code));
+ }
+
+ @Test
+ void consumeUnknownCodeReturnsNull() {
+ assertNull(service.consumeCode("does-not-exist"));
+ }
+
+ @Test
+ void authorizationRequestFieldsAccessible() {
+ OidcCodeService.AuthorizationRequest req = sampleRequest();
+ assertEquals("client-id", req.clientId());
+ assertEquals("https://app.knoe.dev/callback", req.redirectUri());
+ assertEquals("state-xyz", req.state());
+ assertEquals("nonce-abc", req.nonce());
+ assertEquals("alice", req.user().username());
+ }
+}
diff --git a/authority/src/test/java/dev/knoe/authority/session/SessionServiceTest.java b/authority/src/test/java/dev/knoe/authority/session/SessionServiceTest.java
new file mode 100644
index 0000000..f2714a9
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/session/SessionServiceTest.java
@@ -0,0 +1,67 @@
+package dev.knoe.authority.session;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.knoe.authority.config.AuthProperties;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class SessionServiceTest {
+
+ private SessionTokenService tokenService;
+ private SessionService sessionService;
+ private AuthProperties authProperties;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ Clock clock = Clock.fixed(Instant.parse("2026-03-20T00:00:00Z"), ZoneOffset.UTC);
+ tokenService = SessionTokenService.forTests(new ObjectMapper(), clock);
+
+ authProperties = new AuthProperties();
+ authProperties.setSessionSecret("test-secret");
+ authProperties.setCookieName("knoe_session");
+ authProperties.setSessionTtl(Duration.ofHours(1));
+
+ sessionService = new SessionService(authProperties, tokenService);
+ }
+
+ @Test
+ void noCookieReturnsEmpty() {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ assertEquals(Optional.empty(), sessionService.getSessionUser(request));
+ }
+
+ @Test
+ void blankCookieReturnsEmpty() {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setCookies(new jakarta.servlet.http.Cookie("knoe_session", " "));
+ assertEquals(Optional.empty(), sessionService.getSessionUser(request));
+ }
+
+ @Test
+ void validCookieReturnsUser() {
+ String token = tokenService.issue("test-secret",
+ new SessionUser("alice", "alice@knoe.org", List.of()), Duration.ofMinutes(10));
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setCookies(new jakarta.servlet.http.Cookie("knoe_session", token));
+ Optional user = sessionService.getSessionUser(request);
+ assertTrue(user.isPresent());
+ assertEquals("alice", user.get().username());
+ }
+
+ @Test
+ void invalidTokenReturnsEmpty() {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ request.setCookies(new jakarta.servlet.http.Cookie("knoe_session", "not-a-valid-token"));
+ assertEquals(Optional.empty(), sessionService.getSessionUser(request));
+ }
+}
diff --git a/authority/src/test/java/org/prole/authority/session/SessionTokenServiceTest.java b/authority/src/test/java/dev/knoe/authority/session/SessionTokenServiceTest.java
similarity index 98%
rename from authority/src/test/java/org/prole/authority/session/SessionTokenServiceTest.java
rename to authority/src/test/java/dev/knoe/authority/session/SessionTokenServiceTest.java
index a9d565b..ae28bf3 100644
--- a/authority/src/test/java/org/prole/authority/session/SessionTokenServiceTest.java
+++ b/authority/src/test/java/dev/knoe/authority/session/SessionTokenServiceTest.java
@@ -1,4 +1,4 @@
-package org.prole.authority.session;
+package dev.knoe.authority.session;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Clock;
diff --git a/authority/src/test/java/dev/knoe/authority/user/PrincipalNormalizerTest.java b/authority/src/test/java/dev/knoe/authority/user/PrincipalNormalizerTest.java
new file mode 100644
index 0000000..0dbe480
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/user/PrincipalNormalizerTest.java
@@ -0,0 +1,64 @@
+package dev.knoe.authority.user;
+
+import org.junit.jupiter.api.Test;
+import java.util.Optional;
+import static org.junit.jupiter.api.Assertions.*;
+
+class PrincipalNormalizerTest {
+
+ private final PrincipalNormalizer normalizer = new PrincipalNormalizer();
+
+ @Test
+ void nullReturnsEmpty() {
+ assertEquals(Optional.empty(), normalizer.normalizeUsernameFromKerberosPrincipal(null));
+ }
+
+ @Test
+ void blankReturnsEmpty() {
+ assertEquals(Optional.empty(), normalizer.normalizeUsernameFromKerberosPrincipal(" "));
+ }
+
+ @Test
+ void simpleUsernameAtRealm() {
+ assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("alice@KNOE.DEV"));
+ }
+
+ @Test
+ void usernameWithInstanceAndRealm() {
+ assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("alice/admin@KNOE.DEV"));
+ }
+
+ @Test
+ void usernameWithInstanceNoRealm() {
+ assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("alice/admin"));
+ }
+
+ @Test
+ void bareUsernameNoRealm() {
+ assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("alice"));
+ }
+
+ @Test
+ void uppercaseNormalisedToLower() {
+ assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("ALICE@KNOE.DEV"));
+ }
+
+ @Test
+ void leadingTrailingWhitespace() {
+ assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal(" alice@KNOE.DEV "));
+ }
+
+ @Test
+ void atSignOnlyKeepsRealm() {
+ // "@REALM" → at index is 0, so realm is NOT stripped (at > 0 guard),
+ // result is "@knoe.dev" lowercased — not blank, so returned as-is.
+ // This documents the current behaviour; input is invalid anyway.
+ Optional result = normalizer.normalizeUsernameFromKerberosPrincipal("@KNOE.DEV");
+ assertTrue(result.isPresent());
+ }
+
+ @Test
+ void emptyStringReturnsEmpty() {
+ assertEquals(Optional.empty(), normalizer.normalizeUsernameFromKerberosPrincipal(""));
+ }
+}
diff --git a/authority/src/test/java/dev/knoe/authority/web/LoginControllerTest.java b/authority/src/test/java/dev/knoe/authority/web/LoginControllerTest.java
new file mode 100644
index 0000000..22fd735
--- /dev/null
+++ b/authority/src/test/java/dev/knoe/authority/web/LoginControllerTest.java
@@ -0,0 +1,195 @@
+package dev.knoe.authority.web;
+
+import dev.knoe.authority.enroll.GoogleOAuthService;
+import dev.knoe.authority.kerberos.KerberosPasswordService;
+import dev.knoe.authority.kerberos.KerberosSpnegoService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest(properties = {
+ "knoe.auth.enabled=true",
+ "knoe.auth.sessionSecret=test-secret-for-login",
+ "knoe.auth.cookieName=knoe_session",
+ "knoe.auth.sessionTtl=1h",
+ "knoe.auth.emailDomain=knoe.org",
+ "knoe.auth.cookieDomain=knoe.dev",
+ "knoe.auth.formEnabled=true",
+ "knoe.kerberos.servicePrincipal=HTTP/api.knoe.org@EXAMPLE.TEST",
+ "knoe.kerberos.keytabPath=/tmp/does-not-matter",
+ "knoe.kerberos.realm=EXAMPLE.TEST"
+})
+@AutoConfigureMockMvc
+class LoginControllerTest {
+
+ @Autowired private MockMvc mvc;
+
+ @MockBean private KerberosSpnegoService spnego;
+ @MockBean private KerberosPasswordService passwordAuth;
+ @MockBean private GoogleOAuthService googleOAuth;
+
+ // ── GET /auth/login ───────────────────────────────────────────────────────
+
+ @Test
+ void loginPageReturnsHtml() throws Exception {
+ mvc.perform(get("/auth/login"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
+ .andExpect(content().string(org.hamcrest.Matchers.containsString("Knoe Login")));
+ }
+
+ @Test
+ void loginPageWithNextParam() throws Exception {
+ mvc.perform(get("/auth/login").param("next", "/dashboard"))
+ .andExpect(status().isOk())
+ .andExpect(content().string(org.hamcrest.Matchers.containsString("Knoe Login")));
+ }
+
+ // ── GET /auth/spnego ──────────────────────────────────────────────────────
+
+ @Test
+ void spnegoWithoutNegotiateHeaderReturns401() throws Exception {
+ mvc.perform(get("/auth/spnego"))
+ .andExpect(status().isUnauthorized())
+ .andExpect(header().string("WWW-Authenticate", "Negotiate"));
+ }
+
+ @Test
+ void spnegoWithValidTokenRedirects() throws Exception {
+ when(spnego.acceptSpnegoToken(anyString(), anyString(), any()))
+ .thenReturn(new KerberosSpnegoService.Result(true, "alice@EXAMPLE.TEST", new byte[0]));
+
+ mvc.perform(get("/auth/spnego")
+ .header("Authorization", "Negotiate dGVzdA=="))
+ .andExpect(status().is3xxRedirection());
+ }
+
+ @Test
+ void spnegoWithFailedNegotiationReturns401() throws Exception {
+ when(spnego.acceptSpnegoToken(anyString(), anyString(), any()))
+ .thenReturn(new KerberosSpnegoService.Result(false, null, new byte[0]));
+
+ mvc.perform(get("/auth/spnego")
+ .header("Authorization", "Negotiate dGVzdA=="))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void spnegoWithResponseTokenSetsNegotiateHeader() throws Exception {
+ when(spnego.acceptSpnegoToken(anyString(), anyString(), any()))
+ .thenReturn(new KerberosSpnegoService.Result(true, "alice@EXAMPLE.TEST", new byte[]{1, 2, 3}));
+
+ mvc.perform(get("/auth/spnego")
+ .header("Authorization", "Negotiate dGVzdA=="))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(header().exists("WWW-Authenticate"));
+ }
+
+ // ── POST /auth/form ───────────────────────────────────────────────────────
+
+ @Test
+ void formLoginWithBlankUsernameReturns401() throws Exception {
+ mvc.perform(post("/auth/form")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("username", "").param("password", "secret"))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void formLoginWithBlankPasswordReturns401() throws Exception {
+ mvc.perform(post("/auth/form")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("username", "alice").param("password", ""))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void formLoginSuccessRedirects() throws Exception {
+ mvc.perform(post("/auth/form")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("username", "alice").param("password", "correct"))
+ .andExpect(status().is3xxRedirection());
+ }
+
+ @Test
+ void formLoginWithNextParam() throws Exception {
+ mvc.perform(post("/auth/form")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("username", "alice").param("password", "correct")
+ .param("next", "/dashboard"))
+ .andExpect(status().is3xxRedirection());
+ }
+
+ // ── GET /auth/logout ──────────────────────────────────────────────────────
+
+ @Test
+ void logoutRedirects() throws Exception {
+ mvc.perform(get("/auth/logout"))
+ .andExpect(status().is3xxRedirection());
+ }
+
+ @Test
+ void logoutClearsCookie() throws Exception {
+ mvc.perform(get("/auth/logout"))
+ .andExpect(header().string("Set-Cookie", org.hamcrest.Matchers.containsString("Max-Age=0")));
+ }
+
+ // ── GET /auth/login/google ────────────────────────────────────────────────
+
+ @Test
+ void googleLoginRedirectsToGoogle() throws Exception {
+ when(googleOAuth.buildAuthorizationUrl(anyString(), anyString(), anyString()))
+ .thenReturn("https://accounts.google.com/o/oauth2/auth?state=x&nonce=y");
+
+ mvc.perform(get("/auth/login/google"))
+ .andExpect(status().is3xxRedirection())
+ .andExpect(header().string("Location", org.hamcrest.Matchers.containsString("accounts.google.com")));
+ }
+
+ // ── GET /auth/callback/google ─────────────────────────────────────────────
+
+ @Test
+ void googleCallbackStateMismatchReturnsBadRequest() throws Exception {
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("login.googleState", "expected");
+
+ mvc.perform(get("/auth/callback/google")
+ .param("code", "code").param("state", "wrong")
+ .session(session))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void googleCallbackNoStateReturnsBadRequest() throws Exception {
+ mvc.perform(get("/auth/callback/google")
+ .param("code", "code").param("state", "any"))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ void googleCallbackSuccessRedirects() throws Exception {
+ MockHttpSession session = new MockHttpSession();
+ session.setAttribute("login.googleState", "state-abc");
+ session.setAttribute("login.next", null);
+
+ when(googleOAuth.exchangeCode(anyString(), anyString()))
+ .thenReturn(new GoogleOAuthService.GoogleIdentity("sub", "alice@corp.com", "Alice", "corp.com"));
+
+ mvc.perform(get("/auth/callback/google")
+ .param("code", "auth-code").param("state", "state-abc")
+ .session(session))
+ .andExpect(status().is3xxRedirection());
+ }
+}
diff --git a/authority/src/test/java/org/prole/authority/web/VerifyControllerTest.java b/authority/src/test/java/dev/knoe/authority/web/VerifyControllerTest.java
similarity index 91%
rename from authority/src/test/java/org/prole/authority/web/VerifyControllerTest.java
rename to authority/src/test/java/dev/knoe/authority/web/VerifyControllerTest.java
index 3639680..6ef9414 100644
--- a/authority/src/test/java/org/prole/authority/web/VerifyControllerTest.java
+++ b/authority/src/test/java/dev/knoe/authority/web/VerifyControllerTest.java
@@ -1,9 +1,9 @@
-package org.prole.authority.web;
+package dev.knoe.authority.web;
import java.time.Duration;
import org.junit.jupiter.api.Test;
-import org.prole.authority.session.SessionTokenService;
-import org.prole.authority.session.SessionUser;
+import dev.knoe.authority.session.SessionTokenService;
+import dev.knoe.authority.session.SessionUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
diff --git a/docs/knoe-auth-phase-2.md b/docs/knoe-auth-phase-2.md
index 3faf189..9cba518 100644
--- a/docs/knoe-auth-phase-2.md
+++ b/docs/knoe-auth-phase-2.md
@@ -28,7 +28,7 @@ knoe-auth (the Authority) acts as the central Identity Authority for the cluster
- **Normalization**: `PrincipalNormalizer` ensures consistency across all auth methods.
## Namespace & Domains
-- **Java Package**: `org.prole.authority.*`
+- **Java Package**: `dev.knoe.authority.*`
- **Service DNS**: `knoe.dev` (e.g., `api.knoe.dev`, `git.knoe.dev`)
- **OIDC Issuer**: `https://api.knoe.dev/auth`
- **Identity Domain**: `knoey.com`
diff --git a/docs/plans/knoe-auth-round-1.md b/docs/plans/knoe-auth-round-1.md
index 1428bb9..2eae620 100644
--- a/docs/plans/knoe-auth-round-1.md
+++ b/docs/plans/knoe-auth-round-1.md
@@ -53,32 +53,32 @@ The actual files that implement Round 1. Verify with `git ls-files` before assum
### Java application — `authority/`
-The Spring Boot service that implements the enrollment flow, the admin API, and SPNEGO-protected endpoints. Multi-module Maven build (`authority/pom.xml`); the application package is `org.prole.authority` (kept as-is across the prole→knoe rebrand for compatibility).
+The Spring Boot service that implements the enrollment flow, the admin API, and SPNEGO-protected endpoints. Multi-module Maven build (`authority/pom.xml`); the application package is `dev.knoe.authority` (kept as-is across the prole→knoe rebrand for compatibility).
| File | Responsibility |
| --- | --- |
-| `authority/src/main/java/org/prole/authority/KnoeAuthApplication.java` | `@SpringBootApplication` entry point. |
-| `authority/src/main/java/org/prole/authority/HealthController.java` | `/health` endpoint. |
-| `authority/src/main/java/org/prole/authority/web/LoginController.java` | Form-login + SPNEGO challenge for browsers without a ticket. |
-| `authority/src/main/java/org/prole/authority/web/VerifyController.java` | Token-verify endpoint for downstream services. |
-| `authority/src/main/java/org/prole/authority/session/SessionTokenService.java` | Issues HMAC-SHA256 JWT cookies after successful auth. |
-| `authority/src/main/java/org/prole/authority/session/SessionUser.java` | Authenticated principal carried in the security context. |
-| `authority/src/main/java/org/prole/authority/user/PrincipalNormalizer.java` | Strips realm/instance from a Kerberos principal (`alice/admin@KNOE.DEV` → `alice`). |
-| `authority/src/main/java/org/prole/authority/kerberos/KerberosSpnegoService.java` | SPNEGO challenge/response handling. |
-| `authority/src/main/java/org/prole/authority/kerberos/KerberosPasswordService.java` | Password-style auth fallback for browsers that can't do SPNEGO. |
-| `authority/src/main/java/org/prole/authority/kerberos/KadminClient.java` | Shells out to `kadmin.local` (in the KDC sidecar) to `addprinc` and `cpw`. **Sanitizes input.** |
-| `authority/src/main/java/org/prole/authority/enroll/EnrollmentController.java` | Web endpoints: `GET /auth/enroll`, `POST /auth/enroll/verify-otp`, `POST /auth/enroll/identity/start`, `GET /auth/enroll/google-callback`, `GET /auth/enroll/totp`, `POST /auth/enroll/totp/verify`, `POST /auth/enroll/complete`. |
-| `authority/src/main/java/org/prole/authority/enroll/InviteService.java` | CRUD + validation against `knoe.invitation`. OTP hashing (bcrypt) and rate limiting (3 attempts). |
-| `authority/src/main/java/org/prole/authority/enroll/GoogleOAuthService.java` | Exchange OAuth2 code → ID token, validate `email_verified`, return a `GoogleIdentity` record. **No `hd` allowlist.** |
-| `authority/src/main/java/org/prole/authority/enroll/TotpService.java` | Generate TOTP secret, produce `otpauth://` URI, verify codes. |
-| `authority/src/main/java/org/prole/authority/enroll/UserProvisioningService.java` | Transactional orchestrator: inserts user/identity/totp rows, calls `KadminClient`, queues provisioning jobs. |
-| `authority/src/main/java/org/prole/authority/admin/AdminController.java` | `POST /auth/admin/invites`, `GET /auth/admin/users`, `POST /auth/admin/grants`. SPNEGO + admin-role gated. |
-| `authority/src/main/java/org/prole/authority/admin/KnobjectService.java` | CRUD on `knoe.knobject` and `knoe.access_grant`; enqueues `provisioning_job` rows. |
-| `authority/src/main/java/org/prole/authority/provisioning/ProvisioningWorker.java` | `@Scheduled` poller for `knoe.provisioning_job WHERE status = 'pending'`. Dispatches to GitLab/Gitea/CNPG. |
-| `authority/src/main/java/org/prole/authority/config/AuthProperties.java` | Typed binding for `knoe.auth.*` keys. |
-| `authority/src/main/java/org/prole/authority/config/KerberosProperties.java` | Typed binding for `knoe.auth.kerberos.*` keys. |
+| `authority/src/main/java/dev/knoe/authority/KnoeAuthApplication.java` | `@SpringBootApplication` entry point. |
+| `authority/src/main/java/dev/knoe/authority/HealthController.java` | `/health` endpoint. |
+| `authority/src/main/java/dev/knoe/authority/web/LoginController.java` | Form-login + SPNEGO challenge for browsers without a ticket. |
+| `authority/src/main/java/dev/knoe/authority/web/VerifyController.java` | Token-verify endpoint for downstream services. |
+| `authority/src/main/java/dev/knoe/authority/session/SessionTokenService.java` | Issues HMAC-SHA256 JWT cookies after successful auth. |
+| `authority/src/main/java/dev/knoe/authority/session/SessionUser.java` | Authenticated principal carried in the security context. |
+| `authority/src/main/java/dev/knoe/authority/user/PrincipalNormalizer.java` | Strips realm/instance from a Kerberos principal (`alice/admin@KNOE.DEV` → `alice`). |
+| `authority/src/main/java/dev/knoe/authority/kerberos/KerberosSpnegoService.java` | SPNEGO challenge/response handling. |
+| `authority/src/main/java/dev/knoe/authority/kerberos/KerberosPasswordService.java` | Password-style auth fallback for browsers that can't do SPNEGO. |
+| `authority/src/main/java/dev/knoe/authority/kerberos/KadminClient.java` | Shells out to `kadmin.local` (in the KDC sidecar) to `addprinc` and `cpw`. **Sanitizes input.** |
+| `authority/src/main/java/dev/knoe/authority/enroll/EnrollmentController.java` | Web endpoints: `GET /auth/enroll`, `POST /auth/enroll/verify-otp`, `POST /auth/enroll/identity/start`, `GET /auth/enroll/google-callback`, `GET /auth/enroll/totp`, `POST /auth/enroll/totp/verify`, `POST /auth/enroll/complete`. |
+| `authority/src/main/java/dev/knoe/authority/enroll/InviteService.java` | CRUD + validation against `knoe.invitation`. OTP hashing (bcrypt) and rate limiting (3 attempts). |
+| `authority/src/main/java/dev/knoe/authority/enroll/GoogleOAuthService.java` | Exchange OAuth2 code → ID token, validate `email_verified`, return a `GoogleIdentity` record. **No `hd` allowlist.** |
+| `authority/src/main/java/dev/knoe/authority/enroll/TotpService.java` | Generate TOTP secret, produce `otpauth://` URI, verify codes. |
+| `authority/src/main/java/dev/knoe/authority/enroll/UserProvisioningService.java` | Transactional orchestrator: inserts user/identity/totp rows, calls `KadminClient`, queues provisioning jobs. |
+| `authority/src/main/java/dev/knoe/authority/admin/AdminController.java` | `POST /auth/admin/invites`, `GET /auth/admin/users`, `POST /auth/admin/grants`. SPNEGO + admin-role gated. |
+| `authority/src/main/java/dev/knoe/authority/admin/KnobjectService.java` | CRUD on `knoe.knobject` and `knoe.access_grant`; enqueues `provisioning_job` rows. |
+| `authority/src/main/java/dev/knoe/authority/provisioning/ProvisioningWorker.java` | `@Scheduled` poller for `knoe.provisioning_job WHERE status = 'pending'`. Dispatches to GitLab/Gitea/CNPG. |
+| `authority/src/main/java/dev/knoe/authority/config/AuthProperties.java` | Typed binding for `knoe.auth.*` keys. |
+| `authority/src/main/java/dev/knoe/authority/config/KerberosProperties.java` | Typed binding for `knoe.auth.kerberos.*` keys. |
-Tests for the above live under `authority/src/test/java/org/prole/authority/` — notably `web/VerifyControllerTest.java` and `session/SessionTokenServiceTest.java`.
+Tests for the above live under `authority/src/test/java/dev/knoe/authority/` — notably `web/VerifyControllerTest.java` and `session/SessionTokenServiceTest.java`.
### Kubernetes manifests — GKE