Skip to content

Commit

Permalink
Improve OIDC configuration (#513)
Browse files Browse the repository at this point in the history
  • Loading branch information
olevitt authored Nov 5, 2024
1 parent 086217e commit 9d1119f
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 13 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ Configurable properties :
| Key | Default | Description |
| -------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `oidc.issuer-uri` | | Issuer URI, should be the same as the `iss` field of the tokens |
| `oidc.jwk-uri` | | JWK URI, useful when auto discovery is not available or when `iss` is not consistent across tokens (e.g [Google](https://stackoverflow.com/questions/38618826/can-i-get-a-consistent-iss-value-for-a-google-openidconnect-id-token)) |
| `oidc.skip-tls-verify` | `false` | Disable tls cert verification when retrieving keys from the IDP. Not intended for production. Consider mounting the proper `cacerts` instead of disabling the verification. |
| `oidc.jwk-uri` | | JWK URI, useful when auto discovery is not available or when `iss` is not consistent across tokens (e.g [Google](https://stackoverflow.com/questions/38618826/can-i-get-a-consistent-iss-value-for-a-google-openidconnect-id-token)) |
| `oidc.public-key` | | Public key used for validating incoming tokens. Don't provide this if you set `issuer-uri` or `jwk-uri` as it will be bootstrapped from that. This is useful if Onyxia-API has trouble connecting to your IDP (e.g self signed certificate). You can usually get this key directly by loading the issuer URI : (e.g `https://auth.example.com/realms/my-realm`) |
| `oidc.clientID` | | Client id to be used by Onyxia web application |
| `oidc.audience` | | Optional : audience to validate. Must be the same as the token's `aud` field |
| `oidc.username-claim` | `preferred_username` | Claim to be used as user id. Must conform to [RFC 1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names) |
Expand Down Expand Up @@ -116,4 +118,4 @@ Onyxia's catalogs are based on the Helm chart format and especially the `values.
Onyxia is **fully interoperable** with the Helm chart format which means you can use any helm chart repository as a onyxia catalog. But you probably want to use one that includes `values.schema.json` files (those files are optional in helm).
Onyxia extends this format to enhance it and provide more customization tools in the UI.

An example of such extension can be found [here](https://github.com/InseeFrLab/helm-charts-interactive-services/blob/main/charts/jupyter-python/values.schema.json#L190), see `x-onyxia`.
An example of such extension can be found [here](https://github.com/InseeFrLab/helm-charts-interactive-services/blob/main/charts/jupyter-python/values.schema.json#L190), see `x-onyxia`.
5 changes: 5 additions & 0 deletions onyxia-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@
<artifactId>java-semver</artifactId>
<version>0.10.2</version>
</dependency>

<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
</dependencies>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,28 @@
import fr.insee.onyxia.api.services.utils.HttpRequestUtils;
import fr.insee.onyxia.model.User;
import fr.insee.onyxia.model.region.Region;
import java.security.KeyFactory;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.net.ssl.SSLContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.ssl.TrustStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
Expand All @@ -15,6 +36,7 @@
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
Expand All @@ -26,12 +48,12 @@
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
Expand All @@ -49,6 +71,9 @@ public class OIDCConfiguration {
@Value("${oidc.issuer-uri}")
private String issuerUri;

@Value("${oidc.public-key}")
private String publicKey;

@Value("${oidc.jwk-uri}")
private String jwkUri;

Expand All @@ -58,11 +83,16 @@ public class OIDCConfiguration {
@Value("${oidc.clientID}")
private String clientID;

@Value("${oidc.skip-tls-verify}")
private boolean skipTlsVerify;

@Value("${oidc.extra-query-params}")
private String extraQueryParams;

private final HttpRequestUtils httpRequestUtils;

private static final Logger LOGGER = LoggerFactory.getLogger(OIDCConfiguration.class);

@Autowired
public OIDCConfiguration(HttpRequestUtils httpRequestUtils) {
this.httpRequestUtils = httpRequestUtils;
Expand Down Expand Up @@ -217,30 +247,89 @@ public String getExtraQueryParams() {
return extraQueryParams;
}

public String getPublicKey() {
return publicKey;
}

public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}

public void setExtraQueryParams(String extraQueryParams) {
this.extraQueryParams = extraQueryParams;
}

@Bean
@ConditionalOnProperty(prefix = "oidc", name = "issuer-uri")
NimbusJwtDecoder jwtDecoder() {
// If JWK is defined, use that instead of JWT issuer / audience validation
NimbusJwtDecoder decoder = null;
OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
if (StringUtils.isNotEmpty(jwkUri)) {
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkUri).build();
return decoder;
LOGGER.info("OIDC : using JWK URI {} to validate tokens", jwkUri);
decoder = NimbusJwtDecoder.withJwkSetUri(jwkUri).build();
} else if (StringUtils.isNotEmpty(publicKey)) {
LOGGER.info("OIDC : using public key {} to validate tokens", publicKey);
try {
byte[] decodedKey = Base64.getDecoder().decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKey parsedPublicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
decoder = NimbusJwtDecoder.withPublicKey(parsedPublicKey).build();
} catch (Exception e) {
LOGGER.error(
"Fatal : Could not parse or use provided public key, please double check",
e);
System.exit(0);
}
} else {
LOGGER.info("OIDC : using issuerURI {} to validate tokens", issuerUri);
if (skipTlsVerify) {
try {
decoder =
NimbusJwtDecoder.withIssuerLocation(issuerUri)
.restOperations(getRestTemplate())
.build();
} catch (Exception e) {
LOGGER.error("Fatal : failed to disable SSL verification", e);
System.exit(0);
}
} else {
decoder = NimbusJwtDecoder.withIssuerLocation(issuerUri).build();
}
validator = JwtValidators.createDefaultWithIssuer(issuerUri);
}

NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri);

OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);

OAuth2TokenValidator<Jwt> withAudience =
new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
new DelegatingOAuth2TokenValidator<>(validator, audienceValidator);

decoder.setJwtValidator(withAudience);

jwtDecoder.setJwtValidator(withAudience);
return decoder;
}

return jwtDecoder;
public RestTemplate getRestTemplate()
throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
HttpComponentsClientHttpRequestFactory requestFactoryHttp =
new HttpComponentsClientHttpRequestFactory();

TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
SSLContext sslContext =
SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
SSLConnectionSocketFactory sslsf =
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
Registry<ConnectionSocketFactory> socketFactoryRegistry =
RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslsf)
.register("http", new PlainConnectionSocketFactory())
.build();

BasicHttpClientConnectionManager connectionManager =
new BasicHttpClientConnectionManager(socketFactoryRegistry);
CloseableHttpClient httpClient =
HttpClients.custom().setConnectionManager(connectionManager).build();
requestFactoryHttp.setHttpClient(httpClient);
return new RestTemplate(requestFactoryHttp);
}

public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
Expand Down
2 changes: 2 additions & 0 deletions onyxia-api/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
authentication.mode=none
# Open id connect authentication
oidc.issuer-uri=
oidc.skip-tls-verify=false
oidc.jwk-uri=
oidc.public-key=
oidc.clientID=onyxia
oidc.audience=
oidc.username-claim=preferred_username
Expand Down

0 comments on commit 9d1119f

Please sign in to comment.