comparison src/main/java/edu/harvard/iq/dataverse/Shib.java @ 10:a50cf11e5178

Rewrite LGDataverse completely upgrading to dataverse4.0
author Zoe Hong <zhong@mpiwg-berlin.mpg.de>
date Tue, 08 Sep 2015 17:00:21 +0200
parents
children
comparison
equal deleted inserted replaced
9:5926d6419569 10:a50cf11e5178
1 package edu.harvard.iq.dataverse;
2
3 import com.google.gson.JsonArray;
4 import com.google.gson.JsonElement;
5 import com.google.gson.JsonIOException;
6 import com.google.gson.JsonObject;
7 import com.google.gson.JsonParser;
8 import com.google.gson.JsonSyntaxException;
9 import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo;
10 import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean;
11 import edu.harvard.iq.dataverse.authorization.UserIdentifier;
12 import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier;
13 import edu.harvard.iq.dataverse.authorization.groups.impl.shib.ShibGroupServiceBean;
14 import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser;
15 import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider;
16 import edu.harvard.iq.dataverse.authorization.providers.shib.ShibServiceBean;
17 import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUserNameFields;
18 import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUtil;
19 import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
20 import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
21 import edu.harvard.iq.dataverse.util.JsfHelper;
22 import edu.harvard.iq.dataverse.util.SystemConfig;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.InputStreamReader;
26 import java.net.HttpURLConnection;
27 import java.net.MalformedURLException;
28 import java.net.URL;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.List;
32 import java.util.UUID;
33 import java.util.logging.Level;
34 import java.util.logging.Logger;
35 import javax.ejb.EJB;
36 import javax.faces.application.FacesMessage;
37 import javax.faces.context.ExternalContext;
38 import javax.faces.context.FacesContext;
39 import javax.faces.view.ViewScoped;
40 import javax.inject.Inject;
41 import javax.inject.Named;
42 import javax.servlet.http.HttpServletRequest;
43 import org.apache.commons.lang.StringUtils;
44
45 @ViewScoped
46 @Named("Shib")
47 public class Shib implements java.io.Serializable {
48
49 private static final Logger logger = Logger.getLogger(Shib.class.getCanonicalName());
50
51 @Inject
52 DataverseSession session;
53
54 @EJB
55 AuthenticationServiceBean authSvc;
56 @EJB
57 ShibServiceBean shibService;
58 @EJB
59 ShibGroupServiceBean shibGroupService;
60 @EJB
61 SettingsServiceBean settingsService;
62 @EJB
63 SystemConfig systemConfig;
64 @EJB
65 DataverseServiceBean dataverseService;
66
67 HttpServletRequest request;
68
69 /**
70 * @todo these are the attributes we are getting from the IdP at
71 * testshib.org. What other attributes should we expect?
72 *
73 * Here is a dump from https://pdurbin.pagekite.me/Shibboleth.sso/Session
74 *
75 * Miscellaneous
76 *
77 * Session Expiration (barring inactivity): 479 minute(s)
78 *
79 * Client Address: 10.0.2.2
80 *
81 * SSO Protocol: urn:oasis:names:tc:SAML:2.0:protocol
82 *
83 * Identity Provider: https://idp.testshib.org/idp/shibboleth
84 *
85 * Authentication Time: 2014-09-12T17:07:36.137Z
86 *
87 * Authentication Context Class:
88 * urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
89 *
90 * Authentication Context Decl: (none)
91 *
92 *
93 *
94 * Attributes
95 *
96 * affiliation: Member@testshib.org;Staff@testshib.org
97 *
98 * cn: Me Myself And I
99 *
100 * entitlement: urn:mace:dir:entitlement:common-lib-terms
101 *
102 * eppn: myself@testshib.org
103 *
104 * givenName: Me Myself
105 *
106 * persistent-id:
107 * https://idp.testshib.org/idp/shibboleth!https://pdurbin.pagekite.me/shibboleth!zylzL+NruovU5OOGXDOL576jxfo=
108 *
109 * sn: And I
110 *
111 * telephoneNumber: 555-5555
112 *
113 * uid: myself
114 *
115 * unscoped-affiliation: Member;Staff
116 */
117 /**
118 * @todo Resolve potential confusing of having attibutes like "eppn" defined
119 * twice in this class.
120 *
121 * This was used early on in development and should be removed at some
122 * point.
123 */
124 @Deprecated
125 List<String> shibAttrs = Arrays.asList(
126 "Shib-Identity-Provider",
127 "uid",
128 "cn",
129 "sn",
130 "givenName",
131 "telephoneNumber",
132 "eppn",
133 "affiliation",
134 "unscoped-affiliation",
135 "entitlement",
136 "persistent-id"
137 );
138
139 List<String> shibValues = new ArrayList<>();
140 /**
141 * @todo make this configurable?
142 */
143 private final String shibIdpAttribute = "Shib-Identity-Provider";
144 /**
145 * @todo Make attribute used (i.e. "eppn") configurable:
146 * https://github.com/IQSS/dataverse/issues/1422
147 *
148 * OR *maybe* we can rely on people installing Dataverse to configure shibd
149 * to always send "eppn" as an attribute, via attribute mappings or what
150 * have you.
151 */
152 private final String uniquePersistentIdentifier = "eppn";
153 private String userPersistentId;
154 private String internalUserIdentifer;
155 private final String usernameAttribute = "uid";
156 private final String displayNameAttribute = "cn";
157 private final String firstNameAttribute = "givenName";
158 private final String lastNameAttribute = "sn";
159 private final String emailAttribute = "mail";
160 AuthenticatedUserDisplayInfo displayInfo;
161 /**
162 * @todo Remove this boolean some day? Now the mockups show a popup. Should
163 * be re-worked. See also the comment about the lack of a Cancel button.
164 */
165 private boolean visibleTermsOfUse;
166 private final String loginpage = "/loginpage.xhtml";
167 private final String identityProviderProblem = "Problem with Identity Provider";
168
169 /**
170 * We only have one field in which to store a unique
171 * useridentifier/persistentuserid so we have to jam the the "entityId" for
172 * a Shibboleth Identity Provider (IdP) and the unique persistent identifier
173 * per user into the same field and a separator between these two would be
174 * nice, in case we ever want to answer questions like "How many users
175 * logged in from Harvard's Identity Provider?".
176 *
177 * A pipe ("|") is used as a separator because it's considered "unwise" to
178 * use in a URL and the "entityId" for a Shibboleth Identity Provider (IdP)
179 * looks like a URL:
180 * http://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid
181 */
182 private String persistentUserIdSeparator = "|";
183
184 /**
185 * The Shibboleth Identity Provider (IdP), an "entityId" which often but not
186 * always looks like a URL.
187 */
188 String shibIdp;
189 private String builtinUsername;
190 private String builtinPassword;
191 private String existingEmail;
192 private String existingDisplayName;
193 private boolean passwordRejected;
194 private String displayNameToPersist = "(Blank: display name not received from Institution Log In)";
195 // private String firstNameToPersist = "(Blank: first name not received from Institution Log In)";
196 // private String lastNameToPersist = "(Blank: last name not received from Institution Log In)";
197 private String emailToPersist = "(Blank: email received from Institution Log In)";
198 /**
199 * @todo We're not really doing anything with affiliation yet, even though
200 * the mockups show it. The plan is to parse the JSON from
201 * https://dataverse.harvard.edu/Shibboleth.sso/DiscoFeed for example. Check
202 * the "ShibUtil" class
203 */
204 private String affiliationToDisplayAtConfirmation = "Affiliation not provided by institution log in";
205 /**
206 * @todo Once we can persist "position" to the authenticateduser table, we
207 * can revisit this. Maybe we'll use ORCID instead. Dunno.
208 */
209 // private String positionToPersist = "Position not provided by institution log in";
210 /**
211 * @todo localize this
212 */
213 private String friendlyNameForInstitution = "your institution";
214 private State state;
215 private String debugSummary;
216 // private boolean debug = false;
217 private String emailAddress;
218
219 public enum State {
220
221 INIT,
222 REGULAR_LOGIN_INTO_EXISTING_SHIB_ACCOUNT,
223 PROMPT_TO_CREATE_NEW_ACCOUNT,
224 PROMPT_TO_CONVERT_EXISTING_ACCOUNT,
225 };
226
227 public void init() {
228 state = State.INIT;
229 ExternalContext context = FacesContext.getCurrentInstance().getExternalContext();
230 request = (HttpServletRequest) context.getRequest();
231
232 possiblyMutateRequestInDev();
233
234 try {
235 shibIdp = getRequiredValueFromAttribute(shibIdpAttribute);
236 } catch (Exception ex) {
237 /**
238 * @todo is in an antipattern to throw exceptions to control flow?
239 * http://c2.com/cgi/wiki?DontUseExceptionsForFlowControl
240 *
241 * All this exception handling should be handled in the new
242 * ShibServiceBean so it's consistently handled by the API as well.
243 */
244 return;
245 }
246 String shibUserIdentifier;
247 try {
248 shibUserIdentifier = getRequiredValueFromAttribute(uniquePersistentIdentifier);
249 } catch (Exception ex) {
250 return;
251 }
252 String firstName;
253 try {
254 firstName = getRequiredValueFromAttribute(firstNameAttribute);
255 } catch (Exception ex) {
256 return;
257 }
258 String lastName;
259 try {
260 lastName = getRequiredValueFromAttribute(lastNameAttribute);
261 } catch (Exception ex) {
262 return;
263 }
264 ShibUserNameFields shibUserNameFields = ShibUtil.findBestFirstAndLastName(firstName, lastName, null);
265 if (shibUserNameFields != null) {
266 String betterFirstName = shibUserNameFields.getFirstName();
267 if (betterFirstName != null) {
268 firstName = betterFirstName;
269 }
270 String betterLastName = shibUserNameFields.getLastName();
271 if (betterLastName != null) {
272 lastName = betterLastName;
273 }
274 }
275 try {
276 emailAddress = getRequiredValueFromAttribute(emailAttribute);
277 } catch (Exception ex) {
278 String testShibIdpEntityId = "https://idp.testshib.org/idp/shibboleth";
279 if (shibIdp.equals(testShibIdpEntityId)) {
280 logger.info("For " + testShibIdpEntityId + " (which as of this writing doesn't provide the " + emailAttribute + " attribute) setting email address to value of eppn: " + shibUserIdentifier);
281 emailAddress = shibUserIdentifier;
282 } else {
283 // forcing all other IdPs to send us an an email
284 return;
285 }
286 }
287 internalUserIdentifer = generateFriendlyLookingUserIdentifer(usernameAttribute, emailAttribute);
288 logger.info("friendly looking identifer (backend will enforce uniqueness):" + internalUserIdentifer);
289
290 /**
291 * @todo Remove, longer term. For now, commenting out special logic for
292 * always showing Terms of Use for TestShib accounts. The Terms of Use
293 * workflow is captured at
294 * http://datascience.iq.harvard.edu/blog/try-out-single-sign-shibboleth-40-beta
295 */
296 // if (shibIdp.equals("https://idp.testshib.org/idp/shibboleth")) {
297 // StringBuilder sb = new StringBuilder();
298 // String freshNewShibUser = sb.append(userIdentifier).append(UUID.randomUUID()).toString();
299 // logger.info("Will create a new, unique user so the account Terms of Use will be displayed.");
300 // userIdentifier = freshNewShibUser;
301 // }
302 /**
303 * @todo Shouldn't we persist the displayName too? It still exists on
304 * the authenticateduser table.
305 */
306 // String displayName = getDisplayName(displayNameAttribute, firstNameAttribute, lastNameAttribute);
307 String affiliation = getAffiliation();
308 displayInfo = new AuthenticatedUserDisplayInfo(firstName, lastName, emailAddress, affiliation, null);
309
310 userPersistentId = shibIdp + persistentUserIdSeparator + shibUserIdentifier;
311 ShibAuthenticationProvider shibAuthProvider = new ShibAuthenticationProvider();
312 AuthenticatedUser au = authSvc.lookupUser(shibAuthProvider.getId(), userPersistentId);
313 if (au != null) {
314 state = State.REGULAR_LOGIN_INTO_EXISTING_SHIB_ACCOUNT;
315 logger.info("Found user based on " + userPersistentId + ". Logging in.");
316 logger.info("Updating display info for " + au.getName());
317 authSvc.updateAuthenticatedUser(au, displayInfo);
318 logInUserAndSetShibAttributes(au);
319 String prettyFacesHomePageString = getPrettyFacesHomePageString(false);
320 try {
321 FacesContext.getCurrentInstance().getExternalContext().redirect(prettyFacesHomePageString);
322 } catch (IOException ex) {
323 logger.info("Unable to redirect user to homepage at " + prettyFacesHomePageString);
324 }
325 } else {
326 state = State.PROMPT_TO_CREATE_NEW_ACCOUNT;
327 displayNameToPersist = displayInfo.getTitle();
328 // firstNameToPersist = "foo";
329 // lastNameToPersist = "bar";
330 emailToPersist = emailAddress;
331 /**
332 * @todo For Harvard at least, we plan to use "Harvard University"
333 * for affiliation because it's what we get from
334 * https://dataverse.harvard.edu/Shibboleth.sso/DiscoFeed
335 */
336 // affiliationToPersist = "FIXME";
337 /**
338 * @todo for Harvard we plan to use the value(s) from
339 * eduPersonScopedAffiliation which
340 * http://iam.harvard.edu/resources/saml-shibboleth-attributes says
341 * can be One or more of the following values: faculty, staff,
342 * student, affiliate, and member.
343 *
344 * http://dataverse.nl plans to use
345 * urn:mace:dir:attribute-def:eduPersonAffiliation per
346 * http://irclog.iq.harvard.edu/dataverse/2015-02-13#i_16265 . Can
347 * they configure shibd to map eduPersonAffiliation to
348 * eduPersonScopedAffiliation?
349 */
350 // positionToPersist = "FIXME";
351 logger.info("Couldn't find authenticated user based on " + userPersistentId);
352 visibleTermsOfUse = true;
353 /**
354 * Using the email address from the IdP, try to find an existing
355 * user. For TestShib we convert the "eppn" to an email address.
356 *
357 * If found, prompt for password and offer to convert.
358 *
359 * If not found, create a new account. It must be a new user.
360 */
361 String emailAddressToLookUp = emailAddress;
362 if (existingEmail != null) {
363 emailAddressToLookUp = existingEmail;
364 }
365 AuthenticatedUser existingAuthUserFoundByEmail = shibService.findAuthUserByEmail(emailAddressToLookUp);
366 BuiltinUser existingBuiltInUserFoundByEmail = null;
367 if (existingAuthUserFoundByEmail != null) {
368 existingDisplayName = existingAuthUserFoundByEmail.getName();
369 existingBuiltInUserFoundByEmail = shibService.findBuiltInUserByAuthUserIdentifier(existingAuthUserFoundByEmail.getUserIdentifier());
370 if (existingBuiltInUserFoundByEmail != null) {
371 state = State.PROMPT_TO_CONVERT_EXISTING_ACCOUNT;
372 existingDisplayName = existingBuiltInUserFoundByEmail.getDisplayName();
373 debugSummary = "getting username from the builtin user we looked up via email";
374 builtinUsername = existingBuiltInUserFoundByEmail.getUserName();
375 } else {
376 debugSummary = "Could not find a builtin account based on the username. Here we should simply create a new Shibboleth user";
377 }
378 } else {
379 debugSummary = "Could not find an auth user based on email address";
380 }
381
382 }
383 // if (debug) {
384 // printAttributes(request);
385 // }
386 }
387
388 /**
389 * @todo Move this to the shib service bean.
390 */
391 private String getAffiliation() {
392 JsonArray emptyJsonArray = new JsonArray();
393 String discoFeedJson = emptyJsonArray.toString();
394 String discoFeedUrl;
395 if (getDevShibAccountType().equals(DevShibAccountType.PRODUCTION)) {
396 discoFeedUrl = systemConfig.getDataverseSiteUrl() + "/Shibboleth.sso/DiscoFeed";
397 } else {
398 String devUrl = "http://localhost:8080/resources/dev/sample-shib-identities.json";
399 discoFeedUrl = devUrl;
400 }
401 logger.info("Trying to get affiliation from disco feed URL: " + discoFeedUrl);
402 URL url = null;
403 try {
404 url = new URL(discoFeedUrl);
405 } catch (MalformedURLException ex) {
406 logger.info(ex.toString());
407 return null;
408 }
409 if (url == null) {
410 logger.info("url object was null after parsing " + discoFeedUrl);
411 return null;
412 }
413 HttpURLConnection discoFeedRequest = null;
414 try {
415 discoFeedRequest = (HttpURLConnection) url.openConnection();
416 } catch (IOException ex) {
417 logger.info(ex.toString());
418 return null;
419 }
420 if (discoFeedRequest == null) {
421 logger.info("disco feed request was null");
422 return null;
423 }
424 try {
425 discoFeedRequest.connect();
426 } catch (IOException ex) {
427 logger.info(ex.toString());
428 return null;
429 }
430 JsonParser jp = new JsonParser();
431 JsonElement root = null;
432 try {
433 root = jp.parse(new InputStreamReader((InputStream) discoFeedRequest.getInputStream()));
434 } catch (IOException ex) {
435 logger.info(ex.toString());
436 return null;
437 }
438 if (root == null) {
439 logger.info("root was null");
440 return null;
441 }
442 JsonArray rootArray = root.getAsJsonArray();
443 if (rootArray == null) {
444 logger.info("Couldn't get JSON Array from URL");
445 return null;
446 }
447 discoFeedJson = rootArray.toString();
448 logger.fine("Dump of disco feed:" + discoFeedJson);
449 String affiliation = ShibUtil.getDisplayNameFromDiscoFeed(shibIdp, discoFeedJson);
450 if (affiliation != null) {
451 affiliationToDisplayAtConfirmation = affiliation;
452 friendlyNameForInstitution = affiliation;
453 return affiliation;
454 } else {
455 logger.info("Couldn't find an affiliation from " + shibIdp);
456 return null;
457 }
458 }
459
460 /**
461 * "Production" means "don't mess with the HTTP request".
462 */
463 public enum DevShibAccountType {
464
465 PRODUCTION,
466 RANDOM,
467 TESTSHIB1,
468 HARVARD1,
469 HARVARD2,
470 };
471
472 private DevShibAccountType getDevShibAccountType() {
473 DevShibAccountType saneDefault = DevShibAccountType.PRODUCTION;
474 String settingReturned = settingsService.getValueForKey(SettingsServiceBean.Key.DebugShibAccountType);
475 logger.fine("setting returned: " + settingReturned);
476 if (settingReturned != null) {
477 try {
478 DevShibAccountType parsedValue = DevShibAccountType.valueOf(settingReturned);
479 return parsedValue;
480 } catch (IllegalArgumentException ex) {
481 logger.info("Couldn't parse value: " + ex + " - returning a sane default: " + saneDefault);
482 return saneDefault;
483 }
484 } else {
485 logger.fine("Shibboleth dev mode has not been configured. Returning a sane default: " + saneDefault);
486 return saneDefault;
487 }
488
489 }
490
491 /**
492 * This method exists so developers don't have to run Shibboleth locally.
493 * You can populate the request with Shibboleth attributes by changing a
494 * setting like this:
495 *
496 * curl -X PUT -d RANDOM
497 * http://localhost:8080/api/admin/settings/:DebugShibAccountType
498 *
499 * When you're done, feel free to delete the setting:
500 *
501 * curl -X DELETE
502 * http://localhost:8080/api/admin/settings/:DebugShibAccountType
503 */
504 private void possiblyMutateRequestInDev() {
505 switch (getDevShibAccountType()) {
506 case PRODUCTION:
507 logger.fine("Request will not be mutated");
508 break;
509
510 case RANDOM:
511 mutateRequestForDevRandom();
512 break;
513
514 case TESTSHIB1:
515 mutateRequestForDevConstantTestShib1();
516 break;
517
518 case HARVARD1:
519 mutateRequestForDevConstantHarvard1();
520 break;
521
522 case HARVARD2:
523 mutateRequestForDevConstantHarvard2();
524 break;
525
526 default:
527 logger.info("Should never reach here");
528 break;
529 }
530 }
531
532 public String confirmAndCreateAccount() {
533 ShibAuthenticationProvider shibAuthProvider = new ShibAuthenticationProvider();
534 String lookupStringPerAuthProvider = userPersistentId;
535 AuthenticatedUser au = authSvc.createAuthenticatedUser(
536 new UserRecordIdentifier(shibAuthProvider.getId(), lookupStringPerAuthProvider), internalUserIdentifer, displayInfo, true);
537 if (au != null) {
538 logger.info("created user " + au.getIdentifier());
539 } else {
540 logger.info("couldn't create user " + userPersistentId);
541 }
542 logInUserAndSetShibAttributes(au);
543 return getPrettyFacesHomePageString(true);
544 }
545
546 public String confirmAndConvertAccount() {
547 visibleTermsOfUse = false;
548 ShibAuthenticationProvider shibAuthProvider = new ShibAuthenticationProvider();
549 String lookupStringPerAuthProvider = userPersistentId;
550 UserIdentifier userIdentifier = new UserIdentifier(lookupStringPerAuthProvider, internalUserIdentifer);
551 logger.info("builtin username: " + builtinUsername);
552 AuthenticatedUser builtInUserToConvert = shibService.canLogInAsBuiltinUser(builtinUsername, builtinPassword);
553 if (builtInUserToConvert != null) {
554 AuthenticatedUser au = authSvc.convertBuiltInToShib(builtInUserToConvert, shibAuthProvider.getId(), userIdentifier);
555 if (au != null) {
556 authSvc.updateAuthenticatedUser(au, displayInfo);
557 logInUserAndSetShibAttributes(au);
558 debugSummary = "Local account validated and successfully converted to a Shibboleth account. The old account username was " + builtinUsername;
559 JsfHelper.addSuccessMessage("Your Dataverse account is now associated with your institutional account.");
560 return getPrettyFacesHomePageString(true);
561 } else {
562 debugSummary = "Local account validated but unable to convert to Shibboleth account.";
563 }
564 } else {
565 passwordRejected = true;
566 debugSummary = "Username/password combination for local account was invalid";
567 }
568 return null;
569 }
570
571 private void logInUserAndSetShibAttributes(AuthenticatedUser au) {
572 au.setShibIdentityProvider(shibIdp);
573 session.setUser(au);
574 }
575
576 /**
577 * @todo The mockups show a Cancel button but because we're using the
578 * "requiredCheckboxValidator" you are forced to agree to Terms of Use
579 * before clicking Cancel! Argh! The mockups show how we want to display
580 * Terms of Use in a popup anyway so this should all be re-done. No time
581 * now. Here's the mockup:
582 * https://iqssharvard.mybalsamiq.com/projects/loginwithshibboleth-version3-dataverse40/Dataverse%20Account%20III%20-%20Agree%20Terms%20of%20Use
583 */
584 public String cancel() {
585 return loginpage + "?faces-redirect=true";
586 }
587
588 public List<String> getShibValues() {
589 return shibValues;
590 }
591
592 // private void printAttributes(HttpServletRequest request) {
593 // for (String attr : shibAttrs) {
594 //
595 // /**
596 // * @todo explain in Installers Guide that in order for these
597 // * attributes to be found attributePrefix="AJP_" must be added to
598 // * /etc/shibboleth/shibboleth2.xml like this:
599 // *
600 // * <ApplicationDefaults entityID="https://dataverse.org/shibboleth"
601 // * REMOTE_USER="eppn" attributePrefix="AJP_">
602 // *
603 // */
604 // Object attrObject = request.getAttribute(attr);
605 // if (attrObject != null) {
606 // shibValues.add(attr + ": " + attrObject.toString());
607 // }
608 // }
609 // logger.info("shib values: " + shibValues);
610 // }
611 /**
612 * @return The value of a Shib attribute (if non-empty) or null.
613 */
614 private String getValueFromAttribute(String attribute) {
615 Object attributeObject = request.getAttribute(attribute);
616 if (attributeObject != null) {
617 String attributeValue = attributeObject.toString();
618 if (!attributeValue.isEmpty()) {
619 return attributeValue;
620 }
621 }
622 return null;
623 }
624
625 private String getRequiredValueFromAttribute(String attribute) throws Exception {
626 Object attributeObject = request.getAttribute(attribute);
627 if (attributeObject == null) {
628 String msg = " the attribute \"" + attribute + "\" was null. Please contact support.";
629 logger.info(msg);
630 FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, identityProviderProblem, msg));
631 throw new Exception(msg);
632 }
633 String attributeValue = attributeObject.toString();
634 if (attributeValue.isEmpty()) {
635 throw new Exception(attribute + " was empty");
636 }
637 return attributeValue;
638 }
639
640 /**
641 * @todo Move logic to ShibServiceBean
642 */
643 private String generateFriendlyLookingUserIdentifer(String usernameNameAttribute, String emailAttribute) {
644 Object usernameObject = request.getAttribute(usernameNameAttribute);
645 if (usernameObject != null) {
646 String userIdentifier = usernameObject.toString();
647 if (!userIdentifier.isEmpty()) {
648 return userIdentifier;
649 }
650 } else {
651 logger.info("username attribute not sent by IdP");
652 }
653 Object emailObject = request.getAttribute(emailAttribute);
654 if (emailObject != null) {
655 String email = emailObject.toString();
656 if (!email.isEmpty()) {
657 /**
658 * @todo Just grab the first part of the email
659 */
660 String[] parts = email.split("@");
661 try {
662 String firstPart = parts[0];
663 return firstPart;
664 } catch (ArrayIndexOutOfBoundsException ex) {
665 logger.info("odd email address. no @ sign: " + email);
666 }
667 }
668 } else {
669 logger.info("email attribute not sent by IdP");
670 }
671 logger.info("the best we can do is generate a random UUID");
672 return UUID.randomUUID().toString();
673 }
674
675 /**
676 * @return The best display name we can retrieve or construct based on
677 * attributes received from Shibboleth. Shouldn't be null, maybe "Unknown"
678 *
679 * @deprecated AuthenticatedUserDisplayInfo has no place for a display name.
680 */
681 @Deprecated
682 private String getDisplayName(String displayNameAttribute, String firstNameAttribute, String lastNameAttribute) {
683 Object displayNameObject = request.getAttribute(displayNameAttribute);
684 if (displayNameObject != null) {
685 String displayName = displayNameObject.toString();
686 if (!displayName.isEmpty()) {
687 return displayName;
688 } else {
689 return getDisplayNameFromFirstNameLastName(firstNameAttribute, lastNameAttribute);
690 }
691 } else {
692 return getDisplayNameFromFirstNameLastName(firstNameAttribute, lastNameAttribute);
693 }
694 }
695
696 /**
697 * @return First name plus last name if available, just first name or just
698 * last name or "Unknown".
699 *
700 * @deprecated AuthenticatedUserDisplayInfo has no place for a display name.
701 */
702 @Deprecated
703 private String getDisplayNameFromFirstNameLastName(String firstNameAttribute, String lastNameAttribute) {
704 /**
705 * @todo Should the first name attribute be required?
706 */
707 String firstName = getValueFromAttribute(firstNameAttribute);
708 /**
709 * @todo Should the last name attribute be required?
710 */
711 String lastName = getValueFromAttribute(lastNameAttribute);
712 if (firstName != null && lastName != null) {
713 return firstName + " " + lastName;
714 } else if (firstName != null) {
715 return firstName;
716 } else if (lastName != null) {
717 return lastName;
718 } else {
719 return "Unknown";
720 }
721 }
722
723 public String getRootDataverseAlias() {
724 Dataverse rootDataverse = dataverseService.findRootDataverse();
725 if (rootDataverse != null) {
726 String rootDvAlias = rootDataverse.getAlias();
727 if (rootDvAlias != null) {
728 return rootDvAlias;
729 }
730 }
731 return null;
732 }
733
734 /**
735 * @param includeFacetDashRedirect if true, include "faces-redirect=true" in
736 * the string
737 *
738 * @todo Once https://github.com/IQSS/dataverse/issues/1519 is done, revisit
739 * this method and have the home page be "/" rather than "/dataverses/root".
740 *
741 * @todo Like builtin users, Shibboleth should benefit from redirectPage
742 * logic per https://github.com/IQSS/dataverse/issues/1551
743 */
744 public String getPrettyFacesHomePageString(boolean includeFacetDashRedirect) {
745 String plainHomepageString = "/dataverse.xhtml";
746 String rootDvAlias = getRootDataverseAlias();
747 if (includeFacetDashRedirect) {
748 if (rootDvAlias != null) {
749 return plainHomepageString + "?alias=" + rootDvAlias + "&faces-redirect=true";
750 } else {
751 return plainHomepageString + "?faces-redirect=true";
752 }
753 } else {
754 if (rootDvAlias != null) {
755 /**
756 * @todo Is there a constant for "/dataverse/" anywhere? I guess
757 * we'll just hard-code it here.
758 */
759 return "/dataverse/" + rootDvAlias;
760 } else {
761 return plainHomepageString;
762 }
763 }
764 }
765
766 public boolean isDebug() {
767 return systemConfig.isDebugEnabled();
768 }
769
770 public boolean isInit() {
771 return state.equals(State.INIT);
772 }
773
774 public boolean isOfferToCreateNewAccount() {
775 return state.equals(State.PROMPT_TO_CREATE_NEW_ACCOUNT);
776 }
777
778 public boolean isOfferToConvertExistingAccount() {
779 return state.equals(State.PROMPT_TO_CONVERT_EXISTING_ACCOUNT);
780 }
781
782 public String getDisplayNameToPersist() {
783 return displayNameToPersist;
784 }
785
786 // public String getFirstNameToPersist() {
787 // return firstNameToPersist;
788 // }
789 // public String getLastNameToPersist() {
790 // return lastNameToPersist;
791 // }
792 public String getEmailToPersist() {
793 return emailToPersist;
794 }
795
796 public String getAffiliationToDisplayAtConfirmation() {
797 return affiliationToDisplayAtConfirmation;
798 }
799
800 // public String getPositionToPersist() {
801 // return positionToPersist;
802 // }
803 public String getExistingEmail() {
804 return existingEmail;
805 }
806
807 public void setExistingEmail(String existingEmail) {
808 this.existingEmail = existingEmail;
809 }
810
811 public String getExistingDisplayName() {
812 return existingDisplayName;
813 }
814
815 public boolean isPasswordRejected() {
816 return passwordRejected;
817 }
818
819 public String getFriendlyNameForInstitution() {
820 return friendlyNameForInstitution;
821 }
822
823 public void setFriendlyNameForInstitution(String friendlyNameForInstitution) {
824 this.friendlyNameForInstitution = friendlyNameForInstitution;
825 }
826
827 public State getState() {
828 return state;
829 }
830
831 public boolean isVisibleTermsOfUse() {
832 return visibleTermsOfUse;
833 }
834
835 public String getBuiltinUsername() {
836 return builtinUsername;
837 }
838
839 public void setBuiltinUsername(String builtinUsername) {
840 this.builtinUsername = builtinUsername;
841 }
842
843 public String getBuiltinPassword() {
844 return builtinPassword;
845 }
846
847 public void setBuiltinPassword(String builtinPassword) {
848 this.builtinPassword = builtinPassword;
849 }
850
851 public String getDebugSummary() {
852 return debugSummary;
853 }
854
855 public void setDebugSummary(String debugSummary) {
856 this.debugSummary = debugSummary;
857 }
858
859 private void mutateRequestForDevRandom() throws JsonSyntaxException, JsonIOException {
860 // set *something*, at least, even if it's just shortened UUIDs
861 // for (String attr : shibAttrs) {
862 // in dev we don't care if a new, random user is created each time
863 // request.setAttribute(attr, UUID.randomUUID().toString().substring(0, 8));
864 // }
865
866 String sURL = "http://api.randomuser.me";
867 URL url = null;
868 try {
869 url = new URL(sURL);
870 } catch (MalformedURLException ex) {
871 Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex);
872 }
873 HttpURLConnection randomUserRequest = null;
874 try {
875 randomUserRequest = (HttpURLConnection) url.openConnection();
876 } catch (IOException ex) {
877 Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex);
878 }
879 try {
880 randomUserRequest.connect();
881 } catch (IOException ex) {
882 Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex);
883 }
884
885 JsonParser jp = new JsonParser(); //from gson
886 JsonElement root = null;
887 try {
888 root = jp.parse(new InputStreamReader((InputStream) randomUserRequest.getContent())); //convert the input stream to a json element
889 } catch (IOException ex) {
890 Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex);
891 }
892 JsonObject rootObject = root.getAsJsonObject();
893 logger.fine(rootObject.toString());
894 JsonElement results = rootObject.get("results");
895 logger.fine(results.toString());
896 JsonElement firstResult = results.getAsJsonArray().get(0);
897 logger.fine(firstResult.toString());
898 JsonElement user = firstResult.getAsJsonObject().get("user");
899 JsonElement username = user.getAsJsonObject().get("username");
900 JsonElement email = user.getAsJsonObject().get("email");
901 JsonElement password = user.getAsJsonObject().get("password");
902 JsonElement name = user.getAsJsonObject().get("name");
903 JsonElement firstName = name.getAsJsonObject().get("first");
904 JsonElement lastName = name.getAsJsonObject().get("last");
905 /**
906 * @todo Does Harvard really send displayName? At one point they didn't.
907 * Let's simulate the non-sending of displayName here.
908 */
909 // request.setAttribute(displayNameAttribute, StringUtils.capitalise(firstName.getAsString()) + " " + StringUtils.capitalise(lastName.getAsString()));
910 request.setAttribute(lastNameAttribute, StringUtils.capitalise(lastName.getAsString()));
911 request.setAttribute(firstNameAttribute, StringUtils.capitalise(firstName.getAsString()));
912 request.setAttribute(emailAttribute, email.getAsString());
913 // random IDP
914 request.setAttribute(shibIdpAttribute, "https://idp." + password.getAsString() + ".com/idp/shibboleth");
915 /**
916 * Harvard's IdP doesn't send a username so let's test without it by
917 * commenting it out here.
918 */
919 // request.setAttribute(usernameAttribute, username.getAsString());
920 // eppn
921 request.setAttribute(uniquePersistentIdentifier, UUID.randomUUID().toString().substring(0, 8));
922 }
923
924 private void mutateRequestForDevConstantTestShib1() {
925 request.setAttribute(shibIdpAttribute, "https://idp.testshib.org/idp/shibboleth");
926 // the TestShib "eppn" looks like an email address
927 request.setAttribute(uniquePersistentIdentifier, "saml@testshib.org");
928 // request.setAttribute(displayNameAttribute, "Sam El");
929 request.setAttribute(firstNameAttribute, "Samuel;Sam");
930 request.setAttribute(lastNameAttribute, "El");
931 // TestShib doesn't send "mail" attribute so let's mimic that.
932 // request.setAttribute(emailAttribute, "saml@mailinator.com");
933 request.setAttribute(usernameAttribute, "saml");
934 }
935
936 private void mutateRequestForDevConstantHarvard1() {
937 request.setAttribute(shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth");
938 request.setAttribute(uniquePersistentIdentifier, "constantHarvard");
939 // request.setAttribute(displayNameAttribute, "John Harvard");
940 request.setAttribute(firstNameAttribute, "John");
941 request.setAttribute(lastNameAttribute, "Harvard");
942 request.setAttribute(emailAttribute, "jharvard@mailinator.com");
943 request.setAttribute(usernameAttribute, "jharvard");
944 }
945
946 private void mutateRequestForDevConstantHarvard2() {
947 request.setAttribute(shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth");
948 request.setAttribute(uniquePersistentIdentifier, "constantHarvard2");
949 // request.setAttribute(displayNameAttribute, "Grace Hopper");
950 request.setAttribute(firstNameAttribute, "Grace");
951 request.setAttribute(lastNameAttribute, "Hopper");
952 request.setAttribute(emailAttribute, "ghopper@mailinator.com");
953 request.setAttribute(usernameAttribute, "ghopper");
954 }
955
956 }