package 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. import java.util.concurrent.atomic.*; import java.util.logging.*; import votorola.g.logging.*; import javax.servlet.http.*; import org.apache.wicket.*; import org.apache.wicket.markup.html.pages.*; import org.apache.wicket.request.cycle.*; import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.openid4java.consumer.*; import org.openid4java.discovery.*; import org.openid4java.message.*; import votorola.g.locale.*; import votorola.a.web.wic.*; import votorola.g.lang.*; import votorola.g.web.*; import votorola.g.web.wic.*; /** An OpenID authentication return page, continuing from WP_OpenIDLogin. Ultimately, * this is the page that receives the authentication result from the OpenID provider. * The result may be preceded, however, by preliminary requests (relying party discovery) * for a Yadis document, in which the provider verifies the location of this page. So * this page may receive multiple requests, per authentication. * * @see http://openid.net/ * @see Using OpenID * @see http://yadis.org/ * @see WP_Yadis * @see WP_OpenIDReturn.html */ public @ThreadRestricted("wicket") final class WP_OpenIDReturn extends VPageHTML { /** Constructs a WP_OpenIDReturn. */ public WP_OpenIDReturn( final PageParameters pP ) // public - bookmarkable, must have default constructor { final VRequestCycle cycle = VRequestCycle.get(); final HttpServletRequest servletRequest = (HttpServletRequest) cycle.vRequest().getContainerRequest(); logger.finer( "request method " + servletRequest.getMethod() + ", query parameters: " + pP ); boolean acceptsXRDS = false; // till proven otherwise final String acceptHeader = servletRequest.getHeader( "Accept" ); if( acceptHeader != null ) { // logger.finest( "discovery request Accept header is \""+ acceptHeader +"\"" ); for( String typeString: acceptHeader.split( "," )) { typeString = typeString.trim(); try { final ContentType type = new ContentType( typeString.trim() ); float q = 1f; // quality factor, default { String qAttr = type.getAttribute( "q" ); if( qAttr != null ) { q = Float.valueOf( qAttr ); if( q < 0f ) q = 0f; if( q > 1f ) q = 1f; } } if( q == 0f ) continue; // type not accepted if( "application".equals( type.getType() ) && "xrds+xml".equals( type.getSubType() )) { acceptsXRDS = true; break; } } catch( Exception x ) { logger.config( "unable to parse type '"+ typeString +"' in provider's Accept header \""+ acceptHeader +"\": " + x ); } } } // If this is the provider's preliminary discovery request), then respond either // with the Yadis document or with this HTML page. We didn't give the provider a // general realm, so it does discovery here on the return page. It does it twice, // with two HEAD requests, for some reason. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( acceptsXRDS ) { // Assume a discovery request. Give it Yadis in response, rather than this // HTML page. logger.finest( "yes, accepts application/xrds+xml (Yadis), so responding with it" ); throw new RestartResponseException( new WP_Yadis() ); } else if( pP.isEmpty() ) { // Assume a discovery request. Typically this is a HEAD request. Give this // HTML page. logger.finest( "no does not accept application/xrds+xml (Yadis) so responding with HTML that points to it" ); } // Otherwise, it's probably the user's request redirected here by the provider and // carrying the authentication results in the query parameters. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - else { final SessionScope sessionScope = VSession.get().scopeOpenIDReturn(); final WP_OpenIDLogin loginPage = sessionScope.getAndClearLoginPage(); // verify it once only, despite any refresh attempt by user, in order to // avoid nonce timeout and other complications if( loginPage != null ) // verify the provider response against it { final RequestCycleRunner runner = new RequestCycleRunnerW( loginPage.requestCycleRunner() ) { public @Override void run( RequestCycle cycle ) { VSession.get().scopeOpenIDReturn().clear(); super.run( cycle ); } }; final VOWicket app = VOWicket.get(); final BundleFormatter bun = cycle.bunW(); final ParameterList paramO = new ParameterList(); // convert to OpenID library format for( PageParameters.NamedPair p: pP.getAllNamed() ) { paramO.set( new Parameter( p.getKey(), p.getValue() )); } final OpenIDAuthenticator auth = (OpenIDAuthenticator)app.authenticator(); try { final VerificationResult verification; synchronized( auth ) { final ConsumerManager m = auth.consumerManager(); verification = m.verify( HTTPServletRequestX.getRequestURLFull(servletRequest).toString(), paramO, loginPage.openIDDiscoveryInformation() ); } logger.finer( "VerificationResult.getStatusMsg() = " + verification.getStatusMsg() ); final Identifier verifiedID = verification.getVerifiedId(); if( verifiedID == null ) // show failure, and let user try again { final String feedbackMessage; final String statusMsg = verification.getStatusMsg(); // null if user cancels out, at provider if( statusMsg == null ) { // verification.getAuthResponse().toString() is not human readable if( Message.MODE_CANCEL.equals( verification.getAuthResponse().getParameterValue( "openid.mode"))) { feedbackMessage = bun.l( "a.web.wic.authen.WP_OpenIDReturn.cancelled" ); } else feedbackMessage = bun.l( "a.web.wic.authen.WP_OpenIDReturn.fail0" ); } else { feedbackMessage = bun.l( "a.web.wic.authen.WP_OpenIDReturn.fail", statusMsg ); } cycle.setResponsePage( new WP_OpenIDLogin( loginPage, feedbackMessage )); } else // OpenID authentication succeeded { final WP_EmailAuthen1 nextPage = WP_EmailAuthen1.login( verification.getVerifiedId(), loginPage.isPersistent(), loginPage.isReauthenticationRequested(), runner ); if( nextPage == null ) runner.run( cycle ); // login finished else cycle.setResponsePage( nextPage ); } } catch( org.openid4java.OpenIDException x ) // show failure, and let user try again { logger.finer( x.toString() ); cycle.setResponsePage( new WP_OpenIDLogin( loginPage, bun.l( "a.web.wic.authen.WP_OpenIDReturn.fail", ThrowableX.toStringExpanded( x )))); } // cycle.setRedirect( true ); // clear this page's URI from browser, so refresh stays on response page instead of coming back here /// gone in 1.5, as Wicket is supposed to know automatically based on change to response page } // if no response page set above, user will see this return page - abnormal } } // ==================================================================================== /** Session scope for instances of WP_OpenIDReturn. * * @see VSession#scopeOpenIDReturn() */ public static @ThreadSafe class SessionScope implements java.io.Serializable { private static final long serialVersionUID = 3L; /** Constructs a SessionScope. */ public SessionScope( VSession session ) { this.session = session; } private final VSession session; // -------------------------------------------------------------------------------- /** Clears the state of this session scope, releasing any resources it holds. */ void clear() { setLoginPage( null ); } /** The login page, as self-stored here prior to redirecting the user * to the OpenID provider. After the provider subsequently redirects the user * back to the return page URI, the return page uses this stored value to read * the discovery information (and whatever else it needs) from the login page. * * @see #setLoginPage(WP_OpenIDLogin) */ WP_OpenIDLogin getLoginPage() { return loginPageA.get(); } private final AtomicReference loginPageA = new AtomicReference(); /** Atomically sets the login page and returns the old value. * * @see #getLoginPage() */ WP_OpenIDLogin getAndClearLoginPage() { WP_OpenIDLogin page = loginPageA.getAndSet( null ); session.dirty(); // per Session API return page; } /** Sets the login page. * * @see #getLoginPage() */ void setLoginPage( final WP_OpenIDLogin loginPage ) { loginPageA.set( loginPage ); session.dirty(); // per Session API } } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static final Logger logger = LoggerX.i( WP_OpenIDReturn.class ); // - P a g e -------------------------------------------------------------------------- protected @Override void configureResponse( final WebResponse response ) { super.configureResponse( response ); response.setHeader( "X-XRDS-Location", VRequestCycle.get().uriFor(WP_Yadis.class).toString() ); } }