001package votorola.a.web.wic.authen; // Copyright 2008, 2010, 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 com.sun.mail.smtp.*;
004import java.math.*;
005import java.util.logging.*; import votorola.g.logging.*;
006import javax.mail.*;
007import javax.mail.internet.*;
008import votorola.a.response.*;
009import votorola.a.voter.*;
010import votorola.a.web.wic.*;
011import votorola.g.lang.*;
012import votorola.g.locale.*;
013import votorola.g.mail.*;
014import votorola.g.web.wic.*;
015import org.apache.wicket.*;
016import org.apache.wicket.markup.html.basic.*;
017import org.apache.wicket.markup.html.form.*;
018import org.apache.wicket.request.cycle.*;
019import org.apache.wicket.model.*;
020import org.apache.wicket.validation.*;
021
022
023/** A page for the authentication of a user's email address, step 2.
024  * On this page, the user inputs the key sent in the authentication message.
025  *
026  * <p>We might instead have sent the user a callback link, with the key encoded in the
027  * URI, and then handled the complexity of it being entered from a different browser
028  * window, and therefore a different session.  But this is simpler, and good enough for
029  * starters.</p>
030  *
031  *     @see <a href='../../../../../../../a/web/wic/authen/WP_EmailAuthen2.html'
032  *                                           target='_top'>WP_EmailAuthen2.html</a>
033  */
034@ThreadRestricted("wicket") final class WP_EmailAuthen2 extends VPageHTML
035{
036
037
038    /** Constructs a WP_EmailAuthen2 in continuation of WP_OpenIDLogin (step 1).
039      *
040      *     @param claimedUserIAddress the claimed email address, as input by user, but in
041      *       canonical form.
042      *     @see WP_OpenIDLogin#isPersistent()
043      *     @param _runner the runner to handle any post-authentication processing.
044      *
045      *     @throws AddressException if userEmailInput is malformed
046      */
047    WP_EmailAuthen2( final InternetAddress claimedUserIAddress, boolean _persistent,
048      final VRequestCycle cycle, RequestCycleRunner _runner )
049    {
050        claimedUserEmail = claimedUserIAddress.getAddress();
051        persistent = _persistent;
052        runner = _runner;
053        setVersioned( false ); // enforcement of MISMATCH_COUNT_LIMIT - no back up, and retry
054
055        final VOWicket app = VOWicket.get();
056        final BundleFormatter bun = VRequestCycle.get().bunW();
057        final byte[] randomByteArray = new byte[2]; // raw format
058        final BigInteger randomN1, randomN2;
059        final OpenIDAuthenticator auth = (OpenIDAuthenticator)app.authenticator();
060        synchronized( auth )
061        {
062            auth.secureRandomizer().nextBytes( randomByteArray );
063            randomN1 = new BigInteger( /*positive*/1, randomByteArray );
064            auth.secureRandomizer().nextBytes( randomByteArray );
065        }
066        randomN2 = new BigInteger( /*positive*/1, randomByteArray );
067        keyExpected = bun.format( "%1$04X-%2$04X", randomN1, randomN2 );
068
069        try
070        {
071            voteServerIAddress = new InternetAddress( app.serviceEmail() );
072        }
073        catch( AddressException x ) { throw new RuntimeException( x ); }
074        InternetAddressX.trySetPersonal( voteServerIAddress,
075          app.vsRun().voteServer().shortTitle(), "UTF-8" );
076        messageSubject = bun.l( "a.web.wic.authen.WP_EmailAuthen2.message.subject" );
077
078      // LAYOUT
079      // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
080
081        add( new Label( "title", bun.l( "a.web.wic.authen.WP_EmailAuthen2" ) ));
082        add( new Label( "explanation",
083          bun.l( "a.web.wic.authen.WP_EmailAuthen2.explanation",
084            voteServerIAddress.getAddress(), messageSubject, claimedUserEmail )));
085
086        final Form<Void> form = new KeyVerificationForm();
087        add( form );
088
089      // Field
090      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
091        form.add( new Label( "keyLabel",
092          bun.l( "a.web.wic.authen.WP_EmailAuthen2.keyLabel" )));
093        final TextField<String> keyField = new TextField<String>( "key", new PropertyModel<String>(
094          WP_EmailAuthen2.this, "keyReceived" ));
095        invalidStyled( inputLengthConstrained( keyField ));
096        keyField.setRequired( true );
097        keyField.add( new IValidator<String>()
098        {
099            private ValidationError mismatchCountLimitError = null; // till it occurs
100            public void validate( final IValidatable<String> v )
101            {
102             // assert v.getValue() != null : "no null if not INullAcceptingValidator";
103             //// no matter
104                if( mismatchCountLimitError != null )
105                {
106                    v.error( mismatchCountLimitError );
107                    return;
108                }
109
110                if( !keyExpected.equalsIgnoreCase( v.getValue() ))
111                {
112                    ++mismatchCount;
113                    if( mismatchCount < MISMATCH_COUNT_LIMIT )
114                    {
115                        v.error( new ValidationError().setMessage( VRequestCycle.get().bunW().l(
116                          "a.web.wic.authen.WP_EmailAuthen2.key.mismatch" )));
117                        return;
118                    }
119
120                    mismatchCountLimitError = new ValidationError().setMessage(
121                      VRequestCycle.get().bunW().l(
122                      "a.web.wic.authen.WP_EmailAuthen2.key.mismatchCount" ));
123                    v.error( mismatchCountLimitError );
124                }
125            }
126        });
127        form.add( keyField );
128
129        form.add( new Label( "keyDescription",
130          bun.l( "a.web.wic.authen.WP_EmailAuthen2.keyDescription" )));
131
132      // Buttons
133      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
134        {
135            final Button button = new Button( "submit" )
136            {
137                public @Override boolean isEnabled()
138                {
139                    return mismatchCount < MISMATCH_COUNT_LIMIT && super.isEnabled();
140                }
141            };
142            button.add( AttributeModifier.replace( "value",
143              bun.l( "a.web.wic.authen.WP_EmailAuthen2.submit" )));
144            form.add( button );
145        }
146        {
147            final Button button = new Button( "submit-cancel" )
148            {
149                public @Override void onSubmit()
150                {
151                    super.onSubmit();
152                    runner.run( VRequestCycle.get() );
153                }
154            };
155            button.add( AttributeModifier.replace( "value",
156              bun.l( "a.web.wic.authen.WP_EmailAuthen2.submit-cancel" )));
157            button.setDefaultFormProcessing( false );
158            form.add( button );
159        }
160
161      // Feedback Messages
162      // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
163        add( new WC_Feedback( "feedback" ));
164
165    }
166
167
168
169   // ------------------------------------------------------------------------------------
170
171
172    /** Sends the authentication message to the user.
173      *
174      *     @return true if the message was sent, false otherwise, When false a detailed
175      *       error message is output to the log and a feedback message is output to the
176      *       session.  For security reasons the latter contains no details, but only
177      *       refers to the former.
178      */
179    boolean sendMessage( final VRequestCycle cycle )
180    {
181        final VOWicket app = VOWicket.get();
182        final BundleFormatter bun = cycle.bunW();
183        final ReplyBuilder replyB = new ReplyBuilder( bun.bundle() );
184
185        final String homeURIString = cycle.uriFor(app.getHomePage()).toString();
186        synchronized( app.mailLock() )
187        {
188            final SMTPMessage message = new SMTPMessage( app.mailSession() );
189            try
190            {
191                message.setFrom( voteServerIAddress );
192                message.setRecipient( Message.RecipientType.TO,
193                  new InternetAddress( claimedUserEmail ));
194                message.setSubject( messageSubject, "UTF-8" );
195                message.setHeader( "X-Mailer", WP_OpenIDLogin.class.getName() );
196            }
197            catch( MessagingException x ) { throw new RuntimeException( x ); }
198
199            replyB.lappendln( "a.web.wic.authen.WP_EmailAuthen2.message.body",
200              homeURIString, claimedUserEmail, keyExpected );
201            try
202            {
203                message.setText( replyB.toString(), "UTF-8" );
204
205                Exception x = app.mailSender().trySend( message, app.mailSession() );
206                if( x != null ) throw x;
207            }
208            catch( Exception x ) // hide it from the user, at this point, as it might contain the key value (and, in future revs of the code, the receiver page might somehow be accessible to the user)
209            {
210                final Level level = Level.CONFIG;
211                LoggerX.i(getClass()).log( level, /*message*/"unable to send email authentication message", x );
212                VSession.get().error( "Unable to send email authentication message.  The reason has been printed to the server log, at level " + level + "." );
213                return false;
214            }
215            return true;
216        }
217    }
218
219
220
221//// P r i v a t e ///////////////////////////////////////////////////////////////////////
222
223
224    private final String claimedUserEmail;
225      // in canonical form as always, but also so that any message sent shows only the
226      // bare addr-spec, and not any personal part typed by the claimant (which may be
227      // spam)
228
229
230
231    private final String keyExpected;
232
233
234
235    private String keyReceived; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible()
236
237
238
239    private final String messageSubject;
240
241
242
243    private static final int MISMATCH_COUNT_LIMIT = 3;
244
245
246
247    private int mismatchCount; // prevent attack by exhaustive retries
248
249
250
251    private final boolean persistent;
252
253
254
255    private final RequestCycleRunner runner;
256
257
258
259    private final InternetAddress voteServerIAddress;
260
261
262
263   // ====================================================================================
264
265
266    private class KeyVerificationForm extends Form<Void>
267    {
268
269        KeyVerificationForm() { super( "form" ); }
270
271
272        protected @Override void onSubmit()
273        {
274            super.onSubmit();
275            if( !keyExpected.equalsIgnoreCase( keyReceived )) throw new IllegalStateException();
276
277            final VRequestCycle cycle = VRequestCycle.get();
278            WP_OpenIDLogin.setUserInSession( claimedUserEmail, "email", persistent, cycle );
279            runner.run( cycle );
280        }
281
282
283    }
284
285
286
287}