From 9d5827b5225ea9daa92fcd8251b9c97969d20be7 Mon Sep 17 00:00:00 2001 From: chrisfu Date: Thu, 30 Apr 2026 22:14:03 -0700 Subject: [PATCH] =?UTF-8?q?feat(auth):=20knoe-auth=20Phase=202=20=E2=80=94?= =?UTF-8?q?=20OIDC=20provider=20surface=20(discovery,=20authorize,=20token?= =?UTF-8?q?,=20userinfo,=20JWKS),=20RS256=20signing,=20Kerberos/SPNEGO=20i?= =?UTF-8?q?ntegration,=20stateless=20session=20model,=20PrincipalNormalize?= =?UTF-8?q?r,=20typed=20config,=20init=20script,=20architecture=20doc,=20r?= =?UTF-8?q?egression=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Junie --- authority/pom.xml | 2 +- .../org/prole/authority/HealthController.java | 2 +- .../prole/authority/KnoeAuthApplication.java | 2 +- .../authority/config/AuthProperties.java | 4 +- .../authority/config/KerberosProperties.java | 2 +- .../kerberos/KerberosPasswordService.java | 2 +- .../kerberos/KerberosSpnegoService.java | 2 +- .../authority/session/OidcCodeService.java | 2 +- .../authority/session/OidcTokenService.java | 10 ++- .../authority/session/SessionService.java | 4 +- .../session/SessionTokenService.java | 2 +- .../prole/authority/session/SessionUser.java | 2 +- .../authority/user/PrincipalNormalizer.java | 2 +- .../prole/authority/web/JwksController.java | 8 +- .../prole/authority/web/LoginController.java | 24 +++--- .../web/OidcAuthorizeController.java | 10 +-- .../web/OidcDiscoveryController.java | 6 +- .../authority/web/OidcTokenController.java | 35 +++++++-- .../authority/web/OidcUserInfoController.java | 8 +- .../prole/authority/web/VerifyController.java | 8 +- authority/src/main/resources/application.yml | 2 +- .../regression/IdentityRegressionTest.java | 78 +++++++++++++++++++ .../session/SessionTokenServiceTest.java | 2 +- .../authority/web/VerifyControllerTest.java | 6 +- docs/knoe-auth-phase-2.md | 73 +++++++++++++++++ etc/init_knoe_auth.sh | 6 +- 26 files changed, 240 insertions(+), 64 deletions(-) create mode 100644 authority/src/test/java/org/prole/authority/regression/IdentityRegressionTest.java create mode 100644 docs/knoe-auth-phase-2.md diff --git a/authority/pom.xml b/authority/pom.xml index e98636d..ce2cce2 100644 --- a/authority/pom.xml +++ b/authority/pom.xml @@ -10,7 +10,7 @@ - org.knoe + org.prole authority 0.0.1-SNAPSHOT knoe-authority diff --git a/authority/src/main/java/org/prole/authority/HealthController.java b/authority/src/main/java/org/prole/authority/HealthController.java index 9a25058..e6eb896 100644 --- a/authority/src/main/java/org/prole/authority/HealthController.java +++ b/authority/src/main/java/org/prole/authority/HealthController.java @@ -1,4 +1,4 @@ -package org.knoe.authority; +package org.prole.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/org/prole/authority/KnoeAuthApplication.java index 37c88aa..2c764b4 100644 --- a/authority/src/main/java/org/prole/authority/KnoeAuthApplication.java +++ b/authority/src/main/java/org/prole/authority/KnoeAuthApplication.java @@ -1,4 +1,4 @@ -package org.knoe.authority; +package org.prole.authority; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/authority/src/main/java/org/prole/authority/config/AuthProperties.java b/authority/src/main/java/org/prole/authority/config/AuthProperties.java index f849d1f..a55a92a 100644 --- a/authority/src/main/java/org/prole/authority/config/AuthProperties.java +++ b/authority/src/main/java/org/prole/authority/config/AuthProperties.java @@ -1,4 +1,4 @@ -package org.knoe.authority.config; +package org.prole.authority.config; import java.time.Duration; import java.util.ArrayList; @@ -13,7 +13,7 @@ public class AuthProperties { private String cookieDomain = ".knoe.dev"; private Duration sessionTtl = Duration.ofHours(8); private String sessionSecret = ""; - private String emailDomain = "knoe.dev"; + private String emailDomain = "knoey.com"; private boolean formEnabled = false; private List adminPrincipals = new ArrayList<>(); private Oidc oidc = new Oidc(); diff --git a/authority/src/main/java/org/prole/authority/config/KerberosProperties.java b/authority/src/main/java/org/prole/authority/config/KerberosProperties.java index d8d0add..d1b4eae 100644 --- a/authority/src/main/java/org/prole/authority/config/KerberosProperties.java +++ b/authority/src/main/java/org/prole/authority/config/KerberosProperties.java @@ -1,4 +1,4 @@ -package org.knoe.authority.config; +package org.prole.authority.config; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/authority/src/main/java/org/prole/authority/kerberos/KerberosPasswordService.java b/authority/src/main/java/org/prole/authority/kerberos/KerberosPasswordService.java index 60082d3..cb89c15 100644 --- a/authority/src/main/java/org/prole/authority/kerberos/KerberosPasswordService.java +++ b/authority/src/main/java/org/prole/authority/kerberos/KerberosPasswordService.java @@ -1,4 +1,4 @@ -package org.knoe.authority.kerberos; +package org.prole.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/org/prole/authority/kerberos/KerberosSpnegoService.java index 298729d..51f3781 100644 --- a/authority/src/main/java/org/prole/authority/kerberos/KerberosSpnegoService.java +++ b/authority/src/main/java/org/prole/authority/kerberos/KerberosSpnegoService.java @@ -1,4 +1,4 @@ -package org.knoe.authority.kerberos; +package org.prole.authority.kerberos; import java.security.PrivilegedExceptionAction; import java.util.Map; diff --git a/authority/src/main/java/org/prole/authority/session/OidcCodeService.java b/authority/src/main/java/org/prole/authority/session/OidcCodeService.java index 2a524e9..e7c98e8 100644 --- a/authority/src/main/java/org/prole/authority/session/OidcCodeService.java +++ b/authority/src/main/java/org/prole/authority/session/OidcCodeService.java @@ -1,4 +1,4 @@ -package org.knoe.authority.session; +package org.prole.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/org/prole/authority/session/OidcTokenService.java index 40733a8..5c9ae92 100644 --- a/authority/src/main/java/org/prole/authority/session/OidcTokenService.java +++ b/authority/src/main/java/org/prole/authority/session/OidcTokenService.java @@ -1,9 +1,10 @@ -package org.knoe.authority.session; +package org.prole.authority.session; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import org.knoe.authority.config.AuthProperties; +import org.prole.authority.config.AuthProperties; +import org.prole.authority.config.KerberosProperties; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; @@ -25,12 +26,14 @@ import java.util.UUID; public class OidcTokenService { private final AuthProperties authProperties; + private final KerberosProperties kerberosProperties; private final ObjectMapper objectMapper; private KeyPair keyPair; private String kid; - public OidcTokenService(AuthProperties authProperties, ObjectMapper objectMapper) { + public OidcTokenService(AuthProperties authProperties, KerberosProperties kerberosProperties, ObjectMapper objectMapper) { this.authProperties = authProperties; + this.kerberosProperties = kerberosProperties; this.objectMapper = objectMapper; } @@ -83,6 +86,7 @@ public class OidcTokenService { .claim("nonce", nonce) .claim("preferred_username", user.username()) .claim("email", user.email()) + .claim("realm", kerberosProperties.getRealm()) .setHeaderParam("kid", kid) .signWith(keyPair.getPrivate(), SignatureAlgorithm.RS256) .compact(); diff --git a/authority/src/main/java/org/prole/authority/session/SessionService.java b/authority/src/main/java/org/prole/authority/session/SessionService.java index 0e305fc..6997e0e 100644 --- a/authority/src/main/java/org/prole/authority/session/SessionService.java +++ b/authority/src/main/java/org/prole/authority/session/SessionService.java @@ -1,6 +1,6 @@ -package org.knoe.authority.session; +package org.prole.authority.session; -import org.knoe.authority.config.AuthProperties; +import org.prole.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/org/prole/authority/session/SessionTokenService.java index 32a6f11..769a1ad 100644 --- a/authority/src/main/java/org/prole/authority/session/SessionTokenService.java +++ b/authority/src/main/java/org/prole/authority/session/SessionTokenService.java @@ -1,4 +1,4 @@ -package org.knoe.authority.session; +package org.prole.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/org/prole/authority/session/SessionUser.java index 8e48840..d730be7 100644 --- a/authority/src/main/java/org/prole/authority/session/SessionUser.java +++ b/authority/src/main/java/org/prole/authority/session/SessionUser.java @@ -1,4 +1,4 @@ -package org.knoe.authority.session; +package org.prole.authority.session; import java.util.List; diff --git a/authority/src/main/java/org/prole/authority/user/PrincipalNormalizer.java b/authority/src/main/java/org/prole/authority/user/PrincipalNormalizer.java index 693b7ab..5f87587 100644 --- a/authority/src/main/java/org/prole/authority/user/PrincipalNormalizer.java +++ b/authority/src/main/java/org/prole/authority/user/PrincipalNormalizer.java @@ -1,4 +1,4 @@ -package org.knoe.authority.user; +package org.prole.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/org/prole/authority/web/JwksController.java index 5b8efb0..8bba4b9 100644 --- a/authority/src/main/java/org/prole/authority/web/JwksController.java +++ b/authority/src/main/java/org/prole/authority/web/JwksController.java @@ -1,7 +1,7 @@ -package org.knoe.authority.web; +package org.prole.authority.web; -import org.knoe.authority.config.AuthProperties; -import org.knoe.authority.session.OidcTokenService; +import org.prole.authority.config.AuthProperties; +import org.prole.authority.session.OidcTokenService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -19,7 +19,7 @@ public class JwksController { this.oidcTokenService = oidcTokenService; } - @GetMapping("/jwks") + @GetMapping("/jwks.json") public ResponseEntity> getJwks() { if (!authProperties.getOidc().isEnabled()) { return ResponseEntity.notFound().build(); diff --git a/authority/src/main/java/org/prole/authority/web/LoginController.java b/authority/src/main/java/org/prole/authority/web/LoginController.java index 800b67b..755de0f 100644 --- a/authority/src/main/java/org/prole/authority/web/LoginController.java +++ b/authority/src/main/java/org/prole/authority/web/LoginController.java @@ -1,4 +1,4 @@ -package org.knoe.authority.web; +package org.prole.authority.web; import java.net.URI; import java.util.ArrayList; @@ -10,13 +10,14 @@ import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.knoe.authority.config.AuthProperties; -import org.knoe.authority.config.KerberosProperties; -import org.knoe.authority.kerberos.KerberosPasswordService; -import org.knoe.authority.kerberos.KerberosSpnegoService; -import org.knoe.authority.session.SessionTokenService; -import org.knoe.authority.session.SessionUser; -import org.knoe.authority.user.PrincipalNormalizer; +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 org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -37,7 +38,7 @@ public class LoginController { private final KerberosPasswordService passwordAuth; private final PrincipalNormalizer normalizer; private final SessionTokenService sessionTokenService; - private final org.prole.authority.enroll.GoogleOAuthService googleOAuth; + private final GoogleOAuthService googleOAuth; public LoginController( AuthProperties auth, @@ -46,7 +47,7 @@ public class LoginController { KerberosPasswordService passwordAuth, PrincipalNormalizer normalizer, SessionTokenService sessionTokenService, - org.prole.authority.enroll.GoogleOAuthService googleOAuth + GoogleOAuthService googleOAuth ) { this.auth = auth; this.kerberos = kerberos; @@ -195,7 +196,8 @@ public class LoginController { var googleId = googleOAuth.exchangeCode(code, redirectUri); String email = googleId.email(); - String username = email.split("@")[0]; + String username = normalizer.normalizeUsernameFromKerberosPrincipal(email) + .orElseThrow(() -> new IllegalStateException("Failed to normalize Google email")); SessionUser user = new SessionUser(username, email, resolveGroups(username)); setSessionCookie(response, user); diff --git a/authority/src/main/java/org/prole/authority/web/OidcAuthorizeController.java b/authority/src/main/java/org/prole/authority/web/OidcAuthorizeController.java index a89f004..7408535 100644 --- a/authority/src/main/java/org/prole/authority/web/OidcAuthorizeController.java +++ b/authority/src/main/java/org/prole/authority/web/OidcAuthorizeController.java @@ -1,9 +1,9 @@ -package org.knoe.authority.web; +package org.prole.authority.web; -import org.knoe.authority.config.AuthProperties; -import org.knoe.authority.session.OidcCodeService; -import org.knoe.authority.session.SessionService; -import org.knoe.authority.session.SessionUser; +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 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/org/prole/authority/web/OidcDiscoveryController.java index ceb28c6..6f9b8a3 100644 --- a/authority/src/main/java/org/prole/authority/web/OidcDiscoveryController.java +++ b/authority/src/main/java/org/prole/authority/web/OidcDiscoveryController.java @@ -1,6 +1,6 @@ -package org.knoe.authority.web; +package org.prole.authority.web; -import org.knoe.authority.config.AuthProperties; +import org.prole.authority.config.AuthProperties; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -29,7 +29,7 @@ public class OidcDiscoveryController { Map.entry("authorization_endpoint", issuer + "/authorize"), Map.entry("token_endpoint", issuer + "/token"), Map.entry("userinfo_endpoint", issuer + "/userinfo"), - Map.entry("jwks_uri", issuer + "/jwks"), + Map.entry("jwks_uri", issuer + "/jwks.json"), Map.entry("response_types_supported", List.of("code")), Map.entry("subject_types_supported", List.of("public")), Map.entry("id_token_signing_alg_values_supported", List.of("RS256")), diff --git a/authority/src/main/java/org/prole/authority/web/OidcTokenController.java b/authority/src/main/java/org/prole/authority/web/OidcTokenController.java index 6f7cf84..e9a3ada 100644 --- a/authority/src/main/java/org/prole/authority/web/OidcTokenController.java +++ b/authority/src/main/java/org/prole/authority/web/OidcTokenController.java @@ -1,13 +1,14 @@ -package org.knoe.authority.web; +package org.prole.authority.web; -import org.knoe.authority.config.AuthProperties; -import org.knoe.authority.session.OidcCodeService; -import org.knoe.authority.session.OidcTokenService; +import org.prole.authority.config.AuthProperties; +import org.prole.authority.session.OidcCodeService; +import org.prole.authority.session.OidcTokenService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import jakarta.servlet.http.HttpServletRequest; import java.util.Map; @RestController @@ -28,8 +29,9 @@ public class OidcTokenController { @RequestParam("grant_type") String grantType, @RequestParam("code") String code, @RequestParam("redirect_uri") String redirectUri, - @RequestParam("client_id") String clientId, - @RequestParam("client_secret") String clientSecret) { + @RequestParam(value = "client_id", required = false) String clientId, + @RequestParam(value = "client_secret", required = false) String clientSecret, + HttpServletRequest request) { if (!authProperties.getOidc().isEnabled()) { return ResponseEntity.status(403).body(Map.of("error", "oidc_disabled")); @@ -39,8 +41,25 @@ public class OidcTokenController { return ResponseEntity.badRequest().body(Map.of("error", "unsupported_grant_type")); } - if (!clientId.equals(authProperties.getOidc().getClientId()) || - !clientSecret.equals(authProperties.getOidc().getClientSecret())) { + String effectiveClientId = clientId; + String effectiveClientSecret = clientSecret; + + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Basic ")) { + try { + String decoded = new String(java.util.Base64.getDecoder().decode(authHeader.substring(6))); + String[] parts = decoded.split(":", 2); + if (parts.length == 2) { + effectiveClientId = parts[0]; + effectiveClientSecret = parts[1]; + } + } catch (Exception e) { + // Ignore invalid basic auth + } + } + + if (effectiveClientId == null || !effectiveClientId.equals(authProperties.getOidc().getClientId()) || + effectiveClientSecret == null || !effectiveClientSecret.equals(authProperties.getOidc().getClientSecret())) { return ResponseEntity.status(401).body(Map.of("error", "invalid_client")); } diff --git a/authority/src/main/java/org/prole/authority/web/OidcUserInfoController.java b/authority/src/main/java/org/prole/authority/web/OidcUserInfoController.java index 881fd3f..e940423 100644 --- a/authority/src/main/java/org/prole/authority/web/OidcUserInfoController.java +++ b/authority/src/main/java/org/prole/authority/web/OidcUserInfoController.java @@ -1,8 +1,8 @@ -package org.knoe.authority.web; +package org.prole.authority.web; -import org.knoe.authority.config.AuthProperties; -import org.knoe.authority.session.SessionService; -import org.knoe.authority.session.SessionUser; +import org.prole.authority.config.AuthProperties; +import org.prole.authority.session.SessionService; +import org.prole.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/org/prole/authority/web/VerifyController.java index 8ba6662..af7b815 100644 --- a/authority/src/main/java/org/prole/authority/web/VerifyController.java +++ b/authority/src/main/java/org/prole/authority/web/VerifyController.java @@ -1,13 +1,13 @@ -package org.knoe.authority.web; +package org.prole.authority.web; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.Optional; -import org.knoe.authority.config.AuthProperties; -import org.knoe.authority.session.SessionTokenService; -import org.knoe.authority.session.SessionUser; +import org.prole.authority.config.AuthProperties; +import org.prole.authority.session.SessionTokenService; +import org.prole.authority.session.SessionUser; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/authority/src/main/resources/application.yml b/authority/src/main/resources/application.yml index ce595a0..c654a41 100644 --- a/authority/src/main/resources/application.yml +++ b/authority/src/main/resources/application.yml @@ -15,7 +15,7 @@ knoe: sessionTtl: 8h # REQUIRED in production when enabled. Provide via env: KNOE_AUTH_SESSION_SECRET sessionSecret: "" - emailDomain: knoe.dev + emailDomain: knoey.com formEnabled: false # Comma-separated list of bare usernames granted admin group membership. # Override via env: KNOE_AUTH_ADMIN_PRINCIPALS=admin diff --git a/authority/src/test/java/org/prole/authority/regression/IdentityRegressionTest.java b/authority/src/test/java/org/prole/authority/regression/IdentityRegressionTest.java new file mode 100644 index 0000000..3edfd29 --- /dev/null +++ b/authority/src/test/java/org/prole/authority/regression/IdentityRegressionTest.java @@ -0,0 +1,78 @@ +package org.prole.authority.regression; + +import org.junit.jupiter.api.Test; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Regression tests to ensure we don't re-introduce org.knoe packages + * or @knoe.dev user emails in Java source. + */ +class IdentityRegressionTest { + + @Test + void noOrgKnoeInSource() throws IOException { + Path srcDir = Path.of("src/main/java"); + if (!Files.exists(srcDir)) { + // Fallback for different execution contexts + srcDir = Path.of("authority/src/main/java"); + } + + try (Stream paths = Files.walk(srcDir)) { + paths.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".java")) + .forEach(this::checkFileForOrgKnoe); + } + } + + @Test + void noKnoeDevUserEmailsInSource() throws IOException { + Path srcDir = Path.of("src/main/java"); + if (!Files.exists(srcDir)) { + srcDir = Path.of("authority/src/main/java"); + } + + try (Stream paths = Files.walk(srcDir)) { + paths.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".java")) + .forEach(this::checkFileForKnoeDevEmails); + } + } + + private void checkFileForOrgKnoe(Path path) { + try { + String content = Files.readString(path); + assertFalse(content.contains("package org.knoe"), + "File " + path + " contains forbidden package org.knoe"); + assertFalse(content.contains("import org.knoe"), + "File " + path + " contains forbidden import org.knoe"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void checkFileForKnoeDevEmails(Path path) { + // Skip files that are allowed to have knoe.dev (like config or issuer logic) + String fileName = path.getFileName().toString(); + if (fileName.equals("AuthProperties.java") || + fileName.equals("OidcDiscoveryController.java") || + fileName.equals("OidcTokenService.java")) { + return; + } + + try { + String content = Files.readString(path); + // Check for user-like emails @knoe.dev + // We allow https://api.knoe.dev and Kerberos realm @KNOE.DEV + assertFalse(content.matches("(?s).*\"[a-zA-Z0-9._%+-]+@knoe\\.dev\".*"), + "File " + path + " contains forbidden user email @knoe.dev"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/authority/src/test/java/org/prole/authority/session/SessionTokenServiceTest.java b/authority/src/test/java/org/prole/authority/session/SessionTokenServiceTest.java index 288bdee..a9d565b 100644 --- a/authority/src/test/java/org/prole/authority/session/SessionTokenServiceTest.java +++ b/authority/src/test/java/org/prole/authority/session/SessionTokenServiceTest.java @@ -1,4 +1,4 @@ -package org.knoe.authority.session; +package org.prole.authority.session; import com.fasterxml.jackson.databind.ObjectMapper; import java.time.Clock; diff --git a/authority/src/test/java/org/prole/authority/web/VerifyControllerTest.java b/authority/src/test/java/org/prole/authority/web/VerifyControllerTest.java index a1727bd..3639680 100644 --- a/authority/src/test/java/org/prole/authority/web/VerifyControllerTest.java +++ b/authority/src/test/java/org/prole/authority/web/VerifyControllerTest.java @@ -1,9 +1,9 @@ -package org.knoe.authority.web; +package org.prole.authority.web; import java.time.Duration; import org.junit.jupiter.api.Test; -import org.knoe.authority.session.SessionTokenService; -import org.knoe.authority.session.SessionUser; +import org.prole.authority.session.SessionTokenService; +import org.prole.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 new file mode 100644 index 0000000..3faf189 --- /dev/null +++ b/docs/knoe-auth-phase-2.md @@ -0,0 +1,73 @@ +# Phase 2: knoe-auth as OIDC Provider + +## Architecture +knoe-auth (the Authority) acts as the central Identity Authority for the cluster. + +### OIDC Provider (OP) +- Issuer: `https://api.knoe.dev/auth` +- Endpoints: + - `GET /.well-known/openid-configuration`: Discovery + - `GET /jwks.json`: Public keys for token verification + - `GET /authorize`: Authorization endpoint (supports `response_type=code`) + - `POST /token`: Token exchange endpoint + +### Upstream Identity (Google Workspace) +- knoe-auth acts as an OAuth client to Google. +- User flow: Service → knoe-auth → Google → knoe-auth → Service. +- Identity is mapped from Google `email` to internal `canonical user id`. + +### Kerberos/SPNEGO Integration +- knoe-auth preserves Kerberos flows. +- If a user has a valid SPNEGO session, they can be transparently logged into the OIDC flow. +- Form login (Kerberos password) is available as a fallback. + +## Identity Model +- **Canonical User ID**: Lowercase, stable identifier (e.g., `jdoe`). +- **Email**: User's Google Workspace email (e.g., `jdoe@knoey.com`). +- **Principal**: Kerberos principal (e.g., `jdoe@KNOE.DEV`). +- **Normalization**: `PrincipalNormalizer` ensures consistency across all auth methods. + +## Namespace & Domains +- **Java Package**: `org.prole.authority.*` +- **Service DNS**: `knoe.dev` (e.g., `api.knoe.dev`, `git.knoe.dev`) +- **OIDC Issuer**: `https://api.knoe.dev/auth` +- **Identity Domain**: `knoey.com` +- **Kerberos Realm**: `KNOE.DEV` + +## Token Model +### ID Token (JWT) +- Signed using RS256. +- Claims: + - `iss`: `https://api.knoe.dev/auth` + - `sub`: Canonical user ID + - `email`: User's email + - `preferred_username`: Canonical user ID + - `aud`: Client ID + - `exp`, `iat`, `nonce` + +### Access Token +- Currently minimal (opaque or static) as the primary focus is identity (OIDC). + +## Session Model +- Stateless where possible. +- Short-lived browser session via secure, HttpOnly, SameSite=Lax cookies. +- Authorization codes are short-lived and one-time use. + +## Implementation Details +- Stack: Java / Spring Boot (Lightweight). +- Signing: RS256 with key rotation support (via multiple keys in JWKS). +- Configurable via `application.yml` and environment variables. + +## Deployment & Configuration +- `KNOE_AUTH_OIDC_ENABLED`: Enable OIDC surface. +- `KNOE_AUTH_OIDC_ISSUER`: Issuer URL. +- `KNOE_AUTH_OIDC_SIGNING_KEY`: Base64 encoded private key (PKCS#8). +- `KNOE_AUTH_GOOGLE_CLIENT_ID`: Upstream Google client ID. +- `KNOE_AUTH_GOOGLE_CLIENT_SECRET`: Upstream Google client secret. + +## GitLab Integration (Path B) +To switch GitLab to use knoe-auth as OIDC provider: +1. Update GitLab `omniauth` configuration. +2. Change `issuer` to `https://api.knoe.dev/auth`. +3. Update `client_id` and `client_secret` to match knoe-auth config. +4. Verify flow: GitLab → knoe-auth → Google → knoe-auth → GitLab. diff --git a/etc/init_knoe_auth.sh b/etc/init_knoe_auth.sh index 8ae36b2..a9d5d70 100755 --- a/etc/init_knoe_auth.sh +++ b/etc/init_knoe_auth.sh @@ -211,8 +211,8 @@ CREATE INDEX IF NOT EXISTS idx_provisioning_job_status -- Seed well-known knobjects INSERT INTO knoe.knobject (type, name, metadata) VALUES - ('gitea_org', 'knoe.dev', '{"description": "Knoe.DEV Gitea organisation"}'), - ('gitlab_group','knoe.dev', '{"description": "Knoe.DEV GitLab group"}') + ('gitea_org', 'knoey.com', '{"description": "Knoey.com Gitea organisation"}'), + ('gitlab_group','knoey.com', '{"description": "Knoey.com GitLab group"}') ON CONFLICT (type, name) DO NOTHING; SELECT 'knoe-auth schema v1 applied.' AS status; @@ -377,7 +377,7 @@ cmd_initialize() { info "Enrollment URL: ${AUTH_HOST}/auth/enroll?token=" info "" info "Next: create first admin invite:" - info " $0 invite chrisfu@prole.org" + info " $0 invite chrisfu@knoey.com" info "" show_status }