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}