Skip to content

Commit

Permalink
Login Info Endpoint: optimize OAuth IdPs for JSON Response (#3254)
Browse files Browse the repository at this point in the history
* Refactor 'evaluateLoginHint'

* Refactor: move check for idpForRedirect being non-null out of 'evaluateIdpDiscovery'

* Refactor 'evaluateIdpDiscovery'

* Refactor: adjust type of 'loginHintProviders'

* Refactor: remove duplicate parameter 'excludedPrompts' from 'populatePrompts'

* Refactor: move check for '!jsonResponse' to outer if statement

* Remove hiding of passcode prompt if there is only a "uaa" and/or "ldap" IdP

* Only populate OAuth IdPs if jsonResponse is false
  • Loading branch information
adrianhoelzl-sap authored Feb 14, 2025
1 parent 1bafb0e commit d8c10f0
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 129 deletions.
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

0 comments on commit d8c10f0

Please sign in to comment.