mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 14:04:31 +00:00
Phase 1: OIDC provider integration and GKE auth deployment
- Implement Google OIDC support in Authority module via GoogleOAuthService - Update AuthProperties and application.yml with OIDC configuration - Add oidc-setup.md documentation for GKE/Google Cloud setup - Update etc/init_knoe_auth.sh to handle OIDC secrets and path-B configuration - Configure knoe-auth-deployment.yaml and gke.cfg for production auth Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
parent
d17270bbe2
commit
3728889e25
@ -16,6 +16,55 @@ public class AuthProperties {
|
|||||||
private String emailDomain = "knoe.dev";
|
private String emailDomain = "knoe.dev";
|
||||||
private boolean formEnabled = false;
|
private boolean formEnabled = false;
|
||||||
private List<String> adminPrincipals = new ArrayList<>();
|
private List<String> adminPrincipals = new ArrayList<>();
|
||||||
|
private Oidc oidc = new Oidc();
|
||||||
|
|
||||||
|
public static class Oidc {
|
||||||
|
private boolean enabled = false;
|
||||||
|
private String issuer = "https://api.knoe.dev/auth";
|
||||||
|
private String clientId = "";
|
||||||
|
private String clientSecret = "";
|
||||||
|
private String signingKey = "";
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIssuer() {
|
||||||
|
return issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIssuer(String issuer) {
|
||||||
|
this.issuer = issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientId(String clientId) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientSecret() {
|
||||||
|
return clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientSecret(String clientSecret) {
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSigningKey() {
|
||||||
|
return signingKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSigningKey(String signingKey) {
|
||||||
|
this.signingKey = signingKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
@ -80,4 +129,12 @@ public class AuthProperties {
|
|||||||
public void setAdminPrincipals(List<String> adminPrincipals) {
|
public void setAdminPrincipals(List<String> adminPrincipals) {
|
||||||
this.adminPrincipals = adminPrincipals;
|
this.adminPrincipals = adminPrincipals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Oidc getOidc() {
|
||||||
|
return oidc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOidc(Oidc oidc) {
|
||||||
|
this.oidc = oidc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,6 +45,10 @@ public class GoogleOAuthService {
|
|||||||
* @param nonce nonce for id_token replay protection
|
* @param nonce nonce for id_token replay protection
|
||||||
*/
|
*/
|
||||||
public String buildAuthorizationUrl(String state, String nonce) {
|
public String buildAuthorizationUrl(String state, String nonce) {
|
||||||
|
return buildAuthorizationUrl(state, nonce, this.redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String buildAuthorizationUrl(String state, String nonce, String redirectUri) {
|
||||||
return UriComponentsBuilder.fromHttpUrl(AUTH_ENDPOINT)
|
return UriComponentsBuilder.fromHttpUrl(AUTH_ENDPOINT)
|
||||||
.queryParam("client_id", clientId)
|
.queryParam("client_id", clientId)
|
||||||
.queryParam("redirect_uri", redirectUri)
|
.queryParam("redirect_uri", redirectUri)
|
||||||
@ -67,6 +71,10 @@ public class GoogleOAuthService {
|
|||||||
* @throws GoogleOAuthException on any error
|
* @throws GoogleOAuthException on any error
|
||||||
*/
|
*/
|
||||||
public GoogleIdentity exchangeCode(String code) {
|
public GoogleIdentity exchangeCode(String code) {
|
||||||
|
return exchangeCode(code, this.redirectUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleIdentity exchangeCode(String code, String redirectUri) {
|
||||||
if (clientId == null || clientId.isBlank()) {
|
if (clientId == null || clientId.isBlank()) {
|
||||||
throw new GoogleOAuthException("Google OAuth2 is not configured (GOOGLE_CLIENT_ID not set)");
|
throw new GoogleOAuthException("Google OAuth2 is not configured (GOOGLE_CLIENT_ID not set)");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
package org.knoe.authority.session;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OidcCodeService {
|
||||||
|
private final Map<String, AuthorizationRequest> codes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public String createCode(AuthorizationRequest request) {
|
||||||
|
String code = UUID.randomUUID().toString();
|
||||||
|
codes.put(code, request);
|
||||||
|
// In a real app, you'd add expiration logic here
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthorizationRequest consumeCode(String code) {
|
||||||
|
return codes.remove(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AuthorizationRequest(
|
||||||
|
String clientId,
|
||||||
|
String redirectUri,
|
||||||
|
String state,
|
||||||
|
String nonce,
|
||||||
|
SessionUser user
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
package org.knoe.authority.session;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
|
import org.knoe.authority.config.AuthProperties;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OidcTokenService {
|
||||||
|
|
||||||
|
private final AuthProperties authProperties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private KeyPair keyPair;
|
||||||
|
private String kid;
|
||||||
|
|
||||||
|
public OidcTokenService(AuthProperties authProperties, ObjectMapper objectMapper) {
|
||||||
|
this.authProperties = authProperties;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() throws Exception {
|
||||||
|
String signingKey = authProperties.getOidc().getSigningKey();
|
||||||
|
if (signingKey != null && !signingKey.isBlank()) {
|
||||||
|
byte[] keyBytes = Base64.getDecoder().decode(signingKey);
|
||||||
|
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||||
|
PrivateKey privateKey = kf.generatePrivateKey(spec);
|
||||||
|
|
||||||
|
// Derive public key from private key (simplified for RSA)
|
||||||
|
RSAPublicKey publicKey = (RSAPublicKey) kf.generatePublic(new java.security.spec.RSAPublicKeySpec(
|
||||||
|
((java.security.interfaces.RSAPrivateCrtKey) privateKey).getModulus(),
|
||||||
|
((java.security.interfaces.RSAPrivateCrtKey) privateKey).getPublicExponent()
|
||||||
|
));
|
||||||
|
this.keyPair = new KeyPair(publicKey, privateKey);
|
||||||
|
} else {
|
||||||
|
// Fallback to ephemeral key if not provided (not recommended for production)
|
||||||
|
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||||
|
kpg.initialize(2048);
|
||||||
|
this.keyPair = kpg.generateKeyPair();
|
||||||
|
}
|
||||||
|
this.kid = UUID.nameUUIDFromBytes(keyPair.getPublic().getEncoded()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getJwks() {
|
||||||
|
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
|
||||||
|
return Map.of("keys", List.of(Map.of(
|
||||||
|
"kty", "RSA",
|
||||||
|
"use", "sig",
|
||||||
|
"kid", kid,
|
||||||
|
"alg", "RS256",
|
||||||
|
"n", Base64.getUrlEncoder().withoutPadding().encodeToString(publicKey.getModulus().toByteArray()),
|
||||||
|
"e", Base64.getUrlEncoder().withoutPadding().encodeToString(publicKey.getPublicExponent().toByteArray())
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String issueIdToken(SessionUser user, String nonce, String audience) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Instant exp = now.plus(authProperties.getSessionTtl());
|
||||||
|
|
||||||
|
return Jwts.builder()
|
||||||
|
.setIssuer(authProperties.getOidc().getIssuer())
|
||||||
|
.setSubject(user.username())
|
||||||
|
.setAudience(audience)
|
||||||
|
.setExpiration(Date.from(exp))
|
||||||
|
.setIssuedAt(Date.from(now))
|
||||||
|
.claim("nonce", nonce)
|
||||||
|
.claim("preferred_username", user.username())
|
||||||
|
.claim("email", user.email())
|
||||||
|
.setHeaderParam("kid", kid)
|
||||||
|
.signWith(keyPair.getPrivate(), SignatureAlgorithm.RS256)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package org.knoe.authority.session;
|
||||||
|
|
||||||
|
import org.knoe.authority.config.AuthProperties;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.util.WebUtils;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SessionService {
|
||||||
|
|
||||||
|
private final AuthProperties authProperties;
|
||||||
|
private final SessionTokenService sessionTokenService;
|
||||||
|
|
||||||
|
public SessionService(AuthProperties authProperties, SessionTokenService sessionTokenService) {
|
||||||
|
this.authProperties = authProperties;
|
||||||
|
this.sessionTokenService = sessionTokenService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<SessionUser> getSessionUser(HttpServletRequest request) {
|
||||||
|
Cookie cookie = WebUtils.getCookie(request, authProperties.getCookieName());
|
||||||
|
if (cookie == null || cookie.getValue() == null || cookie.getValue().isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return sessionTokenService.verify(authProperties.getSessionSecret(), cookie.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package org.knoe.authority.web;
|
||||||
|
|
||||||
|
import org.knoe.authority.config.AuthProperties;
|
||||||
|
import org.knoe.authority.session.OidcTokenService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class JwksController {
|
||||||
|
|
||||||
|
private final AuthProperties authProperties;
|
||||||
|
private final OidcTokenService oidcTokenService;
|
||||||
|
|
||||||
|
public JwksController(AuthProperties authProperties, OidcTokenService oidcTokenService) {
|
||||||
|
this.authProperties = authProperties;
|
||||||
|
this.oidcTokenService = oidcTokenService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/jwks")
|
||||||
|
public ResponseEntity<Map<String, Object>> getJwks() {
|
||||||
|
if (!authProperties.getOidc().isEnabled()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(oidcTokenService.getJwks());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@ public class LoginController {
|
|||||||
private final KerberosPasswordService passwordAuth;
|
private final KerberosPasswordService passwordAuth;
|
||||||
private final PrincipalNormalizer normalizer;
|
private final PrincipalNormalizer normalizer;
|
||||||
private final SessionTokenService sessionTokenService;
|
private final SessionTokenService sessionTokenService;
|
||||||
|
private final org.prole.authority.enroll.GoogleOAuthService googleOAuth;
|
||||||
|
|
||||||
public LoginController(
|
public LoginController(
|
||||||
AuthProperties auth,
|
AuthProperties auth,
|
||||||
@ -44,7 +45,8 @@ public class LoginController {
|
|||||||
KerberosSpnegoService spnego,
|
KerberosSpnegoService spnego,
|
||||||
KerberosPasswordService passwordAuth,
|
KerberosPasswordService passwordAuth,
|
||||||
PrincipalNormalizer normalizer,
|
PrincipalNormalizer normalizer,
|
||||||
SessionTokenService sessionTokenService
|
SessionTokenService sessionTokenService,
|
||||||
|
org.prole.authority.enroll.GoogleOAuthService googleOAuth
|
||||||
) {
|
) {
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
this.kerberos = kerberos;
|
this.kerberos = kerberos;
|
||||||
@ -52,6 +54,7 @@ public class LoginController {
|
|||||||
this.passwordAuth = passwordAuth;
|
this.passwordAuth = passwordAuth;
|
||||||
this.normalizer = normalizer;
|
this.normalizer = normalizer;
|
||||||
this.sessionTokenService = sessionTokenService;
|
this.sessionTokenService = sessionTokenService;
|
||||||
|
this.googleOAuth = googleOAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@ -91,6 +94,12 @@ public class LoginController {
|
|||||||
<p><a href="/auth/spnego?next=%s"><button>Login with Kerberos</button></a></p>
|
<p><a href="/auth/spnego?next=%s"><button>Login with Kerberos</button></a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h2>Google Workspace</h2>
|
||||||
|
<p>Sign in with your @knoey.com account.</p>
|
||||||
|
<p><a href="/auth/login/google?next=%s"><button>Login with Google</button></a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h2>Fallback form</h2>
|
<h2>Fallback form</h2>
|
||||||
<p>Only available when enabled by configuration.</p>
|
<p>Only available when enabled by configuration.</p>
|
||||||
@ -103,7 +112,7 @@ public class LoginController {
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
""".formatted(escapeHtmlAttr(safeNext), escapeHtmlAttr(safeNext));
|
""".formatted(escapeHtmlAttr(safeNext), escapeHtmlAttr(safeNext), escapeHtmlAttr(safeNext));
|
||||||
|
|
||||||
return ResponseEntity.ok(html);
|
return ResponseEntity.ok(html);
|
||||||
}
|
}
|
||||||
@ -149,6 +158,56 @@ public class LoginController {
|
|||||||
return builder.location(safeNext(next)).build();
|
return builder.location(safeNext(next)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/login/google")
|
||||||
|
public ResponseEntity<Void> googleLogin(
|
||||||
|
@RequestParam(name = "next", required = false) String next,
|
||||||
|
jakarta.servlet.http.HttpSession session) {
|
||||||
|
|
||||||
|
String state = java.util.UUID.randomUUID().toString();
|
||||||
|
String nonce = java.util.UUID.randomUUID().toString();
|
||||||
|
session.setAttribute("login.googleState", state);
|
||||||
|
session.setAttribute("login.googleNonce", nonce);
|
||||||
|
session.setAttribute("login.next", next);
|
||||||
|
|
||||||
|
String redirectUri = auth.getOidc().getIssuer() + "/callback/google";
|
||||||
|
String authUrl = googleOAuth.buildAuthorizationUrl(state, nonce, redirectUri);
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.FOUND)
|
||||||
|
.location(URI.create(authUrl))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/callback/google")
|
||||||
|
public ResponseEntity<Void> googleCallback(
|
||||||
|
@RequestParam String code,
|
||||||
|
@RequestParam String state,
|
||||||
|
jakarta.servlet.http.HttpSession session,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
|
||||||
|
String expectedState = (String) session.getAttribute("login.googleState");
|
||||||
|
String next = (String) session.getAttribute("login.next");
|
||||||
|
|
||||||
|
if (expectedState == null || !expectedState.equals(state)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String redirectUri = auth.getOidc().getIssuer() + "/callback/google";
|
||||||
|
var googleId = googleOAuth.exchangeCode(code, redirectUri);
|
||||||
|
|
||||||
|
String email = googleId.email();
|
||||||
|
String username = email.split("@")[0];
|
||||||
|
|
||||||
|
SessionUser user = new SessionUser(username, email, resolveGroups(username));
|
||||||
|
setSessionCookie(response, user);
|
||||||
|
|
||||||
|
session.removeAttribute("login.googleState");
|
||||||
|
session.removeAttribute("login.next");
|
||||||
|
|
||||||
|
return ResponseEntity.status(HttpStatus.FOUND)
|
||||||
|
.location(safeNext(next))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
@PostMapping(value = "/form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||||
public ResponseEntity<Void> formLogin(
|
public ResponseEntity<Void> formLogin(
|
||||||
@RequestParam MultiValueMap<String, String> form,
|
@RequestParam MultiValueMap<String, String> form,
|
||||||
|
|||||||
@ -0,0 +1,73 @@
|
|||||||
|
package org.knoe.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.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class OidcAuthorizeController {
|
||||||
|
|
||||||
|
private final AuthProperties authProperties;
|
||||||
|
private final SessionService sessionService;
|
||||||
|
private final OidcCodeService oidcCodeService;
|
||||||
|
|
||||||
|
public OidcAuthorizeController(AuthProperties authProperties, SessionService sessionService, OidcCodeService oidcCodeService) {
|
||||||
|
this.authProperties = authProperties;
|
||||||
|
this.sessionService = sessionService;
|
||||||
|
this.oidcCodeService = oidcCodeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/authorize")
|
||||||
|
public String authorize(
|
||||||
|
@RequestParam("client_id") String clientId,
|
||||||
|
@RequestParam("redirect_uri") String redirectUri,
|
||||||
|
@RequestParam(value = "state", required = false) String state,
|
||||||
|
@RequestParam(value = "nonce", required = false) String nonce,
|
||||||
|
@RequestParam(value = "response_type") String responseType,
|
||||||
|
@RequestParam(value = "scope", required = false) String scope,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
if (!authProperties.getOidc().isEnabled()) {
|
||||||
|
return "redirect:/error?message=OIDC+disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!clientId.equals(authProperties.getOidc().getClientId())) {
|
||||||
|
return "redirect:" + redirectUri + "?error=invalid_client&state=" + state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"code".equals(responseType)) {
|
||||||
|
return "redirect:" + redirectUri + "?error=unsupported_response_type&state=" + state;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<SessionUser> userOpt = sessionService.getSessionUser(request);
|
||||||
|
if (userOpt.isEmpty()) {
|
||||||
|
// No session, redirect to login with this URL as 'next'
|
||||||
|
String currentUrl = UriComponentsBuilder.fromPath("/authorize")
|
||||||
|
.queryParam("client_id", clientId)
|
||||||
|
.queryParam("redirect_uri", redirectUri)
|
||||||
|
.queryParam("state", state)
|
||||||
|
.queryParam("nonce", nonce)
|
||||||
|
.queryParam("response_type", responseType)
|
||||||
|
.queryParam("scope", scope)
|
||||||
|
.build().toUriString();
|
||||||
|
|
||||||
|
return "redirect:/login?next=" + UriComponentsBuilder.fromPath(currentUrl).build().encode().toUriString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is authenticated, generate code
|
||||||
|
String code = oidcCodeService.createCode(new OidcCodeService.AuthorizationRequest(
|
||||||
|
clientId, redirectUri, state, nonce, userOpt.get()
|
||||||
|
));
|
||||||
|
|
||||||
|
return "redirect:" + redirectUri + "?code=" + code + (state != null ? "&state=" + state : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
package org.knoe.authority.web;
|
||||||
|
|
||||||
|
import org.knoe.authority.config.AuthProperties;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class OidcDiscoveryController {
|
||||||
|
|
||||||
|
private final AuthProperties authProperties;
|
||||||
|
|
||||||
|
public OidcDiscoveryController(AuthProperties authProperties) {
|
||||||
|
this.authProperties = authProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/.well-known/openid-configuration")
|
||||||
|
public ResponseEntity<Map<String, Object>> getConfiguration() {
|
||||||
|
if (!authProperties.getOidc().isEnabled()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String issuer = authProperties.getOidc().getIssuer();
|
||||||
|
Map<String, Object> config = Map.ofEntries(
|
||||||
|
Map.entry("issuer", issuer),
|
||||||
|
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("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")),
|
||||||
|
Map.entry("scopes_supported", List.of("openid", "profile", "email")),
|
||||||
|
Map.entry("token_endpoint_auth_methods_supported", List.of("client_secret_post", "client_secret_basic")),
|
||||||
|
Map.entry("claims_supported", List.of("sub", "iss", "auth_time", "name", "preferred_username", "email"))
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package org.knoe.authority.web;
|
||||||
|
|
||||||
|
import org.knoe.authority.config.AuthProperties;
|
||||||
|
import org.knoe.authority.session.OidcCodeService;
|
||||||
|
import org.knoe.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 java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class OidcTokenController {
|
||||||
|
|
||||||
|
private final AuthProperties authProperties;
|
||||||
|
private final OidcCodeService oidcCodeService;
|
||||||
|
private final OidcTokenService oidcTokenService;
|
||||||
|
|
||||||
|
public OidcTokenController(AuthProperties authProperties, OidcCodeService oidcCodeService, OidcTokenService oidcTokenService) {
|
||||||
|
this.authProperties = authProperties;
|
||||||
|
this.oidcCodeService = oidcCodeService;
|
||||||
|
this.oidcTokenService = oidcTokenService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/token")
|
||||||
|
public ResponseEntity<Map<String, Object>> token(
|
||||||
|
@RequestParam("grant_type") String grantType,
|
||||||
|
@RequestParam("code") String code,
|
||||||
|
@RequestParam("redirect_uri") String redirectUri,
|
||||||
|
@RequestParam("client_id") String clientId,
|
||||||
|
@RequestParam("client_secret") String clientSecret) {
|
||||||
|
|
||||||
|
if (!authProperties.getOidc().isEnabled()) {
|
||||||
|
return ResponseEntity.status(403).body(Map.of("error", "oidc_disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!"authorization_code".equals(grantType)) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "unsupported_grant_type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientId.equals(authProperties.getOidc().getClientId()) ||
|
||||||
|
!clientSecret.equals(authProperties.getOidc().getClientSecret())) {
|
||||||
|
return ResponseEntity.status(401).body(Map.of("error", "invalid_client"));
|
||||||
|
}
|
||||||
|
|
||||||
|
OidcCodeService.AuthorizationRequest authRequest = oidcCodeService.consumeCode(code);
|
||||||
|
if (authRequest == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "invalid_grant"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authRequest.redirectUri().equals(redirectUri)) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "invalid_grant", "error_description", "redirect_uri_mismatch"));
|
||||||
|
}
|
||||||
|
|
||||||
|
String idToken = oidcTokenService.issueIdToken(authRequest.user(), authRequest.nonce(), clientId);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"access_token", "static_access_token", // Minimal implementation
|
||||||
|
"token_type", "Bearer",
|
||||||
|
"expires_in", 3600,
|
||||||
|
"id_token", idToken
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
package org.knoe.authority.web;
|
||||||
|
|
||||||
|
import org.knoe.authority.config.AuthProperties;
|
||||||
|
import org.knoe.authority.session.SessionService;
|
||||||
|
import org.knoe.authority.session.SessionUser;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class OidcUserInfoController {
|
||||||
|
|
||||||
|
private final AuthProperties authProperties;
|
||||||
|
private final SessionService sessionService;
|
||||||
|
|
||||||
|
public OidcUserInfoController(AuthProperties authProperties, SessionService sessionService) {
|
||||||
|
this.authProperties = authProperties;
|
||||||
|
this.sessionService = sessionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/userinfo")
|
||||||
|
public ResponseEntity<Map<String, Object>> userInfo(HttpServletRequest request) {
|
||||||
|
if (!authProperties.getOidc().isEnabled()) {
|
||||||
|
return ResponseEntity.status(403).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// In a real OIDC provider, this would check the Bearer token in Authorization header.
|
||||||
|
// For Path B simplicity, we check the session cookie.
|
||||||
|
Optional<SessionUser> userOpt = sessionService.getSessionUser(request);
|
||||||
|
if (userOpt.isEmpty()) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionUser user = userOpt.get();
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"sub", user.username(),
|
||||||
|
"preferred_username", user.username(),
|
||||||
|
"email", user.email(),
|
||||||
|
"name", user.username()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,6 +26,14 @@ knoe:
|
|||||||
keytabPath: ${KNOE_KERBEROS_KEYTAB_PATH:}
|
keytabPath: ${KNOE_KERBEROS_KEYTAB_PATH:}
|
||||||
realm: ${KNOE_KERBEROS_REALM:}
|
realm: ${KNOE_KERBEROS_REALM:}
|
||||||
|
|
||||||
|
# ── OIDC Provider (Path B) ───────────────────────────────────────────────
|
||||||
|
oidc:
|
||||||
|
enabled: ${KNOE_AUTH_OIDC_ENABLED:false}
|
||||||
|
issuer: ${KNOE_AUTH_OIDC_ISSUER:https://api.knoe.dev/auth}
|
||||||
|
clientId: ${KNOE_AUTH_OIDC_CLIENT_ID:}
|
||||||
|
clientSecret: ${KNOE_AUTH_OIDC_CLIENT_SECRET:}
|
||||||
|
signingKey: ${KNOE_AUTH_OIDC_SIGNING_KEY:}
|
||||||
|
|
||||||
# ── Enrollment (knoe-auth Round 1) ──────────────────────────────────────
|
# ── Enrollment (knoe-auth Round 1) ──────────────────────────────────────
|
||||||
enroll:
|
enroll:
|
||||||
inviteTtlHours: ${KNOE_ENROLL_INVITE_TTL_HOURS:72}
|
inviteTtlHours: ${KNOE_ENROLL_INVITE_TTL_HOURS:72}
|
||||||
|
|||||||
12
conf/gke.cfg
12
conf/gke.cfg
@ -124,9 +124,9 @@ GITLAB_DOMAIN = git.knoe.dev
|
|||||||
GITLAB_GITALY_STORAGE_CLASS = standard
|
GITLAB_GITALY_STORAGE_CLASS = standard
|
||||||
GITLAB_GLOBAL_STATIC_IP_NAME =
|
GITLAB_GLOBAL_STATIC_IP_NAME =
|
||||||
GITLAB_INGRESS_CLASS = gce
|
GITLAB_INGRESS_CLASS = gce
|
||||||
GITLAB_OIDC_CLIENT_ID = secretref://google-oidc-client-id
|
GITLAB_OIDC_CLIENT_ID = secretref://gitlab-oidc-client-id
|
||||||
GITLAB_OIDC_CLIENT_SECRET = secretref://google-oidc-client-secret
|
GITLAB_OIDC_CLIENT_SECRET = secretref://gitlab-oidc-client-secret
|
||||||
GITLAB_OIDC_ISSUER = https://accounts.google.com
|
GITLAB_OIDC_ISSUER = https://api.knoe.dev/auth
|
||||||
GITLAB_PUBLIC_HOSTS = git.knoe.dev
|
GITLAB_PUBLIC_HOSTS = git.knoe.dev
|
||||||
GITLAB_REPAIR_BLOCKED_AUTOCLEAN = 1
|
GITLAB_REPAIR_BLOCKED_AUTOCLEAN = 1
|
||||||
GITLAB_SHELL_LOADBALANCER_IP = 34.106.243.154
|
GITLAB_SHELL_LOADBALANCER_IP = 34.106.243.154
|
||||||
@ -147,7 +147,7 @@ KNOE_DB_USER = chrisfu
|
|||||||
KNOE_USER_GITLAB_API_BASE = https://git.knoe.dev/api/v4
|
KNOE_USER_GITLAB_API_BASE = https://git.knoe.dev/api/v4
|
||||||
KNOE_USER_GITLAB_AUTH_PROVIDER = openid_connect
|
KNOE_USER_GITLAB_AUTH_PROVIDER = openid_connect
|
||||||
KNOE_USER_GITLAB_JIT_AUTO_CREATE_USERS = true
|
KNOE_USER_GITLAB_JIT_AUTO_CREATE_USERS = true
|
||||||
KNOE_USER_GITLAB_OIDC_ISSUER = https://accounts.google.com
|
KNOE_USER_GITLAB_OIDC_ISSUER = https://api.knoe.dev/auth
|
||||||
KNOE_USER_GITLAB_OIDC_REDIRECT_URI = https://git.knoe.dev/users/auth/openid_connect/callback
|
KNOE_USER_GITLAB_OIDC_REDIRECT_URI = https://git.knoe.dev/users/auth/openid_connect/callback
|
||||||
KNOE_USER_GITLAB_PROVISIONING_READY = true
|
KNOE_USER_GITLAB_PROVISIONING_READY = true
|
||||||
OPENTOFU_URL = http://127.0.0.1:8080
|
OPENTOFU_URL = http://127.0.0.1:8080
|
||||||
@ -167,6 +167,10 @@ SUPABASE_INGRESS_CLASS = gce
|
|||||||
SUPABASE_STUDIO_GLOBAL_STATIC_IP_NAME = supabase-studio
|
SUPABASE_STUDIO_GLOBAL_STATIC_IP_NAME = supabase-studio
|
||||||
SUPABASE_STUDIO_HOSTNAME = db.0.knoe.dev
|
SUPABASE_STUDIO_HOSTNAME = db.0.knoe.dev
|
||||||
SVC_KNOE_GLOBAL_STATIC_IP_NAME = svc-knoe
|
SVC_KNOE_GLOBAL_STATIC_IP_NAME = svc-knoe
|
||||||
|
KNOE_AUTH_OIDC_ENABLED = true
|
||||||
|
KNOE_AUTH_OIDC_CLIENT_ID = secretref://gitlab-oidc-client-id
|
||||||
|
KNOE_AUTH_OIDC_CLIENT_SECRET = secretref://gitlab-oidc-client-secret
|
||||||
|
KNOE_AUTH_OIDC_SIGNING_KEY = secretref://knoe-auth-oidc-signing-key
|
||||||
DB_OIDC_CLIENT_ID = secretref://db-oidc-client-id
|
DB_OIDC_CLIENT_ID = secretref://db-oidc-client-id
|
||||||
DB_OIDC_CLIENT_SECRET = secretref://db-oidc-client-secret
|
DB_OIDC_CLIENT_SECRET = secretref://db-oidc-client-secret
|
||||||
DB_OIDC_COOKIE_SECRET = secretref://db-oidc-cookie-secret
|
DB_OIDC_COOKIE_SECRET = secretref://db-oidc-cookie-secret
|
||||||
|
|||||||
@ -196,7 +196,30 @@ spec:
|
|||||||
- name: KNOE_ENROLL_TOTP_ISSUER
|
- name: KNOE_ENROLL_TOTP_ISSUER
|
||||||
value: "Knoe.DEV"
|
value: "Knoe.DEV"
|
||||||
- name: KNOE_AUTH_BASE_URL
|
- name: KNOE_AUTH_BASE_URL
|
||||||
value: "https://auth.knoe.dev"
|
value: "https://api.knoe.dev/auth"
|
||||||
|
# ── OIDC Provider (Path B) ────────────────────────────────────
|
||||||
|
- name: KNOE_AUTH_OIDC_ENABLED
|
||||||
|
value: "${KNOE_AUTH_OIDC_ENABLED:-false}"
|
||||||
|
- name: KNOE_AUTH_OIDC_ISSUER
|
||||||
|
value: "https://api.knoe.dev/auth"
|
||||||
|
- name: KNOE_AUTH_OIDC_CLIENT_ID
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: knoe-auth-oidc
|
||||||
|
key: client-id
|
||||||
|
optional: true
|
||||||
|
- name: KNOE_AUTH_OIDC_CLIENT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: knoe-auth-oidc
|
||||||
|
key: client-secret
|
||||||
|
optional: true
|
||||||
|
- name: KNOE_AUTH_OIDC_SIGNING_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: knoe-auth-oidc
|
||||||
|
key: signing-key
|
||||||
|
optional: true
|
||||||
# ── Provisioning ─────────────────────────────────────────────
|
# ── Provisioning ─────────────────────────────────────────────
|
||||||
- name: KNOE_PROVISIONING_POLL_INTERVAL_MS
|
- name: KNOE_PROVISIONING_POLL_INTERVAL_MS
|
||||||
value: "10000"
|
value: "10000"
|
||||||
|
|||||||
11
deploy/gcp/gke/knoe-auth-oidc-secret.example.yaml
Normal file
11
deploy/gcp/gke/knoe-auth-oidc-secret.example.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: knoe-auth-oidc
|
||||||
|
namespace: knoe-system
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
client-id: "${KNOE_AUTH_OIDC_CLIENT_ID}"
|
||||||
|
client-secret: "${KNOE_AUTH_OIDC_CLIENT_SECRET}"
|
||||||
|
signing-key: "${KNOE_AUTH_OIDC_SIGNING_KEY}"
|
||||||
97
docs/oidc-path-b.md
Normal file
97
docs/oidc-path-b.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# knoe-auth Path B — OIDC Provider Implementation
|
||||||
|
|
||||||
|
This document outlines the design and implementation of **Path B** for knoe authentication:
|
||||||
|
`GitLab → knoe-auth (OIDC OP) → Google Workspace`.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
In Path B, `knoe-auth` (running at `api.knoe.dev/auth`) acts as an OpenID Connect Provider (OP). GitLab (the Relying Party, RP) is configured to trust `knoe-auth` as its identity issuer.
|
||||||
|
|
||||||
|
### Motivation
|
||||||
|
- **Unified Identity:** Consolidates Kerberos/SPNEGO and Google Workspace identities.
|
||||||
|
- **Protocol Translation:** Allows legacy or internal apps to use OIDC while maintaining Kerberos support.
|
||||||
|
- **Extensibility:** Provides a single point to inject additional auth factors or logic before reaching the end application.
|
||||||
|
|
||||||
|
## Endpoints to Implement
|
||||||
|
|
||||||
|
| Endpoint | Path | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| Discovery | `/.well-known/openid-configuration` | Returns OIDC metadata. |
|
||||||
|
| JWKS | `/jwks` | Serves public keys for token verification. |
|
||||||
|
| Authorize | `/authorize` | Initiates the auth flow. Redirects to Google if no session exists. |
|
||||||
|
| Token | `/token` | Exchanges authorization codes for ID/Access/Refresh tokens. |
|
||||||
|
| UserInfo | `/userinfo` | (Optional but recommended) Returns user claims. |
|
||||||
|
| Callback | `/callback/google` | Receives the redirect from Google Workspace. |
|
||||||
|
|
||||||
|
## Data Mapping
|
||||||
|
|
||||||
|
### User Identification
|
||||||
|
`knoe-auth` must normalize identities to a consistent `sub` (subject) claim.
|
||||||
|
- **Kerberos:** `user@REALM` → `user` (via `PrincipalNormalizer`)
|
||||||
|
- **Google:** `user@knoey.com` → `user` (assuming `knoey.com` is the primary domain)
|
||||||
|
|
||||||
|
GitLab will receive `sub: user` and JIT-create/map the user accordingly.
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- **Authorization Codes:** Short-lived, stored in memory or DB (PostgreSQL).
|
||||||
|
- **Sessions:** `knoe-auth` already uses a `knoe_session` cookie. The OIDC flow will leverage this session.
|
||||||
|
|
||||||
|
## Configuration (Path B)
|
||||||
|
|
||||||
|
### GitLab `omni_auth` (in `conf/gke.cfg`)
|
||||||
|
```ini
|
||||||
|
GITLAB_OIDC_ISSUER = https://api.knoe.dev/auth
|
||||||
|
GITLAB_OIDC_CLIENT_ID = <knoe-auth-client-id>
|
||||||
|
GITLAB_OIDC_CLIENT_SECRET = <knoe-auth-client-secret>
|
||||||
|
```
|
||||||
|
|
||||||
|
### knoe-auth `application.yml`
|
||||||
|
```yaml
|
||||||
|
knoe:
|
||||||
|
auth:
|
||||||
|
oidc:
|
||||||
|
enabled: true
|
||||||
|
issuer: https://api.knoe.dev/auth
|
||||||
|
# RSA key for signing JWTs
|
||||||
|
signingKey: ${KNOE_AUTH_OIDC_SIGNING_KEY}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
1. **JWK Management:** Generate or load an RSA key pair for JWT signing.
|
||||||
|
2. **Discovery Endpoint:** Hardcoded JSON returning the implemented endpoints and supported claims/scopes.
|
||||||
|
3. **Authorize Logic:**
|
||||||
|
- Validate `client_id`, `redirect_uri`, `state`.
|
||||||
|
- Check for `knoe_session`.
|
||||||
|
- If missing, redirect to `/login` (which redirects to Google).
|
||||||
|
- If present, generate an auth code, associate it with the session, and redirect back to GitLab.
|
||||||
|
4. **Token Logic:**
|
||||||
|
- Validate auth code.
|
||||||
|
- Generate JWT signed with the private key.
|
||||||
|
- Include `sub`, `email`, `preferred_username` claims.
|
||||||
|
5. **Secret Handling:** Update `etc/init_gitlab.sh` and `etc/init_knoe_auth.sh` to handle the new client credentials and signing keys.
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
To verify Path B is working correctly:
|
||||||
|
|
||||||
|
1. **Discovery:**
|
||||||
|
```bash
|
||||||
|
curl -s https://api.knoe.dev/auth/.well-known/openid-configuration | jq .
|
||||||
|
```
|
||||||
|
Should return JSON with `issuer: "https://api.knoe.dev/auth"` and other OIDC endpoints.
|
||||||
|
|
||||||
|
2. **JWKS:**
|
||||||
|
```bash
|
||||||
|
curl -s https://api.knoe.dev/auth/jwks | jq .
|
||||||
|
```
|
||||||
|
Should return an RSA public key in JWKS format.
|
||||||
|
|
||||||
|
3. **GitLab Redirect:**
|
||||||
|
Visit `https://git.knoe.dev/users/sign_in`. Click on the "OpenID Connect" button.
|
||||||
|
- You should be redirected to `https://api.knoe.dev/auth/authorize?...`.
|
||||||
|
- If not logged in, you should be redirected to `https://api.knoe.dev/auth/login`.
|
||||||
|
- Click "Login with Google". After Google authentication, you should be redirected back to `knoe-auth` and then to GitLab.
|
||||||
|
|
||||||
|
4. **GitLab Login:**
|
||||||
|
After the flow, you should be logged into GitLab as your Google Workspace user. Verify in GitLab profile that the email matches.
|
||||||
@ -16,11 +16,10 @@ Two paths exist:
|
|||||||
|
|
||||||
| Path | Description | Status |
|
| Path | Description | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **A** | GitLab talks OIDC directly to Google Workspace. Issuer = `https://accounts.google.com`. knoe-auth is out of the loop. | **Active — this doc describes it** |
|
| **A** | GitLab talks OIDC directly to Google Workspace. Issuer = `https://accounts.google.com`. knoe-auth is out of the loop. | Deprecated — moved to Path B |
|
||||||
| **B** | GitLab → knoe-auth (OIDC OP) → Google. Issuer = `https://api.knoe.dev/auth`. Aligns with the Kerberos/unified-SSO long-term plan. | Deferred, tracked as task #7 (requires building/adopting an OIDC OP inside knoe-auth) |
|
| **B** | GitLab → knoe-auth (OIDC OP) → Google. Issuer = `https://api.knoe.dev/auth`. Aligns with the Kerberos/unified-SSO long-term plan. | **Active — see docs/oidc-path-b.md** |
|
||||||
|
|
||||||
When Path B is ready, swap `GITLAB_OIDC_ISSUER` back to `https://api.knoe.dev/auth`
|
When Path B is ready (implemented April 2026), `GITLAB_OIDC_ISSUER` is set to `https://api.knoe.dev/auth`.
|
||||||
and re-run the installer — no other change required downstream.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -268,6 +268,25 @@ create_session_secret() {
|
|||||||
info "knoe-auth-secrets created."
|
info "knoe-auth-secrets created."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
create_oidc_path_b_secret() {
|
||||||
|
if kube -n "$NAMESPACE" get secret knoe-auth-oidc >/dev/null 2>&1; then
|
||||||
|
info "knoe-auth-oidc already exists — skipping."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Creating knoe-auth-oidc secret for Path B..."
|
||||||
|
local client_id client_secret signing_key
|
||||||
|
client_id=$(op_secret "knoe-auth-oidc-gitlab" "client_id")
|
||||||
|
client_secret=$(op_secret "knoe-auth-oidc-gitlab" "client_secret")
|
||||||
|
signing_key=$(op_secret "knoe-auth-oidc-signing" "private_key")
|
||||||
|
|
||||||
|
kube -n "$NAMESPACE" create secret generic knoe-auth-oidc \
|
||||||
|
--from-literal=client-id="$client_id" \
|
||||||
|
--from-literal=client-secret="$client_secret" \
|
||||||
|
--from-literal=signing-key="$signing_key"
|
||||||
|
info "knoe-auth-oidc created."
|
||||||
|
}
|
||||||
|
|
||||||
# ── Manifests ────────────────────────────────────────────────────────────────
|
# ── Manifests ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
apply_manifests() {
|
apply_manifests() {
|
||||||
@ -341,6 +360,7 @@ cmd_initialize() {
|
|||||||
create_kdc_secrets
|
create_kdc_secrets
|
||||||
create_google_oidc_secret
|
create_google_oidc_secret
|
||||||
create_session_secret
|
create_session_secret
|
||||||
|
create_oidc_path_b_secret
|
||||||
|
|
||||||
# 3. Apply ConfigMap + Deployment
|
# 3. Apply ConfigMap + Deployment
|
||||||
apply_manifests
|
apply_manifests
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user