package 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. import java.util.*; import javax.mail.internet.*; import javax.servlet.http.*; import org.apache.wicket.AttributeModifier; import org.apache.wicket.markup.html.IHeaderResponse; import org.apache.wicket.markup.html.basic.*; import org.apache.wicket.markup.html.form.*; import org.apache.wicket.model.*; import org.apache.wicket.request.Request; import org.apache.wicket.request.cycle.*; import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.request.http.handler.RedirectRequestHandler; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.validation.*; import org.openid4java.consumer.*; import org.openid4java.discovery.*; import org.openid4java.message.AuthRequest; import votorola.a.*; import votorola.a.voter.*; import votorola.a.web.wic.*; import votorola.g.*; import votorola.g.lang.*; import votorola.g.locale.*; import votorola.g.mail.*; import votorola.g.web.CookieX; import votorola.g.web.wic.*; import static votorola.a.web.wic.authen.OpenIDAuthenticator.COOKIE_PERSIST_KEY; import static votorola.a.web.wic.authen.OpenIDAuthenticator.COOKIE_PERSIST_USER_EMAIL; /** An OpenID login page. It provides the option of logging in by OpenID or email * authentication. In the latter case, this page performs the same function as * {@linkplain WP_EmailAuthen1 WP_EmailAuthen1}. * *

The constructer accepts a {@linkplain #requestCycleRunner() request cycle runner} * as an argument, to handle any post-login processing. The runner will be called * exactly once if authentication terminates, either successfully or by cancellation. * Typically, the runner is coded to set a response page for the user.

* * @see WP_OpenIDLogin.html */ public @ThreadRestricted("wicket") final class WP_OpenIDLogin extends LoginPage { // This would be easier to code (and use) if each input field were in a separate form. // Then a press of the enter key would activate the field's associated submit button, // instead of the top button regardless. Validation would be simpler, too. The only // benefit of a single form, in fact, is that it simplifies the alignment of component // views by allowing the use of a single alignment table. // // There was minor degradation after moving to Wicket 1.5. This page now appears in // navigation history, which it never used to. /** Constructs a WP_OpenIDLogin that redirects to a newly constructed, bookmarkable, * return page if authentication succeeds. * * @see #respondWithReturnPage(org.apache.wicket.request.cycle.RequestCycle) */ public WP_OpenIDLogin( final PageParameters pP ) { this( pP, null, null, null ); } // must be public, per LoginPage /** Constructs a WP_OpenIDLogin that continues from a previous, failed authentication * attempt. Called by {@linkplain WP_OpenIDReturn WP_OpenIDReturn}. * * @param previousLoginPage the login page of the previous, failed * authentication attempt. * @param previousFailureString a message to present to the user relating the * failure of the previous authentication attempt. */ WP_OpenIDLogin( final WP_OpenIDLogin previousLoginPage, final String previousFailureString ) { this( previousLoginPage.getPageParameters(), previousLoginPage, previousFailureString, previousLoginPage.requestCycleRunner ); } private WP_OpenIDLogin( final PageParameters pP, final WP_OpenIDLogin previousLoginPage, final String previousFailureString, RequestCycleRunner runner ) { super( pP ); if( runner == null ) runner = new RequestCycleRunner() // after super(), or won't compile { public void run( final RequestCycle cycle ) { respondWithReturnPage( cycle ); final VSession.User user = VSession.get().user(); if( user == null ) return; // no successful authentication final String userEmail = user.email(); final VRequestCycle cycleV = (VRequestCycle)cycle; if( persistent ) { final Request req = cycle.getRequest(); final HttpServletRequest reqHS = (HttpServletRequest) cycleV.vRequest().getContainerRequest(); final WebResponse resW = cycleV.vResponse(); WebResponseX.addCookie( resW, OpenIDAuthenticator.newPersistCookie( COOKIE_PERSIST_USER_EMAIL, userEmail, /*toEncode*/true, req )); final VOWicket app = VOWicket.get(); final OpenIDAuthenticator auth = (OpenIDAuthenticator)app.authenticator(); final String key = auth.newPersistKey( userEmail, reqHS.getRemoteAddr() ); WebResponseX.addCookie( resW, OpenIDAuthenticator.newPersistCookie( COOKIE_PERSIST_KEY, key, /*toEncode*/false, req )); final UserSettings.Table table = app.vsRun().userTable(); try { final UserSettings settings = new UserSettings( userEmail, table ); settings.setLoginPersistKey( key ); settings.write( table, VSession.get() ); } catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); } } else clearPersistence( userEmail, cycleV ); } }; requestCycleRunner = runner; final VRequestCycle cycle = VRequestCycle.get(); final BundleFormatter bun = cycle.bunW(); add( new Label( "title", bun.l( "a.web.wic.authen.WP_Login" ) )); final Form y = new LoginForm(); add( y ); // OpenID. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - y.add( new Label( "openid_identifierLabel", bun.l( "a.web.wic.authen.WP_Login.openid_identifier" ))); final TextField openidField = invalidStyled( inputLengthConstrained( new TextField( "openid_identifier", new PropertyModel( WP_OpenIDLogin.this, "openid_identifier" )))); openidField.add( new IValidator() { public void validate( final IValidatable v ) { assert v.getValue() != null : "no null if not INullAcceptingValidator"; openIDDiscoveryInformation = null; openIDAuthRequest = null; // till proven otherwise final VRequestCycle cycle = VRequestCycle.get(); final VOWicket app = VOWicket.get(); final OpenIDAuthenticator auth = (OpenIDAuthenticator)app.authenticator(); try { synchronized( auth ) { final ConsumerManager m = auth.consumerManager(); final List discoveryList = m.discover( v.getValue() ); if( discoveryList.isEmpty() ) { v.error( new ValidationError().setMessage( cycle.bunW().l( "a.web.wic.authen.WP_Login.openid_identifier.xDiscovery.noEndPoint" ))); return; } openIDDiscoveryInformation = m.associate( discoveryList ); if( openIDDiscoveryInformation == null ) { throw new IllegalStateException( "silent association failure, on non-empty discovery list" ); } final String returnToURLString = cycle.uriFor(WP_OpenIDReturn.class).toString(); openIDAuthRequest = m.authenticate( // misnomer, does not authenticate openIDDiscoveryInformation, returnToURLString ); } } catch( org.openid4java.OpenIDException x ) { v.error( new ValidationError().setMessage( cycle.bunW().l( "a.web.wic.authen.WP_Login.openid_identifier.xDiscovery", ThrowableX.toStringExpanded( x )))); // getMessageExpanded() would not provide enough context when toString is 'java.net.UnknownHostException: HOST' } } }); y.add( openidField ); if( previousLoginPage != null ) { openid_identifier = previousLoginPage.openid_identifier; // VSession.get().getFeedbackMessages().warn( /*reporter*/openidField, previousFailureString ); //// does not style field invalid, so (though it's not really validation time): openidField.error( (IValidationError) new ValidationError().setMessage( previousFailureString )); } final VoteServer vS = VOWicket.get().vsRun().voteServer(); y.add( new Label( "openid_identifierDescription", bun.l( "a.web.wic.authen.WP_Login.openid_identifierDescription_XHT", vS.pollwiki().uri().toASCIIString() )).setEscapeModelStrings( false )); // Submit // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { persistent = cycle.vRequest().getCookie(COOKIE_PERSIST_BUTTON) != null; final CheckBox button = new CheckBox( "persist", new PropertyModel( WP_OpenIDLogin.this, "persistent" )); y.add( button ); final Label label = new Label( "persistLabel", bun.l( "a.web.wic.authen.WP_Login.persist" )); label.add( AttributeModifier.replace( "title", bun.l( "a.web.wic.authen.WP_Login.persistFull" ))); y.add( label ); } { final Button button = new Button( "submit" ); button.add( AttributeModifier.replace( "value", bun.l( "a.web.wic.authen.WP_Login.submit" ))); y.add( button ); } // Email // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - y.add( new Label( "or", bun.l( "a.web.wic.authen.WP_Login.or_XHT" )).setEscapeModelStrings( false )); y.add( new Label( "or-loginByEmail", bun.l( "a.web.wic.authen.WP_Login.or-loginByEmail" ))); y.add( new Label( "userEmailLabel", bun.l( "a.web.wic.authen.WP_EmailAuthen1.userEmail" ))); { final TextField emailField = new TextField( "userEmail", new PropertyModel( WP_OpenIDLogin.this, "userEmailInput" )); invalidStyled( inputLengthConstrained( emailField )); emailField.add( new WicEmailAddressValidator() { // extracting conversions from a validator. Improper. Consider using a // converter instead. Cf. votorola.g.util.regex.WicPatternConverter public @Override void validate( final IValidatable v ) { claimedUserIAddress = null; // till proven otherwise super.validate( v ); } public @Override void onSuccess( final InternetAddress iAddress ) { claimedUserIAddress = iAddress; InternetAddressX.canonicalize( claimedUserIAddress ); } }); y.add( emailField ); y.add( new Label( "userEmailDescription", bun.l( "a.web.wic.authen.WP_EmailAuthen1.userEmailDescription" ))); } // Alias // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( vS.testUseMode() != VoteServer.TestUseMode.FULL ) y.add( newNullComponent( "test" )); else y.add( new WC_Alias( "test", WP_OpenIDLogin.this, cycle )); // Feedback // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - y.add( new WC_Feedback( "feedback" )); // Control footer // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final CheckBox button = new CheckBox( "reauth", new PropertyModel( WP_OpenIDLogin.this, "reauthenticationRequested" )); y.add( button ); final Label label = new Label( "reauthLabel", bun.l( "a.web.wic.authen.WP_Login.reauth" )); y.add( label ); final Label labelD = new Label( "reauthDescription", bun.l( "a.web.wic.authen.WP_Login.reauthDescription" )); y.add( labelD ); } } // ------------------------------------------------------------------------------------ /** Clears any cookie-persisted authentication. */ public static void clearPersistence( final String userEmail, final VRequestCycle cycle ) { final Request req = cycle.getRequest(); final WebResponse resW = cycle.vResponse(); WebResponseX.clearCookie( resW, OpenIDAuthenticator.newPersistCookie( COOKIE_PERSIST_USER_EMAIL, null, /*toEncode*/true, req )); WebResponseX.clearCookie( resW, OpenIDAuthenticator.newPersistCookie( COOKIE_PERSIST_KEY, null, /*toEncode*/false, req )); final UserSettings.Table table = VOWicket.get().vsRun().userTable(); try { final UserSettings settings = new UserSettings( userEmail, table ); settings.setLoginPersistKey( null ); settings.write( table, VSession.get() ); } catch( Exception x ) { throw VotorolaRuntimeException.castOrWrapped( x ); } } /** Answers whether a successful authentication is to be persisted in client cookies. * * @see votorola.a.web.wic.VSession.User#isPersistent() */ boolean isPersistent() { return persistent; } private boolean persistent; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() /** Answers whether the user is requesting reauthentication of his/her email address. * This is false when the page is yet unsubmitted. */ boolean isReauthenticationRequested() { return reauthenticationRequested; } private boolean reauthenticationRequested; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() /** The results of discovery on the user's OpenID, if any. * * @return the discovery information, or null if nothing yet discovered. */ DiscoveryInformation openIDDiscoveryInformation() { return openIDDiscoveryInformation; } private DiscoveryInformation openIDDiscoveryInformation; // from field validator /** Persists the state of the "keep me logged in" button in a cookie. * * @see #COOKIE_PERSIST_BUTTON */ static void persistPersistButton( final boolean state, final WebResponse resW ) { final Cookie cookie = new CookieX( COOKIE_PERSIST_BUTTON, "1" ); final int age; if( state ) age = CookieX.DURATION_YEAR_S; else age = 0; // client might postpone this deletion, but not likely cookie.setMaxAge( age ); WebResponseX.addCookie( resW, cookie ); } /** The runner to do any post-login processing, such as redirecting back to the * original referring page. If the authentication is successful, it is guaranteed to * run; otherwise it may or may not. * * @return the runner, which is never null. */ RequestCycleRunner requestCycleRunner() { return requestCycleRunner; } private final RequestCycleRunner requestCycleRunner; /** Sets the newly authenticated user in the session without replacing it. The * session is not replaced here because going back to this login page after OpenID * authentication alters the navigation history by removing the forward pages (Wicket * 1.5.4) such that the user cannot return to them. * * @param _method the name of the authentication method for logging purposes. * @see #isPersistent() */ static void setUserInSession( final String email, String _method, boolean _persistent, VRequestCycle _cycle ) { setUserInSession( IDPair.fromEmail(email), _method, _persistent, /*toReplaceSession*/false, _cycle ); } // - I - H e a d e r - C o n t r i b u t o r ------------------------------------------ public @Override void renderHead( IHeaderResponse r ) { super.renderHead( r ); if( !getSession().getFeedbackMessages().isEmpty() ) { r.renderOnLoadJavaScript( "location.hash = 'feedback'" ); } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private String aliasInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() private transient InternetAddress claimedUserIAddress; // from field validator, in canonical form private String openid_identifier; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() private transient AuthRequest openIDAuthRequest; // from field validator private String userEmailInput; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() // ==================================================================================== private final class LoginForm extends Form { private LoginForm() { super( "form" ); add( new IDFieldValidator() { List> idFields() { final ArrayList> fields = new ArrayList<>( /*initial capacity*/4 ); fields.add( (TextField)get( "openid_identifier" )); fields.add( (TextField)get( "userEmail" )); final TextField field = (TextField) // get( "test.alias" ); /// always gives null for some reason, so: ((WC_Alias)get( "test" )).get( "alias" ); if( field != null ) fields.add( field ); return fields; } }); } protected @Override void onSubmit() { super.onSubmit(); final VRequestCycle cycle = VRequestCycle.get(); persistPersistButton( persistent, cycle.vResponse() ); // Authenticate // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( openid_identifier != null ) onSubmitOpenID( cycle ); else if( userEmailInput != null ) onSubmitEmail( cycle ); // not claimedUserIAddress, which is not properly nulled by an empty field else onSubmitAlias( cycle ); } private void onSubmitAlias( final VRequestCycle cycle ) { if( aliasInput == null ) throw new IllegalStateException(); setUserInSession( IDPair.fromEmail(WC_Alias.toEmail(aliasInput)), "alias", persistent, /*toReplaceSession*/false, cycle ); requestCycleRunner.run( cycle ); } private void onSubmitEmail( final VRequestCycle cycle ) { if( claimedUserIAddress == null ) throw new IllegalStateException(); final WP_EmailAuthen2 authenticationPage = new WP_EmailAuthen2( claimedUserIAddress, persistent, cycle, requestCycleRunner ); cycle.setResponsePage( authenticationPage.sendMessage(cycle)? authenticationPage: new WP_Message() ); } private void onSubmitOpenID( final VRequestCycle cycle ) { if( openIDDiscoveryInformation == null || openIDAuthRequest == null ) { throw new IllegalStateException(); } VSession.get().scopeOpenIDReturn().setLoginPage( WP_OpenIDLogin.this ); cycle.scheduleRequestHandlerAfterCurrent( new RedirectRequestHandler( openIDAuthRequest.getDestinationUrl( /*for GET redirect*/true ))); } } }