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}