View Javadoc
1   /*
2    * $Source$
3    * $Revision$
4    *
5    * Copyright (C) 2000 William Chesters
6    *
7    * Part of Melati (http://melati.org), a framework for the rapid
8    * development of clean, maintainable web applications.
9    *
10   * Melati is free software; Permission is granted to copy, distribute
11   * and/or modify this software under the terms either:
12   *
13   * a) the GNU General Public License as published by the Free Software
14   *    Foundation; either version 2 of the License, or (at your option)
15   *    any later version,
16   *
17   *    or
18   *
19   * b) any version of the Melati Software License, as published
20   *    at http://melati.org
21   *
22   * You should have received a copy of the GNU General Public License and
23   * the Melati Software License along with this program;
24   * if not, write to the Free Software Foundation, Inc.,
25   * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
26   * GNU General Public License and visit http://melati.org to obtain the
27   * Melati Software License.
28   *
29   * Feel free to contact the Developers of Melati (http://melati.org),
30   * if you would like to work out a different arrangement than the options
31   * outlined here.  It is our intention to allow Melati to be used by as
32   * wide an audience as possible.
33   *
34   * This program is distributed in the hope that it will be useful,
35   * but WITHOUT ANY WARRANTY; without even the implied warranty of
36   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
37   * GNU General Public License for more details.
38   *
39   * Contact details for copyright holder:
40   *
41   *     William Chesters <williamc At paneris.org>
42   *     http://paneris.org/~williamc
43   *     Obrechtstraat 114, 2517VX Den Haag, The Netherlands
44   */
45  
46  package org.melati.login;
47  
48  import java.io.IOException;
49  import javax.servlet.http.HttpServletResponse;
50  import org.melati.poem.AccessPoemException;
51  import org.melati.poem.PoemThread;
52  import org.melati.poem.User;
53  import org.melati.Melati;
54  import org.melati.util.UnexpectedExceptionException;
55  
56  
57  
58  /**
59   * An {@link AccessHandler} which uses the HTTP Basic Authentication scheme to
60   * elicit and maintain the user's login and password.
61   *
62   * This implementation doesn't use the servlet session at all,
63   * so it doesn't try to send cookies or
64   * do URL rewriting.
65   *
66   */
67  public class HttpBasicAuthenticationAccessHandler implements AccessHandler {
68    private static final String className =
69            new HttpBasicAuthenticationAccessHandler().getClass().getName();
70  
71    final String REALM = className + ".realm";
72    final String USER = className + ".user";
73  
74    /**
75     * Change here to use session, if that makes sense.
76     * @return false
77     */
78    protected boolean useSession() {
79      return false;
80    }
81  
82    /**
83     * Force a login by sending a 401 error back to the browser.
84     * 
85     * HACK Apache/Netscape appear not to do anything with message, which is
86     * why it's just left as a String.
87     */
88    protected void forceLogin(HttpServletResponse resp,
89                              String realm, String message) {
90      String desc = realm == null ? "<unknown>"
91                                  : realm.replace('"', ' ');
92      resp.setHeader("WWW-Authenticate", "Basic realm=\"" + desc + "\"");
93      // I don't believe there is a lot we can do about an IO exception here
94      try {
95        resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, message);
96      } catch (IOException e) {
97        throw new UnexpectedExceptionException(e);
98      }
99    }
100 
101  /**
102   * Called when an AccessPoemException is trapped.
103   *
104   * @param melati the Melati
105   * @param accessException the particular access exception to handle
106   * @see org.melati.login.AccessHandler#handleAccessException
107   */
108   public void handleAccessException(Melati melati,
109                                     AccessPoemException accessException)
110       throws Exception {
111     String capName = "melati";
112     if (useSession())
113       melati.getSession().setAttribute(REALM, capName);
114     forceLogin(melati.getResponse(), capName, accessException.getMessage());
115   }
116 
117   @Override
118   public Melati establishUser(Melati melati) {
119 
120     HttpAuthorization auth = HttpAuthorization.from(melati.getRequest());
121 
122     if (auth == null) {
123       // No attempt to log in: become `guest'
124 
125       PoemThread.setAccessToken(melati.getDatabase().guestAccessToken());
126       return melati;
127     }
128     else {
129       // They are trying to log in
130 
131       // If allowed, we store the User in the session to avoid repeating the
132       // SELECTion implied by firstWhereEq for every hit
133 
134       User sessionUser =
135           useSession() ? (User)melati.getSession().getAttribute(USER) : null;
136       User user = null;
137 
138       if (sessionUser == null ||
139           !sessionUser.getLogin().equals(auth.username))
140         user = (User)melati.getDatabase().getUserTable().getLoginColumn().
141                    firstWhereEq(auth.username);
142       else
143         user = sessionUser;
144 
145       if (user == null || !user.getPassword_unsafe().equals(auth.password)) {
146 
147         // Login/password authentication failed; we must trigger another
148         // attempt.  But do we know the "realm" (= POEM capability name) for
149         // which they were originally found not to be authorized?
150 
151         String storedRealm;
152         if (useSession() &&
153             (storedRealm = (String)melati.getSession().getAttribute(REALM))
154                  != null) {
155 
156           // The "realm" is stored in the session
157 
158           forceLogin(melati.getResponse(), storedRealm,
159                      "Login/password not recognised");
160           return null;
161         }
162         else {
163 
164           // We don't know the "realm", so we just let the user try again as
165           // `guest' and hopefully trigger the same problem and get the same
166           // message all over again.  Not very satisfactory but the alternative
167           // is providing a default realm like "<unknown>".
168 
169           PoemThread.setAccessToken(melati.getDatabase().guestAccessToken());
170           return melati;
171         }
172       }
173       else {
174 
175         // Login/password authentication succeeded
176 
177         PoemThread.setAccessToken(user);
178 
179         if (useSession() && user != sessionUser)
180           melati.getSession().setAttribute(USER, user);
181 
182         return melati;
183       }
184     }
185   }
186 
187   @Override
188   public void buildRequest(Melati melati)
189       throws IOException {
190   }
191 }