mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 16:04:31 +00:00
feat(auth): knoe-auth Phase 2 — OIDC provider surface (discovery, authorize, token, userinfo, JWKS), RS256 signing, Kerberos/SPNEGO integration, stateless session model, PrincipalNormalizer, typed config, init script, architecture doc, regression tests
Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
parent
2e203f7355
commit
9d5827b522
@ -10,7 +10,7 @@
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>org.knoe</groupId>
|
||||
<groupId>org.prole</groupId>
|
||||
<artifactId>authority</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>knoe-authority</name>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.knoe.authority;
|
||||
package org.prole.authority;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@ -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<String> adminPrincipals = new ArrayList<>();
|
||||
private Oidc oidc = new Oidc();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.knoe.authority.config;
|
||||
package org.prole.authority.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.knoe.authority.kerberos;
|
||||
package org.prole.authority.kerberos;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.knoe.authority.kerberos;
|
||||
package org.prole.authority.kerberos;
|
||||
|
||||
import java.security.PrivilegedExceptionAction;
|
||||
import java.util.Map;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.knoe.authority.session;
|
||||
package org.prole.authority.session;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.knoe.authority.session;
|
||||
package org.prole.authority.session;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.knoe.authority.user;
|
||||
package org.prole.authority.user;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
@ -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<Map<String, Object>> getJwks() {
|
||||
if (!authProperties.getOidc().isEnabled()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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")),
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Path> 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<Path> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package org.knoe.authority.session;
|
||||
package org.prole.authority.session;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.time.Clock;
|
||||
|
||||
@ -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;
|
||||
|
||||
73
docs/knoe-auth-phase-2.md
Normal file
73
docs/knoe-auth-phase-2.md
Normal file
@ -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.
|
||||
@ -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=<invite_token>"
|
||||
info ""
|
||||
info "Next: create first admin invite:"
|
||||
info " $0 invite chrisfu@prole.org"
|
||||
info " $0 invite chrisfu@knoey.com"
|
||||
info ""
|
||||
show_status
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user