better error message when authn servlets are accessed directly
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / authn / provider / IPAddressLoginHandler.java
1 /*
2  * Copyright [2006] [University Corporation for Advanced Internet Development, Inc.]
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package edu.internet2.middleware.shibboleth.idp.authn.provider;
18
19 import java.io.IOException;
20 import java.net.Inet4Address;
21 import java.net.Inet6Address;
22 import java.net.InetAddress;
23 import java.net.UnknownHostException;
24 import java.util.BitSet;
25 import java.util.List;
26 import java.util.concurrent.CopyOnWriteArrayList;
27
28 import javax.servlet.RequestDispatcher;
29 import javax.servlet.ServletException;
30 import javax.servlet.ServletRequest;
31 import javax.servlet.http.HttpServletRequest;
32 import javax.servlet.http.HttpServletResponse;
33
34 import org.apache.log4j.Logger;
35 import org.joda.time.DateTime;
36
37 import edu.internet2.middleware.shibboleth.idp.authn.AuthenticationEngine;
38 import edu.internet2.middleware.shibboleth.idp.authn.LoginHandler;
39 import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
40
41 /**
42  * IP Address authentication handler.
43  * 
44  * This "authenticates" a user based on their IP address. It operates in either default deny or default allow mode, and
45  * evaluates a given request against a list of blocked or permitted IPs. It supports both IPv4 and IPv6.
46  * 
47  * If an Authentication Context Class or DeclRef URI is not specified, it will default to
48  * "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol".
49  */
50 public class IPAddressLoginHandler extends AbstractLoginHandler {
51
52     /** Class logger. */
53     private final Logger log = Logger.getLogger(IPAddressLoginHandler.class);
54
55     /** The URI of the AuthnContextDeclRef or the AuthnContextClass. */
56     private String authnMethodURI = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol";
57
58     /** The username to use for IP-address "authenticated" users. */
59     private String username;
60
61     /** Are the IPs in ipList a permitted list or a deny list. */
62     private boolean defaultDeny;
63
64     /** The list of denied or permitted IPs. */
65     private List<IPEntry> ipList;
66
67     /**
68      * Set the permitted IP addresses.
69      * 
70      * If <code>defaultDeny</code> is <code>true</code> then only the IP addresses in <code>ipList</code> will be
71      * "authenticated." If <code>defaultDeny</code> is <code>false</code>, then all IP addresses except those in
72      * <code>ipList</code> will be authenticated.
73      * 
74      * @param entries A list of IP addresses (with CIDR masks).
75      * @param defaultDeny Does <code>ipList</code> contain a deny or permit list.
76      */
77     public void setEntries(final List<String> entries, boolean defaultDeny) {
78
79         this.defaultDeny = defaultDeny;
80         ipList = new CopyOnWriteArrayList<IPEntry>();
81
82         for (String addr : entries) {
83             try {
84                 ipList.add(new edu.internet2.middleware.shibboleth.idp.authn.provider.IPAddressLoginHandler.IPEntry(addr));
85             } catch (UnknownHostException ex) {
86                 log.error("IPAddressHandler: Error parsing entry \"" + addr + "\". Ignoring.");
87             }
88         }
89     }
90
91     /** {@inheritDoc} */
92     public boolean supportsPassive() {
93         return true;
94     }
95
96     /** {@inheritDoc} */
97     public boolean supportsForceAuthentication() {
98         return true;
99     }
100
101     /**
102      * Get the username for all IP-address authenticated users.
103      * 
104      * @return The username for IP-address authenticated users.
105      */
106     public String getUsername() {
107         return username;
108     }
109
110     /**
111      * Set the username to use for all IP-address authenticated users.
112      * 
113      * @param name The username for IP-address authenticated users.
114      */
115     public void setUsername(String name) {
116         username = name;
117     }
118
119     /** {@inheritDoc} */
120     public void login(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
121
122         if (defaultDeny) {
123             handleDefaultDeny(httpRequest, httpResponse);
124         } else {
125             handleDefaultAllow(httpRequest, httpResponse);
126         }
127
128         try {
129             AuthenticationEngine.returnToAuthenticationEngine(httpRequest, httpResponse);
130         } catch (ServletException e) {
131             // this shouldn't ever happen since the handler can only be accessed through the authentication engine
132             return;
133         }
134     }
135
136     protected void handleDefaultDeny(HttpServletRequest request, HttpServletResponse response) {
137
138         boolean ipAllowed = searchIpList(request);
139
140         if (ipAllowed) {
141             request.setAttribute(LoginHandler.PRINCIPAL_NAME_KEY, username);
142         }
143     }
144
145     protected void handleDefaultAllow(HttpServletRequest request, HttpServletResponse response) {
146
147         boolean ipDenied = searchIpList(request);
148
149         if (!ipDenied) {
150             request.setAttribute(LoginHandler.PRINCIPAL_NAME_KEY, username);
151         }
152     }
153
154     /**
155      * Search the list of InetAddresses for the client's address.
156      * 
157      * @param request The ServletReqeust
158      * 
159      * @return <code>true</code> if the client's address is in <code>ipList</code>
160      */
161     private boolean searchIpList(ServletRequest request) {
162
163         boolean found = false;
164
165         try {
166             InetAddress addr = InetAddress.getByName(request.getRemoteAddr());
167             BitSet addrbits = byteArrayToBitSet(addr.getAddress());
168
169             for (IPEntry entry : ipList) {
170
171                 BitSet netaddr = entry.getNetworkAddress();
172                 BitSet netmask = entry.getNetmask();
173
174                 addrbits.and(netmask);
175                 if (addrbits.equals(netaddr)) {
176                     found = true;
177                     break;
178                 }
179             }
180
181         } catch (UnknownHostException ex) {
182             log.error("IPAddressHandler: Error resolving hostname.", ex);
183             return false;
184         }
185
186         return found;
187     }
188
189     /**
190      * Converts a byte array to a BitSet.
191      * 
192      * The supplied byte array is assumed to have the most signifigant bit in element 0.
193      * 
194      * @param bytes the byte array with most signifigant bit in element 0.
195      * 
196      * @return the BitSet
197      */
198     protected BitSet byteArrayToBitSet(final byte[] bytes) {
199
200         BitSet bits = new BitSet();
201
202         for (int i = 0; i < bytes.length * 8; i++) {
203             if ((bytes[bytes.length - i / 8 - 1] & (1 << (i % 8))) > 0) {
204                 bits.set(i);
205             }
206         }
207
208         return bits;
209     }
210
211     /**
212      * Encapsulates a network address and a netmask on ipList.
213      */
214     protected class IPEntry {
215
216         /** The network address. */
217         private final BitSet networkAddress;
218
219         /** The netmask. */
220         private final BitSet netmask;
221
222         /**
223          * Construct a new IPEntry given a network address in CIDR format.
224          * 
225          * @param entry A CIDR-formatted network address/netmask
226          * 
227          * @throws UnknownHostException If entry is malformed.
228          */
229         public IPEntry(String entry) throws UnknownHostException {
230
231             // quick sanity checks
232             if (entry == null || entry.length() == 0) {
233                 throw new UnknownHostException("entry is null.");
234             }
235
236             int cidrOffset = entry.indexOf("/");
237             if (cidrOffset == -1) {
238                 log.error("IPAddressHandler: invalid entry \"" + entry + "\" -- it lacks a netmask component.");
239                 throw new UnknownHostException("entry lacks a netmask component.");
240             }
241
242             // ensure that only one "/" is present.
243             if (entry.indexOf("/", cidrOffset + 1) != -1) {
244                 log.error("IPAddressHandler: invalid entry \"" + entry + "\" -- too many \"/\" present.");
245                 throw new UnknownHostException("entry has too many netmask components.");
246             }
247
248             String networkString = entry.substring(0, cidrOffset);
249             String netmaskString = entry.substring(cidrOffset + 1, entry.length());
250
251             InetAddress tempAddr = InetAddress.getByName(networkString);
252             networkAddress = byteArrayToBitSet(tempAddr.getAddress());
253
254             int masklen = Integer.parseInt(netmaskString);
255             int addrlen = networkAddress.length();
256
257             // ensure that the netmask isn't too large
258             if ((tempAddr instanceof Inet4Address) && (masklen > 32)) {
259                 throw new UnknownHostException("IPAddressHandler: Netmask is too large for an IPv4 address: " + masklen);
260             } else if ((tempAddr instanceof Inet6Address) && masklen > 128) {
261                 throw new UnknownHostException("IPAddressHandler: Netmask is too large for an IPv6 address: " + masklen);
262             }
263
264             netmask = new BitSet(addrlen);
265             netmask.set(addrlen - masklen, addrlen, true);
266         }
267
268         /**
269          * Get the network address.
270          * 
271          * @return the network address.
272          */
273         public BitSet getNetworkAddress() {
274             return networkAddress;
275         }
276
277         /**
278          * Get the netmask.
279          * 
280          * @return the netmask.
281          */
282         public BitSet getNetmask() {
283             return netmask;
284         }
285     }
286 }