001package votorola.a.web.wic.authen; // Copyright 2008, 2011-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.concurrent.atomic.*; 004import java.util.logging.*; import votorola.g.logging.*; 005import javax.servlet.http.*; 006import org.apache.wicket.*; 007import org.apache.wicket.markup.html.pages.*; 008import org.apache.wicket.request.cycle.*; 009import org.apache.wicket.request.http.WebResponse; 010import org.apache.wicket.request.mapper.parameter.PageParameters; 011import org.openid4java.consumer.*; 012import org.openid4java.discovery.*; 013import org.openid4java.message.*; 014import votorola.g.locale.*; 015import votorola.a.web.wic.*; 016import votorola.g.lang.*; 017import votorola.g.web.*; 018import votorola.g.web.wic.*; 019 020 021/** An OpenID authentication return page, continuing from WP_OpenIDLogin. Ultimately, 022 * this is the page that receives the authentication result from the OpenID provider. 023 * The result may be preceded, however, by preliminary requests (relying party discovery) 024 * for a Yadis document, in which the provider verifies the location of this page. So 025 * this page may receive multiple requests, per authentication. 026 * 027 * @see <a href='http://openid.net/' 028 * >http://openid.net/</a> 029 * @see <a href='http://www.theserverside.com/tt/articles/article.tss?l=OpenID' 030 * >Using OpenID</a> 031 * @see <a href='http://yadis.org/' 032 * >http://yadis.org/</a> 033 * @see WP_Yadis 034 * @see <a href='../../../../../../../a/web/wic/authen/WP_OpenIDReturn.html' target='_top'>WP_OpenIDReturn.html</a> 035 */ 036public @ThreadRestricted("wicket") final class WP_OpenIDReturn extends VPageHTML 037{ 038 039 040 /** Constructs a WP_OpenIDReturn. 041 */ 042 public WP_OpenIDReturn( final PageParameters pP ) // public - bookmarkable, must have default constructor 043 { 044 final VRequestCycle cycle = VRequestCycle.get(); 045 final HttpServletRequest servletRequest = (HttpServletRequest) 046 cycle.vRequest().getContainerRequest(); 047 logger.finer( "request method " + servletRequest.getMethod() + ", query parameters: " + pP ); 048 049 boolean acceptsXRDS = false; // till proven otherwise 050 final String acceptHeader = servletRequest.getHeader( "Accept" ); 051 if( acceptHeader != null ) 052 { 053 // logger.finest( "discovery request Accept header is \""+ acceptHeader +"\"" ); 054 for( String typeString: acceptHeader.split( "," )) 055 { 056 typeString = typeString.trim(); 057 try 058 { 059 final ContentType type = new ContentType( typeString.trim() ); 060 061 float q = 1f; // quality factor, default 062 { 063 String qAttr = type.getAttribute( "q" ); 064 if( qAttr != null ) 065 { 066 q = Float.valueOf( qAttr ); 067 if( q < 0f ) q = 0f; 068 if( q > 1f ) q = 1f; 069 } 070 } 071 if( q == 0f ) continue; // type not accepted 072 073 if( "application".equals( type.getType() ) 074 && "xrds+xml".equals( type.getSubType() )) 075 { 076 acceptsXRDS = true; 077 break; 078 } 079 } 080 catch( Exception x ) { logger.config( "unable to parse type '"+ typeString +"' in provider's Accept header \""+ acceptHeader +"\": " + x ); } 081 } 082 } 083 084 // If this is the provider's preliminary discovery request), then respond either 085 // with the Yadis document or with this HTML page. We didn't give the provider a 086 // general realm, so it does discovery here on the return page. It does it twice, 087 // with two HEAD requests, for some reason. 088 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 089 if( acceptsXRDS ) 090 { 091 // Assume a discovery request. Give it Yadis in response, rather than this 092 // HTML page. 093 094 logger.finest( "yes, accepts application/xrds+xml (Yadis), so responding with it" ); 095 throw new RestartResponseException( new WP_Yadis() ); 096 } 097 else if( pP.isEmpty() ) 098 { 099 // Assume a discovery request. Typically this is a HEAD request. Give this 100 // HTML page. 101 102 logger.finest( "no does not accept application/xrds+xml (Yadis) so responding with HTML that points to it" ); 103 } 104 105 // Otherwise, it's probably the user's request redirected here by the provider and 106 // carrying the authentication results in the query parameters. 107 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 108 else 109 { 110 final SessionScope sessionScope = VSession.get().scopeOpenIDReturn(); 111 final WP_OpenIDLogin loginPage = sessionScope.getAndClearLoginPage(); 112 // verify it once only, despite any refresh attempt by user, in order to 113 // avoid nonce timeout and other complications 114 if( loginPage != null ) // verify the provider response against it 115 { 116 final RequestCycleRunner runner = 117 new RequestCycleRunnerW( loginPage.requestCycleRunner() ) 118 { 119 public @Override void run( RequestCycle cycle ) 120 { 121 VSession.get().scopeOpenIDReturn().clear(); 122 super.run( cycle ); 123 } 124 }; 125 126 final VOWicket app = VOWicket.get(); 127 final BundleFormatter bun = cycle.bunW(); 128 final ParameterList paramO = new ParameterList(); // convert to OpenID library format 129 for( PageParameters.NamedPair p: pP.getAllNamed() ) 130 { 131 paramO.set( new Parameter( p.getKey(), p.getValue() )); 132 } 133 final OpenIDAuthenticator auth = (OpenIDAuthenticator)app.authenticator(); 134 try 135 { 136 final VerificationResult verification; 137 synchronized( auth ) 138 { 139 final ConsumerManager m = auth.consumerManager(); 140 verification = m.verify( 141 HTTPServletRequestX.getRequestURLFull(servletRequest).toString(), 142 paramO, loginPage.openIDDiscoveryInformation() ); 143 } 144 145 logger.finer( "VerificationResult.getStatusMsg() = " + verification.getStatusMsg() ); 146 final Identifier verifiedID = verification.getVerifiedId(); 147 if( verifiedID == null ) // show failure, and let user try again 148 { 149 final String feedbackMessage; 150 final String statusMsg = verification.getStatusMsg(); // null if user cancels out, at provider 151 if( statusMsg == null ) 152 { 153 // verification.getAuthResponse().toString() is not human readable 154 if( Message.MODE_CANCEL.equals( 155 verification.getAuthResponse().getParameterValue( "openid.mode"))) 156 { 157 feedbackMessage = bun.l( "a.web.wic.authen.WP_OpenIDReturn.cancelled" ); 158 } 159 else feedbackMessage = bun.l( "a.web.wic.authen.WP_OpenIDReturn.fail0" ); 160 } 161 else 162 { 163 feedbackMessage = bun.l( 164 "a.web.wic.authen.WP_OpenIDReturn.fail", statusMsg ); 165 } 166 cycle.setResponsePage( new WP_OpenIDLogin( loginPage, feedbackMessage )); 167 } 168 else // OpenID authentication succeeded 169 { 170 final WP_EmailAuthen1 nextPage = WP_EmailAuthen1.login( 171 verification.getVerifiedId(), loginPage.isPersistent(), 172 loginPage.isReauthenticationRequested(), runner ); 173 if( nextPage == null ) runner.run( cycle ); // login finished 174 else cycle.setResponsePage( nextPage ); 175 } 176 } 177 catch( org.openid4java.OpenIDException x ) // show failure, and let user try again 178 { 179 logger.finer( x.toString() ); 180 cycle.setResponsePage( new WP_OpenIDLogin( loginPage, bun.l( 181 "a.web.wic.authen.WP_OpenIDReturn.fail", ThrowableX.toStringExpanded( x )))); 182 } 183 // cycle.setRedirect( true ); // clear this page's URI from browser, so refresh stays on response page instead of coming back here 184 /// gone in 1.5, as Wicket is supposed to know automatically based on change to response page 185 } 186 // if no response page set above, user will see this return page - abnormal 187 } 188 189 } 190 191 192 193 // ==================================================================================== 194 195 196 /** Session scope for instances of WP_OpenIDReturn. 197 * 198 * @see VSession#scopeOpenIDReturn() 199 */ 200 public static @ThreadSafe class SessionScope implements java.io.Serializable 201 { 202 203 private static final long serialVersionUID = 3L; 204 205 206 /** Constructs a SessionScope. 207 */ 208 public SessionScope( VSession session ) { this.session = session; } 209 210 211 private final VSession session; 212 213 214 // -------------------------------------------------------------------------------- 215 216 217 /** Clears the state of this session scope, releasing any resources it holds. 218 */ 219 void clear() { setLoginPage( null ); } 220 221 222 /** The login page, as self-stored here prior to redirecting the user 223 * to the OpenID provider. After the provider subsequently redirects the user 224 * back to the return page URI, the return page uses this stored value to read 225 * the discovery information (and whatever else it needs) from the login page. 226 * 227 * @see #setLoginPage(WP_OpenIDLogin) 228 */ 229 WP_OpenIDLogin getLoginPage() { return loginPageA.get(); } 230 231 232 private final AtomicReference<WP_OpenIDLogin> loginPageA = 233 new AtomicReference<WP_OpenIDLogin>(); 234 235 236 /** Atomically sets the login page and returns the old value. 237 * 238 * @see #getLoginPage() 239 */ 240 WP_OpenIDLogin getAndClearLoginPage() 241 { 242 WP_OpenIDLogin page = loginPageA.getAndSet( null ); 243 session.dirty(); // per Session API 244 return page; 245 } 246 247 248 /** Sets the login page. 249 * 250 * @see #getLoginPage() 251 */ 252 void setLoginPage( final WP_OpenIDLogin loginPage ) 253 { 254 loginPageA.set( loginPage ); 255 session.dirty(); // per Session API 256 } 257 258 } 259 260 261 262//// P r i v a t e /////////////////////////////////////////////////////////////////////// 263 264 265 private static final Logger logger = LoggerX.i( WP_OpenIDReturn.class ); 266 267 268 269 // - P a g e -------------------------------------------------------------------------- 270 271 272 protected @Override void configureResponse( final WebResponse response ) 273 { 274 super.configureResponse( response ); 275 response.setHeader( "X-XRDS-Location", 276 VRequestCycle.get().uriFor(WP_Yadis.class).toString() ); 277 } 278 279 280 281}