Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login Info Endpoint: optimize OAuth IdPs for JSON Response #3254

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
Expand All @@ -82,13 +81,15 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Base64.getDecoder;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Objects.isNull;
import static java.util.Optional.ofNullable;
Expand Down Expand Up @@ -213,12 +214,12 @@ private static Map<String, AbstractIdentityProviderDefinition> concatenateMaps(M

@RequestMapping(value = {"/login"}, headers = "Accept=application/json")
public String infoForLoginJson(Model model, Principal principal, HttpServletRequest request) {
return login(model, principal, Collections.emptyList(), true, request);
return login(model, principal, emptyList(), true, request);
}

@RequestMapping(value = {"/info"}, headers = "Accept=application/json")
public String infoForJson(Model model, Principal principal, HttpServletRequest request) {
return login(model, principal, Collections.emptyList(), true, request);
return login(model, principal, emptyList(), true, request);
}

@RequestMapping(value = {"/login"}, headers = "Accept=text/html, */*")
Expand Down Expand Up @@ -291,22 +292,24 @@ private String login(Model model, Principal principal, List<String> excludedProm
Map<String, SamlIdentityProviderDefinition> samlIdentityProviders;
Map<String, AbstractExternalOAuthIdentityProviderDefinition> oauthIdentityProviders;
Map<String, AbstractIdentityProviderDefinition> allIdentityProviders = Map.of();
Map<String, AbstractIdentityProviderDefinition> loginHintProviders = Map.of();
Map.Entry<String, AbstractIdentityProviderDefinition> loginHintProvider = null;

if (uaaLoginHint != null && (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(uaaLoginHint.getOrigin()))) {
// Login hint: Only try to read the hinted IdP from the database
if (!(OriginKeys.UAA.equals(uaaLoginHint.getOrigin()) || OriginKeys.LDAP.equals(uaaLoginHint.getOrigin()))) {
try {
IdentityProvider loginHintProvider = externalOAuthProviderConfigurator
.retrieveByOrigin(uaaLoginHint.getOrigin(), IdentityZoneHolder.get().getId());
loginHintProviders = Stream.of(loginHintProvider).collect(
new MapCollector<IdentityProvider, String, AbstractIdentityProviderDefinition>(
IdentityProvider::getOriginKey, IdentityProvider::getConfig));
final IdentityProvider idp = externalOAuthProviderConfigurator.retrieveByOrigin(
uaaLoginHint.getOrigin(),
IdentityZoneHolder.get().getId()
);
if (idp != null) {
loginHintProvider = Map.entry(idp.getOriginKey(), idp.getConfig());
}
} catch (EmptyResultDataAccessException ignored) {
// ignore
}
}
if (!loginHintProviders.isEmpty()) {
if (loginHintProvider != null) {
oauthIdentityProviders = Map.of();
samlIdentityProviders = Map.of();
} else {
Expand All @@ -322,7 +325,15 @@ private String login(Model model, Principal principal, List<String> excludedProm
samlIdentityProviders = Map.of();
} else {
samlIdentityProviders = getSamlIdentityProviderDefinitions(allowedIdentityProviderKeys);
oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys);

if (jsonResponse) {
/* the OAuth IdPs and all IdPs are used for determining the redirect; if jsonResponse is true, the
* redirect is ignored anyway */
oauthIdentityProviders = Map.of();
} else {
oauthIdentityProviders = getOauthIdentityProviderDefinitions(allowedIdentityProviderKeys);
}

allIdentityProviders = concatenateMaps(samlIdentityProviders, oauthIdentityProviders);
}

Expand Down Expand Up @@ -352,20 +363,21 @@ private String login(Model model, Principal principal, List<String> excludedProm
fieldUsernameShow = false;
}

Map.Entry<String, AbstractIdentityProviderDefinition> idpForRedirect;
idpForRedirect = evaluateLoginHint(model, samlIdentityProviders,
oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, loginHintParam, uaaLoginHint, loginHintProviders);

idpForRedirect = evaluateIdpDiscovery(model, samlIdentityProviders, oauthIdentityProviders,
allIdentityProviders, allowedIdentityProviderKeys, idpForRedirect, discoveryPerformed, newLoginPageEnabled, defaultIdentityProviderName);
// redirect to external IdP, if necessary
Map.Entry<String, AbstractIdentityProviderDefinition> idpForRedirect = evaluateLoginHint(model, samlIdentityProviders,
oauthIdentityProviders, allIdentityProviders, allowedIdentityProviderKeys, loginHintParam, uaaLoginHint, loginHintProvider);
if (idpForRedirect == null) {
idpForRedirect = evaluateIdpDiscovery(model, samlIdentityProviders, oauthIdentityProviders,
allIdentityProviders, allowedIdentityProviderKeys, discoveryPerformed, newLoginPageEnabled, defaultIdentityProviderName);
}
if (idpForRedirect == null && !jsonResponse && !fieldUsernameShow && allIdentityProviders.size() == 1) {
idpForRedirect = allIdentityProviders.entrySet().stream().findFirst().orElse(null);
}
if (idpForRedirect != null) {
if (idpForRedirect != null && !jsonResponse) {
String externalRedirect = redirectToExternalProvider(
idpForRedirect.getValue(), idpForRedirect.getKey(), request
);
if (externalRedirect != null && !jsonResponse) {
if (externalRedirect != null) {
log.debug("Following external redirect : {}", externalRedirect);
return externalRedirect;
}
Expand Down Expand Up @@ -396,10 +408,8 @@ private String login(Model model, Principal principal, List<String> excludedProm
// Entity ID to start the discovery
model.addAttribute(ENTITY_ID, zonifiedEntityID);

excludedPrompts = new LinkedList<>(excludedPrompts);
String origin = request.getParameter("origin");
populatePrompts(model, excludedPrompts, origin, samlIdentityProviders, oauthIdentityProviders,
excludedPrompts, returnLoginPrompts);
populatePrompts(model, excludedPrompts, origin, samlIdentityProviders, oauthIdentityProviders, returnLoginPrompts);

if (principal == null) {
return getUnauthenticatedRedirect(model, request, discoveryEnabled, discoveryPerformed, accountChooserNeeded, accountChooserEnabled);
Expand Down Expand Up @@ -491,31 +501,45 @@ private void setJsonInfo(
}

private Map.Entry<String, AbstractIdentityProviderDefinition> evaluateIdpDiscovery(
Model model,
Map<String, SamlIdentityProviderDefinition> samlIdentityProviders,
Map<String, AbstractExternalOAuthIdentityProviderDefinition> oauthIdentityProviders,
Map<String, AbstractIdentityProviderDefinition> allIdentityProviders,
List<String> allowedIdentityProviderKeys,
Map.Entry<String, AbstractIdentityProviderDefinition> idpForRedirect,
boolean discoveryPerformed,
boolean newLoginPageEnabled,
String defaultIdentityProviderName
final Model model,
final Map<String, SamlIdentityProviderDefinition> samlIdentityProviders,
final Map<String, AbstractExternalOAuthIdentityProviderDefinition> oauthIdentityProviders,
final Map<String, AbstractIdentityProviderDefinition> allIdentityProviders,
final List<String> allowedIdentityProviderKeys,
final boolean discoveryPerformed,
final boolean newLoginPageEnabled,
final String defaultIdentityProviderName
) {
// Default set, no login_hint given, no error, discovery performed
if (idpForRedirect == null && (discoveryPerformed || !newLoginPageEnabled) && defaultIdentityProviderName != null && !model.containsAttribute(LOGIN_HINT_ATTRIBUTE) && !model.containsAttribute(ERROR_ATTRIBUTE)) {
if (!OriginKeys.UAA.equals(defaultIdentityProviderName) && !OriginKeys.LDAP.equals(defaultIdentityProviderName)) {
if (allIdentityProviders.containsKey(defaultIdentityProviderName)) {
idpForRedirect =
allIdentityProviders.entrySet().stream().filter(entry -> defaultIdentityProviderName.equals(entry.getKey())).findAny().orElse(null);
}
} else if (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(defaultIdentityProviderName)) {
UaaLoginHint loginHint = new UaaLoginHint(defaultIdentityProviderName);
model.addAttribute(LOGIN_HINT_ATTRIBUTE, loginHint.toString());
samlIdentityProviders.clear();
oauthIdentityProviders.clear();
if (model.containsAttribute(LOGIN_HINT_ATTRIBUTE) || model.containsAttribute(ERROR_ATTRIBUTE)) {
return null;
}

if (defaultIdentityProviderName == null) {
return null;
}

if (!discoveryPerformed && newLoginPageEnabled) {
return null;
}

if (!OriginKeys.UAA.equals(defaultIdentityProviderName) && !OriginKeys.LDAP.equals(defaultIdentityProviderName)) {
if (allIdentityProviders.containsKey(defaultIdentityProviderName)) {
return allIdentityProviders.entrySet().stream()
.filter(entry -> defaultIdentityProviderName.equals(entry.getKey()))
.findAny()
.orElse(null);
}
return null;
}
return idpForRedirect;

if (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(defaultIdentityProviderName)) {
final UaaLoginHint loginHint = new UaaLoginHint(defaultIdentityProviderName);
model.addAttribute(LOGIN_HINT_ATTRIBUTE, loginHint.toString());
samlIdentityProviders.clear();
oauthIdentityProviders.clear();
}

return null;
}

private String extractLoginHintParam(HttpSession session, HttpServletRequest request) {
Expand All @@ -526,65 +550,71 @@ private String extractLoginHintParam(HttpSession session, HttpServletRequest req
.orElse(request.getParameter(LOGIN_HINT_ATTRIBUTE));
}

/**
* @return its origin key and configuration if exactly one SAML/OAuth IdP qualifies for a redirect,
* {@code null} otherwise
*/
private Map.Entry<String, AbstractIdentityProviderDefinition> evaluateLoginHint(
Model model,
Map<String, SamlIdentityProviderDefinition> samlIdentityProviders,
Map<String, AbstractExternalOAuthIdentityProviderDefinition> oauthIdentityProviders,
Map<String, AbstractIdentityProviderDefinition> allIdentityProviders,
List<String> allowedIdentityProviderKeys,
String loginHintParam,
UaaLoginHint uaaLoginHint,
Map<String, AbstractIdentityProviderDefinition> loginHintProviders
final Model model,
final Map<String, SamlIdentityProviderDefinition> samlIdentityProviders,
final Map<String, AbstractExternalOAuthIdentityProviderDefinition> oauthIdentityProviders,
final Map<String, AbstractIdentityProviderDefinition> allIdentityProviders,
final List<String> allowedIdentityProviderKeys,
final String loginHintParam,
final UaaLoginHint uaaLoginHint,
final Map.Entry<String, AbstractIdentityProviderDefinition> loginHintProvider
) {
Map.Entry<String, AbstractIdentityProviderDefinition> idpForRedirect = null;
if (loginHintParam != null) {
// parse login_hint in JSON format
if (uaaLoginHint != null) {
log.debug("Received login hint: {}", UaaStringUtils.getCleanedUserControlString(loginHintParam));
log.debug("Received login hint with origin: {}", uaaLoginHint.getOrigin());
if (OriginKeys.UAA.equals(uaaLoginHint.getOrigin()) || OriginKeys.LDAP.equals(uaaLoginHint.getOrigin())) {
if (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(uaaLoginHint.getOrigin())) {
// in case of uaa/ldap, pass value to login page
model.addAttribute(LOGIN_HINT_ATTRIBUTE, loginHintParam);
samlIdentityProviders.clear();
oauthIdentityProviders.clear();
} else {
model.addAttribute(ERROR_ATTRIBUTE, "invalid_login_hint");
}
} else {
// for oidc/saml, trigger the redirect
if (loginHintProviders.size() > 1) {
throw new IllegalStateException(
"There is a misconfiguration with the identity provider(s). Please contact your system administrator."
);
}
if (loginHintProviders.size() == 1) {
idpForRedirect = new ArrayList<>(loginHintProviders.entrySet()).get(0);
log.debug("Setting redirect from origin login_hint to: {}", idpForRedirect);
} else {
log.debug("Client does not allow provider for login_hint with origin key: {}",
uaaLoginHint.getOrigin());
model.addAttribute(ERROR_ATTRIBUTE, "invalid_login_hint");
}
}
if (loginHintParam == null) {
return null;
}

// login hint was provided, but could not be parsed into JSON format -> try old format (email domain)
if (uaaLoginHint == null) {
final List<Map.Entry<String, AbstractIdentityProviderDefinition>> matchingIdentityProviders =
allIdentityProviders.entrySet().stream()
.filter(idp -> {
final List<String> emailDomains = Optional.ofNullable(idp.getValue().getEmailDomain())
.orElse(emptyList());
return emailDomains.contains(loginHintParam);
}).toList();
if (matchingIdentityProviders.size() > 1) {
throw new IllegalStateException(
"There is a misconfiguration with the identity provider(s). Please contact your system administrator."
);
}
if (matchingIdentityProviders.size() == 1) {
final Map.Entry<String, AbstractIdentityProviderDefinition> idpForRedirect = matchingIdentityProviders.get(0);
log.debug("Setting redirect from email domain login hint to: {}", idpForRedirect);
return idpForRedirect;
}
return null;
}

// login hint was provided and could be parsed into JSON format
log.debug("Received login hint: {}", UaaStringUtils.getCleanedUserControlString(loginHintParam));
log.debug("Received login hint with origin: {}", uaaLoginHint.getOrigin());

if (OriginKeys.UAA.equals(uaaLoginHint.getOrigin()) || OriginKeys.LDAP.equals(uaaLoginHint.getOrigin())) {
if (allowedIdentityProviderKeys == null || allowedIdentityProviderKeys.contains(uaaLoginHint.getOrigin())) {
// in case of uaa/ldap, pass value to login page
model.addAttribute(LOGIN_HINT_ATTRIBUTE, loginHintParam);
samlIdentityProviders.clear();
oauthIdentityProviders.clear();
} else {
// login_hint in JSON format was not available, try old format (email domain)
List<Map.Entry<String, AbstractIdentityProviderDefinition>> matchingIdentityProviders =
allIdentityProviders.entrySet().stream().filter(
idp -> ofNullable(idp.getValue().getEmailDomain()).orElse(Collections.emptyList()).contains(
loginHintParam)
).toList();
if (matchingIdentityProviders.size() > 1) {
throw new IllegalStateException(
"There is a misconfiguration with the identity provider(s). Please contact your system administrator."
);
} else if (matchingIdentityProviders.size() == 1) {
idpForRedirect = matchingIdentityProviders.get(0);
log.debug("Setting redirect from email domain login hint to: {}", idpForRedirect);
}
model.addAttribute(ERROR_ATTRIBUTE, "invalid_login_hint");
}

return null;
}

// for oidc/saml, trigger the redirect
if (loginHintProvider != null) {
log.debug("Setting redirect from origin login_hint to: {}", loginHintProvider);
return loginHintProvider;
}
return idpForRedirect;
log.debug("Client does not allow provider for login_hint with origin key: {}", uaaLoginHint.getOrigin());
model.addAttribute(ERROR_ATTRIBUTE, "invalid_login_hint");
return null;
}

@RequestMapping(value = {"/delete_saved_account"})
Expand Down Expand Up @@ -672,28 +702,24 @@ private void populatePrompts(
String origin,
Map<String, SamlIdentityProviderDefinition> samlIdentityProviders,
Map<String, AbstractExternalOAuthIdentityProviderDefinition> oauthIdentityProviders,
List<String> excludedPrompts,
boolean returnLoginPrompts
) {
boolean noIdpsPresent = true;
for (SamlIdentityProviderDefinition idp : samlIdentityProviders.values()) {
if (idp.isShowSamlLink()) {
model.addAttribute(SHOW_LOGIN_LINKS, true);
noIdpsPresent = false;
break;
}
}
for (AbstractExternalOAuthIdentityProviderDefinition oauthIdp : oauthIdentityProviders.values()) {
if (oauthIdp.isShowLinkText()) {
model.addAttribute(SHOW_LOGIN_LINKS, true);
noIdpsPresent = false;
break;
}
}

//make the list writeable
if (noIdpsPresent) {
excludedPrompts.add(PASSCODE);
}
final List<String> excludedPrompts = new LinkedList<>(exclude);

if (!returnLoginPrompts) {
excludedPrompts.add(USERNAME_PARAMETER);
excludedPrompts.add("password");
Expand Down Expand Up @@ -733,7 +759,7 @@ private void populatePrompts(
}
map.put(prompt.getName(), details);
}
for (String excludeThisPrompt : exclude) {
for (String excludeThisPrompt : excludedPrompts) {
map.remove(excludeThisPrompt);
}
model.addAttribute("prompts", map);
Expand Down
Loading
Loading