Skip to content
Snippets Groups Projects
Commit 1f7856d1 authored by uhensler's avatar uhensler
Browse files

OO-2699 add verification of key/secret

parent 4e4eb173
No related branches found
No related tags found
No related merge requests found
Showing
with 227 additions and 44 deletions
......@@ -56,5 +56,12 @@ public interface LTIManager {
*/
public void deleteOutcomes(OLATResource resource);
/**
* Make a LTI request with a HTTP Post request.
* @param signedProps the signed LTI properties
* @param url the url to send the request
* @return the http response content as string or null if the request was not successful
*/
public String post(Map<String,String> signedProps, String url);
}
......@@ -24,9 +24,19 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import javax.persistence.TypedQuery;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.imsglobal.basiclti.BasicLTIUtil;
import org.olat.basesecurity.Authentication;
import org.olat.basesecurity.BaseSecurityManager;
......@@ -35,6 +45,8 @@ import org.olat.core.helpers.Settings;
import org.olat.core.id.Identity;
import org.olat.core.id.User;
import org.olat.core.id.UserConstants;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.core.util.WebappHelper;
import org.olat.ims.lti.LTIContext;
......@@ -55,6 +67,8 @@ import org.springframework.stereotype.Service;
*/
@Service
public class LTIManagerImpl implements LTIManager {
private static final OLog log = Tracing.createLoggerFor(LTIManagerImpl.class);
@Autowired
private DB dbInstance;
......@@ -129,8 +143,12 @@ public class LTIManagerImpl implements LTIManager {
String tool_consumer_instance_url = null;
String tool_consumer_instance_name = WebappHelper.getInstanceId();
String tool_consumer_instance_contact_email = WebappHelper.getMailConfig("mailSupport");
if (props == null) {
props = new HashMap<>();
}
Map<String,String> signedProps = BasicLTIUtil.signProperties(props, url, "POST",
return BasicLTIUtil.signProperties(props, url, "POST",
oauth_consumer_key,
oauth_consumer_secret,
tool_consumer_instance_guid,
......@@ -138,15 +156,13 @@ public class LTIManagerImpl implements LTIManager {
tool_consumer_instance_url,
tool_consumer_instance_name,
tool_consumer_instance_contact_email);
return signedProps;
}
@Override
public Map<String,String> forgeLTIProperties(Identity identity, Locale locale, LTIContext context,
boolean sendName, boolean sendEmail) {
final Identity ident = identity;
final Locale loc = locale;
final Identity ident = identity;
final User u = ident.getUser();
final String lastName = u.getProperty(UserConstants.LASTNAME, loc);
final String firstName = u.getProperty(UserConstants.FIRSTNAME, loc);
......@@ -208,7 +224,7 @@ public class LTIManagerImpl implements LTIManager {
if (!StringHelper.containsNonWhitespace(param)) {
continue;
}
int pos = param.indexOf("=");
int pos = param.indexOf('=');
if (pos < 1 || pos + 1 > param.length()) {
continue;
}
......@@ -291,5 +307,28 @@ public class LTIManagerImpl implements LTIManager {
}
return personSourceId;
}
@Override
public String post(Map<String,String> signedProps, String url) {
String content = null;
// Map the LTI properties to HttpClient parameters
List<NameValuePair> urlParameters = signedProps.keySet().stream()
.map(k -> new BasicNameValuePair(k, signedProps.get(k)))
.collect(Collectors.toList());
// make the http request and evaluate the result
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
HttpPost request = new HttpPost(url);
HttpEntity postParams = new UrlEncodedFormEntity(urlParameters);
request.setEntity(postParams);
HttpResponse httpResponse = httpclient.execute(request);
content = IOUtils.toString(httpResponse.getEntity().getContent());
} catch (Exception e) {
log.error("", e);
}
return content;
}
}
......@@ -19,6 +19,8 @@
*/
package org.olat.modules.card2brain;
import org.olat.modules.card2brain.manager.Card2BrainVerificationResult;
/**
*
* Initial date: 20.04.2017<br>
......@@ -33,5 +35,15 @@ public interface Card2BrainManager {
* @return true if the set of flashcards exists.
*/
public boolean checkSetOfFlashcards(String alias);
/**
* Verify if the key and the secret of the enterprise login are valid.
*
* @param url the url of the verification service
* @param key the key
* @param secret the secret
* @return Card2BrainVerificationResult the result of the verification
*/
public Card2BrainVerificationResult checkEnterpriseLogin(String url, String key, String secret);
}
......@@ -42,6 +42,7 @@ public class Card2BrainModule extends AbstractSpringModule implements ConfigOnOf
public static final String CARD2BRAIN_ENTERPRISE_SECRET= "card2brain.enterpriseSecret";
public static final String CARD2BRAIN_BASE_URL = "card2brain.baseUrl";
public static final String CARD2BRAIN_PEEK_VIEW_URL = "card2brain.peekViewUrl";
public static final String CARD2BRAIN_VERIFY_LTI_URL = "card2brain.verifyLtiUrl";
@Value("${card2brain.enabled:false}")
private boolean enabled;
......@@ -53,6 +54,8 @@ public class Card2BrainModule extends AbstractSpringModule implements ConfigOnOf
private String baseUrl;
@Value("${card2brain.peekViewUrl:null}")
private String peekViewUrl;
@Value("${card2brain.verifyLtiUrl:null}")
private String verifyLtiUrl;
@Autowired
public Card2BrainModule(CoordinatorManager coordinatorManager) {
......@@ -90,6 +93,11 @@ public class Card2BrainModule extends AbstractSpringModule implements ConfigOnOf
if(StringHelper.containsNonWhitespace(peekViewUrlObj)) {
peekViewUrl = peekViewUrlObj;
}
String verifyLtiUrlObj = getStringPropertyValue(CARD2BRAIN_VERIFY_LTI_URL, true);
if(StringHelper.containsNonWhitespace(verifyLtiUrlObj)) {
verifyLtiUrl = verifyLtiUrlObj;
}
}
@Override
......@@ -152,6 +160,15 @@ public class Card2BrainModule extends AbstractSpringModule implements ConfigOnOf
setStringProperty(CARD2BRAIN_PEEK_VIEW_URL, peekViewUrl, true);
}
public String getVerifyLtiUrl() {
return verifyLtiUrl;
}
public void setVerifyLtiUrl(String verifyLtiUrl) {
this.verifyLtiUrl = verifyLtiUrl;
setStringProperty(CARD2BRAIN_VERIFY_LTI_URL, verifyLtiUrl, true);
}
/**
* Check if the use of a certain login is safe.<br>
* - If the enterprise login is enable, it is safe to use a enterprise login.<br>
......
......@@ -19,14 +19,19 @@
*/
package org.olat.modules.card2brain.manager;
import java.util.Map;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.ObjectMapper;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.ims.lti.LTIManager;
import org.olat.modules.card2brain.Card2BrainManager;
import org.olat.modules.card2brain.Card2BrainModule;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -45,6 +50,9 @@ public class Card2BrainManagerImpl implements Card2BrainManager {
@Autowired
private Card2BrainModule card2brainModule;
@Autowired
private LTIManager ltiManager;
@Override
public boolean checkSetOfFlashcards(String alias) {
......@@ -67,4 +75,22 @@ public class Card2BrainManagerImpl implements Card2BrainManager {
return setOfFlashcardExists;
}
@Override
public Card2BrainVerificationResult checkEnterpriseLogin(String url, String key, String secret) {
Card2BrainVerificationResult card2BrainValidationResult = null;
try {
Map<String,String> signedPros = ltiManager.sign(null, url, key, secret);
String content = ltiManager.post(signedPros, url);
ObjectMapper mapper = new ObjectMapper();
card2BrainValidationResult = mapper.readValue(content, Card2BrainVerificationResult.class);
} catch (JsonParseException jsonParseException) {
// ignore and return null
} catch (Exception e) {
log.error("", e);
}
return card2BrainValidationResult;
}
}
/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.modules.card2brain.manager;
/**
* Data object to encapsulate a result of a verification.
*
* Initial date: 09.05.2017<br>
* @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
*
*/
public class Card2BrainVerificationResult {
private boolean success;
private String message;
public Card2BrainVerificationResult(boolean valid, String message) {
this.success = valid;
this.message = message;
}
public Card2BrainVerificationResult() {
// nothing to do
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean valid) {
this.success = valid;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
......@@ -22,6 +22,7 @@ package org.olat.modules.card2brain.ui;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.form.flexible.FormItem;
import org.olat.core.gui.components.form.flexible.FormItemContainer;
import org.olat.core.gui.components.form.flexible.elements.FormLink;
import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
import org.olat.core.gui.components.form.flexible.elements.TextElement;
import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
......@@ -30,7 +31,9 @@ import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.util.StringHelper;
import org.olat.modules.card2brain.Card2BrainManager;
import org.olat.modules.card2brain.Card2BrainModule;
import org.olat.modules.card2brain.manager.Card2BrainVerificationResult;
import org.springframework.beans.factory.annotation.Autowired;
/**
......@@ -48,11 +51,15 @@ public class Card2BrainAdminController extends FormBasicController {
private MultipleSelectionElement enterpriseLoginEnabledEl;
private TextElement enterpriseKeyEl;
private TextElement enterpriseSecretEl;
private FormLink checkLoginButton;
private TextElement baseUrlEl;
private TextElement peekViewUrlEl;
private TextElement verifyLtiUrlEl;
@Autowired
private Card2BrainModule card2BrainModule;
@Autowired
private Card2BrainManager card2BrainManager;
public Card2BrainAdminController(UserRequest ureq, WindowControl wControl) {
super(ureq, wControl);
......@@ -90,6 +97,9 @@ public class Card2BrainAdminController extends FormBasicController {
enterpriseSecretEl = uifactory.addPasswordElement("admin.enterpriseSecret", "admin.enterpriseSecret", 128, enterpriseSecret, formLayout);
enterpriseSecretEl.setMandatory(true);
checkLoginButton = uifactory.addFormLink("admin.verifyKeySecret.button", formLayout, "btn btn-default");
checkLoginButton.getComponent().setSuppressDirtyFormWarning(true); // doesn't work
uifactory.addSpacerElement("Spacer", formLayout, false);
uifactory.addStaticTextElement("admin.expertSettings", null, formLayout);
......@@ -103,6 +113,10 @@ public class Card2BrainAdminController extends FormBasicController {
peekViewUrlEl.setMandatory(true);
peekViewUrlEl.setHelpTextKey("admin.peekViewUrlHelpText", null);
String verifyLtiUrl = card2BrainModule.getVerifyLtiUrl();
verifyLtiUrlEl = uifactory.addTextElement("admin.verifyKeySecret.url", "admin.verifyKeySecret.url", 128, verifyLtiUrl, formLayout);
verifyLtiUrlEl.setMandatory(true);
FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
formLayout.add("buttons", buttonLayout);
uifactory.addFormSubmitButton("save", buttonLayout);
......@@ -112,8 +126,10 @@ public class Card2BrainAdminController extends FormBasicController {
@Override
protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
if(enterpriseLoginEnabledEl == source) {
if (enterpriseLoginEnabledEl == source) {
showHideEnterpriseLoginFields();
} else if (checkLoginButton == source) {
checkKeySecret();
}
super.formInnerEvent(ureq, source, event);
}
......@@ -132,11 +148,14 @@ public class Card2BrainAdminController extends FormBasicController {
String enterpriseSecret = enterpriseSecretEl.getValue();
card2BrainModule.setEnterpriseSecret(enterpriseSecret);
String baseURL = baseUrlEl.getValue();
card2BrainModule.setBaseUrl(baseURL);
String baseUrl = baseUrlEl.getValue();
card2BrainModule.setBaseUrl(baseUrl);
String peekViewUrl = peekViewUrlEl.getValue();
card2BrainModule.setPeekViewUrl(peekViewUrl);
String peekViewURL = peekViewUrlEl.getValue();
card2BrainModule.setPeekViewUrl(peekViewURL);
String verifyLtiUrl = verifyLtiUrlEl.getValue();
card2BrainModule.setVerifyLtiUrl(verifyLtiUrl);
}
@Override
......@@ -145,47 +164,23 @@ public class Card2BrainAdminController extends FormBasicController {
//validate only if the module is enabled
if(card2BrainModule.isEnabled()) {
allOk &= validateEnterpriseLogin();
allOk &= validateBaseUrl();
allOk &= validatePeekViewUrl();
}
return allOk & super.validateFormLogic(ureq);
}
private boolean validateEnterpriseLogin() {
boolean allOk = true;
if (isEnterpriseLoginEnabled()) {
if (!StringHelper.containsNonWhitespace(enterpriseKeyEl.getValue())) {
enterpriseKeyEl.setErrorKey(FORM_MISSING_MANDATORY, null);
allOk &= false;
}
if (!StringHelper.containsNonWhitespace(enterpriseSecretEl.getValue())) {
enterpriseSecretEl.setErrorKey(FORM_MISSING_MANDATORY, null);
allOk &= false;
if (isEnterpriseLoginEnabled()) {
allOk &= validateIsMandatory(enterpriseKeyEl);
allOk &= validateIsMandatory(enterpriseSecretEl);
}
allOk &= validateIsMandatory(baseUrlEl);
allOk &= validateIsMandatory(peekViewUrlEl);
allOk &= validateIsMandatory(verifyLtiUrlEl);
}
return allOk;
return allOk & super.validateFormLogic(ureq);
}
private boolean validateBaseUrl() {
private boolean validateIsMandatory(TextElement textElement) {
boolean allOk = true;
if (!StringHelper.containsNonWhitespace(baseUrlEl.getValue())) {
baseUrlEl.setErrorKey(FORM_MISSING_MANDATORY, null);
allOk &= false;
}
return allOk;
}
private boolean validatePeekViewUrl() {
boolean allOk = true;
if (!StringHelper.containsNonWhitespace(peekViewUrlEl.getValue())) {
peekViewUrlEl.setErrorKey(FORM_MISSING_MANDATORY, null);
if (!StringHelper.containsNonWhitespace(textElement.getValue())) {
textElement.setErrorKey(FORM_MISSING_MANDATORY, null);
allOk &= false;
}
......@@ -201,6 +196,22 @@ public class Card2BrainAdminController extends FormBasicController {
return enterpriseLoginEnabledEl.isAtLeastSelected(1);
}
private void checkKeySecret() {
String verifyLtiUrl = verifyLtiUrlEl.getValue();
String key = enterpriseKeyEl.getValue();
String secret = enterpriseSecretEl.getValue();
Card2BrainVerificationResult verification =
card2BrainManager.checkEnterpriseLogin(verifyLtiUrl, key, secret);
if(verification == null) {
showError("admin.verifyKeySecret.unavaible");
} else if (verification.isSuccess()) {
showInfo("admin.verifyKeySecret.valid");
} else {
showError("admin.verifyKeySecret.invalid", verification.getMessage());
}
}
@Override
protected void doDispose() {
//
......
......@@ -14,3 +14,9 @@ admin.menu.title.alt=card2brain
admin.peekViewUrl=URL der Vorschau
admin.peekViewUrlHelpText=Als Platzhalter f\u00FCr das Alias der Lernkartei ist '%s' zu verwenden.
admin.title=Konfiguration
admin.verifyKeySecret.button=Key/Secret \u00FCberpr\u00FCfen
admin.verifyKeySecret.invalid=Der Key und das Secret sind nicht g\u00FCltig. Anwort vom card2brain-Server: {0}
admin.verifyKeySecret.unavaible=Die Pr\u00FCfung konnte nicht durchgef\u00FChrt werden.
admin.verifyKeySecret.url=URL LTI Key/Secret Verifizierung
admin.verifyKeySecret.valid=Der Key und das Secret sind g\u00FCltig.
......@@ -14,3 +14,8 @@ admin.menu.title.alt=card2brain
admin.peekViewUrl=URL peek view
admin.peekViewUrlHelpText=Use '%s' as a placeholder for the alias of the flashcards.
admin.title=Configuration
admin.verifyKeySecret.button=Verify Key/Secret
admin.verifyKeySecret.invalid=Key and Secret are invalid. Response from card2brain server: {0}
admin.verifyKeySecret.unavaible=The verification was not executed correctly.
admin.verifyKeySecret.url=URL LTI Key/Secret verification
admin.verifyKeySecret.valid=Key and Secret are valid.
......@@ -1218,6 +1218,7 @@ card2brain.enabled=false
card2brain.enterpriseLoginEnabled=false
card2brain.baseUrl=https://card2brain.ch/grails/SSO/lti.dispatch?alias=%s
card2brain.peekViewUrl=https://card2brain.ch/box/%s/embed
card2brain.verifyLtiUrl=https://card2brain.ch/grails/SSO/verifyLti.dispatch
########################################
# Options for monitoring
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment