changeset 1499:31566778c251

new OpenID Connect authentication OpenIdAuthnOps works now!
author robcast
date Thu, 31 Mar 2016 17:05:28 +0200
parents c1b27845aea3
children 90bc0c7664d5
files common/src/main/java/digilib/auth/AuthnOps.java common/src/main/java/digilib/util/XMLMapListLoader.java common/src/main/java/digilib/util/XMLMapLoader.java servlet/pom.xml servlet/src/main/java/digilib/auth/AuthzOpsImpl.java servlet/src/main/java/digilib/auth/IpAuthnOps.java servlet/src/main/java/digilib/auth/IpServletAuthnOps.java servlet/src/main/java/digilib/auth/OpenIdAuthnOps.java servlet2/pom.xml webapp/src/main/webapp/WEB-INF/digilib-auth.xml.template webapp/src/main/webapp/jquery/jquery.digilib.oauth.js
diffstat 11 files changed, 257 insertions(+), 67 deletions(-) [+]
line wrap: on
line diff
--- a/common/src/main/java/digilib/auth/AuthnOps.java	Thu Mar 31 14:08:01 2016 +0200
+++ b/common/src/main/java/digilib/auth/AuthnOps.java	Thu Mar 31 17:05:28 2016 +0200
@@ -1,5 +1,7 @@
 package digilib.auth;
 
+import java.util.List;
+
 /*
  * #%L
  * AuthnOps -- Authentication interface class
@@ -43,6 +45,24 @@
      */
     public boolean isUserInRole(DigilibRequest request, String role) throws AuthOpException;
 
+    /**
+     * Return if the implementation supports getUserRoles().
+     * 
+     * @return
+     */
+    public boolean hasUserRoles();
+    
+    /**
+     * Return the list of roles associated with the user represented by request.
+     * 
+     * Returns null if a list of roles is not available. Users of this API should
+     * check hasUserRoles().
+     * 
+     * @param request
+     * @return
+     * @throws AuthOpException
+     */
+    public List<String> getUserRoles(DigilibRequest request) throws AuthOpException;
 
     /**
      * Configure this AuthnOps instance.
--- a/common/src/main/java/digilib/util/XMLMapListLoader.java	Thu Mar 31 14:08:01 2016 +0200
+++ b/common/src/main/java/digilib/util/XMLMapListLoader.java	Thu Mar 31 17:05:28 2016 +0200
@@ -75,7 +75,7 @@
      * @param entry_tag
      */
     public XMLMapListLoader(String list_tag, String entry_tag) {
-        logger.debug("xmlListLoader(" + list_tag + "," + entry_tag + ")");
+        logger.debug("XMLMapListLoader(" + list_tag + "," + entry_tag + ")");
         listTag = list_tag;
         entryTag = entry_tag;
     }
@@ -141,7 +141,7 @@
                     text = "";
                 }
                 text += new String(ch, start, length);
-                elementData.put(text, CONTENT_KEY);
+                elementData.put(CONTENT_KEY, text);
             }
         }
 
