001package votorola.a.web.wic.authen; // Copyright 2008-2012, Michael Allan.  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE.
002
003import java.util.*;
004import javax.mail.internet.*;
005import javax.servlet.http.*;
006import org.apache.wicket.AttributeModifier;
007import org.apache.wicket.markup.html.IHeaderResponse;
008import org.apache.wicket.markup.html.basic.*;
009import org.apache.wicket.markup.html.form.*;
010import org.apache.wicket.model.*;
011import org.apache.wicket.request.Request;
012import org.apache.wicket.request.cycle.*;
013import org.apache.wicket.request.http.WebResponse;
014import org.apache.wicket.request.http.handler.RedirectRequestHandler;
015import org.apache.wicket.request.mapper.parameter.PageParameters;
016import org.apache.wicket.validation.*;
017import org.openid4java.consumer.*;
018import org.openid4java.discovery.*;
019import org.openid4java.message.AuthRequest;
020import votorola.a.*;
021import votorola.a.voter.*;
022import votorola.a.web.wic.*;
023import votorola.g.*;
024import votorola.g.lang.*;
025import votorola.g.locale.*;
026import votorola.g.mail.*;
027import votorola.g.web.CookieX;
028import votorola.g.web.wic.*;
029
030import static votorola.a.web.wic.authen.OpenIDAuthenticator.COOKIE_PERSIST_KEY;
031import static votorola.a.web.wic.authen.OpenIDAuthenticator.COOKIE_PERSIST_USER_EMAIL;
032
033
034/** An OpenID login page.  It provides the option of logging in by OpenID or email
035  * authentication.  In the latter case, this page performs the same function as
036  * {@linkplain WP_EmailAuthen1 WP_EmailAuthen1}.
037  *
038  * <p>The constructer accepts a {@linkplain #requestCycleRunner() request cycle runner}
039  * as an argument, to handle any post-login processing.  The runner will be called
040  * exactly once if authentication terminates, either successfully or by cancellation.
041  * Typically, the runner is coded to set a response page for the user.</p>
042  *
043  *     @see <a href='../../../../../../../a/web/wic/authen/WP_OpenIDLogin.html'
044  *                                           target='_top'>WP_OpenIDLogin.html</a>
045  */
046public @ThreadRestricted("wicket") final class WP_OpenIDLogin extends LoginPage
047{
048
049    // This would be easier to code (and use) if each input field were in a separate form.
050    // Then a press of the enter key would activate the field's associated submit button,
051    // instead of the top button regardless.  Validation would be simpler, too.  The only
052    // benefit of a single form, in fact, is that it simplifies the alignment of component
053    // views by allowing the use of a single alignment table.
054    //
055    // There was minor degradation after moving to Wicket 1.5.  This page now appears in
056    // navigation history, which it never used to.
057
058
059    /** Constructs a WP_OpenIDLogin that redirects to a newly constructed, bookmarkable,
060      * return page if authentication succeeds.
061      *
062      *     @see #respondWithReturnPage(org.apache.wicket.request.cycle.RequestCycle)
063      */
064    public WP_OpenIDLogin( final PageParameters pP ) { this( pP, null, null, null ); }
065      // must be public, per LoginPage
066
067
068
069  /** Constructs a WP_OpenIDLogin that continues from a previous, failed authentication
070    * attempt.  Called by {@linkplain WP_OpenIDReturn WP_OpenIDReturn}.
071    *
072    *     @param previousLoginPage the login page of the previous, failed
073    *       authentication attempt.
074    *     @param previousFailureString a message to present to the user relating the
075    *       failure of the previous authentication attempt.
076    */
077    WP_OpenIDLogin( final WP_OpenIDLogin previousLoginPage, final String previousFailureString )
078    {
079        this( previousLoginPage.getPageParameters(), previousLoginPage, previousFailureString,
080          previousLoginPage.requestCycleRunner );
081    }
082
083
084
085    private WP_OpenIDLogin( final PageParameters pP, final WP_OpenIDLogin previousLoginPage,
086      final String previousFailureString, RequestCycleRunner runner )
087    {
088        super( pP );
089        if( runner == null ) runner = new RequestCycleRunner() // after super(), or won't compile
090        {
091            public void run( final RequestCycle cycle )
092            {
093                respondWithReturnPage( cycle );
094                final VSession.User user = VSession.get().user();
095                if( user == null ) return; // no successful authentication
096
097                final String userEmail = user.email();
098                final VRequestCycle cycleV = (VRequestCycle)cycle;
099                if( persistent )
100                {
101                    final Request req = cycle.getRequest();
102                    final HttpServletRequest reqHS = (HttpServletRequest)
103                      cycleV.vRequest().getContainerRequest();
104                    final WebResponse resW = cycleV.vResponse();
105                    WebResponseX.addCookie( resW, OpenIDAuthenticator.newPersistCookie(
106                      COOKIE_PERSIST_USER_EMAIL, userEmail, /*toEncode*/true, req ));
107
108                    final VOWicket app = VOWicket.get();
109                    final OpenIDAuthenticator auth = (OpenIDAuthenticator)app.authenticator();
110                    final String key = auth.newPersistKey( userEmail, reqHS.getRemoteAddr() );
111                    WebResponseX.addCookie( resW, OpenIDAuthenticator.newPersistCookie(
112                      COOKIE_PERSIST_KEY, key, /*toEncode*/false, req ));
113
114                    final UserSettings.Table table = app.vsRun().userTable();
115                    try
116                    {
117                        final UserSettings settings = new UserSettings( userEmail, table );
118                        settings.setLoginPersistKey( key );
119                        settings.write( table, VSession.get() );
120                    }
121                    catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); }
122                }
123                else clearPersistence( userEmail, cycleV );
124            }
125        };
126        requestCycleRunner = runner;
127
128        final VRequestCycle cycle = VRequestCycle.get();
129        final BundleFormatter bun = cycle.bunW();
130        add( new Label( "title", bun.l( "a.web.wic.authen.WP_Login" ) ));
131        final Form<Void> y = new LoginForm();
132        add( y );
133
134      // OpenID.
135      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
136        y.add( new Label( "openid_identifierLabel",
137          bun.l( "a.web.wic.authen.WP_Login.openid_identifier" )));
138        final TextField<String> openidField =
139          invalidStyled( inputLengthConstrained( new TextField<String>( "openid_identifier",
140            new PropertyModel<String>( WP_OpenIDLogin.this, "openid_identifier" ))));
141        openidField.add( new IValidator<String>()
142        {
143            public void validate( final IValidatable<String> v )
144            {
145                assert v.getValue() != null : "no null if not INullAcceptingValidator";
146                openIDDiscoveryInformation = null; openIDAuthRequest = null; // till proven otherwise
147
148                final VRequestCycle cycle = VRequestCycle.get();
149                final VOWicket app = VOWicket.get();
150                final OpenIDAuthenticator auth = (OpenIDAuthenticator)app.authenticator();
151                try
152                {
153                    synchronized( auth )
154                    {
155                        final ConsumerManager m = auth.consumerManager();
156                        final List<?> discoveryList = m.discover( v.getValue() );
157                        if( discoveryList.isEmpty() )
158                        {
159                            v.error( new ValidationError().setMessage( cycle.bunW().l(
160                              "a.web.wic.authen.WP_Login.openid_identifier.xDiscovery.noEndPoint" )));
161                            return;
162                        }
163
164                        openIDDiscoveryInformation = m.associate( discoveryList );
165                        if( openIDDiscoveryInformation == null )
166                        {
167                            throw new IllegalStateException(
168                              "silent association failure, on non-empty discovery list" );
169                        }
170
171                        final String returnToURLString =
172                          cycle.uriFor(WP_OpenIDReturn.class).toString();
173                        openIDAuthRequest = m.authenticate( // misnomer, does not authenticate
174                          openIDDiscoveryInformation, returnToURLString );
175                    }
176                }
177                catch( org.openid4java.OpenIDException x )
178                {
179                    v.error( new ValidationError().setMessage( cycle.bunW().l(
180                      "a.web.wic.authen.WP_Login.openid_identifier.xDiscovery",
181                      ThrowableX.toStringExpanded( x )))); // getMessageExpanded() would not provide enough context when toString is 'java.net.UnknownHostException: HOST'
182                }
183            }
184        });
185        y.add( openidField );
186        if( previousLoginPage != null )
187        {
188            openid_identifier = previousLoginPage.openid_identifier;
189         // VSession.get().getFeedbackMessages().warn( /*reporter*/openidField, previousFailureString );
190         //// does not style field invalid, so (though it's not really validation time):
191            openidField.error( (IValidationError)
192              new ValidationError().setMessage( previousFailureString ));
193        }
194
195        final VoteServer vS = VOWicket.get().vsRun().voteServer();
196        y.add( new Label( "openid_identifierDescription",
197          bun.l( "a.web.wic.authen.WP_Login.openid_identifierDescription_XHT",
198            vS.pollwiki().uri().toASCIIString() )).setEscapeModelStrings( false ));
199
200      // Submit
201      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
202        {
203            persistent = cycle.vRequest().getCookie(COOKIE_PERSIST_BUTTON) != null;
204            final CheckBox button = new CheckBox( "persist",
205              new PropertyModel<Boolean>( WP_OpenIDLogin.this, "persistent" ));
206            y.add( button );
207
208            final Label label = new Label( "persistLabel",
209              bun.l( "a.web.wic.authen.WP_Login.persist" ));
210            label.add( AttributeModifier.replace( "title",
211              bun.l( "a.web.wic.authen.WP_Login.persistFull" )));
212            y.add( label );
213        }
214        {
215            final Button button = new Button( "submit" );
216            button.add( AttributeModifier.replace( "value",
217              bun.l( "a.web.wic.authen.WP_Login.submit" )));
218            y.add( button );
219        }
220
221      // Email
222      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
223        y.add( new Label( "or",
224          bun.l( "a.web.wic.authen.WP_Login.or_XHT" )).setEscapeModelStrings( false ));
225        y.add( new Label( "or-loginByEmail", bun.l( "a.web.wic.authen.WP_Login.or-loginByEmail" )));
226
227        y.add( new Label( "userEmailLabel", bun.l( "a.web.wic.authen.WP_EmailAuthen1.userEmail" )));
228        {
229            final TextField<String> emailField = new TextField<String>( "userEmail",
230              new PropertyModel<String>( WP_OpenIDLogin.this, "userEmailInput" ));
231            invalidStyled( inputLengthConstrained( emailField ));
232            emailField.add( new WicEmailAddressValidator()
233            {
234                // extracting conversions from a validator.  Improper.  Consider using a
235                // converter instead.  Cf. votorola.g.util.regex.WicPatternConverter
236                public @Override void validate( final IValidatable<String> v )
237                {
238                    claimedUserIAddress = null; // till proven otherwise
239                    super.validate( v );
240                }
241                public @Override void onSuccess( final InternetAddress iAddress )
242                {
243                    claimedUserIAddress = iAddress;
244                    InternetAddressX.canonicalize( claimedUserIAddress );
245                }
246            });
247            y.add( emailField );
248
249            y.add( new Label( "userEmailDescription",
250              bun.l( "a.web.wic.authen.WP_EmailAuthen1.userEmailDescription" )));
251        }
252
253      // Alias
254      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
255        if( vS.testUseMode() != VoteServer.TestUseMode.FULL ) y.add( newNullComponent( "test" ));
256        else y.add( new WC_Alias( "test", WP_OpenIDLogin.this, cycle ));
257
258      // Feedback
259      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
260        y.add( new WC_Feedback( "feedback" ));
261
262      // Control footer
263      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
264        {
265            final CheckBox button = new CheckBox( "reauth",
266              new PropertyModel<Boolean>( WP_OpenIDLogin.this, "reauthenticationRequested" ));
267            y.add( button );
268
269            final Label label = new Label( "reauthLabel",
270              bun.l( "a.web.wic.authen.WP_Login.reauth" ));
271            y.add( label );
272
273            final Label labelD = new Label( "reauthDescription",
274              bun.l( "a.web.wic.authen.WP_Login.reauthDescription" ));
275            y.add( labelD );
276        }
277    }
278
279
280
281   // ------------------------------------------------------------------------------------
282
283
284    /** Clears any cookie-persisted authentication.
285      */
286    public static void clearPersistence( final String userEmail, final VRequestCycle cycle )
287    {
288        final Request req = cycle.getRequest();
289        final WebResponse resW = cycle.vResponse();
290        WebResponseX.clearCookie( resW, OpenIDAuthenticator.newPersistCookie(
291          COOKIE_PERSIST_USER_EMAIL, null, /*toEncode*/true, req ));
292        WebResponseX.clearCookie( resW, OpenIDAuthenticator.newPersistCookie(
293          COOKIE_PERSIST_KEY, null, /*toEncode*/false, req ));
294
295        final UserSettings.Table table = VOWicket.get().vsRun().userTable();
296        try
297        {
298            final UserSettings settings = new UserSettings( userEmail, table );
299            settings.setLoginPersistKey( null );
300            settings.write( table, VSession.get() );
301        }
302        catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); }
303    }
304
305
306
307    /** Answers whether a successful authentication is to be persisted in client cookies.
308      *
309      *     @see votorola.a.web.wic.VSession.User#isPersistent()
310      */
311    boolean isPersistent() { return persistent; }
312
313
314        private boolean persistent; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
315
316
317
318    /** Answers whether the user is requesting reauthentication of his/her email address.
319      * This is false when the page is yet unsubmitted.
320      */
321    boolean isReauthenticationRequested() { return reauthenticationRequested; }
322
323
324        private boolean reauthenticationRequested; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
325
326
327
328    /** The results of discovery on the user's OpenID, if any.
329      *
330      *     @return the discovery information, or null if nothing yet discovered.
331      */
332    DiscoveryInformation openIDDiscoveryInformation() { return openIDDiscoveryInformation; }
333
334
335        private DiscoveryInformation openIDDiscoveryInformation; // from field validator
336
337
338
339    /** Persists the state of the "keep me logged in" button in a cookie.
340      *
341      *     @see #COOKIE_PERSIST_BUTTON
342      */
343    static void persistPersistButton( final boolean state, final WebResponse resW )
344    {
345        final Cookie cookie = new CookieX( COOKIE_PERSIST_BUTTON, "1" );
346        final int age;
347        if( state ) age = CookieX.DURATION_YEAR_S;
348        else age = 0; // client might postpone this deletion, but not likely
349        cookie.setMaxAge( age );
350        WebResponseX.addCookie( resW, cookie );
351    }
352
353
354
355    /** The runner to do any post-login processing, such as redirecting back to the
356      * original referring page.  If the authentication is successful, it is guaranteed to
357      * run; otherwise it may or may not.
358      *
359      *     @return the runner, which is never null.
360      */
361    RequestCycleRunner requestCycleRunner() { return requestCycleRunner; }
362
363
364        private final RequestCycleRunner requestCycleRunner;
365
366
367
368    /** Sets the newly authenticated user in the session without replacing it.  The
369      * session is not replaced here because going back to this login page after OpenID
370      * authentication alters the navigation history by removing the forward pages (Wicket
371      * 1.5.4) such that the user cannot return to them.
372      *
373      *     @param _method the name of the authentication method for logging purposes.
374      *     @see #isPersistent()
375      */
376    static void setUserInSession( final String email, String _method, boolean _persistent,
377      VRequestCycle _cycle )
378    {
379        setUserInSession( IDPair.fromEmail(email), _method, _persistent, /*toReplaceSession*/false,
380          _cycle );
381    }
382
383
384
385   // - I - H e a d e r - C o n t r i b u t o r ------------------------------------------
386
387
388    public @Override void renderHead( IHeaderResponse r )
389    {
390        super.renderHead( r );
391        if( !getSession().getFeedbackMessages().isEmpty() )
392        {
393            r.renderOnLoadJavaScript( "location.hash = 'feedback'" );
394        }
395    }
396
397
398
399//// P r i v a t e ///////////////////////////////////////////////////////////////////////
400
401
402    private String aliasInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
403
404
405
406    private transient InternetAddress claimedUserIAddress; // from field validator, in canonical form
407
408
409
410    private String openid_identifier; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
411
412
413
414    private transient AuthRequest openIDAuthRequest; // from field validator
415
416
417
418    private String userEmailInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
419
420
421
422   // ====================================================================================
423
424
425    private final class LoginForm extends Form<Void>
426    {
427
428        private LoginForm()
429        {
430            super( "form" );
431            add( new IDFieldValidator()
432            {
433                List<TextField<?>> idFields()
434                {
435                    final ArrayList<TextField<?>> fields = new ArrayList<>( /*initial capacity*/4 );
436                    fields.add( (TextField<?>)get( "openid_identifier" ));
437                    fields.add( (TextField<?>)get( "userEmail" ));
438                    final TextField<?> field = (TextField<?>)
439                   // get( "test.alias" );
440                   /// always gives null for some reason, so:
441                      ((WC_Alias)get( "test" )).get( "alias" );
442                    if( field != null ) fields.add( field );
443                    return fields;
444                }
445            });
446        }
447
448
449        protected @Override void onSubmit()
450        {
451            super.onSubmit();
452            final VRequestCycle cycle = VRequestCycle.get();
453            persistPersistButton( persistent, cycle.vResponse() );
454
455          // Authenticate
456          // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
457            if( openid_identifier != null ) onSubmitOpenID( cycle );
458            else if( userEmailInput != null ) onSubmitEmail( cycle ); // not claimedUserIAddress, which is not properly nulled by an empty field
459            else onSubmitAlias( cycle );
460        }
461
462
463        private void onSubmitAlias( final VRequestCycle cycle )
464        {
465            if( aliasInput == null ) throw new IllegalStateException();
466
467            setUserInSession( IDPair.fromEmail(WC_Alias.toEmail(aliasInput)), "alias", persistent,
468              /*toReplaceSession*/false, cycle );
469            requestCycleRunner.run( cycle );
470        }
471
472
473        private void onSubmitEmail( final VRequestCycle cycle )
474        {
475            if( claimedUserIAddress == null ) throw new IllegalStateException();
476
477            final WP_EmailAuthen2 authenticationPage =
478              new WP_EmailAuthen2( claimedUserIAddress, persistent, cycle, requestCycleRunner );
479            cycle.setResponsePage( authenticationPage.sendMessage(cycle)? authenticationPage:
480              new WP_Message() );
481        }
482
483
484        private void onSubmitOpenID( final VRequestCycle cycle )
485        {
486            if( openIDDiscoveryInformation == null || openIDAuthRequest == null )
487            {
488                throw new IllegalStateException();
489            }
490
491            VSession.get().scopeOpenIDReturn().setLoginPage( WP_OpenIDLogin.this );
492            cycle.scheduleRequestHandlerAfterCurrent( new RedirectRequestHandler(
493              openIDAuthRequest.getDestinationUrl( /*for GET redirect*/true )));
494        }
495
496    }
497
498
499}