View Javadoc

1   package ch.odi.jaaspam;
2   
3   import java.io.IOException;
4   import java.util.Map;
5   import java.util.Set;
6   
7   import javax.security.auth.Subject;
8   import javax.security.auth.callback.Callback;
9   import javax.security.auth.callback.CallbackHandler;
10  import javax.security.auth.callback.ChoiceCallback;
11  import javax.security.auth.callback.ConfirmationCallback;
12  import javax.security.auth.callback.LanguageCallback;
13  import javax.security.auth.callback.NameCallback;
14  import javax.security.auth.callback.PasswordCallback;
15  import javax.security.auth.callback.TextInputCallback;
16  import javax.security.auth.callback.TextOutputCallback;
17  import javax.security.auth.callback.UnsupportedCallbackException;
18  import javax.security.auth.login.AccountExpiredException;
19  import javax.security.auth.login.CredentialExpiredException;
20  import javax.security.auth.login.FailedLoginException;
21  import javax.security.auth.login.LoginException;
22  import javax.security.auth.spi.LoginModule;
23  
24  import ch.odi.pam.Pam;
25  import ch.odi.pam.PamCallback;
26  import ch.odi.pam.PamConstants;
27  import ch.odi.pam.PamError;
28  import ch.odi.pam.PamMessage;
29  import ch.odi.pam.PamResponse;
30  
31  /**
32   * JAAS login module that defers the actual authentication
33   * to PAM. This login module must set the PAM service name
34   * in the JAAS config file. The service name is passed as
35   * the value to the option named <code>service</code>.
36   * <p>Sample configuration:</p>
37   * <code>
38   * pam-sample {
39   *  ch.odi.jaaspam.PamLoginModule required service=login;
40   * };
41   * </code>
42   *
43   * @author Ortwin Gl?ck
44   */
45  public class PamLoginModule implements LoginModule {
46      private Pam pam;
47      private CallbackHandler ch;
48      private Subject subject;
49      private String userPrompt;
50      private String pamUsername;
51      private boolean logged_in = false;
52      private PamPrincipal principal = null;
53      
54      /**
55       * 
56       */
57      public PamLoginModule() {
58      }
59  
60      /**
61       * Initializes PAM with the service name.
62       * 
63       * @param subject The subject to authenticate. Can be empty to make PAM ask for a username.
64       */
65      public void initialize(Subject subject, CallbackHandler callbackHandler,
66              Map sharedState, Map options) {
67          this.subject = subject;
68          String serviceName = (String) options.get("service");
69          if (serviceName == null) throw new IllegalArgumentException("service option not set in JAAS config for PamLoginModule");
70          //TODO: examine subject principals for an existing PamPrincipal
71          pam = new Pam(serviceName, null, new JaasPamCallback());
72          this.ch = callbackHandler;
73      }
74  
75      public boolean login() throws LoginException {
76          if (ch == null) throw new LoginException("Error: no CallbackHandler available " +
77                                       "to garner authentication information from the user");
78          
79          logged_in = false;
80          this.userPrompt = pam.getItem(PamConstants.PAM_USER_PROMPT);
81          if (this.userPrompt == null) {
82              this.userPrompt = PamConstants.PAM_DEFAULT_PROMPT;
83              pam.setItem(PamConstants.PAM_USER_PROMPT, this.userPrompt);
84          }
85          int status = pam.authenticate();
86          String statusMsg = pam.getError(status);
87          switch (status) {
88              case PamConstants.PAM_USER_UNKNOWN:
89              case PamConstants.PAM_AUTH_ERR:
90                  freePam();
91                  throw new FailedLoginException(statusMsg);
92      
93              case PamConstants.PAM_CRED_INSUFFICIENT:
94                  freePam();
95                  throw new CredentialsInsufficientException(statusMsg);
96      
97              case PamConstants.PAM_AUTHINFO_UNAVAIL:
98                  freePam();
99                  throw new AuthInfoUnavailableException(statusMsg);
100     
101             case PamConstants.PAM_MAXTRIES:
102                 freePam();
103                 throw new MaxRetriesException(statusMsg);
104     
105             case PamConstants.PAM_ABORT:
106                 freePam();
107                 throw new PamModuleLoadingException(statusMsg);
108     
109             case PamConstants.PAM_SUCCESS:
110                 /* do nothing and continue after switch */
111                 break;
112     
113             default:
114                 freePam();
115                 throw new LoginException(statusMsg + " (pam_authenticate)");
116         }
117         
118         status = pam.accountManagement();
119         statusMsg = pam.getError(status);
120         switch (status) {
121             case PamConstants.PAM_AUTHTOK_EXPIRED:
122                 freePam();
123                 throw new CredentialExpiredException(statusMsg);
124             
125             case PamConstants.PAM_ACCT_EXPIRED:
126                 freePam();
127                 throw new AccountExpiredException(statusMsg);
128             
129             case PamConstants.PAM_AUTH_ERR:
130                 freePam();
131                 throw new LoginException(statusMsg);
132             
133             case PamConstants.PAM_PERM_DENIED:
134                 freePam();
135                 throw new PermissionDeniedException(statusMsg);
136             
137             case PamConstants.PAM_USER_UNKNOWN:
138                 freePam();
139                 return false;
140             
141             case PamConstants.PAM_AUTHINFO_UNAVAIL:
142                 /* seems account management is not supported for this service */
143                 break;
144             
145             case PamConstants.PAM_SUCCESS:
146                 /* do nothing and continue after switch */
147                 break;
148             
149             //default is to accept login
150         }
151         
152         logged_in = true; 
153         return true;
154     }
155 
156     public boolean commit() throws LoginException {
157         freePam();
158         if (!logged_in) return false;
159         principal = new PamPrincipal(pamUsername);
160         Set principals = subject.getPrincipals();
161         if (!principals.contains(principal)) principals.add(principal);
162         
163         logged_in = false;
164         pamUsername = null;
165         return true;
166     }
167 
168     public boolean abort() throws LoginException {
169         reset();
170         if (!logged_in) return false;
171         if (principal != null) {
172             logout();
173         } else {
174             freePam();
175             pamUsername = null;
176         }
177         return false;
178     }
179 
180     public boolean logout() throws LoginException {
181         if (subject.isReadOnly()) return false;
182         Set principals = subject.getPrincipals();
183         if (!principals.contains(principal)) return false;
184         principals.remove(principal);
185         principal = null;
186         freePam();
187         pamUsername = null;
188         return true;
189     }
190     
191     private void reset() {
192             this.principal = null;
193             this.pamUsername = null;
194             this.logged_in = false;
195     }
196     
197     private void freePam() {
198         if (pam != null) {
199             pam.end();
200             pam = null;
201         }
202     }
203 
204     private class JaasPamCallback implements PamCallback {
205 
206         public int handle(PamMessage[] messages, PamResponse[] responses) {
207             Callback[] callbacks = toCallbacks(messages);
208             try {
209                 ch.handle(callbacks);
210                 answer(callbacks, responses);
211                 return PamConstants.PAM_SUCCESS;
212             } catch (IOException e) {
213                 // TODO Auto-generated catch block
214                 e.printStackTrace();
215             } catch (UnsupportedCallbackException e) {
216                 // TODO Auto-generated catch block
217                 e.printStackTrace();
218             }
219             return PamConstants.PAM_CONV_ERR;
220         }
221         
222         /**
223          * @param callbacks
224          * @param responses
225          */
226         private void answer(Callback[] callbacks, PamResponse[] responses) {
227             String text = null;
228             for (int i = 0; i < callbacks.length; i++) {
229                 PamResponse response = null; 
230                 Callback callback = callbacks[i];
231                 if (callback instanceof NameCallback) {
232                     pamUsername = ((NameCallback) callback).getName();
233                     response = new PamResponse(pamUsername);
234                 } else if (callback instanceof TextInputCallback) {
235                     text = ((TextInputCallback) callback).getText();
236                     response = new PamResponse(text);
237                 } else if (callback instanceof PasswordCallback) {
238                     response = new PamResponse(String.valueOf(((PasswordCallback) callback).getPassword()));
239                 } else if (callback instanceof TextOutputCallback) {
240                     response = new PamResponse("");
241                 } else if (callback instanceof LanguageCallback) {
242                     response = new PamResponse(((LanguageCallback) callback).getLocale().toString());
243                 } else if (callback instanceof ConfirmationCallback) {
244                     ConfirmationCallback cb = (ConfirmationCallback) callback;
245                     String option = cb.getOptions()[cb.getSelectedIndex()];
246                     response = new PamResponse(option);
247                 } else if (callback instanceof ChoiceCallback) {
248                     // there is no PAM standard for that
249                     throw new PamError("Unsupported: ChoiceCallback. Please implement a custom login module");
250                 }
251                 responses[i] = response;
252             }
253             if (pamUsername == null) pamUsername = text; //guess this is the username
254         }
255 
256         private Callback[] toCallbacks(PamMessage[] messages) {
257             Callback[] callbacks = new Callback[messages.length];
258             for (int i = 0; i < messages.length; i++) {
259                 PamMessage message = messages[i];
260                 Callback callback = null;
261                 switch (message.getMsg_style()) {
262                     case PamConstants.PAM_PROMPT_ECHO_ON:
263                         if (userPrompt.equals(message.getMsg())) {
264                             callback = new NameCallback(message.getMsg());
265                         } else {
266                             callback = new TextInputCallback(message.getMsg());
267                         }
268                         break;
269                     
270                     case PamConstants.PAM_PROMPT_ECHO_OFF:
271                         //prompts without echo are probably passwords
272                         callback = new PasswordCallback(message.getMsg(), false);
273                         break;
274                     
275                     case PamConstants.PAM_TEXT_INFO:
276                         callback = new TextOutputCallback(TextOutputCallback.INFORMATION, message.getMsg());
277                         break;
278                     
279                     case PamConstants.PAM_ERROR_MSG:
280                         callback = new TextOutputCallback(TextOutputCallback.ERROR, message.getMsg());
281                         break;
282                     
283                     default:
284                         throw new PamError("message style "+ message.getMsg_style() +" unknown");
285                 }
286                 callbacks[i] = callback;
287             }
288             return callbacks;
289         }
290     }
291 }