--- a/common/src/main/java/digilib/util/XMLMapLoader.java	Thu Mar 31 14:08:01 2016 +0200
+++ b/common/src/main/java/digilib/util/XMLMapLoader.java	Thu Mar 31 17:05:28 2016 +0200
@@ -76,7 +76,7 @@
      * @param value_att
      */
     public XMLMapLoader(String list_tag, String entry_tag, String key_att, String value_att) {
-        logger.debug("xmlListLoader(" + list_tag + "," + entry_tag + "," + key_att + "," + value_att + ")");
+        logger.debug("XMLMapLoader(" + list_tag + "," + entry_tag + "," + key_att + "," + value_att + ")");
         listTag = list_tag;
         entryTag = entry_tag;
         keyAtt = key_att;
--- a/servlet/pom.xml	Thu Mar 31 14:08:01 2016 +0200
+++ b/servlet/pom.xml	Thu Mar 31 17:05:28 2016 +0200
@@ -26,5 +26,10 @@
   		<type>jar</type>
   		<scope>provided</scope>
   	</dependency>
+  	<dependency>
+  		<groupId>org.bitbucket.b_c</groupId>
+  		<artifactId>jose4j</artifactId>
+  		<version>0.5.0</version>
+  	</dependency>
   </dependencies>
 </project>
--- a/servlet/src/main/java/digilib/auth/AuthzOpsImpl.java	Thu Mar 31 14:08:01 2016 +0200
+++ b/servlet/src/main/java/digilib/auth/AuthzOpsImpl.java	Thu Mar 31 17:05:28 2016 +0200
@@ -84,11 +84,26 @@
      */
     public boolean isRoleAuthorized(List<String> rolesRequired, DigilibServletRequest request) throws AuthOpException {
         if (rolesRequired == null) return true;
-        for (String r : rolesRequired) {
-            logger.debug("Testing role: " + r);
-            if (authnOps.isUserInRole(request, r)) {
-                logger.debug("Role Authorized");
-                return true;
+        if (authnOps.hasUserRoles()) {
+            // get and check list of provided roles (less calls)
+            List<String> rolesProvided = authnOps.getUserRoles(request);
+            if (rolesProvided == null) {
+                return false;
+            }
+            for (String r : rolesRequired) {
+                logger.debug("Testing role: " + r);
+                if (rolesProvided.contains(r)) {
+                    return true;
+                }
+            }
+        } else {
+            // check each role separately
+            for (String r : rolesRequired) {
+                logger.debug("Testing role: " + r);
+                if (authnOps.isUserInRole(request, r)) {
+                    logger.debug("Role Authorized");
+                    return true;
+                }
             }
         }
         return false;
--- a/servlet/src/main/java/digilib/auth/IpAuthnOps.java	Thu Mar 31 14:08:01 2016 +0200
+++ b/servlet/src/main/java/digilib/auth/IpAuthnOps.java	Thu Mar 31 17:05:28 2016 +0200
@@ -98,22 +98,39 @@
     }
 
     /* (non-Javadoc)
+     * @see digilib.auth.AuthnOps#hasUserRoles()
+     */
+    @Override
+    public boolean hasUserRoles() {
+        return true;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.auth.AuthnOps#getUserRoles(digilib.conf.DigilibRequest)
+     */
+    @Override
+    public List<String> getUserRoles(DigilibRequest dlRequest) throws AuthOpException {
+        HttpServletRequest request = ((DigilibServletRequest) dlRequest).getServletRequest();
+        String ip = request.getRemoteAddr();
+        logger.debug("Getting roles for ip "+ip);
+        List<String> provided = null;
+        if (ip.contains(":")) {
+            // IPv6
+            provided  = authIP6s.match(ip);
+        } else {
+            // IPv4
+            provided = authIP4s.match(ip);
+        }        
+        return provided;
+    }
+
+    /* (non-Javadoc)
      * @see digilib.auth.AuthnOps#isUserInRole(digilib.conf.DigilibRequest, java.lang.String)
      */
     @Override
     public boolean isUserInRole(DigilibRequest dlRequest, String role) throws AuthOpException {
         // check if the requests address provides a role
-        List<String> provided = null;
-        HttpServletRequest request = ((DigilibServletRequest) dlRequest).getServletRequest();
-        String ip = request.getRemoteAddr();
-        logger.debug("Testing role '"+role+"' for ip "+ip);
-        if (ip.contains(":")) {
-            // IPv6
-            provided = authIP6s.match(ip);
-        } else {
-            // IPv4
-            provided = authIP4s.match(ip);
-        }
+        List<String> provided = getUserRoles(dlRequest);
         if ((provided != null) && (provided.contains(role))) {
             return true;
         }
--- a/servlet/src/main/java/digilib/auth/IpServletAuthnOps.java	Thu Mar 31 14:08:01 2016 +0200
+++ b/servlet/src/main/java/digilib/auth/IpServletAuthnOps.java	Thu Mar 31 17:05:28 2016 +0200
@@ -56,22 +56,31 @@
 public class IpServletAuthnOps extends IpAuthnOps {
 
     /* (non-Javadoc)
+     * @see digilib.auth.IpAuthnOps#hasUserRoles()
+     */
+    @Override
+    public boolean hasUserRoles() {
+        // Servlet API does not support getting roles
+        return false;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.auth.IpAuthnOps#getUserRoles(digilib.conf.DigilibRequest)
+     */
+    @Override
+    public List<String> getUserRoles(DigilibRequest dlRequest) throws AuthOpException {
+        // Servlet API does not support getting roles
+        return null;
+    }
+
+    /* (non-Javadoc)
      * @see digilib.auth.IpAuthnOps#isUserInRole(digilib.conf.DigilibRequest, java.lang.String)
      */
     @Override
     public boolean isUserInRole(DigilibRequest dlRequest, String role) throws AuthOpException {
-        // check if the requests address provides a role
-        List<String> provided = null;
         HttpServletRequest request = ((DigilibServletRequest) dlRequest).getServletRequest();
-        String ip = request.getRemoteAddr();
-        logger.debug("Testing role '"+role+"' for ip "+ip);
-        if (ip.contains(":")) {
-            // IPv6
-            provided = authIP6s.match(ip);
-        } else {
-            // IPv4
-            provided = authIP4s.match(ip);
-        }
+        // check if the requests IP provides a role
+        List<String> provided = super.getUserRoles(dlRequest);
         if ((provided != null) && (provided.contains(role))) {
             return true;
         }
--- a/servlet/src/main/java/digilib/auth/OpenIdAuthnOps.java	Thu Mar 31 14:08:01 2016 +0200
+++ b/servlet/src/main/java/digilib/auth/OpenIdAuthnOps.java	Thu Mar 31 17:05:28 2016 +0200
@@ -1,7 +1,4 @@
 package digilib.auth;
-
-import java.io.File;
-
 /*
  * #%L
  * Authentication class implementation using IP addresses and Servlet user information
@@ -28,41 +25,25 @@
  * Author: Robert Casties (robcast@berlios.de)
  */
 
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
-
-import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
 
 import org.apache.log4j.Logger;
+import org.jose4j.jwk.JsonWebKey;
+import org.jose4j.jwt.JwtClaims;
+import org.jose4j.jwt.MalformedClaimException;
+import org.jose4j.jwt.consumer.InvalidJwtException;
+import org.jose4j.jwt.consumer.JwtConsumer;
+import org.jose4j.jwt.consumer.JwtConsumerBuilder;
+import org.jose4j.jwt.consumer.JwtContext;
+import org.jose4j.lang.JoseException;
 
 import digilib.conf.DigilibConfiguration;
 import digilib.conf.DigilibRequest;
-import digilib.conf.DigilibServletRequest;
-/*
- * #%L
- * Authentication class implementation using IP addresses
- * 
- * Digital Image Library servlet components
- * 
- * %%
- * Copyright (C) 2016 MPIWG Berlin
- * %%
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as 
- * published by the Free Software Foundation, either version 3 of the 
- * License, or (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Lesser Public License for more details.
- * 
- * You should have received a copy of the GNU General Lesser Public 
- * License along with this program.  If not, see
- * <http://www.gnu.org/licenses/lgpl-3.0.html>.
- * #L%
- * Author: Robert Casties (robcast@users.sourceforge.net)
- */
-
+import digilib.util.XMLMapListLoader;
 
 /**
  * Implements AuthnOps using an OpenId Connect ID token.
@@ -80,6 +61,9 @@
  * }
  * </pre>
  * 
+ * A request with an "id_token" parameter containing a valid token signed with the configured key
+ * including the configured issuer (iss) and clientid (aud) is granted the configured roles.
+ * 
  */
 public class OpenIdAuthnOps implements AuthnOps {
 
@@ -88,6 +72,11 @@
 
     protected File configFile;
 
+    protected JwtConsumer firstPassJwtConsumer;
+    protected Map<String, JwtConsumer> idpJwtConsumers;
+    protected Map<String, List<String>> idpRoles;
+    
+
     /* (non-Javadoc)
      * @see digilib.auth.AuthnOps#init(digilib.conf.DigilibConfiguration)
      */
@@ -95,15 +84,139 @@
     public void init(DigilibConfiguration dlConfig) throws AuthOpException {
         configFile = dlConfig.getAsFile("auth-file");
         logger.debug("openidauthnops.init (" + configFile + ")");
+        List<Map<String, String>> idpList;
+        try {
+            // load identity providers
+            XMLMapListLoader idpLoader = new XMLMapListLoader("digilib-oauth", "openid");
+            idpList = idpLoader.loadUri(configFile.toURI());
+        } catch (Exception e) {
+            throw new AuthOpException("ERROR loading auth config file: " + e);
+        }
+        if (idpList == null) {
+            throw new AuthOpException("ERROR unable to load auth config file!");
+        }
         
+        // create Map of roles by issuer
+        idpRoles = new HashMap<String, List<String>>();
+        
+        // build a first pass JwtConsumer that doesn't check signatures or do any validation.
+        firstPassJwtConsumer = new JwtConsumerBuilder()
+                .setSkipAllValidators()
+                .setDisableRequireSignature()
+                .setSkipSignatureVerification()
+                .build();
+        
+        // create Map of configured JwtConsumers by issuer
+        idpJwtConsumers = new HashMap<String, JwtConsumer>();
+        for (Map<String, String> idpDesc : idpList) {
+            String issuer = idpDesc.get("issuer");
+            if (issuer == null) {
+                logger.error("Missing issuer in openid tag!");
+                continue;
+            }
+            
+            String clientid = idpDesc.get("clientid");
+            if (clientid == null) {
+                logger.error("Missing clientid in openid tag! (issuer: "+issuer+")");
+                continue;
+            }
+            
+            String rolestr = idpDesc.get("roles");
+            if (rolestr == null) {
+                logger.error("Missing roles in openid tag! (issuer: "+issuer+")");
+                continue;
+            }
+            // split roles string into list
+            List<String> roles = Arrays.asList(rolestr.split(","));
+            
+            String keytype = idpDesc.get("keytype");
+            if (keytype == null || ! keytype.equals("jwk")) {
+                logger.error("Missing or invalid keytype in openid tag! (issuer: "+issuer+")");
+                continue;
+            }
+            
+            String keyData = idpDesc.get("_text");
+            if (keyData == null || keyData.length() == 0) {
+                logger.error("Missing key data in openid tag! (issuer: "+issuer+")");
+                continue;
+            }
+            
+            try {
+                // create key from JWK data
+                JsonWebKey jwk = JsonWebKey.Factory.newJwk(keyData);
+                // create second pass consumer for validation
+                JwtConsumer secondPassJwtConsumer = new JwtConsumerBuilder()
+                        .setExpectedIssuer(issuer)
+                        .setVerificationKey(jwk.getKey())
+                        .setRequireExpirationTime()
+                        .setAllowedClockSkewInSeconds(300)
+                        .setRequireSubject()
+                        .setExpectedAudience(clientid)
+                        .build();
+                
+                // save consumer and roles
+                idpJwtConsumers.put(issuer, secondPassJwtConsumer);
+                idpRoles.put(issuer, roles);
+                logger.debug("Registered id provider '"+issuer+"'");
+                
+            } catch (JoseException e) {
+                logger.error("Invalid key data in openid tag! (issuer: "+issuer+")");
+                continue;
+            }
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.auth.AuthnOps#hasUserRoles()
+     */
+    @Override
+    public boolean hasUserRoles() {
+        return true;
+    }
+
+    /* (non-Javadoc)
+     * @see digilib.auth.AuthnOps#getUserRoles(digilib.conf.DigilibRequest)
+     */
+    @Override
+    public List<String> getUserRoles(DigilibRequest request) throws AuthOpException {
+        String id_token = request.getAsString("id_token");
+        if (id_token == null || id_token.isEmpty()) {
+            logger.error("Missing id token!");
+            return null;
+        }
+        // the first JwtConsumer is just used to parse the JWT into a JwtContext object.
+        try {
+            JwtContext jwtContext = firstPassJwtConsumer.process(id_token);
+            // extract issuer
+            String issuer = jwtContext.getJwtClaims().getIssuer();
+            // get validating consumer for this issuer
+            JwtConsumer secondPassJwtConsumer = idpJwtConsumers.get(issuer);
+            if (secondPassJwtConsumer == null) {
+                logger.error("Unknown id token issuer: "+issuer);
+                return null;
+            }
+            // validate token
+            secondPassJwtConsumer.processContext(jwtContext);
+            JwtClaims claims = jwtContext.getJwtClaims();
+            String sub = claims.getSubject();
+            logger.debug("id_token authenticated user '"+sub+"'");
+            // get roles
+            List<String> provided = idpRoles.get(issuer);
+            return provided;
+            
+        } catch (InvalidJwtException | MalformedClaimException e) {
+            logger.error("Error validating id token: "+e.getMessage());
+            return null;
+        }
     }
 
     /* (non-Javadoc)
      * @see digilib.auth.IpAuthnOps#isUserInRole(digilib.conf.DigilibRequest, java.lang.String)
      */
     @Override
-    public boolean isUserInRole(DigilibRequest dlRequest, String role) throws AuthOpException {
-        return false;
+    public boolean isUserInRole(DigilibRequest request, String role) throws AuthOpException {
+        List<String> provided = getUserRoles(request);
+        return provided.contains(role);
     }
 
 }
--- a/servlet2/pom.xml	Thu Mar 31 14:08:01 2016 +0200
+++ b/servlet2/pom.xml	Thu Mar 31 17:05:28 2016 +0200
@@ -18,7 +18,7 @@
   	<dependency>
   		<groupId>javax.servlet</groupId>
   		<artifactId>servlet-api</artifactId>
-  		<version>2.3</version>
+  		<version>2.4</version>
   		<type>jar</type>
   		<scope>provided</scope>
   	</dependency>
--- a/webapp/src/main/webapp/WEB-INF/digilib-auth.xml.template	Thu Mar 31 14:08:01 2016 +0200
+++ b/webapp/src/main/webapp/WEB-INF/digilib-auth.xml.template	Thu Mar 31 17:05:28 2016 +0200
@@ -20,8 +20,19 @@
       Roles under "role" must be separated by comma only (no spaces). 
     -->
     <address ip="127" role="local" />
+    <address ip="0:0:0:0:0:0:0:1" role="local" />
     <address ip="130.92.68" role="eastwood-coll,ptolemaios-geo" />
-    <address ip="130.92.151" role="ALL" />
   </digilib-addresses>
 
+  <digilib-oauth>
+    <!-- 
+      A request with an "id_token" parameter containing a valid token 
+      signed with the configured key including the configured issuer (iss)
+      and clientid (aud) is granted the configured roles.
+    -->
+    <openid issuer="https://id.some.where" clientid="myclient" roles="someusers" keytype="jwk">
+      {"kty":"RSA","e":"AQAB","kid":"rsa1","n":"qjQ5U3wXzamg9R...idGpIiVilMDVBs"}
+    </openid>
+  </digilib-oauth>
+
 </auth-config>
--- a/webapp/src/main/webapp/jquery/jquery.digilib.oauth.js	Thu Mar 31 14:08:01 2016 +0200
+++ b/webapp/src/main/webapp/jquery/jquery.digilib.oauth.js	Thu Mar 31 17:05:28 2016 +0200
@@ -74,7 +74,7 @@
         var authReq = {
                 'response_type' : 'id_token token',
                 'client_id' : data.settings.authClientId,
-                'redirect_uri' : url,
+                'redirect_uri' : encodeURIComponent(url),
                 'scope' : 'openid'
         };
         var qs = fn.getParamString(authReq, Object.keys(authReq));