Index: /FCSSimpleEndpoint/trunk/pom.xml
===================================================================
--- /FCSSimpleEndpoint/trunk/pom.xml	(revision 7266)
+++ /FCSSimpleEndpoint/trunk/pom.xml	(revision 7267)
@@ -30,4 +30,16 @@
 
         <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-ext-jdk15on</artifactId>
+          <version>1.70</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.18.2</version>
+        </dependency>
+
+        <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>servlet-api</artifactId>
@@ -40,5 +52,5 @@
             <groupId>eu.clarin.sru</groupId>
             <artifactId>sru-server</artifactId>
-            <version>1.8.0</version>
+            <version>1.10.0-SNAPSHOT</version>
         </dependency>
 
Index: /FCSSimpleEndpoint/trunk/src/main/java/eu/clarin/sru/server/fcs/SimpleEndpointSearchEngineBase.java
===================================================================
--- /FCSSimpleEndpoint/trunk/src/main/java/eu/clarin/sru/server/fcs/SimpleEndpointSearchEngineBase.java	(revision 7266)
+++ /FCSSimpleEndpoint/trunk/src/main/java/eu/clarin/sru/server/fcs/SimpleEndpointSearchEngineBase.java	(revision 7267)
@@ -17,7 +17,10 @@
 package eu.clarin.sru.server.fcs;
 
+import java.io.File;
+import java.io.InputStream;
 import java.net.URI;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 
 import javax.servlet.ServletContext;
@@ -30,4 +33,6 @@
 import org.z3950.zing.cql.CQLNode;
 import org.z3950.zing.cql.CQLTermNode;
+
+import eu.clarin.sru.server.SRUAuthenticationInfoProvider;
 import eu.clarin.sru.server.SRUConfigException;
 import eu.clarin.sru.server.SRUConstants;
@@ -41,4 +46,6 @@
 import eu.clarin.sru.server.SRUServer;
 import eu.clarin.sru.server.SRUServerConfig;
+import eu.clarin.sru.server.fcs.utils.AuthenticationProvider;
+import eu.clarin.sru.server.utils.SRUAuthenticationInfoProviderFactory;
 import eu.clarin.sru.server.utils.SRUSearchEngineBase;
 
@@ -50,5 +57,20 @@
  */
 public abstract class SimpleEndpointSearchEngineBase extends
