/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.kernel.security;
import org.opencastproject.security.api.SecurityService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth.provider.ConsumerAuthentication;
import org.springframework.security.oauth.provider.token.OAuthAccessProviderToken;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
/**
* Callback interface for handing authentication details that are used when an authenticated request for a protected
* resource is received.
*/
public class LtiLaunchAuthenticationHandler
implements org.springframework.security.oauth.provider.OAuthAuthenticationHandler {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(LtiLaunchAuthenticationHandler.class);
/** The Http request parameter, sent by the LTI consumer, containing the user ID. */
public static final String LTI_USER_ID_PARAM = "user_id";
/** The http request paramater containing the Consumer GUI **/
public static final String LTI_CONSUMER_GUID = "tool_consumer_instance_guid";
/** LTI field containing a comma delimeted list of roles */
public static final String ROLES = "roles";
/** The LTI field containing the context_id */
public static final String CONTEXT_ID = "context_id";
/** The prefix for LTI user ids */
public static final String LTI_USER_ID_PREFIX = "lti";
/** The delimiter to use in generated OAUTH id's **/
public static final String LTI_ID_DELIMITER = ":";
/** The Matterhorn Role for OAUTH users **/
private static final String ROLE_OAUTH_USER = "ROLE_OAUTH_USER";
/** The default context for LTI x **/
private static final String DEFAULT_CONTEXT = "LTI";
/** The default learner for LTI **/
private static final String DEFAULT_LEARNER = "USER";
/** The user details service */
protected UserDetailsService userDetailsService = null;
/** the Security Service **/
protected SecurityService securityService;
/** list of keys that will be highly */
protected List<String> highlyTrustedKeys = new ArrayList<String>();
/**
* Constructs a new LTI authentication handler, using the supplied user details service for performing user lookups.
*
* @param userDetailsService
* the user details service used to map user identifiers to more detailed information
*/
public LtiLaunchAuthenticationHandler(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* Full constructor for a LTI authentication handler that includes a list of highly trusted keys
*
* @param userDetailsService
* @param highlyTrustedkeys
*/
public LtiLaunchAuthenticationHandler(UserDetailsService userDetailsService, SecurityService securityService,
List<String> highlyTrustedkeys) {
this.userDetailsService = userDetailsService;
this.securityService = securityService;
this.highlyTrustedKeys = highlyTrustedkeys;
}
/**
* {@inheritDoc}
*
* @see org.springframework.security.oauth.provider.OAuthAuthenticationHandler#createAuthentication(javax.servlet.http.HttpServletRequest,
* org.springframework.security.oauth.provider.ConsumerAuthentication,
* org.springframework.security.oauth.provider.token.OAuthAccessProviderToken)
*/
@Override
public Authentication createAuthentication(HttpServletRequest request, ConsumerAuthentication authentication,
OAuthAccessProviderToken authToken) {
// The User ID must be provided by the LTI consumer
String userIdFromConsumer = request.getParameter(LTI_USER_ID_PARAM);
if (StringUtils.isBlank(userIdFromConsumer)) {
logger.warn("Received authentication request without user id ({})", LTI_USER_ID_PARAM);
return null;
}
// Get the comser guid if provided
String consumerGUID = request.getParameter(LTI_CONSUMER_GUID);
// This is an optional field it could be blank
if (StringUtils.isBlank(consumerGUID)) {
consumerGUID = "UknownConsumer";
}
// We need to construct a complex ID to avoid confusion
userIdFromConsumer = LTI_USER_ID_PREFIX + LTI_ID_DELIMITER + consumerGUID + LTI_ID_DELIMITER + userIdFromConsumer;
// if this is a trusted consumer we trust their details
String oaAuthKey = request.getParameter("oauth_consumer_key");
if (highlyTrustedKeys.contains(oaAuthKey)) {
logger.debug("{} is a trusted key", oaAuthKey);
// If supplied we use the human readable name
String suppliedEid = request.getParameter("lis_person_sourcedid");
// This is an optional field it could be null
if (StringUtils.isNotBlank(suppliedEid)) {
userIdFromConsumer = suppliedEid;
} else {
// if no eid is set we use the supplied ID
userIdFromConsumer = request.getParameter(LTI_USER_ID_PARAM);
}
}
if (logger.isDebugEnabled()) {
logger.debug("LTI user id is : {}", userIdFromConsumer);
}
UserDetails userDetails = null;
Collection<GrantedAuthority> userAuthorities = null;
try {
userDetails = userDetailsService.loadUserByUsername(userIdFromConsumer);
// userDetails returns a Collection<? extends GrantedAuthority>, which cannot be directly casted to a
// Collection<GrantedAuthority>.
// On the other hand, one cannot add non-null elements or modify the existing ones in a Collection<? extends
// GrantedAuthority>. Therefore, we *must* instantiate a new Collection<GrantedAuthority> (an ArrayList in this
// case) and populate it with whatever elements are returned by getAuthorities()
userAuthorities = new HashSet<GrantedAuthority>(userDetails.getAuthorities());
// we still need to enrich this user with the LTI Roles
String roles = request.getParameter(ROLES);
String context = request.getParameter(CONTEXT_ID);
enrichRoleGrants(roles, context, userAuthorities);
} catch (UsernameNotFoundException e) {
// This user is known to the tool consumer, but not to Opencast. Create a user "on the fly"
userAuthorities = new HashSet<GrantedAuthority>();
// We should add the authorities passed in from the tool consumer?
String roles = request.getParameter(ROLES);
String context = request.getParameter(CONTEXT_ID);
enrichRoleGrants(roles, context, userAuthorities);
logger.info("Returning user with {} authorities", userAuthorities.size());
userDetails = new User(userIdFromConsumer, "oauth", true, true, true, true, userAuthorities);
}
// All users need the OAUTH, USER and ANONYMOUS roles
userAuthorities.add(new SimpleGrantedAuthority(ROLE_OAUTH_USER));
userAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));
userAuthorities.add(new SimpleGrantedAuthority("ROLE_ANONYMOUS"));
Authentication ltiAuth = new PreAuthenticatedAuthenticationToken(userDetails, authentication.getCredentials(),
userAuthorities);
SecurityContextHolder.getContext().setAuthentication(ltiAuth);
return ltiAuth;
}
/**
* Enrich A collection of role grants with specified LTI memberships
*
* @param roles
* @param context
* @param userGrants
*/
private void enrichRoleGrants(String roles, String context, Collection<GrantedAuthority> userAuthorities) {
// Roles could be a list
if (roles != null) {
List<String> roleList = Arrays.asList(roles.split(","));
/* Use a generic context and learner if none is given: */
context = StringUtils.isBlank(context) ? DEFAULT_CONTEXT : context;
for (String learner : roleList) {
/* Build the role */
String role;
if (StringUtils.isBlank(learner)) {
role = context + "_" + DEFAULT_LEARNER;
} else {
role = context + "_" + learner;
}
/* Make sure to not accept ROLE_… */
if (role.trim().toUpperCase().startsWith("ROLE_")) {
logger.warn("Discarding attempt to acquire role “{}”", role);
continue;
}
/* Add this role */
logger.debug("Adding role: {}", role);
userAuthorities.add(new SimpleGrantedAuthority(role));
}
}
}
}