Mercurial > hg > LGDataverses
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 } |