-        SRUSearchEngineBase {
+        SRUSearchEngineBase implements SRUAuthenticationInfoProviderFactory {
+    public static final String FCS_AUTHENTICATION_ENABLE_PARAM =
+            "eu.clarin.sru.server.fcs.authentication.enable";
+    public static final String FCS_AUTHENTICATION_AUDIENCE_PARAM =
+            "eu.clarin.sru.server.fcs.authentication.audience";
+    public static final String FCS_AUTHENTICATION_IGNORE_ISSUEDAT_PARAM =
+            "eu.clarin.sru.server.fcs.authentication.ignoreIssuedAt";
+    public static final String FCS_AUTHENTICATION_ACCEPT_ISSUEDAT_PARAM =
+            "eu.clarin.sru.server.fcs.authentication.acceptIssuedAt";
+    public static final String FCS_AUTHENTICATION_ACCEPT_EXPIRESAT_PARAM =
+            "eu.clarin.sru.server.fcs.authentication.acceptExpiresAt";
+    public static final String FCS_AUTHENTICATION_ACCEPT_NOTBEFORE_PARAM =
+            "eu.clarin.sru.server.fcs.authentication.acceptNotBefore";
+    public static final String FCS_AUTHENTICATION_PUBLIC_KEY_PARAM_PREFIX =
+            "eu.clarin.sru.server.fcs.authentication.key.";
+    private static final String RESOURCE_URI_PREFIX = "resource:";
     private static final String X_FCS_ENDPOINT_DESCRIPTION =
             "x-fcs-endpoint-description";
@@ -108,4 +130,89 @@
         doDestroy();
         super.destroy();
+    }
+
+
+    @Override
+    public SRUAuthenticationInfoProvider createAuthenticationInfoProvider(
+            ServletContext context, Map<String, String> params)
+            throws SRUConfigException {
+        String enableAuthentication = params.get(FCS_AUTHENTICATION_ENABLE_PARAM);
+        if (enableAuthentication != null) {
+            if (parseBoolean(enableAuthentication)) {
+                logger.debug("enabling authentication");                
+                AuthenticationProvider.Builder builder =
+                        AuthenticationProvider.Builder.create();
+                
+                String audience = params.get(FCS_AUTHENTICATION_AUDIENCE_PARAM);
+                if (audience != null) {
+                    String[] values = audience.split("\\s*,\\s*");
+                    if (values != null) {
+                        for (String value : values) {
+                            logger.debug("adding audience: {}", value);
+                            builder.withAudience(value);
+                        }
+                    } else {
+                        logger.debug("adding audience: {}", audience);
+                        builder.withAudience(audience);
+                    }
+                }
+                
+                boolean ignoreIssuedAt = parseBoolean(
+                        params.get(FCS_AUTHENTICATION_IGNORE_ISSUEDAT_PARAM));
+                if (ignoreIssuedAt) {
+                    logger.debug("will not verify 'iat' claim");
+                    builder.withIgnoreIssuedAt();
+                } else {
+                    long issuedAtLeeway = parseLong(
+                            params.get(FCS_AUTHENTICATION_ACCEPT_ISSUEDAT_PARAM), -1);
+                    if (issuedAtLeeway > 0) {
+                        logger.debug("allowing {} seconds leeway for 'iat' claim", issuedAtLeeway);
+                        builder.withIssuedAt(issuedAtLeeway);
+                    }
+                }
+                long expiresAtLeeway = parseLong(
+                        params.get(FCS_AUTHENTICATION_ACCEPT_EXPIRESAT_PARAM), -1);
+                if (expiresAtLeeway > 0) {
+                    logger.debug("allowing {} seconds leeway for 'exp' claim", expiresAtLeeway);
+                    builder.withExpiresAt(expiresAtLeeway);
+                }
+                
+                long notBeforeLeeway = parseLong(
+                        params.get(FCS_AUTHENTICATION_ACCEPT_NOTBEFORE_PARAM), -1);
+                if (notBeforeLeeway > 0) {
+                    logger.debug("allowing {} seconds leeway for 'nbf' claim", expiresAtLeeway);
+                    builder.withNotBefore(notBeforeLeeway);
+                }
+
+                // load keys
+                for (Entry<String, String> entry : params.entrySet()) {
+                    if (entry.getKey().startsWith(FCS_AUTHENTICATION_PUBLIC_KEY_PARAM_PREFIX)) {
+                        String keyId = entry.getKey().substring(FCS_AUTHENTICATION_PUBLIC_KEY_PARAM_PREFIX.length()).trim();
+                        if (keyId.isEmpty()) {
+                            throw new SRUConfigException("init-parameter: '" + entry.getKey() + "' is invalid: keyId is empty!");
+                        }
+                        String keyFileName = entry.getValue();
+                        logger.debug("keyId = {}, keyFile = {}", keyId, keyFileName);
+                        if (keyFileName.regionMatches(0, RESOURCE_URI_PREFIX, 0, RESOURCE_URI_PREFIX.length())) {
+                            String path = keyFileName.substring(RESOURCE_URI_PREFIX.length());
+                            logger.debug("loading key '{}' from resource '{}'", keyId, keyFileName);
+                            InputStream in = context.getResourceAsStream(path);
+                            builder.withPublicKey(keyId, in);
+                        } else {
+                            logger.debug("loading key '{}' from file '{}'", keyId, keyFileName);
+                            builder.withPublicKey(keyId, new File(keyFileName));
+                        }
+                    }
+                }
+                AuthenticationProvider authenticationProvider = builder.build();
+                if (authenticationProvider.getKeyCount() == 0) {
+                    logger.warn("No keys configured, all well-formed tokens will be accepted. Make sure, youn know what you are doing!");
+                }
+                return authenticationProvider;
+            } else {
+                logger.debug("explictly disable authentication");
+            }
+        }
+        return null;
     }
 
@@ -251,4 +358,16 @@
 
 
+    private long parseLong(String value, long defaultValue) throws SRUConfigException {
+        if (value != null) {
+            try {
+                return Long.parseLong(value);
+            } catch (NumberFormatException e) {
+                throw new SRUConfigException("invalid long value");
+            }
+        }
+        return defaultValue;
+    }
+    
+    
     private void writeEndpointDescription(XMLStreamWriter writer)
             throws XMLStreamException {
Index: /FCSSimpleEndpoint/trunk/src/main/java/eu/clarin/sru/server/fcs/utils/AuthenticationProvider.java
===================================================================
--- /FCSSimpleEndpoint/trunk/src/main/java/eu/clarin/sru/server/fcs/utils/AuthenticationProvider.java	(revision 7267)
+++ /FCSSimpleEndpoint/trunk/src/main/java/eu/clarin/sru/server/fcs/utils/AuthenticationProvider.java	(revision 7267)
@@ -0,0 +1,334 @@
+/**
+ * This software is copyright (c) 2011-2022 by
+ *  - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ * This is free software. You can redistribute it
+ * and/or modify it under the terms described in
+ * the GNU General Public License v3 of which you
+ * should have received a copy. Otherwise you can download
+ * it from
+ *
+ *   http://www.gnu.org/licenses/gpl-3.0.txt
+ *
+ * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de)
+ *
+ * @license http://www.gnu.org/licenses/gpl-3.0.txt
+ *  GNU General Public License v3
+ */
+package eu.clarin.sru.server.fcs.utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.AlgorithmMismatchException;
+import com.auth0.jwt.exceptions.InvalidClaimException;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.exceptions.SignatureVerificationException;
+import com.auth0.jwt.exceptions.TokenExpiredException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.interfaces.JWTVerifier;
+import com.auth0.jwt.interfaces.Verification;
+
+import eu.clarin.sru.server.SRUAuthenticationInfo;
+import eu.clarin.sru.server.SRUAuthenticationInfoProvider;
+import eu.clarin.sru.server.SRUConfigException;
+import eu.clarin.sru.server.SRUConstants;
+import eu.clarin.sru.server.SRUException;
+
+
+public class AuthenticationProvider implements SRUAuthenticationInfoProvider {
+    private static final Logger logger =
+            LoggerFactory.getLogger(AuthenticationProvider.class);
+    private final List<Verifier> verifiers;
+
+
+    private AuthenticationProvider(List<Verifier> verifiers) {
+        this.verifiers = verifiers;
+    }
+
+
+    public int getKeyCount() {
+        if (verifiers != null) {
+            return verifiers.size();
+        } else {
+            return 0;
+        }
+    }
+
+
+    @Override
+    public SRUAuthenticationInfo getAuthenticationInfo(
+            HttpServletRequest request) throws SRUException {
+        String value = request.getHeader("Authentication");
+        if (value != null) {
+            if (value.regionMatches(true, 0, "Bearer", 0, 6)) {
+                String rawToken = value.substring(6).trim();
+                if (!rawToken.isEmpty()) {
+                    return checkToken(rawToken);
+                }
+            }
+        }
+        return null;
+    }
+
+
+    private AuthenticationInfo checkToken(String rawToken) throws SRUException {
+        try {
+            DecodedJWT token = JWT.decode(rawToken);
+            logger.debug("token: jti={}, iss={}, aud={}, sub={}, iat={}, exp={}, nbt={}",
+                    token.getId(),
+                    token.getIssuer(), token.getAudience(),
+                    token.getSubject(), token.getIssuedAt(),
+                    token.getExpiresAt(), token.getNotBefore());
+            if (verifiers != null) {
+                for (Verifier verifier : verifiers) {
+                    try {
+                        logger.debug("trying to verify token with key '{}'",
+                                verifier.keyId);
+                        if (verifier.verify(token)) {
+                            return new AuthenticationInfo(token.getSubject());
+                        }
+                    } catch (InvalidClaimException e) {
+                        throw new SRUException(SRUConstants.SRU_AUTHENTICATION_ERROR,
+                                "error processing request authentication",
+                                e.getMessage(), e);
+                    } catch (TokenExpiredException e) {
+                        throw new SRUException(SRUConstants.SRU_AUTHENTICATION_ERROR,
+                                "error processing request authentication",
+                                "token expired", e);
+                    }
+                }
+                throw new SRUException(SRUConstants.SRU_AUTHENTICATION_ERROR,
+                        "error processing request authentication",
+                        "Could not verify JSON Web token signature.");
+            } else {
+                logger.debug("");
+                return new AuthenticationInfo(token.getSubject());
+            }
+        } catch (JWTDecodeException e) {
+            throw new SRUException(SRUConstants.SRU_AUTHENTICATION_ERROR,
+                    "error processing request authentication",
+                    "could not decode JSON Web token", e);
+        }
+    }
+
+
+    private static class Verifier {
+        private final String keyId;
+        private final JWTVerifier verifier;
+
+
+        private Verifier(String keyId, JWTVerifier verifier) {
+            this.keyId = keyId;
+            this.verifier = verifier;
+        }
+
+
+        public boolean verify(DecodedJWT token) {
+            try {
+                verifier.verify(token);
+                return true;
+            } catch (AlgorithmMismatchException | SignatureVerificationException e) {
+                return false;
+            }
+        }
+    }
+
+
+    private static class AuthenticationInfo implements SRUAuthenticationInfo {
+        private final String subject;
+
+        private AuthenticationInfo(String subject) {
+            this.subject = subject;
+        }
+
+        @Override
+        public String getAuthentictaionMethod() {
+            return "JWT";
+        }
+
+        @Override
+        public String getSubject() {
+            return subject;
+        }
+
+    }
+
+
+    public static class Builder {
+        private List<Key> keys;
+        private List<String> audiences = null;
+        private boolean ignoreIssuedAt = false;
+        private long issuedAtLeeway = -1;
+        private long expiresAtLeeway = -1;
+        private long notBeforeLeeway = -1;
+        private Verification builder;
+        private Builder() {
+        }
+
+
+        public Builder withAudience(String audience) {
+            if (this.audiences == null) {
+                this.audiences = new ArrayList<>();
+            }
+            this.audiences.add(audience);
+            return this;
+        }
+
+
+        public Builder withIgnoreIssuedAt() {
+            this.ignoreIssuedAt = true;
+            return this;
+        }
+
+
+        public Builder withIssuedAt(long leeway) {
+            if (leeway < 0) {
+                throw new IllegalArgumentException("leeway < 0");
+            }
+            this.issuedAtLeeway = leeway;
+            return this;
+        }
+
+
+        public Builder withExpiresAt(long leeway) {
+            if (leeway < 0) {
+                throw new IllegalArgumentException("leeway < 0");
+            }
+            this.expiresAtLeeway = leeway;
+            return this;
+        }
+
+
+        public Builder withNotBefore(long leeway) {
+            if (leeway < 0) {
+                throw new IllegalArgumentException("leeway < 0");
+            }
+            this.notBeforeLeeway = leeway;
+            return this;
+        }
+
+
+        public Builder withPublicKey(String keyId, File file) throws SRUConfigException {
+            try {
+                loadPublicKeyFromStream(keyId, new FileInputStream(file));
+                return this;
+            } catch (FileNotFoundException e) {
+                throw new SRUConfigException("failed to load key '" + keyId + "'", e);
+            }
+        }
+
+
+        public Builder withPublicKey(String keyId, InputStream stream) throws SRUConfigException {
+            loadPublicKeyFromStream(keyId, stream);
+            return this;
+        }
+
+        private void loadPublicKeyFromStream(String keyId, InputStream publicKeyStream) throws SRUConfigException {
+            PemReader pemReader = null;
+            try {
+                KeyFactory factory = KeyFactory.getInstance("RSA");
+
+                InputStreamReader keyReader =
+                        new InputStreamReader(publicKeyStream, StandardCharsets.UTF_8);
+                pemReader = new PemReader(keyReader);
+
+                PemObject pemObject = pemReader.readPemObject();
+                byte[] content = pemObject.getContent();
+                X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(content);
+                RSAPublicKey publicKey = (RSAPublicKey) factory.generatePublic(pubKeySpec);
+
+                if (keys == null) {
+                    keys = new ArrayList<>();
+                }
+                keys.add(new Key(keyId, publicKey));
+            } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
+                throw new SRUConfigException("failed to load key '" + keyId + "'", e);
+            } finally {
+                if (pemReader != null) {
+                    try {
+                        pemReader.close();
+                    } catch (IOException e) {
+                        /* IGNORE */
+                    }
+                }
+            }
+        }
+
+
+        public AuthenticationProvider build() {
+            List<Verifier> verifiers = null;
+
+            if (keys != null) {
+                verifiers = new ArrayList<>();
+                for (Key key : keys) {
+                    Algorithm algorithm = Algorithm.RSA256(key.publicKey, null);
+
+                    builder = JWT.require(algorithm);
+                    if (audiences != null) {
+                        builder.withAnyOfAudience(
+                                audiences.toArray(new String[audiences.size()]));
+                    }
+                    if (ignoreIssuedAt) {
+                        builder.ignoreIssuedAt();
+                    } else {
+                        if (issuedAtLeeway > 0) {
+                            builder.acceptIssuedAt(issuedAtLeeway);
+                        }
+                    }
+                    if (expiresAtLeeway > 0) {
+                        builder.acceptExpiresAt(expiresAtLeeway);
+                    }
+                    if (notBeforeLeeway > 0) {
+                        builder.acceptNotBefore(notBeforeLeeway);
+                    }
+                    JWTVerifier jwtVerifier = builder.build();
+
+                    if (verifiers == null) {
+                        verifiers = new ArrayList<>();
+                    }
+                    verifiers.add(new Verifier(key.keyId, jwtVerifier));
+                }
+            }
+            return new AuthenticationProvider(verifiers);
+        }
+
+
+        public static Builder create() {
+            return new Builder();
+        }
+
+
+        private static final class Key {
+            public String keyId;
+            public RSAPublicKey publicKey;
+
+
+            private Key(String keyId, RSAPublicKey publicKey) {
+                this.keyId = keyId;
+                this.publicKey = publicKey;
+            }
+        }
+    }
+
+}
