package 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. import com.sun.mail.smtp.*; import java.math.*; import java.util.logging.*; import votorola.g.logging.*; import javax.mail.*; import javax.mail.internet.*; import votorola.a.response.*; import votorola.a.voter.*; import votorola.a.web.wic.*; import votorola.g.lang.*; import votorola.g.locale.*; import votorola.g.mail.*; import votorola.g.web.wic.*; import org.apache.wicket.*; import org.apache.wicket.markup.html.basic.*; import org.apache.wicket.markup.html.form.*; import org.apache.wicket.request.cycle.*; import org.apache.wicket.model.*; import org.apache.wicket.validation.*; /** A page for the authentication of a user's email address, step 2. * On this page, the user inputs the key sent in the authentication message. * *

We might instead have sent the user a callback link, with the key encoded in the * URI, and then handled the complexity of it being entered from a different browser * window, and therefore a different session. But this is simpler, and good enough for * starters.

* * @see WP_EmailAuthen2.html */ @ThreadRestricted("wicket") final class WP_EmailAuthen2 extends VPageHTML { /** Constructs a WP_EmailAuthen2 in continuation of WP_OpenIDLogin (step 1). * * @param claimedUserIAddress the claimed email address, as input by user, but in * canonical form. * @see WP_OpenIDLogin#isPersistent() * @param _runner the runner to handle any post-authentication processing. * * @throws AddressException if userEmailInput is malformed */ WP_EmailAuthen2( final InternetAddress claimedUserIAddress, boolean _persistent, final VRequestCycle cycle, RequestCycleRunner _runner ) { claimedUserEmail = claimedUserIAddress.getAddress(); persistent = _persistent; runner = _runner; setVersioned( false ); // enforcement of MISMATCH_COUNT_LIMIT - no back up, and retry final VOWicket app = VOWicket.get(); final BundleFormatter bun = VRequestCycle.get().bunW(); final byte[] randomByteArray = new byte[2]; // raw format final BigInteger randomN1, randomN2; final OpenIDAuthenticator auth = (OpenIDAuthenticator)app.authenticator(); synchronized( auth ) { auth.secureRandomizer().nextBytes( randomByteArray ); randomN1 = new BigInteger( /*positive*/1, randomByteArray ); auth.secureRandomizer().nextBytes( randomByteArray ); } randomN2 = new BigInteger( /*positive*/1, randomByteArray ); keyExpected = bun.format( "%1$04X-%2$04X", randomN1, randomN2 ); try { voteServerIAddress = new InternetAddress( app.serviceEmail() ); } catch( AddressException x ) { throw new RuntimeException( x ); } InternetAddressX.trySetPersonal( voteServerIAddress, app.vsRun().voteServer().shortTitle(), "UTF-8" ); messageSubject = bun.l( "a.web.wic.authen.WP_EmailAuthen2.message.subject" ); // LAYOUT // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = add( new Label( "title", bun.l( "a.web.wic.authen.WP_EmailAuthen2" ) )); add( new Label( "explanation", bun.l( "a.web.wic.authen.WP_EmailAuthen2.explanation", voteServerIAddress.getAddress(), messageSubject, claimedUserEmail ))); final Form form = new KeyVerificationForm(); add( form ); // Field // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - form.add( new Label( "keyLabel", bun.l( "a.web.wic.authen.WP_EmailAuthen2.keyLabel" ))); final TextField keyField = new TextField( "key", new PropertyModel( WP_EmailAuthen2.this, "keyReceived" )); invalidStyled( inputLengthConstrained( keyField )); keyField.setRequired( true ); keyField.add( new IValidator() { private ValidationError mismatchCountLimitError = null; // till it occurs public void validate( final IValidatable v ) { // assert v.getValue() != null : "no null if not INullAcceptingValidator"; //// no matter if( mismatchCountLimitError != null ) { v.error( mismatchCountLimitError ); return; } if( !keyExpected.equalsIgnoreCase( v.getValue() )) { ++mismatchCount; if( mismatchCount < MISMATCH_COUNT_LIMIT ) { v.error( new ValidationError().setMessage( VRequestCycle.get().bunW().l( "a.web.wic.authen.WP_EmailAuthen2.key.mismatch" ))); return; } mismatchCountLimitError = new ValidationError().setMessage( VRequestCycle.get().bunW().l( "a.web.wic.authen.WP_EmailAuthen2.key.mismatchCount" )); v.error( mismatchCountLimitError ); } } }); form.add( keyField ); form.add( new Label( "keyDescription", bun.l( "a.web.wic.authen.WP_EmailAuthen2.keyDescription" ))); // Buttons // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { final Button button = new Button( "submit" ) { public @Override boolean isEnabled() { return mismatchCount < MISMATCH_COUNT_LIMIT && super.isEnabled(); } }; button.add( AttributeModifier.replace( "value", bun.l( "a.web.wic.authen.WP_EmailAuthen2.submit" ))); form.add( button ); } { final Button button = new Button( "submit-cancel" ) { public @Override void onSubmit() { super.onSubmit(); runner.run( VRequestCycle.get() ); } }; button.add( AttributeModifier.replace( "value", bun.l( "a.web.wic.authen.WP_EmailAuthen2.submit-cancel" ))); button.setDefaultFormProcessing( false ); form.add( button ); } // Feedback Messages // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - add( new WC_Feedback( "feedback" )); } // ------------------------------------------------------------------------------------ /** Sends the authentication message to the user. * * @return true if the message was sent, false otherwise, When false a detailed * error message is output to the log and a feedback message is output to the * session. For security reasons the latter contains no details, but only * refers to the former. */ boolean sendMessage( final VRequestCycle cycle ) { final VOWicket app = VOWicket.get(); final BundleFormatter bun = cycle.bunW(); final ReplyBuilder replyB = new ReplyBuilder( bun.bundle() ); final String homeURIString = cycle.uriFor(app.getHomePage()).toString(); synchronized( app.mailLock() ) { final SMTPMessage message = new SMTPMessage( app.mailSession() ); try { message.setFrom( voteServerIAddress ); message.setRecipient( Message.RecipientType.TO, new InternetAddress( claimedUserEmail )); message.setSubject( messageSubject, "UTF-8" ); message.setHeader( "X-Mailer", WP_OpenIDLogin.class.getName() ); } catch( MessagingException x ) { throw new RuntimeException( x ); } replyB.lappendln( "a.web.wic.authen.WP_EmailAuthen2.message.body", homeURIString, claimedUserEmail, keyExpected ); try { message.setText( replyB.toString(), "UTF-8" ); Exception x = app.mailSender().trySend( message, app.mailSession() ); if( x != null ) throw x; } 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) { final Level level = Level.CONFIG; LoggerX.i(getClass()).log( level, /*message*/"unable to send email authentication message", x ); VSession.get().error( "Unable to send email authentication message. The reason has been printed to the server log, at level " + level + "." ); return false; } return true; } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private final String claimedUserEmail; // in canonical form as always, but also so that any message sent shows only the // bare addr-spec, and not any personal part typed by the claimant (which may be // spam) private final String keyExpected; private String keyReceived; // PropertyModel accesses it by java.lang.reflect.Field.setAccessible() private final String messageSubject; private static final int MISMATCH_COUNT_LIMIT = 3; private int mismatchCount; // prevent attack by exhaustive retries private final boolean persistent; private final RequestCycleRunner runner; private final InternetAddress voteServerIAddress; // ==================================================================================== private class KeyVerificationForm extends Form { KeyVerificationForm() { super( "form" ); } protected @Override void onSubmit() { super.onSubmit(); if( !keyExpected.equalsIgnoreCase( keyReceived )) throw new IllegalStateException(); final VRequestCycle cycle = VRequestCycle.get(); WP_OpenIDLogin.setUserInSession( claimedUserEmail, "email", persistent, cycle ); runner.run( cycle ); } } }