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