Mercurial > hg > digilib
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));