diff --git a/Dockerfile b/Dockerfile index 61eb82a6cd..75f9d71e55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,6 +71,7 @@ ENV CDA_POOL_MAX_ACTIVE="30" ENV CDA_POOL_MAX_IDLE="10" ENV CDA_POOL_MIN_IDLE="5" ENV cwms.dataapi.access.providers="KeyAccessManager,OpenID" +ENV cwms.dataapi.access.providers.surpress=CwmsAAACacAuth ENV cwms.dataapi.access.openid.wellKnownUrl="https:///.well-known/openid-configuration" ENV cwms.dataapi.access.openid.issuer="" ENV cwms.dataapi.access.openid.timeout="604800" diff --git a/access-manager-api/src/main/java/cwms/cda/spi/CdaIdentityProviders.java b/access-manager-api/src/main/java/cwms/cda/spi/CdaIdentityProviders.java index a946a579a2..b3eab7240f 100644 --- a/access-manager-api/src/main/java/cwms/cda/spi/CdaIdentityProviders.java +++ b/access-manager-api/src/main/java/cwms/cda/spi/CdaIdentityProviders.java @@ -4,7 +4,7 @@ import java.util.ServiceLoader; public class CdaIdentityProviders { - + private static final ServiceLoader loader = ServiceLoader.load(IdentityProvider.class); diff --git a/compose_files/api_entry.sh b/compose_files/api_entry.sh new file mode 100755 index 0000000000..97c9a818c1 --- /dev/null +++ b/compose_files/api_entry.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +nohup ./proxy_auth.sh 2>&1 > /dev/null & +echo "auth proxy started now executing $*" +exec $* \ No newline at end of file diff --git a/compose_files/pki/certs/features.properties b/compose_files/pki/certs/features.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/compose_files/proxy_auth.sh b/compose_files/proxy_auth.sh new file mode 100755 index 0000000000..e57c30aa89 --- /dev/null +++ b/compose_files/proxy_auth.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +mkfifo backpipe +#while true; do nc -lk -p 7100 0backpipe; done +nc -lk -p ${APP_PORT:-8081} -e nc auth ${APP_PORT:-8081} \ No newline at end of file diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index a6b5555e02..0c0e3ccc9a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -184,6 +184,7 @@ import cwms.cda.features.CdaFeatures; import cwms.cda.formatters.Formats; import cwms.cda.formatters.csv.CsvExampleGenerator; +import cwms.cda.openapi.OpenApiSchemeProcessor; import cwms.cda.security.Authenticator; import cwms.cda.security.CdaAccessManager; import cwms.cda.security.DataApiPrincipal; @@ -322,6 +323,7 @@ public class ApiServlet extends HttpServlet { JavalinServlet javalin = null; private Authenticator authenticator = new Authenticator(); + private OpenApiSchemeProcessor schemeProcessor = new OpenApiSchemeProcessor(authenticator); private String APP_CONTEXT; @Resource(name = "jdbc/CWMS3") @@ -374,6 +376,7 @@ public void init() { }) .attribute("PolicyFactory", sanitizer) .attribute("ObjectMapper", om) + .attribute("schemeProcessor", schemeProcessor) .before(authenticator) .before(ctx -> { ctx.attribute("sanitizer", sanitizer); @@ -976,32 +979,20 @@ private void getOpenApiOptions(JavalinConfig config) { String provider = CdaAccessManager.class.getSimpleName(); - - Components components = new Components(); - final ArrayList secReqs = new ArrayList<>(); - authenticator.getActiveProviders().forEach(identityProvider -> { - components.addSecuritySchemes(identityProvider.getName(),identityProvider.getScheme()); - SecurityRequirement req = new SecurityRequirement(); - if (!identityProvider.getName().equalsIgnoreCase("guestauth") - && !identityProvider.getName().equalsIgnoreCase("noauth")) { - req.addList(identityProvider.getName()); - secReqs.add(req); - } - }); - List servers = new ArrayList<>(); servers.add(new Server().url(APP_CONTEXT)); OpenApiOptions ops = new OpenApiOptions( - () -> new OpenAPI().components(components) + () -> new OpenAPI() .servers(servers) .info(applicationInfo) .addSecurityItem(new SecurityRequirement().addList(provider)) ); ops.path("/swagger-docs") .responseModifier((ctx,api) -> { + schemeProcessor.apply(ctx, api); api.getPaths().forEach((key,path) -> { - setSecurityRequirements(key,path,secReqs); + setSecurityRequirements(key,path,schemeProcessor.getSecurityRequirements()); // yeah, we really need to figure out how to update everything, this is supported as an annotation in // newer versions. if (key.startsWith("/rss")) { @@ -1066,6 +1057,7 @@ private void getOpenApiOptions(JavalinConfig config) { .activateAnnotationScanningFor("cwms.cda.api"); addEndpointExamples(ops); config.registerPlugin(new OpenApiPlugin(ops)); + } private static void setSecurityRequirements(String key, PathItem path,List secReqs) { diff --git a/cwms-data-api/src/main/java/cwms/cda/openapi/OpenApiSchemeProcessor.java b/cwms-data-api/src/main/java/cwms/cda/openapi/OpenApiSchemeProcessor.java new file mode 100644 index 0000000000..b50e2fdc18 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/openapi/OpenApiSchemeProcessor.java @@ -0,0 +1,50 @@ +package cwms.cda.openapi; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import cwms.cda.security.Authenticator; +import io.javalin.http.Context; +import io.javalin.plugin.openapi.OpenApiModelModifier; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; + +public class OpenApiSchemeProcessor implements OpenApiModelModifier { + + private final Authenticator authenticator; + private final ArrayList secReqs = new ArrayList<>(); + + public OpenApiSchemeProcessor(Authenticator authenticator) + { + this.authenticator = authenticator; + } + + @Override + public OpenAPI apply(Context ctx, OpenAPI api) { + var schemes = api.getComponents().getSecuritySchemes(); + if (schemes != null) + { + schemes.clear(); + } + synchronized (secReqs) { + secReqs.clear(); + authenticator.getActiveProviders().forEach(identityProvider -> { + api.getComponents().addSecuritySchemes(identityProvider.getName(),identityProvider.getScheme()); + SecurityRequirement req = new SecurityRequirement(); + if (!identityProvider.getName().equalsIgnoreCase("guestauth") + && !identityProvider.getName().equalsIgnoreCase("noauth")) { + req.addList(identityProvider.getName()); + secReqs.add(req); + } + }); + } + return api; + } + + + public List getSecurityRequirements() + { + return Collections.unmodifiableList(secReqs); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/security/Authenticator.java b/cwms-data-api/src/main/java/cwms/cda/security/Authenticator.java index 53fa72b634..956ccc516d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/security/Authenticator.java +++ b/cwms-data-api/src/main/java/cwms/cda/security/Authenticator.java @@ -17,11 +17,16 @@ public final class Authenticator implements Handler { private final ArrayList providers = new ArrayList<>(); public Authenticator() { + var surpressed = System.getenv("cwms.dataapi.access.providers.surpress"); + final var supressedList = surpressed == null ? List.of() : List.of(surpressed.split(",")); + CdaIdentityProviders.providers().forEachRemaining(provider -> { - if (provider.getScheme() != null) { + if (!supressedList.contains(provider.getName()) && provider.getScheme() != null) { providers.add(provider); } else { - logger.atSevere().log("Unable to add Identity Provider %s. See earlier logs for specific error message.", provider.getName()); + logger.atSevere() + .log("Unable to add Identity Provider %s. See earlier logs for specific error message.", + provider.getName()); } }); } @@ -36,7 +41,7 @@ public void handle(Context ctx) throws Exception { } } } - + public List getActiveProviders() { return Collections.unmodifiableList(providers); } diff --git a/cwms-data-api/src/main/java/cwms/cda/security/OpenIDConfig.java b/cwms-data-api/src/main/java/cwms/cda/security/OpenIDConfig.java index 44fe78f286..9a0156ffe1 100644 --- a/cwms-data-api/src/main/java/cwms/cda/security/OpenIDConfig.java +++ b/cwms-data-api/src/main/java/cwms/cda/security/OpenIDConfig.java @@ -1,9 +1,18 @@ package cwms.cda.security; import java.io.IOException; +import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.URL; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Base64; +import java.util.Base64.Decoder; import java.util.HashMap; import java.util.Map; @@ -11,49 +20,71 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.flogger.FluentLogger; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SigningKeyResolverAdapter; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme.Type; public class OpenIDConfig { private static final FluentLogger log = FluentLogger.forEnclosingClass(); - + private URL wellKnown; - + private String issuer; private String client_id; private String idp_hint; // keycloak specific kc_idp_hint to direct federation - + private JwtParser jwtParser; private URL jwksUrl; - - public OpenIDConfig(URL wellKnown, String client_id, String idp_hint) throws IOException { + + private OpenIDConfig(URL wellKnown, String client_id, String idp_hint, JwtParser jwtParser) throws IOException { this.wellKnown = wellKnown; this.idp_hint = idp_hint; this.client_id = client_id; + this.jwtParser = jwtParser; + } + + public URL getJwksUrl() { + return jwksUrl; + } + + public static OpenIDConfig from(URL wellKnown, String clientId, String idpHint, int timeout) throws IOException + { HttpURLConnection http = null; try { http = (HttpURLConnection)wellKnown.openConnection(); http.setRequestMethod("GET"); - http.setInstanceFollowRedirects(true); + http.setInstanceFollowRedirects(true); int status = http.getResponseCode(); if (status == 200) { ObjectMapper mapper = new ObjectMapper(); JsonNode node = mapper.readTree(http.getInputStream()); - jwksUrl = new URL(node.get("jwks_uri").asText()); - issuer = node.get("issuer").asText(); + URL jwksUrl = new URL(node.get("jwks_uri").asText()); + String issuer = node.get("issuer").asText(); + + JwtParser jwtParser = Jwts.parserBuilder() + .requireIssuer(issuer) + .setSigningKeyResolver(new UrlResolver(jwksUrl, timeout)) + .build(); + return new OpenIDConfig(wellKnown, clientId, idpHint, jwtParser); } else { - log.atSevere().log("Unable to retrieve data from realm. Response code %d",status); + log.atSevere().log("Unable to retrieve data from realm. Response code %d", status); } } finally { if (http != null) { http.disconnect(); } } + throw new IOException("Unable to retrieve OIDC information from provider."); } - - public URL getJwksUrl() { - return jwksUrl; + + public JwtParser getJwtParser() + { + return this.jwtParser; } static SecurityScheme buildScheme(String wellKnownUrl, String clientId, String idpHint) { @@ -76,8 +107,87 @@ static SecurityScheme buildScheme(String wellKnownUrl, String clientId, String i } public SecurityScheme getScheme() { - + SecurityScheme scheme = buildScheme(wellKnown.toString(), client_id, idp_hint); return scheme; } + + + private static class UrlResolver extends SigningKeyResolverAdapter { + private final URL jwksUrl; + private ZonedDateTime lastCheck; + private final Map realmPublicKeys = new HashMap<>(); + private final int realmPublicKeyTimeoutMinutes; + private KeyFactory keyFactory = null; + + public UrlResolver(URL jwksUrl, int keyTimeoutMinutes) { + this.jwksUrl = jwksUrl; + this.realmPublicKeyTimeoutMinutes = keyTimeoutMinutes; + try { + keyFactory = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException ex) { + log.atSevere().withCause(ex).log("Unable to initialize key factory."); + } + } + + private void updateKey() { + if (realmPublicKeys.isEmpty() || ZonedDateTime.now().isAfter(lastCheck.plusMinutes(realmPublicKeyTimeoutMinutes))) { + log.atInfo().log("Checking for new key at %s",jwksUrl); + try { + realmPublicKeys.clear(); + updateSigningKey(); + } catch (IOException ex) { + log.atSevere().withCause(ex).log("Unable to update key. Will continue to use previous key."); + } catch (InvalidKeySpecException ex) { + log.atSevere().withCause(ex).log("New Public Key was not valid. Will continue to use previous key."); + } + lastCheck = ZonedDateTime.now(); + } + } + + private void updateSigningKey() throws IOException, InvalidKeySpecException { + HttpURLConnection http = null; + try { + http = (HttpURLConnection)jwksUrl.openConnection(); + http.setRequestMethod("GET"); + http.setInstanceFollowRedirects(true); + int status = http.getResponseCode(); + if (status == 200) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode keys = mapper.readTree(http.getInputStream()).get("keys"); + for (JsonNode key: keys) { + String kid = key.get("kid").textValue(); + Decoder b64 = Base64.getUrlDecoder(); // https://datatracker.ietf.org/doc/id/draft-jones-json-web-key-01.html#RFC4648 + String nStr = key.get("n").textValue(); + String eStr = key.get("e").textValue(); + log.atInfo().log("Loading Key %s with parameters (n,e) -> (%s,%s)",kid,nStr,eStr); + BigInteger n = new BigInteger(1,b64.decode(nStr)); + BigInteger e = new BigInteger(1,b64.decode(eStr)); + Key pubKey = keyFactory.generatePublic(new RSAPublicKeySpec(n, e)); + realmPublicKeys.put(kid,pubKey); + } + } else { + log.atSevere().log("Unable to retrieve actual keys. Response code %d",status); + } + } finally { + if (http != null) { + http.disconnect(); + } + } + } + + @Override + public Key resolveSigningKey(JwsHeader header, Claims claims) { + if (!header.getAlgorithm().toLowerCase().startsWith("rs")) { + log.atWarning().log("Request with invalid algorithm '%s'",header.getAlgorithm()); + return null; // we only deal with RSA keys right now. + } + updateKey(); + Key key = realmPublicKeys.get(header.getKeyId()); + if (key == null) { + log.atSevere().log("Key not found for id '%s'",header.getKeyId()); + } + return key; + } + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/security/OpenIdConnectIdentitityProvider.java b/cwms-data-api/src/main/java/cwms/cda/security/OpenIdConnectIdentitityProvider.java index 48d1c8940c..ea99b84e48 100644 --- a/cwms-data-api/src/main/java/cwms/cda/security/OpenIdConnectIdentitityProvider.java +++ b/cwms-data-api/src/main/java/cwms/cda/security/OpenIdConnectIdentitityProvider.java @@ -8,35 +8,21 @@ import io.javalin.http.Context; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SigningKeyResolverAdapter; import io.swagger.v3.oas.models.security.SecurityScheme; import java.io.IOException; -import java.math.BigInteger; -import java.net.HttpURLConnection; import java.net.URL; -import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; import java.security.Principal; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.RSAPublicKeySpec; -import java.time.ZonedDateTime; -import java.util.Base64; -import java.util.Base64.Decoder; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import javax.servlet.http.HttpServletResponse; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.flogger.FluentLogger; @@ -58,37 +44,63 @@ public final class OpenIdConnectIdentitityProvider implements IdentityProvider { private static final boolean CREATE_USERS = Boolean.parseBoolean(System.getProperty(CREATE_USERS_KEY,"true")); - private JwtParser jwtParser = null; - private OpenIDConfig config = null; + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + + private final AtomicReference config = new AtomicReference<>(null); + + private final String wellKnownUrl; + private final String issuer; + private final String clientId; + private final int timeout; + private final String idpHint; public OpenIdConnectIdentitityProvider() { - String wellKnownUrl = System.getProperty(WELL_KNOWN_PROPERTY,System.getenv(WELL_KNOWN_PROPERTY)); - String issuer = System.getProperty(ISSUER_PROPERTY,System.getenv(ISSUER_PROPERTY)); + wellKnownUrl = System.getProperty(WELL_KNOWN_PROPERTY,System.getenv(WELL_KNOWN_PROPERTY)); + issuer = System.getProperty(ISSUER_PROPERTY,System.getenv(ISSUER_PROPERTY)); String timeoutStr = System.getProperty(TIMEOUT_PROPERTY,System.getenv(TIMEOUT_PROPERTY)); - String clientId = System.getProperty(CLIENT_ID, System.getenv(CLIENT_ID)); - String idpHint = System.getProperty(IDP_HINT, System.getenv(IDP_HINT)); - int timeout = 3600; + clientId = System.getProperty(CLIENT_ID, System.getenv(CLIENT_ID)); + idpHint = System.getProperty(IDP_HINT, System.getenv(IDP_HINT)); if (timeoutStr != null && !timeoutStr.isEmpty()) { timeout = Integer.parseInt(timeoutStr); + } else { + timeout = 3600; } - try { - if (wellKnownUrl == null || wellKnownUrl.isEmpty()) { - throw new IOException("OpenID Connect well-known URL is not set."); - } - config = new OpenIDConfig(new URL(wellKnownUrl), clientId, idpHint); - jwtParser = Jwts.parserBuilder() - .requireIssuer(issuer) - .setSigningKeyResolver(new UrlResolver(config.getJwksUrl(),timeout)) - .build(); - } catch (IOException ex) { - // The downstream users of this check if the Provider is valid and respond appropriate. - // To test manually have OpenIDConfig throw an IOException so config stays null and - // see the resulting explained failure in the logs. - // That said it's possible we should maybe just have the system fail completely. - log.atSevere().withCause(ex).log("Unable to initialize realm."); + if (wellKnownUrl == null || wellKnownUrl.isEmpty()) { + log.atInfo().log("OpenID Connect well-known URL is not set; provider will remain disabled."); + executor.shutdown(); + return; + } + // try it once, then every 5 minutes until we get it. + initializeProvider(); + if (config.get() == null) { + executor.scheduleAtFixedRate(this::initializeProvider, 5, 5, TimeUnit.MINUTES); } } + private void initializeProvider() + { + var foundConfig = config.getAndUpdate(c -> { + if (c != null) + { + return c; // already initialized, don't change it. + } + try { + log.atFine().log("Attempting to initalize OIDC provider for %s", wellKnownUrl); + URL wellKnown = new URL(wellKnownUrl); + return OpenIDConfig.from(wellKnown, clientId, idpHint, timeout); + } catch (IOException ex) { + // The downstream users of this check if the Provider is valid and respond appropriate. + // To test manually have OpenIDConfig throw an IOException so config stays null and + // see the resulting explained failure in the logs. + // That said it's possible we should maybe just have the system fail completely. + log.atSevere().withCause(ex).log("Unable to initialize realm."); + } + return c; + }); + if (foundConfig != null || config.get() != null) { + executor.shutdown(); // we have it, don't need to keep polling + } + } @Override public Principal authenticate(Context ctx) { @@ -97,7 +109,7 @@ public Principal authenticate(Context ctx) { private DataApiPrincipal getUserFromToken(Context ctx) throws CwmsAuthException { try { - Jws token = jwtParser.parseClaimsJws(getToken(ctx)); + Jws token = config.get().getJwtParser().parseClaimsJws(getToken(ctx)); Claims claims = token.getBody(); final String issuer = claims.getIssuer(); final String subject = claims.getSubject(); @@ -139,7 +151,8 @@ private String getToken(Context ctx) { @Override public SecurityScheme getScheme() { - return config != null ? config.getScheme() : null; + var configActual = config.get(); + return configActual != null ? configActual.getScheme() : null; } @Override @@ -149,94 +162,13 @@ public String getName() { @Override public boolean canAuth(Context ctx) { + if (config.get() == null) { + return false; + } String header = ctx.header(AUTHORIZATION); if (header == null) { return false; } return header.trim().toLowerCase().startsWith("bearer"); } - - - private static class UrlResolver extends SigningKeyResolverAdapter { - private final URL jwksUrl; - private ZonedDateTime lastCheck; - private final Map realmPublicKeys = new HashMap<>(); - private final int realmPublicKeyTimeoutMinutes; - private KeyFactory keyFactory = null; - - public UrlResolver(URL jwksUrl, int keyTimeoutMinutes) { - this.jwksUrl = jwksUrl; - this.realmPublicKeyTimeoutMinutes = keyTimeoutMinutes; - try { - keyFactory = KeyFactory.getInstance("RSA"); - } catch (NoSuchAlgorithmException ex) { - log.atSevere().withCause(ex).log("Unable to initialize key factory."); - } - } - - /** - * TODO: This needs more, some configurations may be more complex (like the - * authelia test environment) than others. - */ - private void updateKey() { - if (realmPublicKeys.isEmpty() || ZonedDateTime.now().isAfter(lastCheck.plusMinutes(realmPublicKeyTimeoutMinutes))) { - log.atInfo().log("Checking for new key at %s",jwksUrl); - try { - realmPublicKeys.clear(); - updateSigningKey(); - } catch (IOException ex) { - log.atSevere().withCause(ex).log("Unable to update key. Will continue to use previous key."); - } catch (InvalidKeySpecException ex) { - log.atSevere().withCause(ex).log("New Public Key was not valid. Will continue to use previous key."); - } - lastCheck = ZonedDateTime.now(); - } - } - - private void updateSigningKey() throws IOException, InvalidKeySpecException { - HttpURLConnection http = null; - try { - http = (HttpURLConnection)jwksUrl.openConnection(); - http.setRequestMethod("GET"); - http.setInstanceFollowRedirects(true); - int status = http.getResponseCode(); - if (status == 200) { - ObjectMapper mapper = new ObjectMapper(); - JsonNode keys = mapper.readTree(http.getInputStream()).get("keys"); - for (JsonNode key: keys) { - String kid = key.get("kid").textValue(); - Decoder b64 = Base64.getUrlDecoder(); // https://datatracker.ietf.org/doc/id/draft-jones-json-web-key-01.html#RFC4648 - String nStr = key.get("n").textValue(); - String eStr = key.get("e").textValue(); - log.atInfo().log("Loading Key %s with parameters (n,e) -> (%s,%s)",kid,nStr,eStr); - BigInteger n = new BigInteger(1,b64.decode(nStr)); - BigInteger e = new BigInteger(1,b64.decode(eStr)); - Key pubKey = keyFactory.generatePublic(new RSAPublicKeySpec(n, e)); - realmPublicKeys.put(kid,pubKey); - } - } else { - log.atSevere().log("Unable to retrieve actual keys. Response code %d",status); - } - } finally { - if (http != null) { - http.disconnect(); - } - } - } - - @Override - public Key resolveSigningKey(JwsHeader header, Claims claims) { - if (!header.getAlgorithm().toLowerCase().startsWith("rs")) { - log.atWarning().log("Request with invalid algorithm '%s'",header.getAlgorithm()); - return null; // we only deal with RSA keys right now. - } - updateKey(); - Key key = realmPublicKeys.get(header.getKeyId()); - if (key == null) { - log.atSevere().log("Key not found for id '%s'",header.getKeyId()); - } - return key; - } - } - } diff --git a/cwms-data-api/src/test/java/cwms/cda/security/OpenIDConfigTest.java b/cwms-data-api/src/test/java/cwms/cda/security/OpenIDConfigTest.java index cdce8c5042..b7f46b2ff4 100644 --- a/cwms-data-api/src/test/java/cwms/cda/security/OpenIDConfigTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/security/OpenIDConfigTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import io.swagger.v3.oas.models.security.SecurityScheme; @@ -12,6 +13,24 @@ class OpenIDConfigTest { + @Test + void providerRemainsDisabledWhenWellKnownUrlIsMissing() { + String previousWellKnown = System.getProperty(OpenIdConnectIdentitityProvider.WELL_KNOWN_PROPERTY); + try { + System.setProperty(OpenIdConnectIdentitityProvider.WELL_KNOWN_PROPERTY, ""); + + OpenIdConnectIdentitityProvider provider = new OpenIdConnectIdentitityProvider(); + + assertNull(provider.getScheme()); + } finally { + if (previousWellKnown == null) { + System.clearProperty(OpenIdConnectIdentitityProvider.WELL_KNOWN_PROPERTY); + } else { + System.setProperty(OpenIdConnectIdentitityProvider.WELL_KNOWN_PROPERTY, previousWellKnown); + } + } + } + @Test void buildSchemeUsesWellKnownDiscoveryUrlWithoutHttpAuthScheme() { SecurityScheme scheme = OpenIDConfig.buildScheme( diff --git a/docker-compose.yml b/docker-compose.yml index e8f93c5423..dd79581ef0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,8 +48,11 @@ services: data-api: depends_on: - auth: - condition: service_healthy + # to test the OpenIdConnectIdentityProvider setup, we initionally + # reverse the dependency order to force the situtation of + # there not being an OpenId Connect Provider instances ready set and service startup. + #auth: + # condition: service_healthy db: condition: service_healthy db_webuser_permissions: @@ -65,13 +68,18 @@ services: target: api context: . dockerfile: Dockerfile + entrypoint: ["/api_entry.sh"] + command: ["/usr/local/tomcat/bin/catalina.sh", "run"] #command: bash -c "/conf/installcerts.sh && /usr/local/tomcat/bin/catalina.sh run" restart: unless-stopped volumes: - ./compose_files/pki/certs:/conf/ - ./compose_files/togglz/features.properties:/conf/features.properties:ro - ./compose_files/tomcat/logging.properties:/usr/local/tomcat/conf/logging.properties:ro + - ./compose_files/api_entry.sh:/api_entry.sh:ro + - ./compose_files/proxy_auth.sh:/proxy_auth.sh:ro environment: + - APP_PORT=${APP_PORT:-8081} - JAVA_OPTS=-Dproperties.file=/conf/features.properties -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true - CDA_JDBC_DRIVER=oracle.jdbc.driver.OracleDriver - CDA_JDBC_URL=jdbc:oracle:thin:@db/FREEPDB1 @@ -82,14 +90,12 @@ services: - CDA_POOL_MAX_IDLE=5 - CDA_POOL_MIN_IDLE=2 - cwms.dataapi.access.provider=MultipleAccessManager - - cwms.dataapi.access.providers=KeyAccessManager,OpenID + - cwms.dataapi.access.providers.surpress=CwmsAAACacAuth - cwms.dataapi.access.openid.create_users=true - - cwms.dataapi.access.openid.wellKnownUrl=http://auth:${APP_PORT:-8081}/auth/realms/cwms/.well-known/openid-configuration - - cwms.dataapi.access.openid.altAuthUrl=http://localhost:${APP_PORT:-8081} - - cwms.dataapi.access.openid.useAltWellKnown=true + - cwms.dataapi.access.openid.wellKnownUrl=http://localhost:${APP_PORT:-8081}/auth/realms/cwms/.well-known/openid-configuration - cwms.dataapi.access.openid.issuer=http://localhost:${APP_PORT:-8081}/auth/realms/cwms - cwms.dataapi.access.openid.clientId=cwms - # values are not actually used in the local keycloak, however it does fail and leaves them in place for various testing. + # values are not actually used in the local keycloak, however it doesn't fail and leaves them in place for various testing. - cwms.dataapi.access.openid.idpHint=federation-eams,login.gov - blob.store.endpoint=http://minio:9000 - blob.store.region=docker @@ -142,12 +148,14 @@ services: depends_on: traefik: condition: service_healthy - + data-api: + condition: service_healthy + # Proxy for HTTPS for OpenID traefik: - image: "traefik:v3.6.2" + image: "traefik:v3.7.10" ports: - "${APP_PORT:-8081}:80" expose: