diff --git a/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/OrdinisAuthProperties.java b/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/OrdinisAuthProperties.java index 07d1e40..0baa886 100644 --- a/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/OrdinisAuthProperties.java +++ b/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/OrdinisAuthProperties.java @@ -3,28 +3,44 @@ package cloud.nstart.terravault.ordinis.auth; import org.springframework.boot.context.properties.ConfigurationProperties; /** - * Конфиг для @nstart/auth-style JWT auth. + * Конфиг JWT auth для интеграции с Keycloak (@nstart/auth). * - *
{@code publicKey} — RSA public key (PEM, без BEGIN/END заголовков, base64). - * Подбрасывается через Vault: secret/ordinis/cuod/jwt-public-key.
+ *{@code issuerUri} — OIDC issuer URL, например + * {@code https://auth.nstart.space/realms/nstart}. Spring Security сам тянет + * JWK Set через {@code .well-known/openid-configuration}, поддерживает rotation + * ключей. Если не задан — JWT validation выключен (permissive mode для dev). + * + *
{@code jwkSetUri} — опциональный override (если discovery недоступен). + * Обычно не нужен. + * + *
{@code audience} — опциональная валидация {@code aud} claim. Когда узнаем + * client_id, выставляем сюда (например {@code ordinis-cuod}). + * + *
{@code rolesClaim} — путь к ролям внутри JWT. Для Keycloak realm-роли + * лежат в {@code realm_access.roles} (массив). Для resource-ролей — + * {@code resource_access.{client_id}.roles}. Default: realm_access.roles. * *
{@code requireAuthentication=true} → 401 на любой запрос без валидного JWT. - * При false работает permissive режим — анонимный запрос имеет только PUBLIC scope, - * что подходит для read-api на raw публичных endpoint'ах.
+ * При false работает permissive режим — анонимный запрос имеет только PUBLIC scope. * - *{@code allowQueryScope=true} включает legacy {@code ?as_scope=...} query param - * (для smoke-тестов и dev). В prod рекомендуется false.
+ *{@code allowQueryScope=true} включает legacy {@code ?as_scope=...} + * (для smoke-тестов и dev). В prod — false. */ @ConfigurationProperties(prefix = "ordinis.auth") public record OrdinisAuthProperties( - String publicKey, - String issuer, - String scopeClaim, + String issuerUri, + String jwkSetUri, + String audience, + String rolesClaim, boolean requireAuthentication, boolean allowQueryScope) { public OrdinisAuthProperties { - if (scopeClaim == null) scopeClaim = "scopes"; - if (issuer == null) issuer = ""; + if (rolesClaim == null || rolesClaim.isBlank()) rolesClaim = "realm_access.roles"; + } + + public boolean enabled() { + return (issuerUri != null && !issuerUri.isBlank()) + || (jwkSetUri != null && !jwkSetUri.isBlank()); } } diff --git a/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/ScopeContext.java b/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/ScopeContext.java index c386d38..7e7ea8c 100644 --- a/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/ScopeContext.java +++ b/ordinis-auth/src/main/java/cloud/nstart/terravault/ordinis/auth/ScopeContext.java @@ -9,19 +9,28 @@ import org.springframework.stereotype.Component; import java.util.Collection; import java.util.EnumSet; -import java.util.List; +import java.util.Map; import java.util.Set; /** - * Резолвер активных scope для текущего запроса. + * Резолвер scope для текущего запроса. * *
Источник scope (приоритет сверху-вниз): *
JWT всегда побеждает query param — защита от scope escalation через URL.
*/
@Component
public class ScopeContext {
@@ -32,7 +41,6 @@ public class ScopeContext {
this.props = props;
}
- /** Достаёт scope текущей аутентифицированной сессии (из JWT) или PUBLIC если анонимна. */
public Set JWT verification через OIDC issuer-uri (Keycloak): JWK Set автоматически
+ * discover'ится с {@code {issuer}/.well-known/openid-configuration}, ротация
+ * ключей бесплатна.
*/
@Configuration
@EnableConfigurationProperties(OrdinisAuthProperties.class)
@@ -32,50 +38,63 @@ public class SecurityConfig {
OrdinisAuthProperties props) throws Exception {
http
.csrf(csrf -> csrf.disable())
- .sessionManagement(sm -> sm.sessionCreationPolicy(
- org.springframework.security.config.http.SessionCreationPolicy.STATELESS))
+ .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/actuator/**").permitAll();
auth.requestMatchers("/error").permitAll();
if (props.requireAuthentication()) {
auth.anyRequest().authenticated();
} else {
- // Permissive: запрос без JWT → анонимный, но scope-фильтр оставит только PUBLIC.
auth.anyRequest().permitAll();
}
});
- // Resource server только если есть public key (иначе nimbus сломается).
- if (props.publicKey() != null && !props.publicKey().isBlank()) {
+ if (props.enabled()) {
http.oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults()));
} else {
- log.warn("ordinis.auth.public-key не задан — JWT validation отключён, scope только из query/anonymous");
+ log.warn("ordinis.auth.issuer-uri не задан — JWT validation отключён, scope только из query/anonymous");
}
return http.build();
}
@Bean
- public JwtDecoder jwtDecoder(OrdinisAuthProperties props) throws Exception {
- if (props.publicKey() == null || props.publicKey().isBlank()) {
- // Nimbus требует ключ; возвращаем decoder который всегда падает — он не
- // будет вызван, потому что oauth2ResourceServer не подключён.
+ public JwtDecoder jwtDecoder(OrdinisAuthProperties props) {
+ if (!props.enabled()) {
return token -> {
throw new org.springframework.security.oauth2.jwt.BadJwtException(
- "JWT validation disabled: ordinis.auth.public-key не сконфигурирован");
+ "JWT validation disabled: ordinis.auth.issuer-uri не сконфигурирован");
};
}
- String pem = props.publicKey()
- .replaceAll("-----BEGIN PUBLIC KEY-----", "")
- .replaceAll("-----END PUBLIC KEY-----", "")
- .replaceAll("\\s+", "");
- byte[] der = Base64.getDecoder().decode(pem);
- RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
- .generatePublic(new X509EncodedKeySpec(der));
- NimbusJwtDecoder decoder = NimbusJwtDecoder.withPublicKey(publicKey).build();
- if (props.issuer() != null && !props.issuer().isBlank()) {
- decoder.setJwtValidator(
- org.springframework.security.oauth2.jwt.JwtValidators.createDefaultWithIssuer(props.issuer()));
+
+ NimbusJwtDecoder decoder;
+ if (props.jwkSetUri() != null && !props.jwkSetUri().isBlank()) {
+ decoder = NimbusJwtDecoder.withJwkSetUri(props.jwkSetUri()).build();
+ } else {
+ // OIDC discovery: загружает /.well-known/openid-configuration → jwks_uri.
+ decoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(props.issuerUri());
}
+
+ OAuth2TokenValidator
- *
- */
public Set