Test Failed
Pull Request — master (#3063)
by
unknown
16:18
created

DelegatedClientAuthenticationAction(Clients,AuthenticationSystemSupport,CentralAuthenticationService,String,String,boolean,Pro   A

Complexity

Conditions 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
c 0
b 0
f 0
cc 1
rs 9.4285
1
package org.apereo.cas.support.pac4j.web.flow;
2
3
import org.apache.commons.lang3.StringEscapeUtils;
4
import org.apache.commons.lang3.StringUtils;
5
import org.apereo.cas.CasProtocolConstants;
6
import org.apereo.cas.CentralAuthenticationService;
7
import org.apereo.cas.authentication.AuthenticationResult;
8
import org.apereo.cas.authentication.AuthenticationSystemSupport;
9
import org.apereo.cas.authentication.principal.ClientCredential;
10
import org.apereo.cas.authentication.principal.Service;
11
import org.apereo.cas.authentication.principal.WebApplicationService;
12
import org.apereo.cas.ticket.TicketGrantingTicket;
13
import org.apereo.cas.util.Pac4jUtils;
14
import org.apereo.cas.web.support.WebUtils;
15
import org.pac4j.core.client.BaseClient;
16
import org.pac4j.core.client.Clients;
17
import org.pac4j.core.client.IndirectClient;
18
import org.pac4j.core.context.WebContext;
19
import org.pac4j.core.credentials.Credentials;
20
import org.pac4j.core.exception.HttpAction;
21
import org.pac4j.core.profile.CommonProfile;
22
import org.pac4j.core.profile.UserProfile;
23
import org.pac4j.core.profile.service.ProfileService;
24
import org.slf4j.Logger;
25
import org.slf4j.LoggerFactory;
26
import org.springframework.http.HttpStatus;
27
import org.springframework.web.servlet.ModelAndView;
28
import org.springframework.webflow.action.AbstractAction;
29
import org.springframework.webflow.context.ExternalContext;
30
import org.springframework.webflow.execution.Event;
31
import org.springframework.webflow.execution.RequestContext;
32
33
import javax.servlet.http.HttpServletRequest;
34
import javax.servlet.http.HttpServletResponse;
35
import javax.servlet.http.HttpSession;
36
import java.io.Serializable;
37
import java.util.HashMap;
38
import java.util.LinkedHashSet;
39
import java.util.Map;
40
import java.util.Optional;
41
import java.util.Set;
42
import java.util.regex.Matcher;
43
import java.util.regex.Pattern;
44
45
/**
46
 * This class represents an action to put at the beginning of the webflow.
47
 * <p>
48
 * Before any authentication, redirection urls are computed for the different clients defined as well as the theme,
49
 * locale, method and service are saved into the web session.</p>
50
 * After authentication, appropriate information are expected on this callback url to finish the authentication
51
 * process with the provider.
52
 *
53
 * @author Jerome Leleu
54
 * @since 3.5.0
55
 */
56
public class DelegatedClientAuthenticationAction extends AbstractAction {
57
    /**
58
     * Stop the webflow for pac4j and route to view.
59
     */
60
    public static final String STOP_WEBFLOW = "stopWebflow";
61
62
    /**
63
     * Stop the webflow.
64
     */
65
    public static final String STOP = "stop";
66
67
    /**
68
     * Client action state id in the webflow.
69
     */
70
    public static final String CLIENT_ACTION = "clientAction";
71
72
    /**
73
     * All the urls and names of the pac4j clients.
74
     */
75
    public static final String PAC4J_URLS = "pac4jUrls";
76
77
    /**
78
     * View id that stops the webflow.
79
     */
80
    public static final String VIEW_ID_STOP_WEBFLOW = "casPac4jStopWebflow";
81
82
    private static final Logger LOGGER = LoggerFactory.getLogger(DelegatedClientAuthenticationAction.class);
83
84
    private static final Pattern PAC4J_CLIENT_SUFFIX_PATTERN = Pattern.compile("Client\\d*");
85
    private static final Pattern PAC4J_CLIENT_CSS_CLASS_SUBSTITUTION_PATTERN = Pattern.compile("\\W");
86
87
    private final Clients clients;
88
    private final AuthenticationSystemSupport authenticationSystemSupport;
89
    private final CentralAuthenticationService centralAuthenticationService;
90
    private final String themeParamName;
91
    private final String localParamName;
92
    private final boolean autoRedirect;
93
    private final ProfileService<CommonProfile> profileService;
94
95
    public DelegatedClientAuthenticationAction(final Clients clients, final AuthenticationSystemSupport authenticationSystemSupport,
96
                                               final CentralAuthenticationService centralAuthenticationService, final String themeParamName,
97
                                               final String localParamName, final boolean autoRedirect,
98
                                               final ProfileService<CommonProfile> profileService) {
99
        this.clients = clients;
100
        this.authenticationSystemSupport = authenticationSystemSupport;
101
        this.centralAuthenticationService = centralAuthenticationService;
102
        this.themeParamName = themeParamName;
103
        this.localParamName = localParamName;
104
        this.autoRedirect = autoRedirect;
105
        this.profileService = profileService;
106
    }
107
108
    @Override
109
    protected Event doExecute(final RequestContext context) throws Exception {
110
        final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
111
        final HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context);
112
        final HttpSession session = request.getSession();
113
114
        // web context
115
        final WebContext webContext = Pac4jUtils.getPac4jJ2EContext(request, response);
116
117
        // get client
118
        final String clientName = request.getParameter(this.clients.getClientNameParameter());
119
        LOGGER.debug("clientName: [{}]", clientName);
120
121
        if (hasDelegationRequestFailed(request, response.getStatus()).isPresent()) {
122
            return stopWebflow();
123
        }
124
125
        if (StringUtils.isNotBlank(clientName)) {
126
            final BaseClient<Credentials, CommonProfile> client = (BaseClient<Credentials, CommonProfile>) this.clients.findClient(clientName);
127
            LOGGER.debug("Client: [{}]", client);
128
129
            final Credentials credentials;
130
            try {
131
                credentials = client.getCredentials(webContext);
132
                LOGGER.debug("Retrieved credentials: [{}]", credentials);
133
            } catch (final Exception e) {
134
                LOGGER.debug("The request requires http action", e);
135
                return stopWebflow();
136
            }
137
138
            final Service service = (Service) session.getAttribute(CasProtocolConstants.PARAMETER_SERVICE);
139
            context.getFlowScope().put(CasProtocolConstants.PARAMETER_SERVICE, service);
140
            LOGGER.debug("Retrieve service: [{}]", service);
141
            if (service != null) {
142
                request.setAttribute(CasProtocolConstants.PARAMETER_SERVICE, service.getId());
143
            }
144
145
            restoreRequestAttribute(request, session, this.themeParamName);
146
            restoreRequestAttribute(request, session, this.localParamName);
147
            restoreRequestAttribute(request, session, CasProtocolConstants.PARAMETER_METHOD);
148
149
            if (credentials != null) {
150
                final ClientCredential clientCredential = new ClientCredential(credentials);
151
                final AuthenticationResult authenticationResult =
152
                        this.authenticationSystemSupport.handleAndFinalizeSingleAuthenticationTransaction(service, clientCredential);
153
                final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationResult);
154
                WebUtils.putTicketGrantingTicketInScopes(context, tgt);
155
156
                // Prepare for future Single Logout - save the profile
157
                prepareForFutureSingleLogout(client, credentials, webContext);
158
159
                return success();
160
            }
161
        }
162
163
        // no or aborted authentication : go to login page
164
        prepareForLoginPage(context);
165
        if (response.getStatus() == HttpStatus.UNAUTHORIZED.value()) {
166
            return stopWebflow();
167
        }
168
169
        if (this.autoRedirect) {
170
            final Set<ProviderLoginPageConfiguration> urls = context.getFlowScope().get(PAC4J_URLS, Set.class);
171
            if (urls != null && urls.size() == 1) {
172
                final ProviderLoginPageConfiguration cfg = urls.stream().findFirst().get();
173
                LOGGER.debug("Auto-redirecting to client url [{}]", cfg.getRedirectUrl());
174
                response.sendRedirect(cfg.getRedirectUrl());
175
                final ExternalContext externalContext = context.getExternalContext();
176
                externalContext.recordResponseComplete();
177
                return stopWebflow();
178
            }
179
        }
180
        return error();
181
    }
182
183
    /**
184
     * Prepare the data for the login page.
185
     *
186
     * @param context The current webflow context
187
     */
188
    protected void prepareForLoginPage(final RequestContext context) {
189
        final HttpServletRequest request = WebUtils.getHttpServletRequestFromExternalWebflowContext(context);
190
        final HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(context);
191
        final HttpSession session = request.getSession();
192
193
        // web context
194
        final WebContext webContext = Pac4jUtils.getPac4jJ2EContext(request, response);
195
196
        // save parameters in web session
197
        final WebApplicationService service = WebUtils.getService(context);
198
        LOGGER.debug("Save service: [{}]", service);
199
        session.setAttribute(CasProtocolConstants.PARAMETER_SERVICE, service);
200
        saveRequestParameter(request, session, this.themeParamName);
201
        saveRequestParameter(request, session, this.localParamName);
202
        saveRequestParameter(request, session, CasProtocolConstants.PARAMETER_METHOD);
203
204
        final Set<ProviderLoginPageConfiguration> urls = new LinkedHashSet<>();
205
206
        this.clients.findAllClients()
207
                .stream()
208
                .filter(IndirectClient.class::isInstance)
209
                .forEach(client -> {
210
                    try {
211
                        final IndirectClient indirectClient = (IndirectClient) client;
212
213
                        final String name = client.getName();
214
                        final Matcher matcher = PAC4J_CLIENT_SUFFIX_PATTERN.matcher(client.getClass().getSimpleName());
215
                        final String type = matcher.replaceAll(StringUtils.EMPTY).toLowerCase();
216
                        final String redirectionUrl = indirectClient.getRedirectAction(webContext).getLocation();
217
                        LOGGER.debug("[{}] -> [{}]", name, redirectionUrl);
218
                        urls.add(new ProviderLoginPageConfiguration(name, redirectionUrl, type, getCssClass(name)));
219
                    } catch (final HttpAction e) {
220
                        if (e.getCode() == HttpStatus.UNAUTHORIZED.value()) {
221
                            LOGGER.debug("Authentication request was denied from the provider [{}]", client.getName());
222
                        } else {
223
                            LOGGER.warn(e.getMessage(), e);
224
                        }
225
                    } catch (final Exception e) {
226
                        LOGGER.error("Cannot process client [{}]", client, e);
227
                    }
228
                });
229
        if (!urls.isEmpty()) {
230
            context.getFlowScope().put(PAC4J_URLS, urls);
231
        } else if (response.getStatus() != HttpStatus.UNAUTHORIZED.value()) {
232
            LOGGER.warn("No clients could be determined based on the provided configuration");
233
        }
234
    }
235
    
236
    /**
237
     * Get a valid CSS class for the given provider name.
238
     * 
239
     * @param name Name of the provider
240
     */
241
    private String getCssClass(final String name) {
242
        String computedCssClass = "fa fa-lock";
243
        if (name != null) {
244
            computedCssClass = computedCssClass.concat(" " + PAC4J_CLIENT_CSS_CLASS_SUBSTITUTION_PATTERN.matcher(name).replaceAll("-"));
245
        }
246
        LOGGER.debug("cssClass for {} is {} ", name, computedCssClass);
247
        return computedCssClass;            
248
    }
249
250
    /**
251
     * Restore an attribute in web session as an attribute in request.
252
     *
253
     * @param request The HTTP request
254
     * @param session The HTTP session
255
     * @param name    The name of the parameter
256
     */
257
    private static void restoreRequestAttribute(final HttpServletRequest request, final HttpSession session, final String name) {
258
        final String value = (String) session.getAttribute(name);
259
        request.setAttribute(name, value);
260
    }
261
262
    /**
263
     * Save a request parameter in the web session.
264
     *
265
     * @param request The HTTP request
266
     * @param session The HTTP session
267
     * @param name    The name of the parameter
268
     */
269
    private static void saveRequestParameter(final HttpServletRequest request, final HttpSession session, final String name) {
270
        final String value = request.getParameter(name);
271
        if (value != null) {
272
            session.setAttribute(name, value);
273
        }
274
    }
275
276
    private Event stopWebflow() {
277
        return new Event(this, STOP);
278
    }
279
280
    /**
281
     * Determine if request has errors.
282
     *
283
     * @param request the request
284
     * @param status  the status
285
     * @return the optional model and view, if request is an error.
286
     */
287
    public static Optional<ModelAndView> hasDelegationRequestFailed(final HttpServletRequest request, final int status) {
288
        final Map<String, String[]> params = request.getParameterMap();
289
        if (params.containsKey("error") || params.containsKey("error_code") || params.containsKey("error_description")
290
                || params.containsKey("error_message")) {
291
            final Map<String, Object> model = new HashMap<>();
292
            if (params.containsKey("error_code")) {
293
                model.put("code", StringEscapeUtils.escapeHtml4(request.getParameter("error_code")));
294
            } else {
295
                model.put("code", status);
296
            }
297
            model.put("error", StringEscapeUtils.escapeHtml4(request.getParameter("error")));
298
            model.put("reason", StringEscapeUtils.escapeHtml4(request.getParameter("error_reason")));
299
300
            if (params.containsKey("error_description")) {
301
                model.put("description", StringEscapeUtils.escapeHtml4(request.getParameter("error_description")));
302
            } else if (params.containsKey("error_message")) {
303
                model.put("description", StringEscapeUtils.escapeHtml4(request.getParameter("error_message")));
304
            }
305
            model.put(CasProtocolConstants.PARAMETER_SERVICE, request.getAttribute(CasProtocolConstants.PARAMETER_SERVICE));
306
            model.put("client", StringEscapeUtils.escapeHtml4(request.getParameter("client_name")));
307
308
            LOGGER.debug("Delegation request has failed. Details are [{}]", model);
309
            return Optional.of(new ModelAndView("casPac4jStopWebflow", model));
310
        }
311
        return Optional.empty();
312
    }
313
314
    /**
315
     * Prepares for a future SAML Single Logout by saving the current user profile through a PAC4J profile service.
316
     * 
317
     * @param client
318
     *            The current PAC4J client.
319
     * @param credentials
320
     *            Credentials from the user.
321
     * @param webContext
322
     *            PAC4J web context.
323
     * 
324
     * @throws Exception
325
     *             If anything fails.
326
     */
327
    private void prepareForFutureSingleLogout(final BaseClient<Credentials, ? extends UserProfile> client, final Credentials credentials,
328
            final WebContext webContext) throws HttpAction {
329
        if (profileService != null) {
330
            final CommonProfile profile = client.getUserProfile(credentials, webContext);
331
            /* Unclear point here. What should the password be? Can it be the TGT ID? See SingleLogoutPreparationAction:57; we use
332
             * the TGT ID to read he profile from the service there.
333
             */
334
            final String password = "TODO";
335
            profileService.create(profile, password);
336
        }
337
    }
338
339
340
    /**
341
     * The Provider login page configuration.
342
     */
343
    public static class ProviderLoginPageConfiguration implements Serializable {
344
        private static final long serialVersionUID = 6216882278086699364L;
345
        private final String name;
346
        private final String redirectUrl;
347
        private final String type;
348
        private final String cssClass;
349
350
        /**
351
         * Instantiates a new Provider ui configuration.
352
         *
353
         * @param name        the name
354
         * @param redirectUrl the redirect url
355
         * @param type        the type
356
         * @param cssClass    for SAML2 clients, the class name used for custom styling of the redirect link
357
         */
358
        ProviderLoginPageConfiguration(final String name, final String redirectUrl, final String type, final String cssClass) {
359
            this.name = name;
360
            this.redirectUrl = redirectUrl;
361
            this.type = type;
362
            this.cssClass = cssClass;
363
        }
364
365
        public String getName() {
366
            return name;
367
        }
368
369
        public String getRedirectUrl() {
370
            return redirectUrl;
371
        }
372
373
        public String getType() {
374
            return type;
375
        }
376
        
377
        public String getCssClass() {
378
            return cssClass;
379
        }
380
    }
381
}
382