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