From 2d52d39f7871fe65998a95ce99d7447c8f52e194 Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Mon, 4 Dec 2023 15:49:05 +0300 Subject: [PATCH 1/9] feat: Support multiple Identity Providers #74 --- .../java/com/epam/aidial/core/AiDial.java | 13 +- src/main/java/com/epam/aidial/core/Proxy.java | 5 +- .../core/security/AccessTokenValidator.java | 58 +++++++ .../core/security/IdentityProvider.java | 46 +++--- src/main/resources/aidial.settings.json | 16 +- .../com/epam/aidial/core/FileApiTest.java | 2 +- .../security/AccessTokenValidatorTest.java | 143 ++++++++++++++++++ .../core/security/IdentityProviderTest.java | 57 ++++--- src/test/resources/aidial.settings.json | 11 +- 9 files changed, 275 insertions(+), 76 deletions(-) create mode 100644 src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java create mode 100644 src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java diff --git a/src/main/java/com/epam/aidial/core/AiDial.java b/src/main/java/com/epam/aidial/core/AiDial.java index 428d834b7..0a2cc6a21 100644 --- a/src/main/java/com/epam/aidial/core/AiDial.java +++ b/src/main/java/com/epam/aidial/core/AiDial.java @@ -7,6 +7,7 @@ import com.epam.aidial.core.limiter.RateLimiter; import com.epam.aidial.core.log.GfLogStore; import com.epam.aidial.core.log.LogStore; +import com.epam.aidial.core.security.AccessTokenValidator; import com.epam.aidial.core.security.IdentityProvider; import com.epam.aidial.core.storage.BlobStorage; import com.epam.aidial.core.upstream.UpstreamBalancer; @@ -22,6 +23,7 @@ import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.json.Json; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.core.metrics.MetricsOptions; import io.vertx.micrometer.MicrometerMetricsOptions; @@ -65,19 +67,12 @@ void start() throws Exception { LogStore logStore = new GfLogStore(vertx); RateLimiter rateLimiter = new RateLimiter(); UpstreamBalancer upstreamBalancer = new UpstreamBalancer(); - - IdentityProvider identityProvider = new IdentityProvider(settings("identityProvider"), vertx, jwksUrl -> { - try { - return new UrlJwkProvider(new URL(jwksUrl)); - } catch (MalformedURLException e) { - throw new IllegalArgumentException(e); - } - }); + AccessTokenValidator accessTokenValidator = new AccessTokenValidator(settings.getJsonArray("identityProviders", new JsonArray()), vertx); if (storage == null) { Storage storageConfig = Json.decodeValue(settings("storage").toBuffer(), Storage.class); storage = new BlobStorage(storageConfig); } - Proxy proxy = new Proxy(vertx, client, configStore, logStore, rateLimiter, upstreamBalancer, identityProvider, storage); + Proxy proxy = new Proxy(vertx, client, configStore, logStore, rateLimiter, upstreamBalancer, accessTokenValidator, storage); server = vertx.createHttpServer(new HttpServerOptions(settings("server"))).requestHandler(proxy); open(server, HttpServer::listen); diff --git a/src/main/java/com/epam/aidial/core/Proxy.java b/src/main/java/com/epam/aidial/core/Proxy.java index 0dedbf19a..b74dcac5a 100644 --- a/src/main/java/com/epam/aidial/core/Proxy.java +++ b/src/main/java/com/epam/aidial/core/Proxy.java @@ -8,6 +8,7 @@ import com.epam.aidial.core.controller.ControllerSelector; import com.epam.aidial.core.limiter.RateLimiter; import com.epam.aidial.core.log.LogStore; +import com.epam.aidial.core.security.AccessTokenValidator; import com.epam.aidial.core.security.ExtractedClaims; import com.epam.aidial.core.security.IdentityProvider; import com.epam.aidial.core.storage.BlobStorage; @@ -53,7 +54,7 @@ public class Proxy implements Handler { private final LogStore logStore; private final RateLimiter rateLimiter; private final UpstreamBalancer upstreamBalancer; - private final IdentityProvider identityProvider; + private final AccessTokenValidator tokenValidator; private final BlobStorage storage; @Override @@ -129,7 +130,7 @@ private void handleRequest(HttpServerRequest request) throws Exception { request.pause(); final boolean isJwtMustBeValidated = key.getUserAuth() != UserAuth.DISABLED; - Future extractedClaims = identityProvider.extractClaims(authorization, isJwtMustBeValidated); + Future extractedClaims = tokenValidator.extractClaims(authorization, isJwtMustBeValidated); extractedClaims.onComplete(result -> { try { diff --git a/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java b/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java new file mode 100644 index 000000000..4061907a0 --- /dev/null +++ b/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java @@ -0,0 +1,58 @@ +package com.epam.aidial.core.security; + +import com.auth0.jwk.UrlJwkProvider; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.common.annotations.VisibleForTesting; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonArray; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +public class AccessTokenValidator { + + private final List providers = new ArrayList<>(); + + public AccessTokenValidator(JsonArray idpConfig, Vertx vertx) { + int size = idpConfig.size(); + if (size < 1) { + throw new IllegalArgumentException("At least one identity provider is required"); + } + for (int i = 0; i < idpConfig.size(); i++) { + providers.add(new IdentityProvider(idpConfig.getJsonObject(i), vertx, jwksUrl -> { + try { + return new UrlJwkProvider(new URL(jwksUrl)); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + })); + } + } + + public Future extractClaims(String authHeader, boolean isJwtMustBeValidated) { + try { + if (authHeader == null) { + return isJwtMustBeValidated ? Future.failedFuture(new IllegalArgumentException("Token is missed")) : Future.succeededFuture(); + } + String encodedToken = authHeader.split(" ")[1]; + DecodedJWT jwt = IdentityProvider.decodeJwtToken(encodedToken); + for (IdentityProvider idp : providers) { + if (idp.match(jwt)) { + return idp.extractClaims(jwt, isJwtMustBeValidated); + } + } + return Future.failedFuture(new IllegalArgumentException("Unknown Identity Provider")); + } catch (Throwable e) { + return Future.failedFuture(e); + } + } + + @VisibleForTesting + void setProviders(List providers) { + this.providers.clear(); + this.providers.addAll(providers); + } +} diff --git a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java index 16cf5491c..e5dfa4cc0 100644 --- a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java +++ b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java @@ -22,6 +22,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import java.util.regex.Pattern; import static java.util.Collections.EMPTY_LIST; @@ -49,6 +50,8 @@ public class IdentityProvider { private final long negativeCacheExpirationMs; + private final Pattern issuerPattern; + public IdentityProvider(JsonObject settings, Vertx vertx, Function jwkProviderSupplier) { if (settings == null) { throw new IllegalArgumentException("Identity provider settings are missed"); @@ -74,6 +77,9 @@ public IdentityProvider(JsonObject settings, Vertx vertx, Function evictExpiredJwks()); } @@ -114,7 +120,7 @@ private List extractUserRoles(DecodedJWT token) { return EMPTY_LIST; } - private DecodedJWT decodeJwtToken(String encodedToken) { + public static DecodedJWT decodeJwtToken(String encodedToken) { return JWT.decode(encodedToken); } @@ -132,21 +138,20 @@ private Future getJwk(String kid) { })); } - private Future decodeAndVerifyJwtToken(String encodedToken) { - DecodedJWT jwt = decodeJwtToken(encodedToken); + private Future verifyJwt(DecodedJWT jwt) { String kid = jwt.getKeyId(); Future future = getJwk(kid); - return future.map(jwkResult -> verifyJwt(encodedToken, jwkResult)); + return future.map(jwkResult -> verifyJwt(jwt, jwkResult)); } - private DecodedJWT verifyJwt(String encodedToken, JwkResult jwkResult) { + private DecodedJWT verifyJwt(DecodedJWT jwt, JwkResult jwkResult) { Exception error = jwkResult.error(); if (error != null) { throw new RuntimeException(error); } Jwk jwk = jwkResult.jwk(); try { - return JWT.require(Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null)).build().verify(encodedToken); + return JWT.require(Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null)).build().verify(jwt); } catch (JwkException e) { throw new RuntimeException(e); } @@ -173,28 +178,19 @@ private String extractUserHash(DecodedJWT decodedJwt) { return keyClaim; } - public Future extractClaims(String authHeader, boolean isJwtMustBeVerified) { - try { - if (authHeader == null) { - return isJwtMustBeVerified ? Future.failedFuture(new IllegalArgumentException("Token is missed")) : Future.succeededFuture(); - } - // Take the 1st authorization parameter from the header value: - // Authorization: - String encodedToken = authHeader.split(" ")[1]; - return extractClaimsFromEncodedToken(encodedToken, isJwtMustBeVerified); - } catch (Throwable e) { - return Future.failedFuture(e); + Future extractClaims(DecodedJWT decodedJwt, boolean isJwtMustBeVerified) { + if (decodedJwt == null) { + return isJwtMustBeVerified ? Future.failedFuture(new IllegalArgumentException("decoded JWT must not be null")) : Future.succeededFuture(); } + Future decodedJwtFuture = isJwtMustBeVerified ? verifyJwt(decodedJwt) + : Future.succeededFuture(decodedJwt); + return decodedJwtFuture.map(jwt -> new ExtractedClaims(extractUserSub(jwt), extractUserRoles(jwt), + extractUserHash(jwt))); } - public Future extractClaimsFromEncodedToken(String encodedToken, boolean isJwtMustBeVerified) { - if (encodedToken == null) { - return Future.succeededFuture(); - } - Future decodedJwt = isJwtMustBeVerified ? decodeAndVerifyJwtToken(encodedToken) - : Future.succeededFuture(decodeJwtToken(encodedToken)); - return decodedJwt.map(jwt -> new ExtractedClaims(extractUserSub(jwt), extractUserRoles(jwt), - extractUserHash(jwt))); + boolean match(DecodedJWT jwt) { + String issuer = jwt.getIssuer(); + return issuerPattern.matcher(issuer).matches(); } private record JwkResult(Jwk jwk, Exception error, long expirationTime) { diff --git a/src/main/resources/aidial.settings.json b/src/main/resources/aidial.settings.json index dfcab82f2..bedc9d668 100644 --- a/src/main/resources/aidial.settings.json +++ b/src/main/resources/aidial.settings.json @@ -38,10 +38,18 @@ "files": ["aidial.config.json"], "reload": 60000 }, - "identityProvider": { - "jwksUrl": "http://fakeJwksUrl:8080", - "rolePath": "roles" - }, + "identityProviders": [ + { + "jwksUrl": "http://fakeJwksUrl:8080", + "rolePath": "roles1", + "issuerPattern": "issuer1" + }, + { + "jwksUrl": "http://fakeJwksUrl:8081", + "rolePath": "roles2", + "issuerPattern": "issuer1" + } + ], "storage": { "provider" : "s3", "endpoint" : "http://localhost:9000", diff --git a/src/test/java/com/epam/aidial/core/FileApiTest.java b/src/test/java/com/epam/aidial/core/FileApiTest.java index ab7d79078..f45118c8a 100644 --- a/src/test/java/com/epam/aidial/core/FileApiTest.java +++ b/src/test/java/com/epam/aidial/core/FileApiTest.java @@ -375,6 +375,6 @@ private static MultipartForm generateMultipartForm(String fileName, String conte private static String generateJwtToken(String user) { Algorithm algorithm = Algorithm.HMAC256("secret_key"); - return JWT.create().withClaim("sub", user).sign(algorithm); + return JWT.create().withClaim("iss", "issuer").withClaim("sub", user).sign(algorithm); } } diff --git a/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java b/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java new file mode 100644 index 000000000..c4debd182 --- /dev/null +++ b/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java @@ -0,0 +1,143 @@ +package com.epam.aidial.core.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Collections; +import java.util.List; + +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class AccessTokenValidatorTest { + + @Mock + private Vertx vertx; + + private JsonArray idpConfig; + + @BeforeEach + public void beforeEach() { + idpConfig = new JsonArray(); + idpConfig.add(JsonObject.of("jwksUrl", "http://host1/keys", "rolePath", "role1", "issuerPattern", "issue1")); + idpConfig.add(JsonObject.of("jwksUrl", "http://host2/keys", "rolePath", "role2", "issuerPattern", "issue2")); + } + + @Test + public void testExtractClaims_00() { + AccessTokenValidator validator = new AccessTokenValidator(idpConfig, vertx); + Future future = validator.extractClaims(null, false); + assertNotNull(future); + future.onComplete(res -> { + assertTrue(res.succeeded()); + assertNull(res.result()); + }); + } + + + @Test + public void testExtractClaims_01() { + AccessTokenValidator validator = new AccessTokenValidator(idpConfig, vertx); + Future future = validator.extractClaims(null, true); + assertNotNull(future); + future.onComplete(res -> { + assertTrue(res.failed()); + assertNotNull(res.cause()); + }); + } + + @Test + public void testExtractClaims_02() { + AccessTokenValidator validator = new AccessTokenValidator(idpConfig, vertx); + Future future = validator.extractClaims("bad-auth-header", true); + assertNotNull(future); + future.onComplete(res -> { + assertTrue(res.failed()); + assertNotNull(res.cause()); + }); + } + + @Test + public void testExtractClaims_03() { + AccessTokenValidator validator = new AccessTokenValidator(idpConfig, vertx); + Future future = validator.extractClaims("bearer bad-token", true); + assertNotNull(future); + future.onComplete(res -> { + assertTrue(res.failed()); + assertNotNull(res.cause()); + }); + } + + @Test + public void testExtractClaims_04() throws NoSuchAlgorithmException { + AccessTokenValidator validator = new AccessTokenValidator(idpConfig, vertx); + IdentityProvider provider1 = mock(IdentityProvider.class); + when(provider1.match(any(DecodedJWT.class))).thenReturn(false); + IdentityProvider provider2 = mock(IdentityProvider.class); + when(provider2.match(any(DecodedJWT.class))).thenReturn(false); + List providerList = List.of(provider1, provider2); + validator.setProviders(providerList); + KeyPair keyPair = generateRsa256Pair(); + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate()); + String token = JWT.create().withClaim("iss", "unknown-issuer").sign(algorithm); + Future future = validator.extractClaims(getBearerHeaderValue(token), true); + assertNotNull(future); + future.onComplete(res -> { + assertTrue(res.failed()); + assertNotNull(res.cause()); + }); + } + + @Test + public void testExtractClaims_05() throws NoSuchAlgorithmException { + AccessTokenValidator validator = new AccessTokenValidator(idpConfig, vertx); + IdentityProvider provider1 = mock(IdentityProvider.class); + when(provider1.match(any(DecodedJWT.class))).thenReturn(false); + IdentityProvider provider2 = mock(IdentityProvider.class); + when(provider2.match(any(DecodedJWT.class))).thenReturn(true); + when(provider2.extractClaims(any(DecodedJWT.class), eq(true))).thenReturn(Future.succeededFuture(new ExtractedClaims("sub", Collections.emptyList(), "hash"))); + List providerList = List.of(provider1, provider2); + validator.setProviders(providerList); + KeyPair keyPair = generateRsa256Pair(); + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate()); + String token = JWT.create().withClaim("iss", "issuer2").sign(algorithm); + Future future = validator.extractClaims(getBearerHeaderValue(token), true); + assertNotNull(future); + future.onComplete(res -> { + assertTrue(res.succeeded()); + assertNotNull(res.result()); + }); + } + + private static String getBearerHeaderValue(String token) { + return String.format("bearer %s", token); + } + + private static KeyPair generateRsa256Pair() throws NoSuchAlgorithmException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(512); + return keyGen.genKeyPair(); + } + +} diff --git a/src/test/java/com/epam/aidial/core/security/IdentityProviderTest.java b/src/test/java/com/epam/aidial/core/security/IdentityProviderTest.java index 3d51c3e9e..a2415817d 100644 --- a/src/test/java/com/epam/aidial/core/security/IdentityProviderTest.java +++ b/src/test/java/com/epam/aidial/core/security/IdentityProviderTest.java @@ -5,6 +5,7 @@ import com.auth0.jwk.JwkProvider; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Promise; @@ -27,6 +28,7 @@ import java.util.Map; 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; @@ -58,6 +60,7 @@ public void beforeEach() { settings = new JsonObject(); settings.put("jwksUrl", "http://host/jwks"); settings.put("rolePath", "roles"); + settings.put("issuerPattern", "issuer"); } @Test @@ -71,35 +74,13 @@ public void testExtractClaims_00() { }); } - @Test - public void testExtractClaims_01() { - IdentityProvider identityProvider = new IdentityProvider(settings, vertx, url -> jwkProvider); - Future result = identityProvider.extractClaims("bad-token", true); - assertNotNull(result); - result.onComplete(res -> { - assertTrue(res.failed()); - assertNotNull(res.cause()); - }); - } - - @Test - public void testExtractClaims_02() { - IdentityProvider identityProvider = new IdentityProvider(settings, vertx, url -> jwkProvider); - Future result = identityProvider.extractClaims("bad-token", true); - assertNotNull(result); - result.onComplete(res -> { - assertTrue(res.failed()); - assertNotNull(res.cause()); - }); - } - @Test public void testExtractClaims_03() { IdentityProvider identityProvider = new IdentityProvider(settings, vertx, url -> jwkProvider); Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate()); String token = JWT.create().withClaim("roles", List.of("manager")).sign(algorithm); - Future result = identityProvider.extractClaims(getBearerToken(token), false); + Future result = identityProvider.extractClaims(JWT.decode(token), false); assertNotNull(result); result.onComplete(res -> { assertTrue(res.succeeded()); @@ -117,7 +98,7 @@ public void testExtractClaims_04() { Map claim = Map.of("some", "val", "k1", 12, "p1", Map.of("p2", Map.of("p3", List.of("r1", "r2")))); String token = JWT.create().withClaim("p0", claim).sign(algorithm); - Future result = identityProvider.extractClaims(getBearerToken(token), false); + Future result = identityProvider.extractClaims(JWT.decode(token), false); assertNotNull(result); result.onComplete(res -> { assertTrue(res.succeeded()); @@ -135,7 +116,7 @@ public void testExtractClaims_05() { Map claim = Map.of("some", "val", "k1", 12, "p1", List.of("r1", "r2")); String token = JWT.create().withClaim("p0", claim).sign(algorithm); - Future result = identityProvider.extractClaims(getBearerToken(token), false); + Future result = identityProvider.extractClaims(JWT.decode(token), false); assertNotNull(result); result.onComplete(res -> { assertTrue(res.succeeded()); @@ -153,7 +134,7 @@ public void testExtractClaims_06() { Map claim = Map.of("some", "val", "k1", 12, "p1", Map.of("p2", List.of("p3", List.of("r1", "r2")))); String token = JWT.create().withClaim("p0", claim).sign(algorithm); - Future result = identityProvider.extractClaims(getBearerToken(token), false); + Future result = identityProvider.extractClaims(JWT.decode(token), false); assertNotNull(result); result.onComplete(res -> { assertTrue(res.succeeded()); @@ -177,7 +158,7 @@ public void testExtractClaims_07() throws JwkException { return p.future(); }); String token = JWT.create().withHeader(Map.of("kid", "kid1")).withClaim("roles", List.of("manager")).sign(algorithm); - Future result = identityProvider.extractClaims(getBearerToken(token), true); + Future result = identityProvider.extractClaims(JWT.decode(token), true); assertNotNull(result); result.onComplete(res -> { assertTrue(res.succeeded()); @@ -199,7 +180,7 @@ public void testExtractClaims_08() throws JwkException { return p.future(); }); String token = JWT.create().withHeader(Map.of("kid", "kid1")).withClaim("roles", List.of("manager")).sign(algorithm); - Future result = identityProvider.extractClaims(getBearerToken(token), true); + Future result = identityProvider.extractClaims(JWT.decode(token), true); assertNotNull(result); result.onComplete(res -> { assertTrue(res.failed()); @@ -234,7 +215,7 @@ public void testExtractClaims_10() throws JwkException, NoSuchAlgorithmException return p.future(); }); String token = JWT.create().withHeader(Map.of("kid", "kid1")).withClaim("roles", List.of("manager")).sign(algorithm); - Future result = identityProvider.extractClaims(getBearerToken(token), true); + Future result = identityProvider.extractClaims(JWT.decode(token), true); assertNotNull(result); result.onComplete(res -> { assertTrue(res.failed()); @@ -242,8 +223,22 @@ public void testExtractClaims_10() throws JwkException, NoSuchAlgorithmException }); } - private static String getBearerToken(String token) { - return String.format("Bearer %s", token); + @Test + public void testMatch_Failure() { + IdentityProvider identityProvider = new IdentityProvider(settings, vertx, url -> jwkProvider); + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate()); + String token = JWT.create().withClaim("iss", "bad-iss").sign(algorithm); + DecodedJWT jwt = JWT.decode(token); + assertFalse(identityProvider.match(jwt)); + } + + @Test + public void testMatch_Success() { + IdentityProvider identityProvider = new IdentityProvider(settings, vertx, url -> jwkProvider); + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate()); + String token = JWT.create().withClaim("iss", "issuer").sign(algorithm); + DecodedJWT jwt = JWT.decode(token); + assertTrue(identityProvider.match(jwt)); } private static KeyPair generateRsa256Pair() throws NoSuchAlgorithmException { diff --git a/src/test/resources/aidial.settings.json b/src/test/resources/aidial.settings.json index 83c0211dd..3a16bb853 100644 --- a/src/test/resources/aidial.settings.json +++ b/src/test/resources/aidial.settings.json @@ -38,8 +38,11 @@ "files": ["aidial.config.json"], "reload": 60000 }, - "identityProvider": { - "jwksUrl": "http://fakeJwksUrl:8080", - "rolePath": "roles" - } + "identityProviders": [ + { + "jwksUrl": "http://fakeJwksUrl:8080", + "rolePath": "roles", + "issuerPattern": "issuer" + } + ] } From 4cbed1665fb773fc8aab87dc09970ecc5f3b7e62 Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Mon, 4 Dec 2023 16:26:04 +0300 Subject: [PATCH 2/9] chore: update readme --- README.md | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a51e5082e..a8e38dd24 100644 --- a/README.md +++ b/README.md @@ -27,26 +27,27 @@ Static settings are used on startup and cannot be changed while application is r * File specified in "AIDIAL_SETTINGS" environment variable. * Default resource file: src/main/resources/aidial.settings.json. -| Setting | Default |Description -|--------------------------------------|--------------------|- -| config.files | aidial.config.json |Config files with parts of the whole config. -| config.reload | 60000 |Config reload interval in milliseconds. -| identityProvider.jwksUrl | - |Url to jwks provider. -| identityProvider.rolePath | - |Path to the claim user roles in JWT token, e.g. `resource_access.chatbot-ui.roles` or just `roles`. -| identityProvider.loggingKey | - |User information to search in claims of JWT token. -| identityProvider.loggingSalt | - |Salt to hash user information for logging. -| identityProvider.cacheSize | 10 |How many JWT tokens to cache. -| identityProvider.cacheExpiration | 10 |How long to retain JWT token in cache. -| identityProvider.cacheExpirationUnit | MINUTES |Unit of cache expiration. -| vertx.* | - |Vertx settings. -| server.* | - |Vertx HTTP server settings for incoming requests. -| client.* | - |Vertx HTTP client settings for outbound requests. -| storage.provider | - |Specifies blob storage provider. Supported providers: s3, aws-s3, azureblob, google-cloud-storage -| storage.endpoint | - |Optional. Specifies endpoint url for s3 compatible storages -| storage.identity | - |Blob storage access key -| storage.credential | - |Blob storage secret key -| storage.bucket | - |Blob storage bucket -| storage.createBucket | false |Indicates whether bucket should be created on start-up +| Setting | Default |Description +|------------------------------------------|--------------------|- +| config.files | aidial.config.json |Config files with parts of the whole config. +| config.reload | 60000 |Config reload interval in milliseconds. +| identityProviders | - |List of identity providers +| identityProviders[*].jwksUrl | - |Url to jwks provider. +| identityProviders[*].rolePath | - |Path to the claim user roles in JWT token, e.g. `resource_access.chatbot-ui.roles` or just `roles`. +| identityProviders[*].loggingKey | - |User information to search in claims of JWT token. +| identityProviders[*].loggingSalt | - |Salt to hash user information for logging. +| identityProviders[*].cacheSize | 10 |How many JWT tokens to cache. +| identityProviders[*].cacheExpiration | 10 |How long to retain JWT token in cache. +| identityProviders[*].cacheExpirationUnit | MINUTES |Unit of cache expiration. +| vertx.* | - |Vertx settings. +| server.* | - |Vertx HTTP server settings for incoming requests. +| client.* | - |Vertx HTTP client settings for outbound requests. +| storage.provider | - |Specifies blob storage provider. Supported providers: s3, aws-s3, azureblob, google-cloud-storage +| storage.endpoint | - |Optional. Specifies endpoint url for s3 compatible storages +| storage.identity | - |Blob storage access key +| storage.credential | - |Blob storage secret key +| storage.bucket | - |Blob storage bucket +| storage.createBucket | false |Indicates whether bucket should be created on start-up ### Dynamic settings Dynamic settings are stored in JSON files, specified via "config.files" static setting, and reloaded at interval, specified via "config.reload" static setting. From 042254804c980929ac25e5584e73b09525439daa Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Mon, 4 Dec 2023 16:33:27 +0300 Subject: [PATCH 3/9] chore: update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a8e38dd24..f34215dce 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Static settings are used on startup and cannot be changed while application is r | identityProviders[*].cacheSize | 10 |How many JWT tokens to cache. | identityProviders[*].cacheExpiration | 10 |How long to retain JWT token in cache. | identityProviders[*].cacheExpirationUnit | MINUTES |Unit of cache expiration. +| identityProviders[*].issuerPattern | - |Regexp to match the claim "iss" to identity provider | vertx.* | - |Vertx settings. | server.* | - |Vertx HTTP server settings for incoming requests. | client.* | - |Vertx HTTP client settings for outbound requests. From 498123e0fcb1ef03f8370fa843a9c484f47d965c Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Tue, 5 Dec 2023 12:39:11 +0300 Subject: [PATCH 4/9] chore: run idp directly in case of single idp in the list --- .../core/security/AccessTokenValidator.java | 3 +++ .../core/security/IdentityProvider.java | 10 +++++++-- .../security/AccessTokenValidatorTest.java | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java b/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java index 4061907a0..1ed66b0b3 100644 --- a/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java +++ b/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java @@ -39,6 +39,9 @@ public Future extractClaims(String authHeader, boolean isJwtMus } String encodedToken = authHeader.split(" ")[1]; DecodedJWT jwt = IdentityProvider.decodeJwtToken(encodedToken); + if (providers.size() == 1) { + return providers.get(0).extractClaims(jwt, isJwtMustBeValidated); + } for (IdentityProvider idp : providers) { if (idp.match(jwt)) { return idp.extractClaims(jwt, isJwtMustBeValidated); diff --git a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java index e5dfa4cc0..63c12b00b 100644 --- a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java +++ b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java @@ -50,7 +50,7 @@ public class IdentityProvider { private final long negativeCacheExpirationMs; - private final Pattern issuerPattern; + private Pattern issuerPattern; public IdentityProvider(JsonObject settings, Vertx vertx, Function jwkProviderSupplier) { if (settings == null) { @@ -78,7 +78,10 @@ public IdentityProvider(JsonObject settings, Vertx vertx, Function evictExpiredJwks()); @@ -189,6 +192,9 @@ Future extractClaims(DecodedJWT decodedJwt, boolean isJwtMustBe } boolean match(DecodedJWT jwt) { + if (issuerPattern == null) { + return false; + } String issuer = jwt.getIssuer(); return issuerPattern.matcher(issuer).matches(); } diff --git a/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java b/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java index c4debd182..4a1dab676 100644 --- a/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java +++ b/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java @@ -27,6 +27,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -130,6 +132,25 @@ public void testExtractClaims_05() throws NoSuchAlgorithmException { }); } + @Test + public void testExtractClaims_06() throws NoSuchAlgorithmException { + AccessTokenValidator validator = new AccessTokenValidator(idpConfig, vertx); + IdentityProvider provider = mock(IdentityProvider.class); + when(provider.extractClaims(any(DecodedJWT.class), eq(true))).thenReturn(Future.succeededFuture(new ExtractedClaims("sub", Collections.emptyList(), "hash"))); + List providerList = List.of(provider); + validator.setProviders(providerList); + KeyPair keyPair = generateRsa256Pair(); + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate()); + String token = JWT.create().withClaim("iss", "issuer").sign(algorithm); + Future future = validator.extractClaims(getBearerHeaderValue(token), true); + assertNotNull(future); + future.onComplete(res -> { + assertTrue(res.succeeded()); + assertNotNull(res.result()); + verify(provider, never()).match(any(DecodedJWT.class)); + }); + } + private static String getBearerHeaderValue(String token) { return String.format("bearer %s", token); } From 91c65f58787973ce703db953fb90454d99cc405a Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Tue, 5 Dec 2023 16:07:36 +0300 Subject: [PATCH 5/9] fix: fix Unit tests --- .../core/security/AccessTokenValidatorTest.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java b/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java index 4a1dab676..8c70c07a6 100644 --- a/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java +++ b/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -128,7 +129,11 @@ public void testExtractClaims_05() throws NoSuchAlgorithmException { assertNotNull(future); future.onComplete(res -> { assertTrue(res.succeeded()); - assertNotNull(res.result()); + ExtractedClaims claims = res.result(); + assertNotNull(claims); + assertEquals("sub", claims.sub()); + assertEquals(Collections.emptyList(), claims.userRoles()); + assertEquals("hash", claims.userHash()); }); } @@ -146,7 +151,11 @@ public void testExtractClaims_06() throws NoSuchAlgorithmException { assertNotNull(future); future.onComplete(res -> { assertTrue(res.succeeded()); - assertNotNull(res.result()); + ExtractedClaims claims = res.result(); + assertNotNull(claims); + assertEquals("sub", claims.sub()); + assertEquals(Collections.emptyList(), claims.userRoles()); + assertEquals("hash", claims.userHash()); verify(provider, never()).match(any(DecodedJWT.class)); }); } From 279fe859140c484579ce307e14dfec8782782bdd Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Tue, 5 Dec 2023 18:51:44 +0300 Subject: [PATCH 6/9] fix: make issuerPattern final --- .../java/com/epam/aidial/core/security/IdentityProvider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java index 63c12b00b..740cb61ee 100644 --- a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java +++ b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java @@ -50,7 +50,7 @@ public class IdentityProvider { private final long negativeCacheExpirationMs; - private Pattern issuerPattern; + private final Pattern issuerPattern; public IdentityProvider(JsonObject settings, Vertx vertx, Function jwkProviderSupplier) { if (settings == null) { @@ -81,6 +81,8 @@ public IdentityProvider(JsonObject settings, Vertx vertx, Function Date: Fri, 8 Dec 2023 14:27:54 +0300 Subject: [PATCH 7/9] chore: change data type of identity providers in config providers --- src/main/java/com/epam/aidial/core/AiDial.java | 2 +- .../epam/aidial/core/security/AccessTokenValidator.java | 8 ++++---- src/main/resources/aidial.settings.json | 8 ++++---- .../aidial/core/security/AccessTokenValidatorTest.java | 9 ++++----- src/test/resources/aidial.settings.json | 6 +++--- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/epam/aidial/core/AiDial.java b/src/main/java/com/epam/aidial/core/AiDial.java index 0a2cc6a21..97cebe8be 100644 --- a/src/main/java/com/epam/aidial/core/AiDial.java +++ b/src/main/java/com/epam/aidial/core/AiDial.java @@ -67,7 +67,7 @@ void start() throws Exception { LogStore logStore = new GfLogStore(vertx); RateLimiter rateLimiter = new RateLimiter(); UpstreamBalancer upstreamBalancer = new UpstreamBalancer(); - AccessTokenValidator accessTokenValidator = new AccessTokenValidator(settings.getJsonArray("identityProviders", new JsonArray()), vertx); + AccessTokenValidator accessTokenValidator = new AccessTokenValidator(settings("identityProviders"), vertx); if (storage == null) { Storage storageConfig = Json.decodeValue(settings("storage").toBuffer(), Storage.class); storage = new BlobStorage(storageConfig); diff --git a/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java b/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java index 1ed66b0b3..f46621c65 100644 --- a/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java +++ b/src/main/java/com/epam/aidial/core/security/AccessTokenValidator.java @@ -5,7 +5,7 @@ import com.google.common.annotations.VisibleForTesting; import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; import java.net.MalformedURLException; import java.net.URL; @@ -16,13 +16,13 @@ public class AccessTokenValidator { private final List providers = new ArrayList<>(); - public AccessTokenValidator(JsonArray idpConfig, Vertx vertx) { + public AccessTokenValidator(JsonObject idpConfig, Vertx vertx) { int size = idpConfig.size(); if (size < 1) { throw new IllegalArgumentException("At least one identity provider is required"); } - for (int i = 0; i < idpConfig.size(); i++) { - providers.add(new IdentityProvider(idpConfig.getJsonObject(i), vertx, jwksUrl -> { + for (String idpKey : idpConfig.fieldNames()) { + providers.add(new IdentityProvider(idpConfig.getJsonObject(idpKey), vertx, jwksUrl -> { try { return new UrlJwkProvider(new URL(jwksUrl)); } catch (MalformedURLException e) { diff --git a/src/main/resources/aidial.settings.json b/src/main/resources/aidial.settings.json index bedc9d668..7efa82300 100644 --- a/src/main/resources/aidial.settings.json +++ b/src/main/resources/aidial.settings.json @@ -38,18 +38,18 @@ "files": ["aidial.config.json"], "reload": 60000 }, - "identityProviders": [ - { + "identityProviders": { + "idp1": { "jwksUrl": "http://fakeJwksUrl:8080", "rolePath": "roles1", "issuerPattern": "issuer1" }, - { + "idp2": { "jwksUrl": "http://fakeJwksUrl:8081", "rolePath": "roles2", "issuerPattern": "issuer1" } - ], + }, "storage": { "provider" : "s3", "endpoint" : "http://localhost:9000", diff --git a/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java b/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java index 8c70c07a6..42acb99ee 100644 --- a/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java +++ b/src/test/java/com/epam/aidial/core/security/AccessTokenValidatorTest.java @@ -5,7 +5,6 @@ import com.auth0.jwt.interfaces.DecodedJWT; import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,13 +37,13 @@ public class AccessTokenValidatorTest { @Mock private Vertx vertx; - private JsonArray idpConfig; + private JsonObject idpConfig; @BeforeEach public void beforeEach() { - idpConfig = new JsonArray(); - idpConfig.add(JsonObject.of("jwksUrl", "http://host1/keys", "rolePath", "role1", "issuerPattern", "issue1")); - idpConfig.add(JsonObject.of("jwksUrl", "http://host2/keys", "rolePath", "role2", "issuerPattern", "issue2")); + idpConfig = new JsonObject(); + idpConfig.put("idp1", JsonObject.of("jwksUrl", "http://host1/keys", "rolePath", "role1", "issuerPattern", "issue1")); + idpConfig.put("ipd2", JsonObject.of("jwksUrl", "http://host2/keys", "rolePath", "role2", "issuerPattern", "issue2")); } @Test diff --git a/src/test/resources/aidial.settings.json b/src/test/resources/aidial.settings.json index 3a16bb853..681cdecd6 100644 --- a/src/test/resources/aidial.settings.json +++ b/src/test/resources/aidial.settings.json @@ -38,11 +38,11 @@ "files": ["aidial.config.json"], "reload": 60000 }, - "identityProviders": [ - { + "identityProviders": { + "ipd1": { "jwksUrl": "http://fakeJwksUrl:8080", "rolePath": "roles", "issuerPattern": "issuer" } - ] + } } From 1937057f8510c84a3e6e195924ea34cb6887d0bf Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Fri, 8 Dec 2023 16:48:24 +0300 Subject: [PATCH 8/9] fix: fix extract roles --- .../aidial/core/security/IdentityProvider.java | 2 +- .../core/security/IdentityProviderTest.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java index 740cb61ee..0145c3563 100644 --- a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java +++ b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java @@ -105,7 +105,7 @@ private List extractUserRoles(DecodedJWT token) { return token.getClaim(rolePath[0]).asList(String.class); } Map claim = token.getClaim(rolePath[0]).asMap(); - for (int i = 1; i < rolePath.length; i++) { + for (int i = 1; claim != null && i < rolePath.length; i++) { Object next = claim.get(rolePath[i]); if (next == null) { return EMPTY_LIST; diff --git a/src/test/java/com/epam/aidial/core/security/IdentityProviderTest.java b/src/test/java/com/epam/aidial/core/security/IdentityProviderTest.java index a2415817d..18b62dff8 100644 --- a/src/test/java/com/epam/aidial/core/security/IdentityProviderTest.java +++ b/src/test/java/com/epam/aidial/core/security/IdentityProviderTest.java @@ -223,6 +223,23 @@ public void testExtractClaims_10() throws JwkException, NoSuchAlgorithmException }); } + @Test + public void testExtractClaims_11() { + settings.put("rolePath", "p0.p1.p2.p3"); + IdentityProvider identityProvider = new IdentityProvider(settings, vertx, url -> jwkProvider); + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate()); + + String token = JWT.create().withClaim("some", "val").sign(algorithm); + Future result = identityProvider.extractClaims(JWT.decode(token), false); + assertNotNull(result); + result.onComplete(res -> { + assertTrue(res.succeeded()); + ExtractedClaims claims = res.result(); + assertNotNull(claims); + assertEquals(Collections.emptyList(), claims.userRoles()); + }); + } + @Test public void testMatch_Failure() { IdentityProvider identityProvider = new IdentityProvider(settings, vertx, url -> jwkProvider); From 440d4fea2a657bd4702e3670584f825b91ff64b5 Mon Sep 17 00:00:00 2001 From: Aliaksandr Stsiapanay Date: Fri, 8 Dec 2023 18:45:53 +0300 Subject: [PATCH 9/9] fix: fix PR comments --- README.md | 44 +++++++++---------- .../core/security/IdentityProvider.java | 8 +++- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index f34215dce..8cc988e9a 100644 --- a/README.md +++ b/README.md @@ -27,28 +27,28 @@ Static settings are used on startup and cannot be changed while application is r * File specified in "AIDIAL_SETTINGS" environment variable. * Default resource file: src/main/resources/aidial.settings.json. -| Setting | Default |Description -|------------------------------------------|--------------------|- -| config.files | aidial.config.json |Config files with parts of the whole config. -| config.reload | 60000 |Config reload interval in milliseconds. -| identityProviders | - |List of identity providers -| identityProviders[*].jwksUrl | - |Url to jwks provider. -| identityProviders[*].rolePath | - |Path to the claim user roles in JWT token, e.g. `resource_access.chatbot-ui.roles` or just `roles`. -| identityProviders[*].loggingKey | - |User information to search in claims of JWT token. -| identityProviders[*].loggingSalt | - |Salt to hash user information for logging. -| identityProviders[*].cacheSize | 10 |How many JWT tokens to cache. -| identityProviders[*].cacheExpiration | 10 |How long to retain JWT token in cache. -| identityProviders[*].cacheExpirationUnit | MINUTES |Unit of cache expiration. -| identityProviders[*].issuerPattern | - |Regexp to match the claim "iss" to identity provider -| vertx.* | - |Vertx settings. -| server.* | - |Vertx HTTP server settings for incoming requests. -| client.* | - |Vertx HTTP client settings for outbound requests. -| storage.provider | - |Specifies blob storage provider. Supported providers: s3, aws-s3, azureblob, google-cloud-storage -| storage.endpoint | - |Optional. Specifies endpoint url for s3 compatible storages -| storage.identity | - |Blob storage access key -| storage.credential | - |Blob storage secret key -| storage.bucket | - |Blob storage bucket -| storage.createBucket | false |Indicates whether bucket should be created on start-up +| Setting | Default |Description +|-----------------------------------------|--------------------|- +| config.files | aidial.config.json |Config files with parts of the whole config. +| config.reload | 60000 |Config reload interval in milliseconds. +| identityProviders | - |List of identity providers +| identityProviders.*.jwksUrl | - |Url to jwks provider. +| identityProviders.*.rolePath | - |Path to the claim user roles in JWT token, e.g. `resource_access.chatbot-ui.roles` or just `roles`. +| identityProviders.*.loggingKey | - |User information to search in claims of JWT token. +| identityProviders.*.loggingSalt | - |Salt to hash user information for logging. +| identityProviders.*.cacheSize | 10 |How many JWT tokens to cache. +| identityProviders.*.cacheExpiration | 10 |How long to retain JWT token in cache. +| identityProviders.*.cacheExpirationUnit | MINUTES |Unit of cache expiration. +| identityProviders.*.issuerPattern | - |Regexp to match the claim "iss" to identity provider +| vertx.* | - |Vertx settings. +| server.* | - |Vertx HTTP server settings for incoming requests. +| client.* | - |Vertx HTTP client settings for outbound requests. +| storage.provider | - |Specifies blob storage provider. Supported providers: s3, aws-s3, azureblob, google-cloud-storage +| storage.endpoint | - |Optional. Specifies endpoint url for s3 compatible storages +| storage.identity | - |Blob storage access key +| storage.credential | - |Blob storage secret key +| storage.bucket | - |Blob storage bucket +| storage.createBucket | false |Indicates whether bucket should be created on start-up ### Dynamic settings Dynamic settings are stored in JSON files, specified via "config.files" static setting, and reloaded at interval, specified via "config.reload" static setting. diff --git a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java index 0145c3563..5c0dbe590 100644 --- a/src/main/java/com/epam/aidial/core/security/IdentityProvider.java +++ b/src/main/java/com/epam/aidial/core/security/IdentityProvider.java @@ -102,10 +102,14 @@ private void evictExpiredJwks() { @SuppressWarnings("unchecked") private List extractUserRoles(DecodedJWT token) { if (rolePath.length == 1) { - return token.getClaim(rolePath[0]).asList(String.class); + List roles = token.getClaim(rolePath[0]).asList(String.class); + return roles == null ? EMPTY_LIST : roles; } Map claim = token.getClaim(rolePath[0]).asMap(); - for (int i = 1; claim != null && i < rolePath.length; i++) { + if (claim == null) { + return EMPTY_LIST; + } + for (int i = 1; i < rolePath.length; i++) { Object next = claim.get(rolePath[i]); if (next == null) { return EMPTY_LIST;