001/*
002 *  Copyright 2022 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.extrausermgt.authentication.oidc;
017
018import java.io.IOException;
019import java.net.URI;
020import java.net.URISyntaxException;
021import java.net.URL;
022import java.util.Date;
023import java.util.List;
024import java.util.Map;
025import java.util.stream.Collectors;
026import java.util.stream.Stream;
027
028import org.apache.avalon.framework.context.Context;
029import org.apache.avalon.framework.context.ContextException;
030import org.apache.avalon.framework.context.Contextualizable;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.cocoon.ProcessingException;
035import org.apache.cocoon.components.ContextHelper;
036import org.apache.cocoon.environment.Redirector;
037import org.apache.cocoon.environment.Request;
038import org.apache.cocoon.environment.Session;
039import org.apache.commons.lang3.StringUtils;
040import org.apache.commons.lang3.Strings;
041
042import org.ametys.core.authentication.AbstractCredentialProvider;
043import org.ametys.core.authentication.AuthenticateAction;
044import org.ametys.core.authentication.BlockingCredentialProvider;
045import org.ametys.core.authentication.LogoutCapable;
046import org.ametys.core.authentication.NonBlockingCredentialProvider;
047import org.ametys.core.user.UserIdentity;
048import org.ametys.core.user.directory.NotUniqueUserException;
049import org.ametys.core.user.directory.StoredUser;
050import org.ametys.core.user.directory.UserDirectory;
051import org.ametys.core.user.population.UserPopulation;
052import org.ametys.plugins.extrausermgt.authentication.oidc.endofauthenticationprocess.EndOfAuthenticationProcess;
053import org.ametys.runtime.authentication.AccessDeniedException;
054import org.ametys.workspaces.extrausermgt.authentication.oidc.OIDCCallbackAction;
055
056import com.nimbusds.jose.JOSEException;
057import com.nimbusds.jose.JWSAlgorithm;
058import com.nimbusds.jose.proc.BadJOSEException;
059import com.nimbusds.jwt.JWT;
060import com.nimbusds.oauth2.sdk.AuthorizationCode;
061import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
062import com.nimbusds.oauth2.sdk.AuthorizationGrant;
063import com.nimbusds.oauth2.sdk.ParseException;
064import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
065import com.nimbusds.oauth2.sdk.ResponseType;
066import com.nimbusds.oauth2.sdk.Scope;
067import com.nimbusds.oauth2.sdk.SerializeException;
068import com.nimbusds.oauth2.sdk.TokenErrorResponse;
069import com.nimbusds.oauth2.sdk.TokenRequest;
070import com.nimbusds.oauth2.sdk.TokenResponse;
071import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
072import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
073import com.nimbusds.oauth2.sdk.auth.Secret;
074import com.nimbusds.oauth2.sdk.http.HTTPResponse;
075import com.nimbusds.oauth2.sdk.id.ClientID;
076import com.nimbusds.oauth2.sdk.id.Issuer;
077import com.nimbusds.oauth2.sdk.id.State;
078import com.nimbusds.oauth2.sdk.token.AccessToken;
079import com.nimbusds.oauth2.sdk.token.RefreshToken;
080import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
081import com.nimbusds.openid.connect.sdk.LogoutRequest;
082import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
083import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
084import com.nimbusds.openid.connect.sdk.Prompt;
085import com.nimbusds.openid.connect.sdk.UserInfoRequest;
086import com.nimbusds.openid.connect.sdk.UserInfoResponse;
087import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
088import com.nimbusds.openid.connect.sdk.claims.UserInfo;
089import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
090import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
091
092/**
093 * Sign in (through Google, facebook...) using the OpenId Connect (OIDC) protocol.
094 */
095public abstract class AbstractOIDCCredentialProvider extends AbstractCredentialProvider implements OIDCBasedCredentialProvider, BlockingCredentialProvider, NonBlockingCredentialProvider, Contextualizable, Serviceable, LogoutCapable
096{
097    /** Session attribute for OIDC */
098    public static final String REDIRECT_URI_SESSION_ATTRIBUTE = "oidc_actualRedirectUri";
099    /** Session attribute for OIDC*/
100    public static final String TOKEN_SESSION_ATTRIBUTE = "oidc_token";
101    /** Session attribute for OIDC id token */
102    public static final String IDTOKEN_SESSION_ATTRIBUTE = "oidc_id_token";
103    /** Session date attribute for OIDC*/
104    public static final String EXPDATE_SESSION_ATTRIBUTE = "oidc_expirationDate";
105    /** Session attribute for OIDC*/
106    public static final String REFRESH_TOKEN_SESSION_ATTRIBUTE = "oidc_refreshToken";
107    /** Session attribute for OIDC*/
108    public static final String STATE_SESSION_ATTRIBUTE = "oidc_state";
109
110    private static final String __ATTRIBUTE_SILENT = "oidc_silent";
111    
112    /** Scope  for the authentication request */
113    protected Scope _scope;
114    
115    /** URI for the authentication request */
116    protected URI _authUri;
117    
118    /** URI for the token request */
119    protected URI _tokenEndpointUri;
120    
121    /** URI for the user info request */
122    protected URI _userInfoEndpoint;
123    
124    /** URI for the log out request */
125    protected URI _endSessionEndPoint;
126
127    /** jwk URL for the validation of the token */
128    protected URL _jwkSetURL;
129    
130    /** Issuer  for the validation of the token */
131    protected Issuer _iss;
132    
133    /** Ametys context */
134    protected Context _context;
135    /** Client ID */
136    protected ClientID _clientID;
137    /** Client secret */
138    protected Secret _clientSecret;
139    
140    /** If we should try to authenticate silently */
141    protected boolean _silent;
142    
143    private EndOfAuthenticationProcess _endOfAuthenticationProcess;
144    
145    public void contextualize(Context context) throws ContextException
146    {
147        _context = context;
148    }
149    
150    public void service(ServiceManager manager) throws ServiceException
151    {
152        _endOfAuthenticationProcess = (EndOfAuthenticationProcess) manager.lookup(EndOfAuthenticationProcess.ROLE);
153    }
154    
155    @Override
156    public void init(String id, String cpModelId, Map<String, Object> paramValues, String label) throws Exception
157    {
158        super.init(id, cpModelId, paramValues, label);
159        _clientID = new ClientID(paramValues.get("authentication.oidc.idclient").toString());
160        _clientSecret = new Secret(paramValues.get("authentication.oidc.clientsecret").toString());
161        _silent = (boolean) paramValues.get("authentication.oidc.silent");
162
163        initUrisScope();
164    }
165    
166    public String getClientId()
167    {
168        return _clientID.getValue();
169    }
170    
171    public String getIssuer()
172    {
173        return _iss.getValue();
174    }
175    
176    public URL getJwkSetURL()
177    {
178        return _jwkSetURL;
179    }
180    
181    /**
182     * get the client authentication info for the token end point
183     * @return the client authentication
184     */
185    protected ClientAuthentication getClientAuthentication()
186    {
187        return new ClientSecretBasic(_clientID, _clientSecret);
188    }
189    
190    public boolean blockingGrantAnonymousRequest()
191    {
192        return false;
193    }
194    
195    @Override
196    public boolean nonBlockingGrantAnonymousRequest()
197    {
198        return false;
199    }
200    
201    public boolean blockingIsStillConnected(UserIdentity userIdentity, Redirector redirector) throws Exception
202    {
203        Request request = ContextHelper.getRequest(_context);
204        Session session = request.getSession(true);
205   
206        Date expDat = (Date) session.getAttribute(EXPDATE_SESSION_ATTRIBUTE);
207        if (new Date().before(expDat))
208        {
209            return true;
210        }
211        
212        RefreshToken refreshToken = (RefreshToken) session.getAttribute(REFRESH_TOKEN_SESSION_ATTRIBUTE);
213        AuthorizationGrant refreshTokenGrant = new RefreshTokenGrant(refreshToken);
214        
215        // The credentials to authenticate the client at the token endpoint
216        ClientAuthentication clientAuth = getClientAuthentication();
217        
218        // Make the token request
219        OIDCTokens tokens = requestToken(clientAuth, refreshTokenGrant);
220
221        // idToken to validate the token
222        JWT idToken = tokens.getIDToken();
223        // accessToken to be able to access the user info
224        AccessToken accessToken = tokens.getAccessToken();
225        IDTokenClaimsSet claims = validateIdToken(idToken);
226        session.setAttribute(EXPDATE_SESSION_ATTRIBUTE, claims.getExpirationTime());
227        session.setAttribute(TOKEN_SESSION_ATTRIBUTE, accessToken);
228        session.setAttribute(IDTOKEN_SESSION_ATTRIBUTE, idToken);
229        
230        return true;
231    }
232    
233    @Override
234    public boolean nonBlockingIsStillConnected(UserIdentity userIdentity, Redirector redirector) throws Exception
235    {
236        return blockingIsStillConnected(userIdentity, redirector);
237    }
238    
239    private UserIdentity _login(boolean silent, Redirector redirector) throws Exception
240    {
241        Request request = ContextHelper.getRequest(_context);
242        Session session = request.getSession(true);
243        
244        URI redirectUri = _buildRedirectUri();
245
246        getLogger().debug("OIDCCredentialProvider callback URI: {}", redirectUri);
247        
248        boolean wasSilent = false;
249        if (silent)
250        {
251            wasSilent = "true".equals(session.getAttribute(__ATTRIBUTE_SILENT));
252        }
253   
254        String code = request.getParameter("code");
255        // if the code is null, then this is the first time the user sign-in
256        // if no state are stored in session, then the code belongs to a previous session. Restart
257        if (code == null || session.getAttribute(STATE_SESSION_ATTRIBUTE) == null)
258        {
259            signIn(redirector, redirectUri, silent, wasSilent, session);
260            return null;
261        }
262
263        // we got an authorization code
264        // but first, check the state to prevent CSRF attacks
265        checkState();
266        AuthorizationCode authCode = new AuthorizationCode(code);
267        // get the tokens (id token and access token)
268        OIDCTokens tokens = requestToken(authCode, redirectUri);
269
270        // idToken to validate the token
271        JWT idToken = tokens.getIDToken();
272        // accessToken to be able to access the user info
273        AccessToken accessToken = tokens.getAccessToken();
274        RefreshToken refreshToken = tokens.getRefreshToken();
275        
276        session.setAttribute(REFRESH_TOKEN_SESSION_ATTRIBUTE, refreshToken);
277        
278        // validate id token
279        IDTokenClaimsSet claims = validateIdToken(idToken);
280
281        // set expirationTime
282        claims.getExpirationTime();
283        session.setAttribute(EXPDATE_SESSION_ATTRIBUTE, claims.getExpirationTime());
284        session.setAttribute(IDTOKEN_SESSION_ATTRIBUTE, idToken);
285        
286        UserInfo userInfo = getUserInfo(accessToken);
287        
288        // then the user is finally logged in
289        return getUserIdentity(userInfo, request, redirector);
290    }
291
292    public UserIdentity blockingGetUserIdentity(Redirector redirector) throws Exception
293    {
294        return _login(false, redirector);
295    }
296    
297    public UserIdentity nonBlockingGetUserIdentity(Redirector redirector) throws Exception
298    {
299        if (!_silent)
300        {
301            return null;
302        }
303        
304        return _login(true, redirector);
305    }
306    
307    public void blockingUserNotAllowed(Redirector redirector) throws Exception
308    {
309        // Nothing to do.
310    }
311
312    @Override
313    public void nonBlockingUserNotAllowed(Redirector redirector) throws Exception
314    {
315        // Nothing to do.
316    }
317
318    public void blockingUserAllowed(UserIdentity userIdentity, Redirector redirector) throws Exception
319    {
320        Request request = ContextHelper.getRequest(_context);
321        Session session = request.getSession(true);
322        String redirectUri = (String) session.getAttribute(AbstractOIDCCredentialProvider.REDIRECT_URI_SESSION_ATTRIBUTE);
323        redirector.redirect(true, redirectUri);
324    }
325    
326    @Override
327    public void nonBlockingUserAllowed(UserIdentity userIdentity, Redirector redirector) throws Exception
328    {
329        blockingUserAllowed(userIdentity, redirector);
330    }
331
332    public boolean requiresNewWindow()
333    {
334        return true;
335    }
336    
337    private UserPopulation _getPopulation(Request request)
338    {
339        @SuppressWarnings("unchecked")
340        List<UserPopulation> userPopulations = (List<UserPopulation>) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_AVAILABLE_USER_POPULATIONS_LIST);
341
342        // If the list has only one element
343        if (userPopulations.size() == 1)
344        {
345            return userPopulations.get(0);
346        }
347
348        // In this list a population was maybe chosen?
349        final String chosenUserPopulationId = (String) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_USER_POPULATION_ID);
350        if (StringUtils.isNotBlank(chosenUserPopulationId))
351        {
352            return userPopulations.stream()
353                    .filter(userPopulation -> Strings.CS.equals(userPopulation.getId(), chosenUserPopulationId))
354                    .findFirst()
355                    .get();
356        }
357
358        // Cannot work here...
359        throw new IllegalStateException("The " + this.getClass().getName() + " does not work when population is not known");
360    }
361    
362    /**
363     * Initialize the URIs
364     * @throws AccessDeniedException If an error occurs
365     */
366    protected abstract void initUrisScope() throws AccessDeniedException;
367    
368    /**
369     * Builds the redirect URI and the actual redirect URI
370     * @return The redirect <code>URI</code> and saves the actual redirect <code>URI</code>
371     * @throws URISyntaxException If an error occurs
372     */
373    private URI _buildRedirectUri() throws URISyntaxException
374    {
375        Request request = ContextHelper.getRequest(_context);
376        
377        // creation of the actual redirect URI (The one we actually want to go back to)
378        StringBuilder actualRedirectUri = new StringBuilder(request.getRequestURI());
379        String queryString = request.getQueryString();
380        if (queryString != null)
381        {
382            // remove any existing code or state from the request param if any exist, they are outdated
383            queryString = Stream.of(StringUtils.split(queryString, "&"))
384                    .filter(str -> !Strings.CS.startsWithAny(str, "code=", "state="))
385                    .collect(Collectors.joining("&"));
386            if (StringUtils.isNotEmpty(queryString))
387            {
388                actualRedirectUri.append("?");
389                actualRedirectUri.append(queryString);
390            }
391        }
392        
393        // saving the actualRedirectUri to enable its use in "OIDCCallbackAction"
394        Session session = request.getSession(true);
395        session.setAttribute(REDIRECT_URI_SESSION_ATTRIBUTE, actualRedirectUri.toString());
396        
397        // creation of redirect URI (the issuer (google, facebook, etc.) is going to redirect to)
398        return buildAbsoluteURI(request, OIDCCallbackAction.CALLBACK_URL);
399    }
400
401    /**
402     * Transform the path into an absolute URI based on the current request
403     * @param request the current request
404     * @param path the path
405     * @return the URI
406     */
407    protected URI buildAbsoluteURI(Request request, String path)
408    {
409        StringBuilder uriBuilder = new StringBuilder()
410            .append(request.getScheme())
411            .append("://")
412            .append(request.getServerName());
413        
414        if (request.isSecure())
415        {
416            if (request.getServerPort() != 443)
417            {
418                uriBuilder.append(":");
419                uriBuilder.append(request.getServerPort());
420            }
421        }
422        else
423        {
424            if (request.getServerPort() != 80)
425            {
426                uriBuilder.append(":");
427                uriBuilder.append(request.getServerPort());
428            }
429        }
430
431        uriBuilder.append(request.getContextPath());
432        uriBuilder.append(path);
433        
434        return URI.create(uriBuilder.toString());
435    }
436    
437    /**
438     * Sign the user in by sending an authentication request to the issuer
439     * @param redirector The redirector
440     * @param redirectUri The redirect URI
441     * @param silent if the user should be silently signed in
442     * @param wasSilent indicates that we already passed through this, to prevent infinite loops
443     * @param session The current session
444     * @throws ProcessingException If an error occurs
445     * @throws IOException If an error occurs
446     */
447    protected void signIn(Redirector redirector, URI redirectUri, boolean silent, boolean wasSilent, Session session) throws ProcessingException, IOException
448    {
449        // sign-in request: redirect the client through the actual authentication process
450
451        if (wasSilent)
452        {
453            // already passed through this, there should have been some error somewhere
454            return;
455        }
456        
457        if (silent)
458        {
459            session.setAttribute(__ATTRIBUTE_SILENT, "true");
460        }
461        
462        // creation of the state used to secure the process
463        State state = new State();
464        session.setAttribute(STATE_SESSION_ATTRIBUTE, state);
465
466        // compose the request
467        AuthenticationRequest authenticationRequest = new AuthenticationRequest.Builder(new ResponseType(ResponseType.Value.CODE), _scope, _clientID, redirectUri)
468                                                                               .endpointURI(_authUri)
469                                                                               .state(state)
470                                                                               .prompt(silent ? Prompt.Type.NONE : null)
471                                                                               .build();
472        
473        String authReqURI = authenticationRequest.toURI().toString() + "&access_type=offline";
474        
475        redirector.redirect(false, authReqURI);
476    }
477    
478    /**
479     * Checks the State parameter of the request to prevent CSRF attacks
480     * @throws AccessDeniedException If an error occurs
481     */
482    protected void checkState() throws AccessDeniedException
483    {
484        Request request = ContextHelper.getRequest(_context);
485        Session session = request.getSession(true);
486        String storedState = session.getAttribute(STATE_SESSION_ATTRIBUTE).toString();
487        String stateRequest = request.getParameter("state");
488        
489        if (!storedState.equals(stateRequest))
490        {
491            getLogger().error("OIDC state mismatch. Method checkState of AbstractOIDCCredentialProvider");
492            throw new AccessDeniedException("OIDC state mismatch");
493        }
494        
495        session.setAttribute(STATE_SESSION_ATTRIBUTE, null);
496    }
497    
498    /**
499     * Request the tokens (ID token and Access token)
500     * @param authCode The authorization code from the authentication request
501     * @param redirectUri The redirect URI
502     * @return The <code>OIDCTokens</code> that contains the access token and the id token
503     * @throws AccessDeniedException If an error occurs
504     */
505    protected OIDCTokens requestToken(AuthorizationCode authCode, URI redirectUri) throws AccessDeniedException
506    {
507        // token request: checking if the user is known
508        TokenRequest tokenReq = new TokenRequest(_tokenEndpointUri, getClientAuthentication(), new AuthorizationCodeGrant(authCode, redirectUri), null);
509        // sending request
510        HTTPResponse tokenHTTPResp = null;
511        try
512        {
513            tokenHTTPResp = tokenReq.toHTTPRequest().send();
514        }
515        catch (SerializeException | IOException e)
516        {
517            getLogger().error("OIDC token request failed ", e);
518            throw new AccessDeniedException("OIDC token request failed");
519        }
520
521        // cast the HTTPResponse to TokenResponse
522        TokenResponse tokenResponse = null;
523        try
524        {
525            tokenResponse = OIDCTokenResponseParser.parse(tokenHTTPResp);
526        }
527        catch (ParseException e)
528        {
529            getLogger().error("OIDC token request result invalid ", e);
530            throw new AccessDeniedException("OIDC token request result invalid");
531        }
532
533        if (tokenResponse instanceof TokenErrorResponse)
534        {
535            getLogger().error("OIDC token request invalid token response instance of TokenErrorResponse in method requestToken from AbstractOIDCCredentialProvider");
536            throw new AccessDeniedException("OIDC token request result invalid");
537        }
538
539        // get the tokens
540        OIDCTokenResponse  accessTokenResponse = (OIDCTokenResponse) tokenResponse;
541        
542        return accessTokenResponse.getOIDCTokens();
543    }
544    
545    /**
546     * Request the tokens using a refresh token
547     * @param clientAuth The client authentication
548     * @param refreshTokenGrant The refreshtokenGrant
549     * @return The <code>OIDCTokens</code> that contains the access token and the id token
550     * @throws AccessDeniedException If an error occurs
551     * @throws URISyntaxException If an error occurs
552     */
553    protected OIDCTokens requestToken(ClientAuthentication clientAuth, AuthorizationGrant refreshTokenGrant) throws AccessDeniedException, URISyntaxException
554    {
555        // token request: checking if the user is known
556        TokenRequest tokenReq = new TokenRequest(_tokenEndpointUri, clientAuth, refreshTokenGrant, null);
557        // sending request
558       
559        HTTPResponse tokenHTTPResp = null;
560        try
561        {
562            tokenHTTPResp = tokenReq.toHTTPRequest().send();
563        }
564        catch (SerializeException | IOException e)
565        {
566            getLogger().error("OIDC token request failed ", e);
567            throw new AccessDeniedException("OIDC token request failed");
568        }
569
570        // cast the HTTPResponse to TokenResponse
571        TokenResponse tokenResponse = null;
572        try
573        {
574            tokenResponse = OIDCTokenResponseParser.parse(tokenHTTPResp);
575        }
576        catch (ParseException e)
577        {
578            getLogger().error("OIDC token request result invalid ", e);
579            throw new AccessDeniedException("OIDC token request result invalid");
580        }
581
582        if (tokenResponse instanceof TokenErrorResponse)
583        {
584            getLogger().error("OIDC token request result invalid: tokenResponse instance of TokenErrorResponse in method requestToken from AbstractOIDCCredentialProvider");
585            throw new AccessDeniedException("OIDC token request result invalid");
586        }
587
588        // get the tokens
589        OIDCTokenResponse  accessTokenResponse = (OIDCTokenResponse) tokenResponse;
590        
591        return accessTokenResponse.getOIDCTokens();
592    }
593    
594    /**
595     * Validate the id token from the token request
596     * @param idToken The id token from the token request
597     * @return The <code>IDTokenClaimsSet</code> that contains information on the connection such as the expiration time
598     * @throws AccessDeniedException If an error occurs
599     */
600    protected IDTokenClaimsSet validateIdToken(JWT idToken) throws AccessDeniedException
601    {
602        JWSAlgorithm jwsAlg = JWSAlgorithm.RS256;
603        // create validator for signed ID tokens
604        IDTokenValidator validator = new IDTokenValidator(_iss, _clientID, jwsAlg, _jwkSetURL);
605        IDTokenClaimsSet claims;
606        
607        try
608        {
609            claims = validator.validate(idToken, null);
610        }
611        catch (BadJOSEException e)
612        {
613            getLogger().error("OIDC invalid : issuer, clientId, jwsAlg or jwkSetURL", e);
614            throw new AccessDeniedException("OIDC invalid signature issuer, clientId, jwsAlg or jwkSetURL");
615        }
616        catch (JOSEException e)
617        {
618            getLogger().error("OIDC error while validating token", e);
619            throw new AccessDeniedException("OIDC error while validating token");
620        }
621        
622        return claims;
623    }
624    
625    /**
626     * Request the userInfo using the user info end point and an access token
627     * @param accessToken the access token to retrieve the user info
628     * @return a representation of the user info from the scope requested with the token
629     * @throws IOException if an error occurred while contacting the end point
630     * @throws ParseException if an error occurred while parsing the end point answer
631     */
632    protected UserInfo getUserInfo(AccessToken accessToken) throws IOException, ParseException
633    {
634        HTTPResponse httpResponse = new UserInfoRequest(_userInfoEndpoint, accessToken).toHTTPRequest().send();
635        UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);
636        
637        if (userInfoResponse.indicatesSuccess())
638        {
639            return userInfoResponse.toSuccessResponse().getUserInfo();
640        }
641        else
642        {
643            String error = userInfoResponse.toErrorResponse().getErrorObject().toJSONObject().toJSONString();
644            getLogger().error("Failed to retrieve the user info. The server indicate the following error :\n" + error);
645            throw new AccessDeniedException("Failed to retrieve the user info. The server indicate the following error :\n" + error);
646        }
647    }
648
649    /**
650     * Compute a user identity based on the user info
651     * @param userInfo the user info
652     * @param request the original request
653     * @param redirector the redirector to use if need be
654     * @return the identified user info or null if no matching user were found
655     * @throws NotUniqueUserException if multiple user matched
656     */
657    protected UserIdentity getUserIdentity(UserInfo userInfo, Request request, Redirector redirector) throws NotUniqueUserException
658    {
659        // get the user email
660        String login = userInfo.getEmailAddress();
661        if (login == null)
662        {
663            getLogger().error("Email not found, connection canceled ");
664            throw new AccessDeniedException("Email not found, connection canceled");
665        }
666        
667        // create a UserIdentity from the email
668        UserPopulation userPopulation = _getPopulation(request);
669        UserIdentity user = _getUserIdentity(login, userPopulation);
670    
671        // If we found a UserIdentity, we return it
672        if (user != null)
673        {
674            return user;
675        }
676    
677        // If not, we are going to pre-sign-up the user with its email, firstname and lastname
678        String firstName = userInfo.getGivenName();
679        String lastName = userInfo.getFamilyName();
680        if (firstName == null || lastName == null)
681        {
682            getLogger().info("The fields could not be pre-filled");
683        }
684        
685        // We call the temporarySignup method from the endOfSignupProcess, which will do nothing if it is in the CMS and temporary sign the user up if it is the site
686        _endOfAuthenticationProcess.unexistingUser(login, firstName, lastName, userPopulation, redirector, request);
687        
688        return null;
689    }
690
691    private UserIdentity _getUserIdentity(String login, UserPopulation userPopulation) throws NotUniqueUserException
692    {
693        StoredUser storedUser = null;
694        
695        for (UserDirectory userDirectory : userPopulation.getUserDirectories())
696        {
697            storedUser = userDirectory.getStoredUser(login);
698
699            if (storedUser == null)
700            {
701                // Try to get user by email
702                storedUser = userDirectory.getStoredUserByEmail(login);
703            }
704            
705            if (storedUser != null)
706            {
707                return userDirectory.getUserIdentity(storedUser);
708            }
709        }
710        
711        return null;
712    }
713    
714    public void logout(Redirector redirector) throws ProcessingException
715    {
716        if (_endSessionEndPoint != null)
717        {
718            Request request = ContextHelper.getRequest(_context);
719            Session session = request.getSession(false);
720            if (session != null)
721            {
722                JWT idToken = (JWT) session.getAttribute(IDTOKEN_SESSION_ATTRIBUTE);
723                URI uri = new LogoutRequest(_endSessionEndPoint, idToken, buildAbsoluteURI(request, request.getRequestURI().substring(request.getContextPath().length())), null).toURI();
724                try
725                {
726                    redirector.redirect(false, uri.toString());
727                }
728                catch (IOException e)
729                {
730                    throw new ProcessingException("Failed to redirect to " + uri.toString(), e);
731                }
732            }
733        }
734    }
735}