Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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://<prefix>/.well-known/openid-configuration"
ENV cwms.dataapi.access.openid.issuer="<issuer>"
ENV cwms.dataapi.access.openid.timeout="604800"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.ServiceLoader;

public class CdaIdentityProviders {

private static final ServiceLoader<IdentityProvider> loader = ServiceLoader.load(IdentityProvider.class);


Expand Down
5 changes: 5 additions & 0 deletions compose_files/api_entry.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

nohup ./proxy_auth.sh 2>&1 > /dev/null &
echo "auth proxy started now executing $*"
exec $*
Empty file.
5 changes: 5 additions & 0 deletions compose_files/proxy_auth.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

mkfifo backpipe
#while true; do nc -lk -p 7100 0<backpipe | nc auth 7100 1>backpipe; done
nc -lk -p ${APP_PORT:-8081} -e nc auth ${APP_PORT:-8081}
22 changes: 7 additions & 15 deletions cwms-data-api/src/main/java/cwms/cda/ApiServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -374,6 +376,7 @@ public void init() {
})
.attribute("PolicyFactory", sanitizer)
.attribute("ObjectMapper", om)
.attribute("schemeProcessor", schemeProcessor)
.before(authenticator)
.before(ctx -> {
ctx.attribute("sanitizer", sanitizer);
Expand Down Expand Up @@ -976,32 +979,20 @@ private void getOpenApiOptions(JavalinConfig config) {

String provider = CdaAccessManager.class.getSimpleName();


Components components = new Components();
final ArrayList<SecurityRequirement> 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<Server> 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")) {
Expand Down Expand Up @@ -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<SecurityRequirement> secReqs) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SecurityRequirement> 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<SecurityRequirement> getSecurityRequirements()
{
return Collections.unmodifiableList(secReqs);
}
}
11 changes: 8 additions & 3 deletions cwms-data-api/src/main/java/cwms/cda/security/Authenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ public final class Authenticator implements Handler {
private final ArrayList<IdentityProvider> 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());
}
});
}
Expand All @@ -36,7 +41,7 @@ public void handle(Context ctx) throws Exception {
}
}
}

public List<IdentityProvider> getActiveProviders() {
return Collections.unmodifiableList(providers);
}
Expand Down
136 changes: 123 additions & 13 deletions cwms-data-api/src/main/java/cwms/cda/security/OpenIDConfig.java
Original file line number Diff line number Diff line change
@@ -1,59 +1,90 @@
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;

import com.fasterxml.jackson.databind.JsonNode;
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) {
Expand All @@ -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<String,Key> 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;
}
}
}
Loading
Loading