From c7a50c40ae84aa003eb9f9983ae72fb6cab5ba28 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 1 Nov 2021 09:56:58 -0500 Subject: [PATCH 001/589] Use explicit version from antora.yml --- docs/antora.yml | 5 +---- docs/spring-security-docs.gradle | 21 ++------------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 0237a48024..45645cd5b4 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,5 +1,2 @@ name: ROOT -title: Spring Security -start_page: ROOT:index.adoc -nav: -- modules/ROOT/nav.adoc +version: 5.6 diff --git a/docs/spring-security-docs.gradle b/docs/spring-security-docs.gradle index 5ee9288720..460507c45b 100644 --- a/docs/spring-security-docs.gradle +++ b/docs/spring-security-docs.gradle @@ -2,12 +2,6 @@ apply plugin: 'io.spring.convention.docs' apply plugin: 'java' tasks.register("generateAntora") { - group = "Documentation" - description = "Generates antora files" - dependsOn 'generateAntoraYml', 'generateAntoraComponentVersion' -} - -tasks.register("generateAntoraYml") { group = "Documentation" description = "Generates the antora.yml for dynamic properties" doLast { @@ -30,7 +24,8 @@ tasks.register("generateAntoraYml") { def outputFile = new File("$buildDir/generateAntora/antora.yml") outputFile.getParentFile().mkdirs() outputFile.createNewFile() - outputFile.setText("""name: ROOT + def antoraYmlText = file("antora.yml").getText() + outputFile.setText("""$antoraYmlText title: Spring Security start_page: ROOT:index.adoc asciidoc: @@ -49,18 +44,6 @@ ${ymlVersions} } } -tasks.register("generateAntoraComponentVersion") { - group = "Documentation" - description = "Generates the antora.component.version file" - doLast { - def outputFile = new File("$buildDir/generateAntora/antora.component.version") - outputFile.getParentFile().mkdirs() - outputFile.createNewFile() - def antoraVersion = project.version.replaceAll(/^(\d+\.\d+)\.\d+(-\w+)?$/, '$1') - outputFile.setText("$antoraVersion") - } -} - dependencies { From 46c5b91500600485dd7260411d88ecac2bb88173 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 1 Nov 2021 10:28:08 -0500 Subject: [PATCH 002/589] Put nav in generated docs --- docs/spring-security-docs.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/spring-security-docs.gradle b/docs/spring-security-docs.gradle index 460507c45b..0c4747cd59 100644 --- a/docs/spring-security-docs.gradle +++ b/docs/spring-security-docs.gradle @@ -28,6 +28,8 @@ tasks.register("generateAntora") { outputFile.setText("""$antoraYmlText title: Spring Security start_page: ROOT:index.adoc +nav: +- modules/ROOT/nav.adoc asciidoc: attributes: icondir: icons From 48c8532a21d0aab17eac400ec3540b01249b5962 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 5 Nov 2021 11:27:38 -0600 Subject: [PATCH 003/589] Restructure LDAP Logs Issue gh-6311 --- .../DefaultSpringSecurityContextSource.java | 5 +-- .../security/ldap/LdapUtils.java | 4 +-- .../ldap/SpringSecurityLdapTemplate.java | 12 +++---- .../AbstractLdapAuthenticationProvider.java | 3 +- .../authentication/BindAuthenticator.java | 15 +++++--- .../PasswordComparisonAuthenticator.java | 16 +++++++-- .../SpringSecurityAuthenticationSource.java | 6 ++-- .../PasswordPolicyAwareContextSource.java | 9 ++--- .../PasswordPolicyControlExtractor.java | 2 +- .../PasswordPolicyResponseControl.java | 16 +++++---- .../search/FilterBasedLdapUserSearch.java | 25 ++++++++------ .../DefaultLdapAuthoritiesPopulator.java | 12 +++---- .../ldap/userdetails/LdapUserDetailsImpl.java | 34 ++++++------------- .../userdetails/LdapUserDetailsMapper.java | 4 +-- .../NestedLdapAuthoritiesPopulator.java | 10 +++--- 15 files changed, 92 insertions(+), 81 deletions(-) diff --git a/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java b/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java index 0d8e8403b7..0471113f6c 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java +++ b/ldap/src/main/java/org/springframework/security/ldap/DefaultSpringSecurityContextSource.java @@ -28,6 +28,7 @@ import java.util.StringTokenizer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.log.LogMessage; import org.springframework.ldap.core.support.DirContextAuthenticationStrategy; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy; @@ -72,7 +73,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource { String url = tokenizer.nextToken(); String urlRootDn = LdapUtils.parseRootDnFromUrl(url); urls.add(url.substring(0, url.lastIndexOf(urlRootDn))); - this.logger.info(" URL '" + url + "', root DN is '" + urlRootDn + "'"); + this.logger.info(LogMessage.format("Configure with URL %s and root DN %s", url, urlRootDn)); Assert.isTrue(rootDn == null || rootDn.equals(urlRootDn), "Root DNs must be the same when using multiple URLs"); rootDn = (rootDn != null) ? rootDn : urlRootDn; @@ -89,7 +90,7 @@ public class DefaultSpringSecurityContextSource extends LdapContextSource { // Remove the pooling flag unless authenticating as the 'manager' user. if (!DefaultSpringSecurityContextSource.this.userDn.equals(dn) && env.containsKey(SUN_LDAP_POOLING_FLAG)) { - DefaultSpringSecurityContextSource.this.logger.debug("Removing pooling flag for user " + dn); + DefaultSpringSecurityContextSource.this.logger.trace("Removing pooling flag for user " + dn); env.remove(SUN_LDAP_POOLING_FLAG); } } diff --git a/ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java b/ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java index a222bf48a8..5909eb8f7a 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java +++ b/ldap/src/main/java/org/springframework/security/ldap/LdapUtils.java @@ -53,7 +53,7 @@ public final class LdapUtils { } } catch (NamingException ex) { - logger.error("Failed to close context.", ex); + logger.debug("Failed to close context.", ex); } } @@ -64,7 +64,7 @@ public final class LdapUtils { } } catch (NamingException ex) { - logger.error("Failed to close enumeration.", ex); + logger.debug("Failed to close enumeration.", ex); } } diff --git a/ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java b/ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java index 08c281b1e9..42d4067b26 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java +++ b/ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java @@ -166,7 +166,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate { encodedParams[i] = LdapEncoder.filterEncode(params[i].toString()); } String formattedFilter = MessageFormat.format(filter, encodedParams); - logger.debug(LogMessage.format("Using filter: %s", formattedFilter)); + logger.trace(LogMessage.format("Using filter: %s", formattedFilter)); HashSet>> result = new HashSet<>(); ContextMapper roleMapper = (ctx) -> { DirContextAdapter adapter = (DirContextAdapter) ctx; @@ -223,7 +223,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate { String attributeName) { Object[] values = adapter.getObjectAttributes(attributeName); if (values == null || values.length == 0) { - logger.debug(LogMessage.format("No attribute value found for '%s'", attributeName)); + logger.debug(LogMessage.format("Did not find attribute value for %s", attributeName)); return; } List stringValues = new ArrayList<>(); @@ -233,9 +233,9 @@ public class SpringSecurityLdapTemplate extends LdapTemplate { stringValues.add((String) value); } else { - logger.debug(LogMessage.format("Attribute:%s contains a non string value of type[%s]", - attributeName, value.getClass())); stringValues.add(value.toString()); + logger.debug(LogMessage.format("Coerced attribute value for %s of type %s to a String", + attributeName, value.getClass())); } } } @@ -270,7 +270,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate { final DistinguishedName searchBaseDn = new DistinguishedName(base); final NamingEnumeration resultsEnum = ctx.search(searchBaseDn, filter, params, buildControls(searchControls)); - logger.debug(LogMessage.format("Searching for entry under DN '%s', base = '%s', filter = '%s'", ctxBaseDn, + logger.trace(LogMessage.format("Searching for entry under DN '%s', base = '%s', filter = '%s'", ctxBaseDn, searchBaseDn, filter)); Set results = new HashSet<>(); try { @@ -284,7 +284,7 @@ public class SpringSecurityLdapTemplate extends LdapTemplate { } catch (PartialResultException ex) { LdapUtils.closeEnumeration(resultsEnum); - logger.info("Ignoring PartialResultException"); + logger.trace("Ignoring PartialResultException"); } if (results.size() != 1) { throw new IncorrectResultSizeDataAccessException(1, results.size()); diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java index 9a0e1a56eb..5b7fb37ce5 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/AbstractLdapAuthenticationProvider.java @@ -24,7 +24,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; -import org.springframework.core.log.LogMessage; import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; @@ -68,7 +67,6 @@ public abstract class AbstractLdapAuthenticationProvider implements Authenticati UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken) authentication; String username = userToken.getName(); String password = (String) authentication.getCredentials(); - this.logger.debug(LogMessage.format("Processing authentication request for user: %s", username)); if (!StringUtils.hasLength(username)) { throw new BadCredentialsException( this.messages.getMessage("LdapAuthenticationProvider.emptyUsername", "Empty Username")); @@ -104,6 +102,7 @@ public abstract class AbstractLdapAuthenticationProvider implements Authenticati UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user, password, this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); + this.logger.debug("Authenticated user"); return result; } diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java index 1c4fa66eff..8dc0a39eee 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/BindAuthenticator.java @@ -65,7 +65,7 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { String username = authentication.getName(); String password = (String) authentication.getCredentials(); if (!StringUtils.hasLength(password)) { - logger.debug(LogMessage.format("Rejecting empty password for user %s", username)); + logger.debug(LogMessage.format("Failed to authenticate since no credentials provided")); throw new BadCredentialsException( this.messages.getMessage("BindAuthenticator.emptyPassword", "Empty Password")); } @@ -76,11 +76,18 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { break; } } + if (user == null) { + logger.debug(LogMessage.of(() -> "Failed to bind with any user DNs " + getUserDns(username))); + } // Otherwise use the configured search object to find the user and authenticate // with the returned DN. if (user == null && getUserSearch() != null) { + logger.trace("Searching for user using " + getUserSearch()); DirContextOperations userFromSearch = getUserSearch().searchForUser(username); user = bindWithDn(userFromSearch.getDn().toString(), username, password, userFromSearch.getAttributes()); + if (user == null) { + logger.debug("Failed to find user using " + getUserSearch()); + } } if (user == null) { throw new BadCredentialsException( @@ -98,13 +105,12 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { DistinguishedName userDn = new DistinguishedName(userDnStr); DistinguishedName fullDn = new DistinguishedName(userDn); fullDn.prepend(ctxSource.getBaseLdapPath()); - logger.debug(LogMessage.format("Attempting to bind as %s", fullDn)); + logger.trace(LogMessage.format("Attempting to bind as %s", fullDn)); DirContext ctx = null; try { ctx = getContextSource().getContext(fullDn.toString(), password); // Check for password policy control PasswordPolicyControl ppolicy = PasswordPolicyControlExtractor.extractControl(ctx); - logger.debug("Retrieving attributes..."); if (attrs == null || attrs.size() == 0) { attrs = ctx.getAttributes(userDn, getUserAttributes()); } @@ -112,6 +118,7 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { if (ppolicy != null) { result.setAttributeValue(ppolicy.getID(), ppolicy); } + logger.debug(LogMessage.format("Bound %s", fullDn)); return result; } catch (NamingException ex) { @@ -141,7 +148,7 @@ public class BindAuthenticator extends AbstractLdapAuthenticator { * logger. */ protected void handleBindException(String userDn, String username, Throwable cause) { - logger.debug(LogMessage.format("Failed to bind as %s: %s", userDn, cause)); + logger.trace(LogMessage.format("Failed to bind as %s", userDn), cause); } } diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java index a64b8b0368..7d79d358ef 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/PasswordComparisonAuthenticator.java @@ -76,25 +76,37 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic user = ldapTemplate.retrieveEntry(userDn, getUserAttributes()); } catch (NameNotFoundException ignore) { + logger.trace(LogMessage.format("Failed to retrieve user with %s", userDn), ignore); } if (user != null) { break; } } + if (user == null) { + logger.debug(LogMessage.of(() -> "Failed to retrieve user with any user DNs " + getUserDns(username))); + } if (user == null && getUserSearch() != null) { + logger.trace("Searching for user using " + getUserSearch()); user = getUserSearch().searchForUser(username); + if (user == null) { + logger.debug("Failed to find user using " + getUserSearch()); + } } if (user == null) { throw new UsernameNotFoundException("User not found: " + username); } - if (logger.isDebugEnabled()) { - logger.debug(LogMessage.format("Performing LDAP compare of password attribute '%s' for user '%s'", + if (logger.isTraceEnabled()) { + logger.trace(LogMessage.format("Comparing password attribute '%s' for user '%s'", this.passwordAttributeName, user.getDn())); } if (this.usePasswordAttrCompare && isPasswordAttrCompare(user, password)) { + logger.debug(LogMessage.format("Locally matched password attribute '%s' for user '%s'", + this.passwordAttributeName, user.getDn())); return user; } if (isLdapPasswordCompare(user, ldapTemplate, password)) { + logger.debug(LogMessage.format("LDAP-matched password attribute '%s' for user '%s'", + this.passwordAttributeName, user.getDn())); return user; } throw new BadCredentialsException( diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java index 14584623fc..0db213cfaa 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/SpringSecurityAuthenticationSource.java @@ -48,7 +48,7 @@ public class SpringSecurityAuthenticationSource implements AuthenticationSource public String getPrincipal() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { - log.warn("No Authentication object set in SecurityContext - returning empty String as Principal"); + log.debug("Returning empty String as Principal since authentication is null"); return ""; } Object principal = authentication.getPrincipal(); @@ -57,7 +57,7 @@ public class SpringSecurityAuthenticationSource implements AuthenticationSource return details.getDn(); } if (authentication instanceof AnonymousAuthenticationToken) { - log.debug("Anonymous Authentication, returning empty String as Principal"); + log.debug("Returning empty String as Principal since authentication is anonymous"); return ""; } throw new IllegalArgumentException( @@ -71,7 +71,7 @@ public class SpringSecurityAuthenticationSource implements AuthenticationSource public String getCredentials() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { - log.warn("No Authentication object set in SecurityContext - returning empty String as Credentials"); + log.debug("Returning empty String as Credentials since authentication is null"); return ""; } return (String) authentication.getCredentials(); diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java index 6fb79ffd4f..9b5d299881 100755 --- a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java @@ -50,8 +50,7 @@ public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityConte if (principal.equals(this.userDn)) { return super.getContext(principal, credentials); } - this.logger - .debug(LogMessage.format("Binding as '%s', prior to reconnect as user '%s'", this.userDn, principal)); + this.logger.trace(LogMessage.format("Binding as %s, prior to reconnect as user %s", this.userDn, principal)); // First bind as manager user before rebinding as the specific principal. LdapContext ctx = (LdapContext) super.getContext(this.userDn, this.password); Control[] rctls = { new PasswordPolicyControl(false) }; @@ -63,8 +62,7 @@ public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityConte catch (javax.naming.NamingException ex) { PasswordPolicyResponseControl ctrl = PasswordPolicyControlExtractor.extractControl(ctx); if (this.logger.isDebugEnabled()) { - this.logger.debug("Failed to obtain context", ex); - this.logger.debug("Password policy response: " + ctrl); + this.logger.debug(LogMessage.format("Failed to bind with %s", ctrl), ex); } LdapUtils.closeContext(ctx); if (ctrl != null && ctrl.isLocked()) { @@ -72,8 +70,7 @@ public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityConte } throw LdapUtils.convertLdapException(ex); } - this.logger.debug( - LogMessage.of(() -> "PPolicy control returned: " + PasswordPolicyControlExtractor.extractControl(ctx))); + this.logger.debug(LogMessage.of(() -> "Bound with " + PasswordPolicyControlExtractor.extractControl(ctx))); return ctx; } diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java index 79f007e408..adffca9546 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyControlExtractor.java @@ -43,7 +43,7 @@ public final class PasswordPolicyControlExtractor { ctrls = ctx.getResponseControls(); } catch (javax.naming.NamingException ex) { - logger.error("Failed to obtain response controls", ex); + logger.trace("Failed to obtain response controls", ex); } for (int i = 0; ctrls != null && i < ctrls.length; i++) { if (ctrls[i] instanceof PasswordPolicyResponseControl) { diff --git a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java index bb1c8b0898..730b23291a 100755 --- a/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java +++ b/ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyResponseControl.java @@ -31,6 +31,7 @@ import netscape.ldap.ber.stream.BERTagDecoder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.log.LogMessage; import org.springframework.dao.DataRetrievalFailureException; /** @@ -158,19 +159,21 @@ public class PasswordPolicyResponseControl extends PasswordPolicyControl { */ @Override public String toString() { - StringBuilder sb = new StringBuilder("PasswordPolicyResponseControl"); + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append(" ["); if (hasError()) { - sb.append(", error: ").append(this.errorStatus.getDefaultMessage()); + sb.append("error=").append(this.errorStatus.getDefaultMessage()).append("; "); } if (this.graceLoginsRemaining != Integer.MAX_VALUE) { - sb.append(", warning: ").append(this.graceLoginsRemaining).append(" grace logins remain"); + sb.append("warning=").append(this.graceLoginsRemaining).append(" grace logins remain; "); } if (this.timeBeforeExpiration != Integer.MAX_VALUE) { - sb.append(", warning: time before expiration is ").append(this.timeBeforeExpiration); + sb.append("warning=time before expiration is ").append(this.timeBeforeExpiration).append("; "); } if (!hasError() && !hasWarning()) { - sb.append(" (no error, no warning)"); + sb.append("(no error, no warning)"); } + sb.append("]"); return sb.toString(); } @@ -192,7 +195,8 @@ public class PasswordPolicyResponseControl extends PasswordPolicyControl { new ByteArrayInputStream(PasswordPolicyResponseControl.this.encodedValue), bread); int size = seq.size(); if (logger.isDebugEnabled()) { - logger.debug("PasswordPolicyResponse, ASN.1 sequence has " + size + " elements"); + logger.debug(LogMessage.format("Received PasswordPolicyResponse whose ASN.1 sequence has %d elements", + size)); } for (int i = 0; i < seq.size(); i++) { BERTag elt = (BERTag) seq.elementAt(i); diff --git a/ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java b/ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java index 86c6b4e8e7..c00a0b8c49 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java +++ b/ldap/src/main/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearch.java @@ -79,8 +79,8 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch { this.searchBase = searchBase; setSearchSubtree(true); if (searchBase.length() == 0) { - logger.info( - "SearchBase not set. Searches will be performed from the root: " + contextSource.getBaseLdapPath()); + logger.info(LogMessage.format("Searches will be performed from the root %s since SearchBase not set", + contextSource.getBaseLdapPath())); } } @@ -93,11 +93,14 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch { */ @Override public DirContextOperations searchForUser(String username) { - logger.debug(LogMessage.of(() -> "Searching for user '" + username + "', with user search " + this)); + logger.trace(LogMessage.of(() -> "Searching for user '" + username + "', with " + this)); SpringSecurityLdapTemplate template = new SpringSecurityLdapTemplate(this.contextSource); template.setSearchControls(this.searchControls); try { - return template.searchForSingleEntry(this.searchBase, this.searchFilter, new String[] { username }); + DirContextOperations operations = template.searchForSingleEntry(this.searchBase, this.searchFilter, + new String[] { username }); + logger.debug(LogMessage.of(() -> "Found user '" + username + "', with " + this)); + return operations; } catch (IncorrectResultSizeDataAccessException ex) { if (ex.getActualSize() == 0) { @@ -151,12 +154,14 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("[ searchFilter: '").append(this.searchFilter).append("', "); - sb.append("searchBase: '").append(this.searchBase).append("'"); - sb.append(", scope: ").append( - (this.searchControls.getSearchScope() != SearchControls.SUBTREE_SCOPE) ? "single-level, " : "subtree"); - sb.append(", searchTimeLimit: ").append(this.searchControls.getTimeLimit()); - sb.append(", derefLinkFlag: ").append(this.searchControls.getDerefLinkFlag()).append(" ]"); + sb.append(getClass().getSimpleName()).append(" ["); + sb.append("searchFilter=").append(this.searchFilter).append("; "); + sb.append("searchBase=").append(this.searchBase).append("; "); + sb.append("scope=").append( + (this.searchControls.getSearchScope() != SearchControls.SUBTREE_SCOPE) ? "single-level" : "subtree") + .append("; "); + sb.append("searchTimeLimit=").append(this.searchControls.getTimeLimit()).append("; "); + sb.append("derefLinkFlag=").append(this.searchControls.getDerefLinkFlag()).append(" ]"); return sb.toString(); } diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java index 8ef21554c4..f16ba86b28 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java @@ -163,10 +163,10 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator getLdapTemplate().setSearchControls(getSearchControls()); this.groupSearchBase = groupSearchBase; if (groupSearchBase == null) { - logger.info("groupSearchBase is null. No group search will be performed."); + logger.info("Will not perform group search since groupSearchBase is null."); } else if (groupSearchBase.length() == 0) { - logger.info("groupSearchBase is empty. Searches will be performed from the context source base"); + logger.info("Will perform group search from the context source base since groupSearchBase is empty."); } this.authorityMapper = (record) -> { String role = record.get(this.groupRoleAttribute).get(0); @@ -199,7 +199,6 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator @Override public final Collection getGrantedAuthorities(DirContextOperations user, String username) { String userDn = user.getNameInNamespace(); - logger.debug(LogMessage.format("Getting authorities for user %s", userDn)); Set roles = getGroupMembershipRoles(userDn, username); Set extraRoles = getAdditionalRoles(user, username); if (extraRoles != null) { @@ -210,6 +209,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator } List result = new ArrayList<>(roles.size()); result.addAll(roles); + logger.debug(LogMessage.format("Retrieved authorities for user %s", userDn)); return result; } @@ -218,12 +218,12 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator return new HashSet<>(); } Set authorities = new HashSet<>(); - logger.debug(LogMessage.of(() -> "Searching for roles for user '" + username + "', DN = " + "'" + userDn - + "', with filter " + this.groupSearchFilter + " in search base '" + getGroupSearchBase() + "'")); + logger.trace(LogMessage.of(() -> "Searching for roles for user " + username + " with DN " + userDn + + " and filter " + this.groupSearchFilter + " in search base " + getGroupSearchBase())); Set>> userRoles = getLdapTemplate().searchForMultipleAttributeValues( getGroupSearchBase(), this.groupSearchFilter, new String[] { userDn, username }, new String[] { this.groupRoleAttribute }); - logger.debug(LogMessage.of(() -> "Roles from search: " + userRoles)); + logger.debug(LogMessage.of(() -> "Found roles from search " + userRoles)); for (Map> role : userRoles) { authorities.add(this.authorityMapper.apply(role)); } diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java index 29a7323e3d..a0ef3b333b 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsImpl.java @@ -146,30 +146,16 @@ public class LdapUserDetailsImpl implements LdapUserDetails, PasswordPolicyData @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append(super.toString()).append(": "); - sb.append("Dn: ").append(this.dn).append("; "); - sb.append("Username: ").append(this.username).append("; "); - sb.append("Password: [PROTECTED]; "); - sb.append("Enabled: ").append(this.enabled).append("; "); - sb.append("AccountNonExpired: ").append(this.accountNonExpired).append("; "); - sb.append("CredentialsNonExpired: ").append(this.credentialsNonExpired).append("; "); - sb.append("AccountNonLocked: ").append(this.accountNonLocked).append("; "); - if (this.getAuthorities() != null && !this.getAuthorities().isEmpty()) { - sb.append("Granted Authorities: "); - boolean first = true; - for (Object authority : this.getAuthorities()) { - if (first) { - first = false; - } - else { - sb.append(", "); - } - sb.append(authority.toString()); - } - } - else { - sb.append("Not granted any authorities"); - } + sb.append(getClass().getSimpleName()).append(" ["); + sb.append("Dn=").append(this.dn).append("; "); + sb.append("Username=").append(this.username).append("; "); + sb.append("Password=[PROTECTED]; "); + sb.append("Enabled=").append(this.enabled).append("; "); + sb.append("AccountNonExpired=").append(this.accountNonExpired).append("; "); + sb.append("CredentialsNonExpired=").append(this.credentialsNonExpired).append("; "); + sb.append("AccountNonLocked=").append(this.accountNonLocked).append("; "); + sb.append("Granted Authorities=").append(getAuthorities()); + sb.append("]"); return sb.toString(); } diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java index 56e724a075..b44e30a0d8 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsMapper.java @@ -54,7 +54,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper { public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection authorities) { String dn = ctx.getNameInNamespace(); - this.logger.debug(LogMessage.format("Mapping user details from context with DN: %s", dn)); + this.logger.debug(LogMessage.format("Mapping user details from context with DN %s", dn)); LdapUserDetailsImpl.Essence essence = new LdapUserDetailsImpl.Essence(); essence.setDn(dn); Object passwordValue = ctx.getObjectAttribute(this.passwordAttributeName); @@ -67,7 +67,7 @@ public class LdapUserDetailsMapper implements UserDetailsContextMapper { String[] rolesForAttribute = ctx.getStringAttributes(this.roleAttributes[i]); if (rolesForAttribute == null) { this.logger.debug( - LogMessage.format("Couldn't read role attribute '%s' for user $s", this.roleAttributes[i], dn)); + LogMessage.format("Couldn't read role attribute %s for user %s", this.roleAttributes[i], dn)); continue; } for (String role : rolesForAttribute) { diff --git a/ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java b/ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java index 33d55d7c5b..b61068ec8f 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java +++ b/ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java @@ -166,13 +166,13 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula private void performNestedSearch(String userDn, String username, Set authorities, int depth) { if (depth == 0) { // back out of recursion - logger.debug(LogMessage.of(() -> "Search aborted, max depth reached," + " for roles for user '" + username - + "', DN = " + "'" + userDn + "', with filter " + getGroupSearchFilter() + " in search base '" + logger.debug(LogMessage.of(() -> "Aborted search since max depth reached," + " for roles for user '" + + username + " with DN = " + userDn + " and filter " + getGroupSearchFilter() + " in search base '" + getGroupSearchBase() + "'")); return; } - logger.debug(LogMessage.of(() -> "Searching for roles for user '" + username + "', DN = " + "'" + userDn - + "', with filter " + getGroupSearchFilter() + " in search base '" + getGroupSearchBase() + "'")); + logger.trace(LogMessage.of(() -> "Searching for roles for user " + username + " with DN " + userDn + + " and filter " + getGroupSearchFilter() + " in search base " + getGroupSearchBase())); if (getAttributeNames() == null) { setAttributeNames(new HashSet<>()); } @@ -182,7 +182,7 @@ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopula Set>> userRoles = getLdapTemplate().searchForMultipleAttributeValues( getGroupSearchBase(), getGroupSearchFilter(), new String[] { userDn, username }, getAttributeNames().toArray(new String[0])); - logger.debug(LogMessage.format("Roles from search: %s", userRoles)); + logger.debug(LogMessage.format("Found roles from search %s", userRoles)); for (Map> record : userRoles) { boolean circular = false; String dn = record.get(SpringSecurityLdapTemplate.DN_KEY).get(0); From 76ebbb84f7e6fd6f2e061e208e3b148ef0a6b0ba Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 1 Nov 2021 14:50:25 -0600 Subject: [PATCH 004/589] Separate Namespace Servlet Docs Issue gh-10367 --- .../config/doc/XsdDocumentedTests.java | 29 +- docs/modules/ROOT/nav.adoc | 7 +- .../features/integrations/concurrency.adoc | 2 +- .../ROOT/pages/servlet/appendix/index.adoc | 2 +- .../namespace/authentication-manager.adoc | 292 ++++ .../{namespace.adoc => namespace/http.adoc} | 1330 ++--------------- .../servlet/appendix/namespace/index.adoc | 9 + .../servlet/appendix/namespace/ldap.adoc | 291 ++++ .../appendix/namespace/method-security.adoc | 340 +++++ .../servlet/appendix/namespace/websocket.adoc | 74 + .../pages/servlet/authentication/cas.adoc | 2 +- .../pages/servlet/authentication/jaas.adoc | 2 +- .../pages/servlet/authentication/logout.adoc | 4 +- .../servlet/configuration/xml-namespace.adoc | 2 +- .../ROOT/pages/servlet/exploits/csrf.adoc | 2 +- .../pages/servlet/integrations/websocket.adoc | 8 +- .../pages/servlet/oauth2/oauth2-client.adoc | 2 +- .../pages/servlet/oauth2/oauth2-login.adoc | 2 +- 18 files changed, 1210 insertions(+), 1190 deletions(-) create mode 100644 docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc rename docs/modules/ROOT/pages/servlet/appendix/{namespace.adoc => namespace/http.adoc} (62%) create mode 100644 docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc create mode 100644 docs/modules/ROOT/pages/servlet/appendix/namespace/ldap.adoc create mode 100644 docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc create mode 100644 docs/modules/ROOT/pages/servlet/appendix/namespace/websocket.adoc diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java index 35d857c84a..e8782a3b84 100644 --- a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java +++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java @@ -16,9 +16,10 @@ package org.springframework.security.config.doc; +import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -60,7 +61,7 @@ public class XsdDocumentedTests { "nsa-frame-options-from-parameter"); // @formatter:on - String referenceLocation = "../docs/modules/ROOT/pages/servlet/appendix/namespace.adoc"; + String referenceLocation = "../docs/modules/ROOT/pages/servlet/appendix/namespace"; String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd"; @@ -163,7 +164,7 @@ public class XsdDocumentedTests { public void countReferencesWhenReviewingDocumentationThenEntireSchemaIsIncluded() throws IOException { Map elementsByElementName = this.xml.elementsByElementName(this.schemaDocumentLocation); // @formatter:off - List documentIds = Files.lines(Paths.get(this.referenceLocation)) + List documentIds = namespaceLines() .filter((line) -> line.matches("\\[\\[(nsa-.*)\\]\\]")) .map((line) -> line.substring(2, line.length() - 2)) .collect(Collectors.toList()); @@ -189,7 +190,7 @@ public class XsdDocumentedTests { Map> docAttrNameToParents = new TreeMap<>(); String docAttrName = null; Map> currentDocAttrNameToElmt = null; - List lines = Files.readAllLines(Paths.get(this.referenceLocation)); + List lines = namespaceLines().collect(Collectors.toList()); for (String line : lines) { if (line.matches("^\\[\\[.*\\]\\]$")) { String id = line.substring(2, line.length() - 2); @@ -212,6 +213,13 @@ public class XsdDocumentedTests { String elmtId = line.replaceAll(expression, "$1"); currentDocAttrNameToElmt.computeIfAbsent(docAttrName, (key) -> new ArrayList<>()).add(elmtId); } + else { + expression = ".*xref:.*#(nsa-.*)\\[.*\\]"; + if (line.matches(expression)) { + String elmtId = line.replaceAll(expression, "$1"); + currentDocAttrNameToElmt.computeIfAbsent(docAttrName, (key) -> new ArrayList<>()).add(elmtId); + } + } } } Map elementNameToElement = this.xml.elementsByElementName(this.schemaDocumentLocation); @@ -295,4 +303,17 @@ public class XsdDocumentedTests { assertThat(notDocAttrIds).isEmpty(); } + private Stream namespaceLines() { + return Stream.of(new File(this.referenceLocation).listFiles()).map(File::toPath).flatMap(this::fileLines); + } + + private Stream fileLines(Path path) { + try { + return Files.lines(path); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + } diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index f08028dcdf..c6f8d0e8fb 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -100,7 +100,12 @@ *** xref:servlet/test/mockmvc/result-handlers.adoc[Security ResultHandlers] ** xref:servlet/appendix/index.adoc[Appendix] *** xref:servlet/appendix/database-schema.adoc[Database Schemas] -*** xref:servlet/appendix/namespace.adoc[XML Namespace] +*** xref:servlet/appendix/namespace/index.adoc[XML Namespace] +**** xref:servlet/appendix/namespace/authentication-manager.adoc[Authentication Services] +**** xref:servlet/appendix/namespace/http.adoc[Web Security] +**** xref:servlet/appendix/namespace/method-security.adoc[Method Security] +**** xref:servlet/appendix/namespace/ldap.adoc[LDAP Security] +**** xref:servlet/appendix/namespace/websocket.adoc[WebSocket Security] *** xref:servlet/appendix/faq.adoc[FAQ] * xref:reactive/index.adoc[Reactive Applications] ** xref:reactive/getting-started.adoc[Getting Started] diff --git a/docs/modules/ROOT/pages/features/integrations/concurrency.adoc b/docs/modules/ROOT/pages/features/integrations/concurrency.adoc index fbb049c9e9..32535f2720 100644 --- a/docs/modules/ROOT/pages/features/integrations/concurrency.adoc +++ b/docs/modules/ROOT/pages/features/integrations/concurrency.adoc @@ -44,7 +44,7 @@ fun run() { While very simple, it makes it seamless to transfer the SecurityContext from one Thread to another. This is important since, in most cases, the SecurityContextHolder acts on a per Thread basis. -For example, you might have used Spring Security's xref:servlet/appendix/namespace.adoc#nsa-global-method-security[] support to secure one of your services. +For example, you might have used Spring Security's xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[] support to secure one of your services. You can now easily transfer the `SecurityContext` of the current `Thread` to the `Thread` that invokes the secured service. An example of how you might do this can be found below: diff --git a/docs/modules/ROOT/pages/servlet/appendix/index.adoc b/docs/modules/ROOT/pages/servlet/appendix/index.adoc index 3fecd174b4..9c84348fea 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/index.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/index.adoc @@ -4,5 +4,5 @@ This is an appendix for Servlet based Spring Security. It has the following sections: * xref:servlet/appendix/database-schema.adoc[Database Schemas] -* xref:servlet/appendix/namespace.adoc[XML Namespace] +* xref:servlet/appendix/namespace/index.adoc[XML Namespace] * xref:servlet/appendix/faq.adoc[FAQ] diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc new file mode 100644 index 0000000000..5452a2b799 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/authentication-manager.adoc @@ -0,0 +1,292 @@ +[[nsa-authentication]] += Authentication Services +Before Spring Security 3.0, an `AuthenticationManager` was automatically registered internally. +Now you must register one explicitly using the `` element. +This creates an instance of Spring Security's `ProviderManager` class, which needs to be configured with a list of one or more `AuthenticationProvider` instances. +These can either be created using syntax elements provided by the namespace, or they can be standard bean definitions, marked for addition to the list using the `authentication-provider` element. + + +[[nsa-authentication-manager]] +== +Every Spring Security application which uses the namespace must have include this element somewhere. +It is responsible for registering the `AuthenticationManager` which provides authentication services to the application. +All elements which create `AuthenticationProvider` instances should be children of this element. + + +[[nsa-authentication-manager-attributes]] +=== Attributes + + +[[nsa-authentication-manager-alias]] +* **alias** +This attribute allows you to define an alias name for the internal instance for use in your own configuration. + + +[[nsa-authentication-manager-erase-credentials]] +* **erase-credentials** +If set to true, the AuthenticationManager will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. +Literally it maps to the `eraseCredentialsAfterAuthentication` property of the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. + + +[[nsa-authentication-manager-id]] +* **id** +This attribute allows you to define an id for the internal instance for use in your own configuration. +It is the same as the alias element, but provides a more consistent experience with elements that use the id attribute. + + +[[nsa-authentication-manager-children]] +=== Child Elements of + + +* <> +* xref:servlet/appendix/namespace/ldap.adoc#nsa-ldap-authentication-provider[ldap-authentication-provider] + + + +[[nsa-authentication-provider]] +== +Unless used with a `ref` attribute, this element is shorthand for configuring a `DaoAuthenticationProvider`. +`DaoAuthenticationProvider` loads user information from a `UserDetailsService` and compares the username/password combination with the values supplied at login. +The `UserDetailsService` instance can be defined either by using an available namespace element (`jdbc-user-service` or by using the `user-service-ref` attribute to point to a bean defined elsewhere in the application context). + + + +[[nsa-authentication-provider-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-authentication-provider-attributes]] +=== Attributes + + +[[nsa-authentication-provider-ref]] +* **ref** +Defines a reference to a Spring bean that implements `AuthenticationProvider`. + +If you have written your own `AuthenticationProvider` implementation (or want to configure one of Spring Security's own implementations as a traditional bean for some reason, then you can use the following syntax to add it to the internal list of `ProviderManager`: + +[source,xml] +---- + + + + + + +---- + + + + +[[nsa-authentication-provider-user-service-ref]] +* **user-service-ref** +A reference to a bean that implements UserDetailsService that may be created using the standard bean element or the custom user-service element. + + +[[nsa-authentication-provider-children]] +=== Child Elements of + + +* <> +* xref:servlet/appendix/namespace/ldap.adoc#nsa-ldap-user-service[ldap-user-service] +* <> +* <> + + + +[[nsa-jdbc-user-service]] +== +Causes creation of a JDBC-based UserDetailsService. + + +[[nsa-jdbc-user-service-attributes]] +=== Attributes + + +[[nsa-jdbc-user-service-authorities-by-username-query]] +* **authorities-by-username-query** +An SQL statement to query for a user's granted authorities given a username. + +The default is + +[source] +---- +select username, authority from authorities where username = ? +---- + + + + +[[nsa-jdbc-user-service-cache-ref]] +* **cache-ref** +Defines a reference to a cache for use with a UserDetailsService. + + +[[nsa-jdbc-user-service-data-source-ref]] +* **data-source-ref** +The bean ID of the DataSource which provides the required tables. + + +[[nsa-jdbc-user-service-group-authorities-by-username-query]] +* **group-authorities-by-username-query** +An SQL statement to query user's group authorities given a username. +The default is + ++ + +[source] +---- +select +g.id, g.group_name, ga.authority +from +groups g, group_members gm, group_authorities ga +where +gm.username = ? and g.id = ga.group_id and g.id = gm.group_id +---- + + + + +[[nsa-jdbc-user-service-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-jdbc-user-service-role-prefix]] +* **role-prefix** +A non-empty string prefix that will be added to role strings loaded from persistent storage (default is "ROLE_"). +Use the value "none" for no prefix in cases where the default is non-empty. + + +[[nsa-jdbc-user-service-users-by-username-query]] +* **users-by-username-query** +An SQL statement to query a username, password, and enabled status given a username. +The default is + ++ + +[source] +---- +select username, password, enabled from users where username = ? +---- + + + + +[[nsa-password-encoder]] +== +Authentication providers can optionally be configured to use a password encoder as described in the xref:features/authentication/password-storage.adoc#authentication-password-storage[Password Storage]. +This will result in the bean being injected with the appropriate `PasswordEncoder` instance. + + +[[nsa-password-encoder-parents]] +=== Parent Elements of + + +* <> +* xref:servlet/appendix/namespace/authentication-manager.adoc#nsa-password-compare[password-compare] + + + +[[nsa-password-encoder-attributes]] +=== Attributes + + +[[nsa-password-encoder-hash]] +* **hash** +Defines the hashing algorithm used on user passwords. +We recommend strongly against using MD4, as it is a very weak hashing algorithm. + + +[[nsa-password-encoder-ref]] +* **ref** +Defines a reference to a Spring bean that implements `PasswordEncoder`. + + +[[nsa-user-service]] +== +Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. +Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. + + +[[nsa-user-service-attributes]] +=== Attributes + + +[[nsa-user-service-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-user-service-properties]] +* **properties** +The location of a Properties file where each line is in the format of + ++ + +[source] +---- +username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] +---- + + + + +[[nsa-user-service-children]] +=== Child Elements of + + +* <> + + + +[[nsa-user]] +== +Represents a user in the application. + + +[[nsa-user-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-user-attributes]] +=== Attributes + + +[[nsa-user-authorities]] +* **authorities** +One of more authorities granted to the user. +Separate authorities with a comma (but no space). +For example, "ROLE_USER,ROLE_ADMINISTRATOR" + + +[[nsa-user-disabled]] +* **disabled** +Can be set to "true" to mark an account as disabled and unusable. + + +[[nsa-user-locked]] +* **locked** +Can be set to "true" to mark an account as locked and unusable. + + +[[nsa-user-name]] +* **name** +The username assigned to the user. + + +[[nsa-user-password]] +* **password** +The password assigned to the user. +This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). +This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. +If omitted, the namespace will generate a random value, preventing its accidental use for authentication. +Cannot be empty. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc similarity index 62% rename from docs/modules/ROOT/pages/servlet/appendix/namespace.adoc rename to docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index a6518b4ed1..de5e3c6d26 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -1,22 +1,14 @@ -[[appendix-namespace]] -= The Security Namespace -This appendix provides a reference to the elements available in the security namespace and information on the underlying beans they create (a knowledge of the individual classes and how they work together is assumed - you can find more information in the project Javadoc and elsewhere in this document). -If you haven't used the namespace before, please read the xref:servlet/configuration/xml-namespace.adoc#ns-config[introductory chapter] on namespace configuration, as this is intended as a supplement to the information there. -Using a good quality XML editor while editing a configuration based on the schema is recommended as this will provide contextual information on which elements and attributes are available as well as comments explaining their purpose. -The namespace is written in https://relaxng.org/[RELAX NG] Compact format and later converted into an XSD schema. -If you are familiar with this format, you may wish to examine the https://raw.githubusercontent.com/spring-projects/spring-security/main/config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc[schema file] directly. - [[nsa-web]] -== Web Application Security += Web Application Security [[nsa-debug]] -=== +== Enables Spring Security debugging infrastructure. This will provide human-readable (multi-line) debugging information to monitor requests coming into the security filters. This may include sensitive information, such as request parameters or headers, and should only be used in a development environment. [[nsa-http]] -=== +== If you use an `` element within your application, a `FilterChainProxy` bean named "springSecurityFilterChain" is created and the configuration within the element is used to build a filter chain within `FilterChainProxy`. As of Spring Security 3.1, additional `http` elements can be used to add extra filter chains footnote:[See the pass:specialcharacters,macros[xref:servlet/configuration/xml-namespace.adoc#ns-web-xml[introductory chapter]] for how to set up the mapping from your `web.xml` ]. @@ -34,7 +26,7 @@ These are fixed and cannot be replaced with alternatives. [[nsa-http-attributes]] -==== Attributes +=== Attributes The attributes on the `` element control some of the properties on the core filters. @@ -151,7 +143,7 @@ The default value is true. [[nsa-http-children]] -==== Child Elements of +=== Child Elements of * <> * <> * <> @@ -177,18 +169,18 @@ The default value is true. [[nsa-access-denied-handler]] -=== -This element allows you to set the `errorPage` property for the default `AccessDeniedHandler` used by the `ExceptionTranslationFilter`, using the <> attribute, or to supply your own implementation using the<> attribute. +== +This element allows you to set the `errorPage` property for the default `AccessDeniedHandler` used by the `ExceptionTranslationFilter`, using the <> attribute, or to supply your own implementation using the <> attribute. This is discussed in more detail in the section on the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[ExceptionTranslationFilter]. [[nsa-access-denied-handler-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-access-denied-handler-attributes]] -==== Attributes +=== Attributes [[nsa-access-denied-handler-error-page]] @@ -202,12 +194,12 @@ Defines a reference to a Spring bean of type `AccessDeniedHandler`. [[nsa-cors]] -=== +== This element allows for configuring a `CorsFilter`. If no `CorsFilter` or `CorsConfigurationSource` is specified and Spring MVC is on the classpath, a `HandlerMappingIntrospector` is used as the `CorsConfigurationSource`. [[nsa-cors-attributes]] -==== Attributes +=== Attributes The attributes on the `` element control the headers element. [[nsa-cors-ref]] @@ -219,12 +211,12 @@ Optional attribute that specifies the bean name of a `CorsFilter`. Optional attribute that specifies the bean name of a `CorsConfigurationSource` to be injected into a `CorsFilter` created by the XML namespace. [[nsa-cors-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-headers]] -=== +== This element allows for configuring additional (security) headers to be send with the response. It enables easy configuration for several headers and also allows for setting custom headers through the <> element. Additional information, can be found in the xref:features/exploits/headers.adoc#headers[Security Headers] section of the reference. @@ -248,7 +240,7 @@ https://www.w3.org/TR/CSP2/[Content Security Policy (CSP)] is a mechanism that w ** `Feature-Policy` - Can be set using the <> element, https://wicg.github.io/feature-policy/[Feature-Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser. [[nsa-headers-attributes]] -==== Attributes +=== Attributes The attributes on the `` element control the headers element. @@ -264,14 +256,14 @@ The default is false (the headers are enabled). [[nsa-headers-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-headers-children]] -==== Child Elements of +=== Child Elements of * <> @@ -289,12 +281,12 @@ The default is false (the headers are enabled). [[nsa-cache-control]] -=== +== Adds `Cache-Control`, `Pragma`, and `Expires` headers to ensure that the browser does not cache your secured pages. [[nsa-cache-control-attributes]] -==== Attributes +=== Attributes [[nsa-cache-control-disabled]] * **disabled** @@ -303,7 +295,7 @@ Default false. [[nsa-cache-control-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -311,13 +303,13 @@ Default false. [[nsa-hsts]] -=== +== When enabled adds the https://tools.ietf.org/html/rfc6797[Strict-Transport-Security] header to the response for any secure request. This allows the server to instruct browsers to automatically use HTTPS for future requests. [[nsa-hsts-attributes]] -==== Attributes +=== Attributes [[nsa-hsts-disabled]] * **disabled** @@ -347,20 +339,20 @@ Specifies if preload should be included. Default false. [[nsa-hsts-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-hpkp]] -=== +== When enabled adds the https://tools.ietf.org/html/rfc7469[Public Key Pinning Extension for HTTP] header to the response for any secure request. This allows HTTPS websites to resist impersonation by attackers using mis-issued or otherwise fraudulent certificates. [[nsa-hpkp-attributes]] -==== Attributes +=== Attributes [[nsa-hpkp-disabled]] * **disabled** @@ -391,28 +383,28 @@ Specifies the URI to which the browser should report pin validation failures. [[nsa-hpkp-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-pins]] -=== +== The list of pins [[nsa-pins-children]] -==== Child Elements of +=== Child Elements of * <> [[nsa-pin]] -=== +== A pin is specified using the base64-encoded SPKI fingerprint as value and the cryptographic hash algorithm as attribute [[nsa-pin-attributes]] -==== Attributes +=== Attributes [[nsa-pin-algorithm]] * **algorithm** @@ -421,19 +413,19 @@ Default is SHA256. [[nsa-pin-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-content-security-policy]] -=== +== When enabled adds the https://www.w3.org/TR/CSP2/[Content Security Policy (CSP)] header to the response. CSP is a mechanism that web applications can leverage to mitigate content injection vulnerabilities, such as cross-site scripting (XSS). [[nsa-content-security-policy-attributes]] -==== Attributes +=== Attributes [[nsa-content-security-policy-policy-directives]] * **policy-directives** @@ -445,18 +437,18 @@ Set to true, to enable the Content-Security-Policy-Report-Only header for report Defaults to false. [[nsa-content-security-policy-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-referrer-policy]] -=== +== When enabled adds the https://www.w3.org/TR/referrer-policy/[Referrer Policy] header to the response. [[nsa-referrer-policy-attributes]] -==== Attributes +=== Attributes [[nsa-referrer-policy-policy]] * **policy** @@ -464,37 +456,37 @@ The policy for the Referrer-Policy header. Default "no-referrer". [[nsa-referrer-policy-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-feature-policy]] -=== +== When enabled adds the https://wicg.github.io/feature-policy/[Feature Policy] header to the response. [[nsa-feature-policy-attributes]] -==== Attributes +=== Attributes [[nsa-feature-policy-policy-directives]] * **policy-directives** The security policy directive(s) for the Feature-Policy header. [[nsa-feature-policy-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-frame-options]] -=== +== When enabled adds the https://tools.ietf.org/html/draft-ietf-websec-x-frame-options[X-Frame-Options header] to the response, this allows newer browsers to do some security checks and prevent https://en.wikipedia.org/wiki/Clickjacking[clickjacking] attacks. [[nsa-frame-options-attributes]] -==== Attributes +=== Attributes [[nsa-frame-options-disabled]] * **disabled** @@ -515,34 +507,34 @@ On the other hand, if you specify SAMEORIGIN, you can still use the page in a fr [[nsa-frame-options-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-permissions-policy]] -=== +== Adds the https://w3c.github.io/webappsec-permissions-policy/[Permissions-Policy header] to the response. [[nsa-permissions-policy-attributes]] -==== Attributes +=== Attributes [[nsa-permissions-policy-policy]] * **policy** The policy value to write for the `Permissions-Policy` header [[nsa-permissions-policy-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-xss-protection]] -=== +== Adds the https://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-iv-the-xss-filter.aspx[X-XSS-Protection header] to the response to assist in protecting against https://en.wikipedia.org/wiki/Cross-site_scripting#Non-Persistent[reflected / Type-1 Cross-Site Scripting (XSS)] attacks. This is in no-way a full protection to XSS attacks! [[nsa-xss-protection-attributes]] -==== Attributes +=== Attributes [[nsa-xss-protection-disabled]] @@ -564,20 +556,20 @@ Note that there are sometimes ways of bypassing this mode which can often times [[nsa-xss-protection-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-content-type-options]] -=== +== Add the X-Content-Type-Options header with the value of nosniff to the response. This https://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx[disables MIME-sniffing] for IE8+ and Chrome extensions. [[nsa-content-type-options-attributes]] -==== Attributes +=== Attributes [[nsa-content-type-options-disabled]] * **disabled** @@ -585,7 +577,7 @@ Specifies if Content Type Options should be disabled. Default false. [[nsa-content-type-options-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -593,12 +585,12 @@ Default false. [[nsa-header]] -===
+==
Add additional headers to the response, both the name and value need to be specified. [[nsa-header-attributes]] -==== Attributes +=== Attributes [[nsa-header-name]] @@ -617,7 +609,7 @@ Reference to a custom implementation of the `HeaderWriter` interface. [[nsa-header-parents]] -==== Parent Elements of
+=== Parent Elements of
* <> @@ -625,13 +617,13 @@ Reference to a custom implementation of the `HeaderWriter` interface. [[nsa-anonymous]] -=== +== Adds an `AnonymousAuthenticationFilter` to the stack and an `AnonymousAuthenticationProvider`. Required if you are using the `IS_AUTHENTICATED_ANONYMOUSLY` attribute. [[nsa-anonymous-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -639,7 +631,7 @@ Required if you are using the `IS_AUTHENTICATED_ANONYMOUSLY` attribute. [[nsa-anonymous-attributes]] -==== Attributes +=== Attributes [[nsa-anonymous-enabled]] @@ -671,14 +663,14 @@ if unset, defaults to `anonymousUser`. [[nsa-csrf]] -=== +== This element will add https://en.wikipedia.org/wiki/Cross-site_request_forgery[Cross Site Request Forger (CSRF)] protection to the application. It also updates the default RequestCache to only replay "GET" requests upon successful authentication. Additional information can be found in the xref:features/exploits/csrf.adoc#csrf[Cross Site Request Forgery (CSRF)] section of the reference. [[nsa-csrf-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -686,7 +678,7 @@ Additional information can be found in the xref:features/exploits/csrf.adoc#csrf [[nsa-csrf-attributes]] -==== Attributes +=== Attributes [[nsa-csrf-disabled]] * **disabled** @@ -707,14 +699,14 @@ Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS". [[nsa-custom-filter]] -=== +== This element is used to add a filter to the filter chain. It doesn't create any additional beans but is used to select a bean of type `javax.servlet.Filter` which is already defined in the application context and add that at a particular position in the filter chain maintained by Spring Security. Full details can be found in the xref:servlet/configuration/xml-namespace.adoc#ns-custom-filters[ namespace chapter]. [[nsa-custom-filter-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -722,7 +714,7 @@ Full details can be found in the xref:servlet/configuration/xml-namespace.adoc#n [[nsa-custom-filter-attributes]] -==== Attributes +=== Attributes [[nsa-custom-filter-after]] @@ -749,24 +741,24 @@ Defines a reference to a Spring bean that implements `Filter`. [[nsa-expression-handler]] -=== +== Defines the `SecurityExpressionHandler` instance which will be used if expression-based access-control is enabled. A default implementation (with no ACL support) will be used if not supplied. [[nsa-expression-handler-parents]] -==== Parent Elements of +=== Parent Elements of -* <> +* xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[global-method-security] * <> -* <> -* <> +* xref:servlet/appendix/namespace/method-security.adoc#nsa-method-security[method-security] +* xref:servlet/appendix/namespace/websocket.adoc#nsa-websocket-message-broker[websocket-message-broker] [[nsa-expression-handler-attributes]] -==== Attributes +=== Attributes [[nsa-expression-handler-ref]] @@ -775,7 +767,7 @@ Defines a reference to a Spring bean that implements `SecurityExpressionHandler` [[nsa-form-login]] -=== +== Used to add an `UsernamePasswordAuthenticationFilter` to the filter stack and an `LoginUrlAuthenticationEntryPoint` to the application context to provide authentication on demand. This will always take precedence over other namespace-created entry points. If no attributes are supplied, a login page will be generated automatically at the URL "/login" footnote:[ @@ -785,7 +777,7 @@ The class `DefaultLoginPageGeneratingFilter` is responsible for rendering the lo [[nsa-form-login-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -793,7 +785,7 @@ The class `DefaultLoginPageGeneratingFilter` is responsible for rendering the lo [[nsa-form-login-attributes]] -==== Attributes +=== Attributes [[nsa-form-login-always-use-default-target]] @@ -870,17 +862,17 @@ Maps a `ForwardAuthenticationFailureHandler` to `authenticationFailureHandler` p [[nsa-oauth2-login]] -=== +== The xref:servlet/oauth2/oauth2-login.adoc#oauth2login[OAuth 2.0 Login] feature configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. [[nsa-oauth2-login-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-oauth2-login-attributes]] -==== Attributes +=== Attributes [[nsa-oauth2-login-client-registration-repository-ref]] @@ -954,17 +946,17 @@ Reference to the `JwtDecoderFactory` used by `OidcAuthorizationCodeAuthenticatio [[nsa-oauth2-client]] -=== +== Configures xref:servlet/oauth2/oauth2-client.adoc#oauth2client[OAuth 2.0 Client] support. [[nsa-oauth2-client-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-oauth2-client-attributes]] -==== Attributes +=== Attributes [[nsa-oauth2-client-client-registration-repository-ref]] @@ -983,24 +975,24 @@ Reference to the `OAuth2AuthorizedClientService`. [[nsa-oauth2-client-children]] -==== Child Elements of +=== Child Elements of * <> [[nsa-authorization-code-grant]] -=== +== Configures xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-auth-grant-support[OAuth 2.0 Authorization Code Grant]. [[nsa-authorization-code-grant-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-authorization-code-grant-attributes]] -==== Attributes +=== Attributes [[nsa-authorization-code-grant-authorization-request-repository-ref]] @@ -1019,30 +1011,30 @@ Reference to the `OAuth2AccessTokenResponseClient`. [[nsa-client-registrations]] -=== +== A container element for client(s) registered (xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration]) with an OAuth 2.0 or OpenID Connect 1.0 Provider. [[nsa-client-registrations-children]] -==== Child Elements of +=== Child Elements of * <> * <> [[nsa-client-registration]] -=== +== Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. [[nsa-client-registration-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-client-registration-attributes]] -==== Attributes +=== Attributes [[nsa-client-registration-registration-id]] @@ -1093,18 +1085,18 @@ A reference to the associated provider. May reference a `` element or [[nsa-provider]] -=== +== The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. [[nsa-provider-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-provider-attributes]] -==== Attributes +=== Attributes [[nsa-provider-provider-id]] @@ -1148,23 +1140,23 @@ The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (J The URI used to initially configure a `ClientRegistration` using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. [[nsa-oauth2-resource-server]] -=== +== Adds a `BearerTokenAuthenticationFilter`, `BearerTokenAuthenticationEntryPoint`, and `BearerTokenAccessDeniedHandler` to the configuration. In addition, either `` or `` must be specified. [[nsa-oauth2-resource-server-parents]] -==== Parents Elements of +=== Parents Elements of * <> [[nsa-oauth2-resource-server-children]] -==== Child Elements of +=== Child Elements of * <> * <> [[nsa-oauth2-resource-server-attributes]] -==== Attributes +=== Attributes [[nsa-oauth2-resource-server-authentication-manager-resolver-ref]] * **authentication-manager-resolver-ref** @@ -1179,18 +1171,18 @@ Reference to a `BearerTokenResolver` which will retrieve the bearer token from t Reference to a `AuthenticationEntryPoint` which will handle unauthorized requests [[nsa-jwt]] -=== +== Represents an OAuth 2.0 Resource Server that will authorize JWTs [[nsa-jwt-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-jwt-attributes]] -==== Attributes +=== Attributes [[nsa-jwt-jwt-authentication-converter-ref]] * **jwt-authentication-converter-ref** @@ -1205,16 +1197,16 @@ Reference to a `JwtDecoder`. This is a larger component that overrides `jwk-set- The JWK Set Uri used to load signing verification keys from an OAuth 2.0 Authorization Server [[nsa-opaque-token]] -=== +== Represents an OAuth 2.0 Resource Server that will authorize opaque tokens [[nsa-opaque-token-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-opaque-token-attributes]] -==== Attributes +=== Attributes [[nsa-opaque-token-introspector-ref]] * **introspector-ref** @@ -1233,13 +1225,13 @@ The Client Id to use for client authentication against the provided `introspecti The Client Secret to use for client authentication against the provided `introspection-uri`. [[nsa-http-basic]] -=== +== Adds a `BasicAuthenticationFilter` and `BasicAuthenticationEntryPoint` to the configuration. The latter will only be used as the configuration entry point if form-based login is not enabled. [[nsa-http-basic-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1247,7 +1239,7 @@ The latter will only be used as the configuration entry point if form-based logi [[nsa-http-basic-attributes]] -==== Attributes +=== Attributes [[nsa-http-basic-authentication-details-source-ref]] @@ -1261,13 +1253,13 @@ Sets the `AuthenticationEntryPoint` which is used by the `BasicAuthenticationFil [[nsa-http-firewall]] -=== Element +== Element This is a top-level element which can be used to inject a custom implementation of `HttpFirewall` into the `FilterChainProxy` created by the namespace. The default implementation should be suitable for most applications. [[nsa-http-firewall-attributes]] -==== Attributes +=== Attributes [[nsa-http-firewall-ref]] @@ -1276,7 +1268,7 @@ Defines a reference to a Spring bean that implements `HttpFirewall`. [[nsa-intercept-url]] -=== +== This element is used to define the set of URL patterns that the application is interested in and to configure how they should be handled. It is used to construct the `FilterInvocationSecurityMetadataSource` used by the `FilterSecurityInterceptor`. It is also responsible for configuring a `ChannelProcessingFilter` if particular URLs need to be accessed by HTTPS, for example. @@ -1285,7 +1277,7 @@ So the most specific patterns should come first and the most general should come [[nsa-intercept-url-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1294,7 +1286,7 @@ So the most specific patterns should come first and the most general should come [[nsa-intercept-url-attributes]] -==== Attributes +=== Attributes [[nsa-intercept-url-access]] @@ -1341,12 +1333,12 @@ NOTE: This property is invalid for < +== Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. [[nsa-jee-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1354,7 +1346,7 @@ Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integ [[nsa-jee-attributes]] -==== Attributes +=== Attributes [[nsa-jee-mappable-roles]] @@ -1368,13 +1360,13 @@ A reference to a user-service (or UserDetailsService bean) Id [[nsa-logout]] -=== +== Adds a `LogoutFilter` to the filter stack. This is configured with a `SecurityContextLogoutHandler`. [[nsa-logout-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1382,7 +1374,7 @@ This is configured with a `SecurityContextLogoutHandler`. [[nsa-logout-attributes]] -==== Attributes +=== Attributes [[nsa-logout-delete-cookies]] @@ -1419,7 +1411,7 @@ May be used to supply an instance of `LogoutSuccessHandler` which will be invoke [[nsa-openid-login]] -=== +== Similar to `` and has the same attributes. The default value for `login-processing-url` is "/login/openid". An `OpenIDAuthenticationFilter` and `OpenIDAuthenticationProvider` will be registered. @@ -1428,7 +1420,7 @@ Again, this can be specified by `id`, using the `user-service-ref` attribute, or [[nsa-openid-login-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1436,7 +1428,7 @@ Again, this can be specified by `id`, using the `user-service-ref` attribute, or [[nsa-openid-login-attributes]] -==== Attributes +=== Attributes [[nsa-openid-login-always-use-default-target]] @@ -1514,13 +1506,13 @@ Defaults to "username". [[nsa-openid-login-children]] -==== Child Elements of +=== Child Elements of * <> [[nsa-attribute-exchange]] -=== +== The `attribute-exchange` element defines the list of attributes which should be requested from the identity provider. An example can be found in the xref:servlet/authentication/openid.adoc#servlet-openid[OpenID Support] section of the namespace configuration chapter. More than one can be used, in which case each must have an `identifier-match` attribute, containing a regular expression which is matched against the supplied OpenID identifier. @@ -1528,7 +1520,7 @@ This allows different attribute lists to be fetched from different providers (Go [[nsa-attribute-exchange-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1536,7 +1528,7 @@ This allows different attribute lists to be fetched from different providers (Go [[nsa-attribute-exchange-attributes]] -==== Attributes +=== Attributes [[nsa-attribute-exchange-identifier-match]] @@ -1545,7 +1537,7 @@ A regular expression which will be compared against the claimed identity, when d [[nsa-attribute-exchange-children]] -==== Child Elements of +=== Child Elements of * <> @@ -1553,12 +1545,12 @@ A regular expression which will be compared against the claimed identity, when d [[nsa-openid-attribute]] -=== +== Attributes used when making an OpenID AX https://openid.net/specs/openid-attribute-exchange-1_0.html#fetch_request[ Fetch Request] [[nsa-openid-attribute-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1566,7 +1558,7 @@ Attributes used when making an OpenID AX https://openid.net/specs/openid-attribu [[nsa-openid-attribute-attributes]] -==== Attributes +=== Attributes [[nsa-openid-attribute-count]] @@ -1595,23 +1587,23 @@ For example, https://axschema.org/contact/email. See your OP's documentation for valid attribute types. [[nsa-password-management]] -=== +== This element configures password management. [[nsa-password-management-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-password-management-attributes]] -==== Attributes +=== Attributes [[nsa-password-management-change-password-page]] * **change-password-page** The change password page. Defaults to "/change-password". [[nsa-port-mappings]] -=== +== By default, an instance of `PortMapperImpl` will be added to the configuration for use in redirecting to secure and insecure URLs. This element can optionally be used to override the default mappings which that class defines. Each child `` element defines a pair of HTTP:HTTPS ports. @@ -1620,7 +1612,7 @@ An example of overriding these can be found in xref:servlet/exploits/http.adoc#s [[nsa-port-mappings-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1628,7 +1620,7 @@ An example of overriding these can be found in xref:servlet/exploits/http.adoc#s [[nsa-port-mappings-children]] -==== Child Elements of +=== Child Elements of * <> @@ -1636,12 +1628,12 @@ An example of overriding these can be found in xref:servlet/exploits/http.adoc#s [[nsa-port-mapping]] -=== +== Provides a method to map http ports to https ports when forcing a redirect. [[nsa-port-mapping-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1649,7 +1641,7 @@ Provides a method to map http ports to https ports when forcing a redirect. [[nsa-port-mapping-attributes]] -==== Attributes +=== Attributes [[nsa-port-mapping-http]] @@ -1663,13 +1655,13 @@ The https port to use. [[nsa-remember-me]] -=== +== Adds the `RememberMeAuthenticationFilter` to the stack. This in turn will be configured with either a `TokenBasedRememberMeServices`, a `PersistentTokenBasedRememberMeServices` or a user-specified bean implementing `RememberMeServices` depending on the attribute settings. [[nsa-remember-me-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1677,7 +1669,7 @@ This in turn will be configured with either a `TokenBasedRememberMeServices`, a [[nsa-remember-me-attributes]] -==== Attributes +=== Attributes [[nsa-remember-me-authentication-success-handler-ref]] @@ -1757,17 +1749,17 @@ If there are multiple instances, you can specify a bean `id` explicitly using th [[nsa-request-cache]] -=== Element +== Element Sets the `RequestCache` instance which will be used by the `ExceptionTranslationFilter` to store request information before invoking an `AuthenticationEntryPoint`. [[nsa-request-cache-parents]] -==== Parent Elements of +=== Parent Elements of * <> [[nsa-request-cache-attributes]] -==== Attributes +=== Attributes [[nsa-request-cache-ref]] @@ -1776,12 +1768,12 @@ Defines a reference to a Spring bean that is a `RequestCache`. [[nsa-session-management]] -=== +== Session-management related functionality is implemented by the addition of a `SessionManagementFilter` to the filter stack. [[nsa-session-management-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1789,7 +1781,7 @@ Session-management related functionality is implemented by the addition of a `Se [[nsa-session-management-attributes]] -==== Attributes +=== Attributes [[nsa-session-management-invalid-session-url]] @@ -1831,7 +1823,7 @@ See the Javadoc for this class for more details. [[nsa-session-management-children]] -==== Child Elements of +=== Child Elements of * <> @@ -1839,7 +1831,7 @@ See the Javadoc for this class for more details. [[nsa-concurrency-control]] -=== +== Adds support for concurrent session control, allowing limits to be placed on the number of active sessions a user can have. A `ConcurrentSessionFilter` will be created, and a `ConcurrentSessionControlAuthenticationStrategy` will be used with the `SessionManagementFilter`. If a `form-login` element has been declared, the strategy object will also be injected into the created authentication filter. @@ -1847,7 +1839,7 @@ An instance of `SessionRegistry` (a `SessionRegistryImpl` instance unless the us [[nsa-concurrency-control-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1855,7 +1847,7 @@ An instance of `SessionRegistry` (a `SessionRegistryImpl` instance unless the us [[nsa-concurrency-control-attributes]] -==== Attributes +=== Attributes [[nsa-concurrency-control-error-if-maximum-exceeded]] @@ -1893,7 +1885,7 @@ The other concurrent session control beans will be wired up to use it. [[nsa-x509]] -=== +== Adds support for X.509 authentication. An `X509AuthenticationFilter` will be added to the stack and an `Http403ForbiddenEntryPoint` bean will be created. The latter will only be used if no other authentication mechanisms are in use (its only functionality is to return an HTTP 403 error code). @@ -1901,7 +1893,7 @@ A `PreAuthenticatedAuthenticationProvider` will also be created which delegates [[nsa-x509-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1909,7 +1901,7 @@ A `PreAuthenticatedAuthenticationProvider` will also be created which delegates [[nsa-x509-attributes]] -==== Attributes +=== Attributes [[nsa-x509-authentication-details-source-ref]] @@ -1929,12 +1921,12 @@ If not set, an attempt will be made to locate a suitable instance automatically [[nsa-filter-chain-map]] -=== +== Used to explicitly configure a FilterChainProxy instance with a FilterChainMap [[nsa-filter-chain-map-attributes]] -==== Attributes +=== Attributes [[nsa-filter-chain-map-request-matcher]] @@ -1944,7 +1936,7 @@ Currently the options are 'ant' (for ant path patterns), 'regex' for regular exp [[nsa-filter-chain-map-children]] -==== Child Elements of +=== Child Elements of * <> @@ -1952,13 +1944,13 @@ Currently the options are 'ant' (for ant path patterns), 'regex' for regular exp [[nsa-filter-chain]] -=== +== Used within to define a specific URL pattern and the list of filters which apply to the URLs matching that pattern. When multiple filter-chain elements are assembled in a list in order to configure a FilterChainProxy, the most specific patterns must be placed at the top of the list, with most general ones at the bottom. [[nsa-filter-chain-parents]] -==== Parent Elements of +=== Parent Elements of * <> @@ -1966,7 +1958,7 @@ When multiple filter-chain elements are assembled in a list in order to configur [[nsa-filter-chain-attributes]] -==== Attributes +=== Attributes [[nsa-filter-chain-filters]] @@ -1986,7 +1978,7 @@ A reference to a `RequestMatcher` that will be used to determine if any `Filter` [[nsa-filter-security-metadata-source]] -=== +== Used to explicitly configure a FilterSecurityMetadataSource bean for use with a FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy explicitly, rather than using the element. The intercept-url elements used should only contain pattern, method and access attributes. @@ -1994,7 +1986,7 @@ Any others will result in a configuration error. [[nsa-filter-security-metadata-source-attributes]] -==== Attributes +=== Attributes [[nsa-filter-security-metadata-source-id]] @@ -2017,1011 +2009,7 @@ If the expression evaluates to 'true', access will be granted. [[nsa-filter-security-metadata-source-children]] -==== Child Elements of +=== Child Elements of * <> - -[[nsa-websocket-security]] -== WebSocket Security - -Spring Security 4.0+ provides support for authorizing messages. -One concrete example of where this is useful is to provide authorization in WebSocket based applications. - -[[nsa-websocket-message-broker]] -=== - -The websocket-message-broker element has two different modes. -If the <> is not specified, then it will do the following things: - -* Ensure that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver. -This allows the use of `@AuthenticationPrincipal` to resolve the principal of the current `Authentication` -* Ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel. -This populates the SecurityContextHolder with the user that is found in the Message -* Ensures that a ChannelSecurityInterceptor is registered with the clientInboundChannel. -This allows authorization rules to be specified for a message. -* Ensures that a CsrfChannelInterceptor is registered with the clientInboundChannel. -This ensures that only requests from the original domain are enabled. -* Ensures that a CsrfTokenHandshakeInterceptor is registered with WebSocketHttpRequestHandler, TransportHandlingSockJsService, or DefaultSockJsService. -This ensures that the expected CsrfToken from the HttpServletRequest is copied into the WebSocket Session attributes. - -If additional control is necessary, the id can be specified and a ChannelSecurityInterceptor will be assigned to the specified id. -All the wiring with Spring's messaging infrastructure can then be done manually. -This is more cumbersome, but provides greater control over the configuration. - - -[[nsa-websocket-message-broker-attributes]] -==== Attributes - -[[nsa-websocket-message-broker-id]] -* **id** A bean identifier, used for referring to the ChannelSecurityInterceptor bean elsewhere in the context. -If specified, Spring Security requires explicit configuration within Spring Messaging. -If not specified, Spring Security will automatically integrate with the messaging infrastructure as described in <> - -[[nsa-websocket-message-broker-same-origin-disabled]] -* **same-origin-disabled** Disables the requirement for CSRF token to be present in the Stomp headers (default false). -Changing the default is useful if it is necessary to allow other origins to make SockJS connections. - -[[nsa-websocket-message-broker-children]] -==== Child Elements of - - -* <> -* <> - -[[nsa-intercept-message]] -=== - -Defines an authorization rule for a message. - - -[[nsa-intercept-message-parents]] -==== Parent Elements of - - -* <> - - -[[nsa-intercept-message-attributes]] -==== Attributes - -[[nsa-intercept-message-pattern]] -* **pattern** An ant based pattern that matches on the Message destination. -For example, "/**" matches any Message with a destination; "/admin/**" matches any Message that has a destination that starts with "/admin/**". - -[[nsa-intercept-message-type]] -* **type** The type of message to match on. -Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). - -[[nsa-intercept-message-access]] -* **access** The expression used to secure the Message. -For example, "denyAll" will deny access to all of the matching Messages; "permitAll" will grant access to all of the matching Messages; "hasRole('ADMIN') requires the current user to have the role 'ROLE_ADMIN' for the matching Messages. - -[[nsa-authentication]] -== Authentication Services -Before Spring Security 3.0, an `AuthenticationManager` was automatically registered internally. -Now you must register one explicitly using the `` element. -This creates an instance of Spring Security's `ProviderManager` class, which needs to be configured with a list of one or more `AuthenticationProvider` instances. -These can either be created using syntax elements provided by the namespace, or they can be standard bean definitions, marked for addition to the list using the `authentication-provider` element. - - -[[nsa-authentication-manager]] -=== -Every Spring Security application which uses the namespace must have include this element somewhere. -It is responsible for registering the `AuthenticationManager` which provides authentication services to the application. -All elements which create `AuthenticationProvider` instances should be children of this element. - - -[[nsa-authentication-manager-attributes]] -==== Attributes - - -[[nsa-authentication-manager-alias]] -* **alias** -This attribute allows you to define an alias name for the internal instance for use in your own configuration. - - -[[nsa-authentication-manager-erase-credentials]] -* **erase-credentials** -If set to true, the AuthenticationManager will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. -Literally it maps to the `eraseCredentialsAfterAuthentication` property of the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`ProviderManager`]. - - -[[nsa-authentication-manager-id]] -* **id** -This attribute allows you to define an id for the internal instance for use in your own configuration. -It is the same as the alias element, but provides a more consistent experience with elements that use the id attribute. - - -[[nsa-authentication-manager-children]] -==== Child Elements of - - -* <> -* <> - - - -[[nsa-authentication-provider]] -=== -Unless used with a `ref` attribute, this element is shorthand for configuring a `DaoAuthenticationProvider`. -`DaoAuthenticationProvider` loads user information from a `UserDetailsService` and compares the username/password combination with the values supplied at login. -The `UserDetailsService` instance can be defined either by using an available namespace element (`jdbc-user-service` or by using the `user-service-ref` attribute to point to a bean defined elsewhere in the application context). - - - -[[nsa-authentication-provider-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-authentication-provider-attributes]] -==== Attributes - - -[[nsa-authentication-provider-ref]] -* **ref** -Defines a reference to a Spring bean that implements `AuthenticationProvider`. - -If you have written your own `AuthenticationProvider` implementation (or want to configure one of Spring Security's own implementations as a traditional bean for some reason, then you can use the following syntax to add it to the internal list of `ProviderManager`: - -[source,xml] ----- - - - - - - ----- - - - - -[[nsa-authentication-provider-user-service-ref]] -* **user-service-ref** -A reference to a bean that implements UserDetailsService that may be created using the standard bean element or the custom user-service element. - - -[[nsa-authentication-provider-children]] -==== Child Elements of - - -* <> -* <> -* <> -* <> - - - -[[nsa-jdbc-user-service]] -=== -Causes creation of a JDBC-based UserDetailsService. - - -[[nsa-jdbc-user-service-attributes]] -==== Attributes - - -[[nsa-jdbc-user-service-authorities-by-username-query]] -* **authorities-by-username-query** -An SQL statement to query for a user's granted authorities given a username. - -The default is - -[source] ----- -select username, authority from authorities where username = ? ----- - - - - -[[nsa-jdbc-user-service-cache-ref]] -* **cache-ref** -Defines a reference to a cache for use with a UserDetailsService. - - -[[nsa-jdbc-user-service-data-source-ref]] -* **data-source-ref** -The bean ID of the DataSource which provides the required tables. - - -[[nsa-jdbc-user-service-group-authorities-by-username-query]] -* **group-authorities-by-username-query** -An SQL statement to query user's group authorities given a username. -The default is - -+ - -[source] ----- -select -g.id, g.group_name, ga.authority -from -groups g, group_members gm, group_authorities ga -where -gm.username = ? and g.id = ga.group_id and g.id = gm.group_id ----- - - - - -[[nsa-jdbc-user-service-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-jdbc-user-service-role-prefix]] -* **role-prefix** -A non-empty string prefix that will be added to role strings loaded from persistent storage (default is "ROLE_"). -Use the value "none" for no prefix in cases where the default is non-empty. - - -[[nsa-jdbc-user-service-users-by-username-query]] -* **users-by-username-query** -An SQL statement to query a username, password, and enabled status given a username. -The default is - -+ - -[source] ----- -select username, password, enabled from users where username = ? ----- - - - - -[[nsa-password-encoder]] -=== -Authentication providers can optionally be configured to use a password encoder as described in the xref:features/authentication/password-storage.adoc#authentication-password-storage[Password Storage]. -This will result in the bean being injected with the appropriate `PasswordEncoder` instance. - - -[[nsa-password-encoder-parents]] -==== Parent Elements of - - -* <> -* <> - - - -[[nsa-password-encoder-attributes]] -==== Attributes - - -[[nsa-password-encoder-hash]] -* **hash** -Defines the hashing algorithm used on user passwords. -We recommend strongly against using MD4, as it is a very weak hashing algorithm. - - -[[nsa-password-encoder-ref]] -* **ref** -Defines a reference to a Spring bean that implements `PasswordEncoder`. - - -[[nsa-user-service]] -=== -Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. -Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. - - -[[nsa-user-service-attributes]] -==== Attributes - - -[[nsa-user-service-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-user-service-properties]] -* **properties** -The location of a Properties file where each line is in the format of - -+ - -[source] ----- -username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] ----- - - - - -[[nsa-user-service-children]] -==== Child Elements of - - -* <> - - - -[[nsa-user]] -=== -Represents a user in the application. - - -[[nsa-user-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-user-attributes]] -==== Attributes - - -[[nsa-user-authorities]] -* **authorities** -One of more authorities granted to the user. -Separate authorities with a comma (but no space). -For example, "ROLE_USER,ROLE_ADMINISTRATOR" - - -[[nsa-user-disabled]] -* **disabled** -Can be set to "true" to mark an account as disabled and unusable. - - -[[nsa-user-locked]] -* **locked** -Can be set to "true" to mark an account as locked and unusable. - - -[[nsa-user-name]] -* **name** -The username assigned to the user. - - -[[nsa-user-password]] -* **password** -The password assigned to the user. -This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). -This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. -If omitted, the namespace will generate a random value, preventing its accidental use for authentication. -Cannot be empty. - - - -== Method Security - -[[nsa-method-security]] -=== -This element is the primary means of adding support for securing methods on Spring Security beans. -Methods can be secured by the use of annotations (defined at the interface or class level) or by defining a set of pointcuts. - -[[nsa-method-security-attributes]] -==== attributes - -[[nsa-method-security-pre-post-enabled]] -* **pre-post-enabled** -Enables Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) for this application context. -Defaults to "true". - -[[nsa-method-security-secured-enabled]] -* **secured-enabled** -Enables Spring Security's @Secured annotation for this application context. -Defaults to "false". - -[[nsa-method-security-jsr250-enabled]] -* **jsr250-enabled** -Enables JSR-250 authorization annotations (@RolesAllowed, @PermitAll, @DenyAll) for this application context. -Defaults to "false". - -[[nsa-method-security-proxy-target-class]] -* **proxy-target-class** -If true, class based proxying will be used instead of interface based proxying. -Defaults to "false". - -[[nsa-method-security-children]] -==== Child Elements of - -* <> - -[[nsa-global-method-security]] -=== -This element is the primary means of adding support for securing methods on Spring Security beans. -Methods can be secured by the use of annotations (defined at the interface or class level) or by defining a set of pointcuts as child elements, using AspectJ syntax. - - -[[nsa-global-method-security-attributes]] -==== Attributes - - -[[nsa-global-method-security-access-decision-manager-ref]] -* **access-decision-manager-ref** -Method security uses the same `AccessDecisionManager` configuration as web security, but this can be overridden using this attribute. -By default an AffirmativeBased implementation is used for with a RoleVoter and an AuthenticatedVoter. - - -[[nsa-global-method-security-authentication-manager-ref]] -* **authentication-manager-ref** -A reference to an `AuthenticationManager` that should be used for method security. - - -[[nsa-global-method-security-jsr250-annotations]] -* **jsr250-annotations** -Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). -This will require the javax.annotation.security classes on the classpath. -Setting this to true also adds a `Jsr250Voter` to the `AccessDecisionManager`, so you need to make sure you do this if you are using a custom implementation and want to use these annotations. - - -[[nsa-global-method-security-metadata-source-ref]] -* **metadata-source-ref** -An external `MethodSecurityMetadataSource` instance can be supplied which will take priority over other sources (such as the default annotations). - - -[[nsa-global-method-security-mode]] -* **mode** -This attribute can be set to "aspectj" to specify that AspectJ should be used instead of the default Spring AOP. -Secured methods must be woven with the `AnnotationSecurityAspect` from the `spring-security-aspects` module. - -It is important to note that AspectJ follows Java's rule that annotations on interfaces are not inherited. -This means that methods that define the Security annotations on the interface will not be secured. -Instead, you must place the Security annotation on the class when using AspectJ. - - -[[nsa-global-method-security-order]] -* **order** -Allows the advice "order" to be set for the method security interceptor. - - -[[nsa-global-method-security-pre-post-annotations]] -* **pre-post-annotations** -Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. -Defaults to "disabled". - - -[[nsa-global-method-security-proxy-target-class]] -* **proxy-target-class** -If true, class based proxying will be used instead of interface based proxying. - - -[[nsa-global-method-security-run-as-manager-ref]] -* **run-as-manager-ref** -A reference to an optional `RunAsManager` implementation which will be used by the configured `MethodSecurityInterceptor` - - -[[nsa-global-method-security-secured-annotations]] -* **secured-annotations** -Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. -Defaults to "disabled". - - -[[nsa-global-method-security-children]] -==== Child Elements of - - -* <> -* <> -* <> -* <> - - - -[[nsa-after-invocation-provider]] -=== -This element can be used to decorate an `AfterInvocationProvider` for use by the security interceptor maintained by the `` namespace. -You can define zero or more of these within the `global-method-security` element, each with a `ref` attribute pointing to an `AfterInvocationProvider` bean instance within your application context. - - -[[nsa-after-invocation-provider-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-after-invocation-provider-attributes]] -==== Attributes - - -[[nsa-after-invocation-provider-ref]] -* **ref** -Defines a reference to a Spring bean that implements `AfterInvocationProvider`. - - -[[nsa-pre-post-annotation-handling]] -=== -Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replaced entirely. -Only applies if these annotations are enabled. - - -[[nsa-pre-post-annotation-handling-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-pre-post-annotation-handling-children]] -==== Child Elements of - - -* <> -* <> -* <> - - - -[[nsa-invocation-attribute-factory]] -=== -Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. - - -[[nsa-invocation-attribute-factory-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-invocation-attribute-factory-attributes]] -==== Attributes - - -[[nsa-invocation-attribute-factory-ref]] -* **ref** -Defines a reference to a Spring bean Id. - - -[[nsa-post-invocation-advice]] -=== -Customizes the `PostInvocationAdviceProvider` with the ref as the `PostInvocationAuthorizationAdvice` for the element. - - -[[nsa-post-invocation-advice-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-post-invocation-advice-attributes]] -==== Attributes - - -[[nsa-post-invocation-advice-ref]] -* **ref** -Defines a reference to a Spring bean Id. - - -[[nsa-pre-invocation-advice]] -=== -Customizes the `PreInvocationAuthorizationAdviceVoter` with the ref as the `PreInvocationAuthorizationAdviceVoter` for the element. - - -[[nsa-pre-invocation-advice-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-pre-invocation-advice-attributes]] -==== Attributes - - -[[nsa-pre-invocation-advice-ref]] -* **ref** -Defines a reference to a Spring bean Id. - - -[[nsa-protect-pointcut]] -=== Securing Methods using -`` -Rather than defining security attributes on an individual method or class basis using the `@Secured` annotation, you can define cross-cutting security constraints across whole sets of methods and interfaces in your service layer using the `` element. -You can find an example in the xref:servlet/authorization/method-security.adoc#ns-protect-pointcut[namespace introduction]. - - -[[nsa-protect-pointcut-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-protect-pointcut-attributes]] -==== Attributes - - -[[nsa-protect-pointcut-access]] -* **access** -Access configuration attributes list that applies to all methods matching the pointcut, e.g. -"ROLE_A,ROLE_B" - - -[[nsa-protect-pointcut-expression]] -* **expression** -An AspectJ expression, including the `execution` keyword. -For example, `execution(int com.foo.TargetObject.countLength(String))`. - - -[[nsa-intercept-methods]] -=== -Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods - - -[[nsa-intercept-methods-attributes]] -==== Attributes - - -[[nsa-intercept-methods-access-decision-manager-ref]] -* **access-decision-manager-ref** -Optional AccessDecisionManager bean ID to be used by the created method security interceptor. - - -[[nsa-intercept-methods-children]] -==== Child Elements of - - -* <> - - - -[[nsa-method-security-metadata-source]] -=== -Creates a MethodSecurityMetadataSource instance - - -[[nsa-method-security-metadata-source-attributes]] -==== Attributes - - -[[nsa-method-security-metadata-source-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-method-security-metadata-source-use-expressions]] -* **use-expressions** -Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. -Defaults to 'false'. -If enabled, each attribute should contain a single Boolean expression. -If the expression evaluates to 'true', access will be granted. - - -[[nsa-method-security-metadata-source-children]] -==== Child Elements of - - -* <> - - - -[[nsa-protect]] -=== -Defines a protected method and the access control configuration attributes that apply to it. -We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". - - -[[nsa-protect-parents]] -==== Parent Elements of - - -* <> -* <> - - - -[[nsa-protect-attributes]] -==== Attributes - - -[[nsa-protect-access]] -* **access** -Access configuration attributes list that applies to the method, e.g. -"ROLE_A,ROLE_B". - - -[[nsa-protect-method]] -* **method** -A method name - - -[[nsa-ldap]] -== LDAP Namespace Options -LDAP is covered in some details in xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap[its own chapter]. -We will expand on that here with some explanation of how the namespace options map to Spring beans. -The LDAP implementation uses Spring LDAP extensively, so some familiarity with that project's API may be useful. - - -[[nsa-ldap-server]] -=== Defining the LDAP Server using the -`` Element -This element sets up a Spring LDAP `ContextSource` for use by the other LDAP beans, defining the location of the LDAP server and other information (such as a username and password, if it doesn't allow anonymous access) for connecting to it. -It can also be used to create an embedded server for testing. -Details of the syntax for both options are covered in the xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap[LDAP chapter]. -The actual `ContextSource` implementation is `DefaultSpringSecurityContextSource` which extends Spring LDAP's `LdapContextSource` class. -The `manager-dn` and `manager-password` attributes map to the latter's `userDn` and `password` properties respectively. - -If you only have one server defined in your application context, the other LDAP namespace-defined beans will use it automatically. -Otherwise, you can give the element an "id" attribute and refer to it from other namespace beans using the `server-ref` attribute. -This is actually the bean `id` of the `ContextSource` instance, if you want to use it in other traditional Spring beans. - - -[[nsa-ldap-server-attributes]] -==== Attributes - -[[nsa-ldap-server-mode]] -* **mode** -Explicitly specifies which embedded ldap server should use. Values are `apacheds` and `unboundid`. By default, it will depends if the library is available in the classpath. - -[[nsa-ldap-server-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-ldap-server-ldif]] -* **ldif** -Explicitly specifies an ldif file resource to load into an embedded LDAP server. -The ldif should be a Spring resource pattern (i.e. classpath:init.ldif). -The default is classpath*:*.ldif - - -[[nsa-ldap-server-manager-dn]] -* **manager-dn** -Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. -If omitted, anonymous access will be used. - - -[[nsa-ldap-server-manager-password]] -* **manager-password** -The password for the manager DN. -This is required if the manager-dn is specified. - - -[[nsa-ldap-server-port]] -* **port** -Specifies an IP port number. -Used to configure an embedded LDAP server, for example. -The default value is 33389. - - -[[nsa-ldap-server-root]] -* **root** -Optional root suffix for the embedded LDAP server. -Default is "dc=springframework,dc=org" - - -[[nsa-ldap-server-url]] -* **url** -Specifies the ldap server URL when not using the embedded LDAP server. - - -[[nsa-ldap-authentication-provider]] -=== -This element is shorthand for the creation of an `LdapAuthenticationProvider` instance. -By default this will be configured with a `BindAuthenticator` instance and a `DefaultAuthoritiesPopulator`. -As with all namespace authentication providers, it must be included as a child of the `authentication-provider` element. - - -[[nsa-ldap-authentication-provider-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-ldap-authentication-provider-attributes]] -==== Attributes - - -[[nsa-ldap-authentication-provider-group-role-attribute]] -* **group-role-attribute** -The LDAP attribute name which contains the role name which will be used within Spring Security. -Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupRoleAttribute` property. -Defaults to "cn". - - -[[nsa-ldap-authentication-provider-group-search-base]] -* **group-search-base** -Search base for group membership searches. -Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupSearchBase` constructor argument. -Defaults to "" (searching from the root). - - -[[nsa-ldap-authentication-provider-group-search-filter]] -* **group-search-filter** -Group search filter. -Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupSearchFilter` property. -Defaults to `+(uniqueMember={0})+`. -The substituted parameter is the DN of the user. - - -[[nsa-ldap-authentication-provider-role-prefix]] -* **role-prefix** -A non-empty string prefix that will be added to role strings loaded from persistent. -Maps to the ``DefaultLdapAuthoritiesPopulator``'s `rolePrefix` property. -Defaults to "ROLE_". -Use the value "none" for no prefix in cases where the default is non-empty. - - -[[nsa-ldap-authentication-provider-server-ref]] -* **server-ref** -The optional server to use. -If omitted, and a default LDAP server is registered (using with no Id), that server will be used. - - -[[nsa-ldap-authentication-provider-user-context-mapper-ref]] -* **user-context-mapper-ref** -Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry - - -[[nsa-ldap-authentication-provider-user-details-class]] -* **user-details-class** -Allows the objectClass of the user entry to be specified. -If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object - - -[[nsa-ldap-authentication-provider-user-dn-pattern]] -* **user-dn-pattern** -If your users are at a fixed location in the directory (i.e. you can work out the DN directly from the username without doing a directory search), you can use this attribute to map directly to the DN. -It maps directly to the `userDnPatterns` property of `AbstractLdapAuthenticator`. -The value is a specific pattern used to build the user's DN, for example `+uid={0},ou=people+`. -The key `+{0}+` must be present and will be substituted with the username. - - -[[nsa-ldap-authentication-provider-user-search-base]] -* **user-search-base** -Search base for user searches. -Defaults to "". -Only used with a 'user-search-filter'. - -+ - -If you need to perform a search to locate the user in the directory, then you can set these attributes to control the search. -The `BindAuthenticator` will be configured with a `FilterBasedLdapUserSearch` and the attribute values map directly to the first two arguments of that bean's constructor. -If these attributes aren't set and no `user-dn-pattern` has been supplied as an alternative, then the default search values of `+user-search-filter="(uid={0})"+` and `user-search-base=""` will be used. - - -[[nsa-ldap-authentication-provider-user-search-filter]] -* **user-search-filter** -The LDAP filter used to search for users (optional). -For example `+(uid={0})+`. -The substituted parameter is the user's login name. - -+ - -If you need to perform a search to locate the user in the directory, then you can set these attributes to control the search. -The `BindAuthenticator` will be configured with a `FilterBasedLdapUserSearch` and the attribute values map directly to the first two arguments of that bean's constructor. -If these attributes aren't set and no `user-dn-pattern` has been supplied as an alternative, then the default search values of `+user-search-filter="(uid={0})"+` and `user-search-base=""` will be used. - - -[[nsa-ldap-authentication-provider-children]] -==== Child Elements of - - -* <> - - - -[[nsa-password-compare]] -=== -This is used as child element to `` and switches the authentication strategy from `BindAuthenticator` to `PasswordComparisonAuthenticator`. - - -[[nsa-password-compare-parents]] -==== Parent Elements of - - -* <> - - - -[[nsa-password-compare-attributes]] -==== Attributes - - -[[nsa-password-compare-hash]] -* **hash** -Defines the hashing algorithm used on user passwords. -We recommend strongly against using MD4, as it is a very weak hashing algorithm. - - -[[nsa-password-compare-password-attribute]] -* **password-attribute** -The attribute in the directory which contains the user password. -Defaults to "userPassword". - - -[[nsa-password-compare-children]] -==== Child Elements of - - -* <> - - - -[[nsa-ldap-user-service]] -=== -This element configures an LDAP `UserDetailsService`. -The class used is `LdapUserDetailsService` which is a combination of a `FilterBasedLdapUserSearch` and a `DefaultLdapAuthoritiesPopulator`. -The attributes it supports have the same usage as in ``. - - -[[nsa-ldap-user-service-attributes]] -==== Attributes - - -[[nsa-ldap-user-service-cache-ref]] -* **cache-ref** -Defines a reference to a cache for use with a UserDetailsService. - - -[[nsa-ldap-user-service-group-role-attribute]] -* **group-role-attribute** -The LDAP attribute name which contains the role name which will be used within Spring Security. -Defaults to "cn". - - -[[nsa-ldap-user-service-group-search-base]] -* **group-search-base** -Search base for group membership searches. -Defaults to "" (searching from the root). - - -[[nsa-ldap-user-service-group-search-filter]] -* **group-search-filter** -Group search filter. -Defaults to `+(uniqueMember={0})+`. -The substituted parameter is the DN of the user. - - -[[nsa-ldap-user-service-id]] -* **id** -A bean identifier, used for referring to the bean elsewhere in the context. - - -[[nsa-ldap-user-service-role-prefix]] -* **role-prefix** -A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. -"ROLE_"). -Use the value "none" for no prefix in cases where the default is non-empty. - - -[[nsa-ldap-user-service-server-ref]] -* **server-ref** -The optional server to use. -If omitted, and a default LDAP server is registered (using with no Id), that server will be used. - - -[[nsa-ldap-user-service-user-context-mapper-ref]] -* **user-context-mapper-ref** -Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry - - -[[nsa-ldap-user-service-user-details-class]] -* **user-details-class** -Allows the objectClass of the user entry to be specified. -If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object - - -[[nsa-ldap-user-service-user-search-base]] -* **user-search-base** -Search base for user searches. -Defaults to "". -Only used with a 'user-search-filter'. - - -[[nsa-ldap-user-service-user-search-filter]] -* **user-search-filter** -The LDAP filter used to search for users (optional). -For example `+(uid={0})+`. -The substituted parameter is the user's login name. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc new file mode 100644 index 0000000000..4f36c2c2cb --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc @@ -0,0 +1,9 @@ +[[appendix-namespace]] += The Security Namespace +:page-section-summary-toc: 1 + +This appendix provides a reference to the elements available in the security namespace and information on the underlying beans they create (a knowledge of the individual classes and how they work together is assumed - you can find more information in the project Javadoc and elsewhere in this document). +If you haven't used the namespace before, please read the xref:servlet/configuration/xml-namespace.adoc#ns-config[introductory chapter] on namespace configuration, as this is intended as a supplement to the information there. +Using a good quality XML editor while editing a configuration based on the schema is recommended as this will provide contextual information on which elements and attributes are available as well as comments explaining their purpose. +The namespace is written in https://relaxng.org/[RELAX NG] Compact format and later converted into an XSD schema. +If you are familiar with this format, you may wish to examine the https://raw.githubusercontent.com/spring-projects/spring-security/main/config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc[schema file] directly. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/ldap.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/ldap.adoc new file mode 100644 index 0000000000..f3c07e6d76 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/ldap.adoc @@ -0,0 +1,291 @@ +[[nsa-ldap]] += LDAP Namespace Options +LDAP is covered in some details in xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap[its own chapter]. +We will expand on that here with some explanation of how the namespace options map to Spring beans. +The LDAP implementation uses Spring LDAP extensively, so some familiarity with that project's API may be useful. + + +[[nsa-ldap-server]] +== Defining the LDAP Server using the +`` Element +This element sets up a Spring LDAP `ContextSource` for use by the other LDAP beans, defining the location of the LDAP server and other information (such as a username and password, if it doesn't allow anonymous access) for connecting to it. +It can also be used to create an embedded server for testing. +Details of the syntax for both options are covered in the xref:servlet/authentication/passwords/ldap.adoc#servlet-authentication-ldap[LDAP chapter]. +The actual `ContextSource` implementation is `DefaultSpringSecurityContextSource` which extends Spring LDAP's `LdapContextSource` class. +The `manager-dn` and `manager-password` attributes map to the latter's `userDn` and `password` properties respectively. + +If you only have one server defined in your application context, the other LDAP namespace-defined beans will use it automatically. +Otherwise, you can give the element an "id" attribute and refer to it from other namespace beans using the `server-ref` attribute. +This is actually the bean `id` of the `ContextSource` instance, if you want to use it in other traditional Spring beans. + + +[[nsa-ldap-server-attributes]] +=== Attributes + +[[nsa-ldap-server-mode]] +* **mode** +Explicitly specifies which embedded ldap server should use. Values are `apacheds` and `unboundid`. By default, it will depends if the library is available in the classpath. + +[[nsa-ldap-server-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-ldap-server-ldif]] +* **ldif** +Explicitly specifies an ldif file resource to load into an embedded LDAP server. +The ldif should be a Spring resource pattern (i.e. classpath:init.ldif). +The default is classpath*:*.ldif + + +[[nsa-ldap-server-manager-dn]] +* **manager-dn** +Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. +If omitted, anonymous access will be used. + + +[[nsa-ldap-server-manager-password]] +* **manager-password** +The password for the manager DN. +This is required if the manager-dn is specified. + + +[[nsa-ldap-server-port]] +* **port** +Specifies an IP port number. +Used to configure an embedded LDAP server, for example. +The default value is 33389. + + +[[nsa-ldap-server-root]] +* **root** +Optional root suffix for the embedded LDAP server. +Default is "dc=springframework,dc=org" + + +[[nsa-ldap-server-url]] +* **url** +Specifies the ldap server URL when not using the embedded LDAP server. + + +[[nsa-ldap-authentication-provider]] +== +This element is shorthand for the creation of an `LdapAuthenticationProvider` instance. +By default this will be configured with a `BindAuthenticator` instance and a `DefaultAuthoritiesPopulator`. +As with all namespace authentication providers, it must be included as a child of the `authentication-provider` element. + + +[[nsa-ldap-authentication-provider-parents]] +=== Parent Elements of + + +* xref:servlet/appendix/namespace/authentication-manager.adoc#nsa-authentication-manager[authentication-manager] + + + +[[nsa-ldap-authentication-provider-attributes]] +=== Attributes + + +[[nsa-ldap-authentication-provider-group-role-attribute]] +* **group-role-attribute** +The LDAP attribute name which contains the role name which will be used within Spring Security. +Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupRoleAttribute` property. +Defaults to "cn". + + +[[nsa-ldap-authentication-provider-group-search-base]] +* **group-search-base** +Search base for group membership searches. +Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupSearchBase` constructor argument. +Defaults to "" (searching from the root). + + +[[nsa-ldap-authentication-provider-group-search-filter]] +* **group-search-filter** +Group search filter. +Maps to the ``DefaultLdapAuthoritiesPopulator``'s `groupSearchFilter` property. +Defaults to `+(uniqueMember={0})+`. +The substituted parameter is the DN of the user. + + +[[nsa-ldap-authentication-provider-role-prefix]] +* **role-prefix** +A non-empty string prefix that will be added to role strings loaded from persistent. +Maps to the ``DefaultLdapAuthoritiesPopulator``'s `rolePrefix` property. +Defaults to "ROLE_". +Use the value "none" for no prefix in cases where the default is non-empty. + + +[[nsa-ldap-authentication-provider-server-ref]] +* **server-ref** +The optional server to use. +If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + + +[[nsa-ldap-authentication-provider-user-context-mapper-ref]] +* **user-context-mapper-ref** +Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry + + +[[nsa-ldap-authentication-provider-user-details-class]] +* **user-details-class** +Allows the objectClass of the user entry to be specified. +If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object + + +[[nsa-ldap-authentication-provider-user-dn-pattern]] +* **user-dn-pattern** +If your users are at a fixed location in the directory (i.e. you can work out the DN directly from the username without doing a directory search), you can use this attribute to map directly to the DN. +It maps directly to the `userDnPatterns` property of `AbstractLdapAuthenticator`. +The value is a specific pattern used to build the user's DN, for example `+uid={0},ou=people+`. +The key `+{0}+` must be present and will be substituted with the username. + + +[[nsa-ldap-authentication-provider-user-search-base]] +* **user-search-base** +Search base for user searches. +Defaults to "". +Only used with a 'user-search-filter'. + ++ + +If you need to perform a search to locate the user in the directory, then you can set these attributes to control the search. +The `BindAuthenticator` will be configured with a `FilterBasedLdapUserSearch` and the attribute values map directly to the first two arguments of that bean's constructor. +If these attributes aren't set and no `user-dn-pattern` has been supplied as an alternative, then the default search values of `+user-search-filter="(uid={0})"+` and `user-search-base=""` will be used. + + +[[nsa-ldap-authentication-provider-user-search-filter]] +* **user-search-filter** +The LDAP filter used to search for users (optional). +For example `+(uid={0})+`. +The substituted parameter is the user's login name. + ++ + +If you need to perform a search to locate the user in the directory, then you can set these attributes to control the search. +The `BindAuthenticator` will be configured with a `FilterBasedLdapUserSearch` and the attribute values map directly to the first two arguments of that bean's constructor. +If these attributes aren't set and no `user-dn-pattern` has been supplied as an alternative, then the default search values of `+user-search-filter="(uid={0})"+` and `user-search-base=""` will be used. + + +[[nsa-ldap-authentication-provider-children]] +=== Child Elements of + + +* <> + + + +[[nsa-password-compare]] +== +This is used as child element to `` and switches the authentication strategy from `BindAuthenticator` to `PasswordComparisonAuthenticator`. + + +[[nsa-password-compare-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-password-compare-attributes]] +=== Attributes + + +[[nsa-password-compare-hash]] +* **hash** +Defines the hashing algorithm used on user passwords. +We recommend strongly against using MD4, as it is a very weak hashing algorithm. + + +[[nsa-password-compare-password-attribute]] +* **password-attribute** +The attribute in the directory which contains the user password. +Defaults to "userPassword". + + +[[nsa-password-compare-children]] +=== Child Elements of + + +* xref:servlet/appendix/namespace/authentication-manager.adoc#nsa-password-encoder[password-encoder] + + + +[[nsa-ldap-user-service]] +== +This element configures an LDAP `UserDetailsService`. +The class used is `LdapUserDetailsService` which is a combination of a `FilterBasedLdapUserSearch` and a `DefaultLdapAuthoritiesPopulator`. +The attributes it supports have the same usage as in ``. + + +[[nsa-ldap-user-service-attributes]] +=== Attributes + + +[[nsa-ldap-user-service-cache-ref]] +* **cache-ref** +Defines a reference to a cache for use with a UserDetailsService. + + +[[nsa-ldap-user-service-group-role-attribute]] +* **group-role-attribute** +The LDAP attribute name which contains the role name which will be used within Spring Security. +Defaults to "cn". + + +[[nsa-ldap-user-service-group-search-base]] +* **group-search-base** +Search base for group membership searches. +Defaults to "" (searching from the root). + + +[[nsa-ldap-user-service-group-search-filter]] +* **group-search-filter** +Group search filter. +Defaults to `+(uniqueMember={0})+`. +The substituted parameter is the DN of the user. + + +[[nsa-ldap-user-service-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-ldap-user-service-role-prefix]] +* **role-prefix** +A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. +"ROLE_"). +Use the value "none" for no prefix in cases where the default is non-empty. + + +[[nsa-ldap-user-service-server-ref]] +* **server-ref** +The optional server to use. +If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + + +[[nsa-ldap-user-service-user-context-mapper-ref]] +* **user-context-mapper-ref** +Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry + + +[[nsa-ldap-user-service-user-details-class]] +* **user-details-class** +Allows the objectClass of the user entry to be specified. +If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object + + +[[nsa-ldap-user-service-user-search-base]] +* **user-search-base** +Search base for user searches. +Defaults to "". +Only used with a 'user-search-filter'. + + +[[nsa-ldap-user-service-user-search-filter]] +* **user-search-filter** +The LDAP filter used to search for users (optional). +For example `+(uid={0})+`. +The substituted parameter is the user's login name. diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc new file mode 100644 index 0000000000..3d50d5507c --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc @@ -0,0 +1,340 @@ += Method Security + +[[nsa-method-security]] +== +This element is the primary means of adding support for securing methods on Spring Security beans. +Methods can be secured by the use of annotations (defined at the interface or class level) or by defining a set of pointcuts. + +[[nsa-method-security-attributes]] +=== attributes + +[[nsa-method-security-pre-post-enabled]] +* **pre-post-enabled** +Enables Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) for this application context. +Defaults to "true". + +[[nsa-method-security-secured-enabled]] +* **secured-enabled** +Enables Spring Security's @Secured annotation for this application context. +Defaults to "false". + +[[nsa-method-security-jsr250-enabled]] +* **jsr250-enabled** +Enables JSR-250 authorization annotations (@RolesAllowed, @PermitAll, @DenyAll) for this application context. +Defaults to "false". + +[[nsa-method-security-proxy-target-class]] +* **proxy-target-class** +If true, class based proxying will be used instead of interface based proxying. +Defaults to "false". + +[[nsa-method-security-children]] +=== Child Elements of + +* xref:servlet/appendix/namespace/http.adoc#nsa-expression-handler[expression-handler] + +[[nsa-global-method-security]] +== +This element is the primary means of adding support for securing methods on Spring Security beans. +Methods can be secured by the use of annotations (defined at the interface or class level) or by defining a set of pointcuts as child elements, using AspectJ syntax. + + +[[nsa-global-method-security-attributes]] +=== Attributes + + +[[nsa-global-method-security-access-decision-manager-ref]] +* **access-decision-manager-ref** +Method security uses the same `AccessDecisionManager` configuration as web security, but this can be overridden using this attribute. +By default an AffirmativeBased implementation is used for with a RoleVoter and an AuthenticatedVoter. + + +[[nsa-global-method-security-authentication-manager-ref]] +* **authentication-manager-ref** +A reference to an `AuthenticationManager` that should be used for method security. + + +[[nsa-global-method-security-jsr250-annotations]] +* **jsr250-annotations** +Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). +This will require the javax.annotation.security classes on the classpath. +Setting this to true also adds a `Jsr250Voter` to the `AccessDecisionManager`, so you need to make sure you do this if you are using a custom implementation and want to use these annotations. + + +[[nsa-global-method-security-metadata-source-ref]] +* **metadata-source-ref** +An external `MethodSecurityMetadataSource` instance can be supplied which will take priority over other sources (such as the default annotations). + + +[[nsa-global-method-security-mode]] +* **mode** +This attribute can be set to "aspectj" to specify that AspectJ should be used instead of the default Spring AOP. +Secured methods must be woven with the `AnnotationSecurityAspect` from the `spring-security-aspects` module. + +It is important to note that AspectJ follows Java's rule that annotations on interfaces are not inherited. +This means that methods that define the Security annotations on the interface will not be secured. +Instead, you must place the Security annotation on the class when using AspectJ. + + +[[nsa-global-method-security-order]] +* **order** +Allows the advice "order" to be set for the method security interceptor. + + +[[nsa-global-method-security-pre-post-annotations]] +* **pre-post-annotations** +Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. +Defaults to "disabled". + + +[[nsa-global-method-security-proxy-target-class]] +* **proxy-target-class** +If true, class based proxying will be used instead of interface based proxying. + + +[[nsa-global-method-security-run-as-manager-ref]] +* **run-as-manager-ref** +A reference to an optional `RunAsManager` implementation which will be used by the configured `MethodSecurityInterceptor` + + +[[nsa-global-method-security-secured-annotations]] +* **secured-annotations** +Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. +Defaults to "disabled". + + +[[nsa-global-method-security-children]] +=== Child Elements of + + +* <> +* xref:servlet/appendix/namespace/http.adoc#nsa-expression-handler[expression-handler] +* <> +* <> + + + +[[nsa-after-invocation-provider]] +== +This element can be used to decorate an `AfterInvocationProvider` for use by the security interceptor maintained by the `` namespace. +You can define zero or more of these within the `global-method-security` element, each with a `ref` attribute pointing to an `AfterInvocationProvider` bean instance within your application context. + + +[[nsa-after-invocation-provider-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-after-invocation-provider-attributes]] +=== Attributes + + +[[nsa-after-invocation-provider-ref]] +* **ref** +Defines a reference to a Spring bean that implements `AfterInvocationProvider`. + + +[[nsa-pre-post-annotation-handling]] +== +Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replaced entirely. +Only applies if these annotations are enabled. + + +[[nsa-pre-post-annotation-handling-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-pre-post-annotation-handling-children]] +=== Child Elements of + + +* <> +* <> +* <> + + + +[[nsa-invocation-attribute-factory]] +== +Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. + + +[[nsa-invocation-attribute-factory-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-invocation-attribute-factory-attributes]] +=== Attributes + + +[[nsa-invocation-attribute-factory-ref]] +* **ref** +Defines a reference to a Spring bean Id. + + +[[nsa-post-invocation-advice]] +== +Customizes the `PostInvocationAdviceProvider` with the ref as the `PostInvocationAuthorizationAdvice` for the element. + + +[[nsa-post-invocation-advice-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-post-invocation-advice-attributes]] +=== Attributes + + +[[nsa-post-invocation-advice-ref]] +* **ref** +Defines a reference to a Spring bean Id. + + +[[nsa-pre-invocation-advice]] +== +Customizes the `PreInvocationAuthorizationAdviceVoter` with the ref as the `PreInvocationAuthorizationAdviceVoter` for the element. + + +[[nsa-pre-invocation-advice-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-pre-invocation-advice-attributes]] +=== Attributes + + +[[nsa-pre-invocation-advice-ref]] +* **ref** +Defines a reference to a Spring bean Id. + + +[[nsa-protect-pointcut]] +== Securing Methods using +`` +Rather than defining security attributes on an individual method or class basis using the `@Secured` annotation, you can define cross-cutting security constraints across whole sets of methods and interfaces in your service layer using the `` element. +You can find an example in the xref:servlet/authorization/method-security.adoc#ns-protect-pointcut[namespace introduction]. + + +[[nsa-protect-pointcut-parents]] +=== Parent Elements of + + +* <> + + + +[[nsa-protect-pointcut-attributes]] +=== Attributes + + +[[nsa-protect-pointcut-access]] +* **access** +Access configuration attributes list that applies to all methods matching the pointcut, e.g. +"ROLE_A,ROLE_B" + + +[[nsa-protect-pointcut-expression]] +* **expression** +An AspectJ expression, including the `execution` keyword. +For example, `execution(int com.foo.TargetObject.countLength(String))`. + + +[[nsa-intercept-methods]] +== +Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods + + +[[nsa-intercept-methods-attributes]] +=== Attributes + + +[[nsa-intercept-methods-access-decision-manager-ref]] +* **access-decision-manager-ref** +Optional AccessDecisionManager bean ID to be used by the created method security interceptor. + + +[[nsa-intercept-methods-children]] +=== Child Elements of + + +* <> + + + +[[nsa-method-security-metadata-source]] +== +Creates a MethodSecurityMetadataSource instance + + +[[nsa-method-security-metadata-source-attributes]] +=== Attributes + + +[[nsa-method-security-metadata-source-id]] +* **id** +A bean identifier, used for referring to the bean elsewhere in the context. + + +[[nsa-method-security-metadata-source-use-expressions]] +* **use-expressions** +Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. +Defaults to 'false'. +If enabled, each attribute should contain a single Boolean expression. +If the expression evaluates to 'true', access will be granted. + + +[[nsa-method-security-metadata-source-children]] +=== Child Elements of + + +* <> + + + +[[nsa-protect]] +== +Defines a protected method and the access control configuration attributes that apply to it. +We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". + + +[[nsa-protect-parents]] +=== Parent Elements of + + +* <> +* <> + + + +[[nsa-protect-attributes]] +=== Attributes + + +[[nsa-protect-access]] +* **access** +Access configuration attributes list that applies to the method, e.g. +"ROLE_A,ROLE_B". + + +[[nsa-protect-method]] +* **method** +A method name diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/websocket.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/websocket.adoc new file mode 100644 index 0000000000..fde54bc642 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/websocket.adoc @@ -0,0 +1,74 @@ +[[nsa-websocket-security]] += WebSocket Security + +Spring Security 4.0+ provides support for authorizing messages. +One concrete example of where this is useful is to provide authorization in WebSocket based applications. + +[[nsa-websocket-message-broker]] +== + +The websocket-message-broker element has two different modes. +If the <> is not specified, then it will do the following things: + +* Ensure that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver. +This allows the use of `@AuthenticationPrincipal` to resolve the principal of the current `Authentication` +* Ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel. +This populates the SecurityContextHolder with the user that is found in the Message +* Ensures that a ChannelSecurityInterceptor is registered with the clientInboundChannel. +This allows authorization rules to be specified for a message. +* Ensures that a CsrfChannelInterceptor is registered with the clientInboundChannel. +This ensures that only requests from the original domain are enabled. +* Ensures that a CsrfTokenHandshakeInterceptor is registered with WebSocketHttpRequestHandler, TransportHandlingSockJsService, or DefaultSockJsService. +This ensures that the expected CsrfToken from the HttpServletRequest is copied into the WebSocket Session attributes. + +If additional control is necessary, the id can be specified and a ChannelSecurityInterceptor will be assigned to the specified id. +All the wiring with Spring's messaging infrastructure can then be done manually. +This is more cumbersome, but provides greater control over the configuration. + + +[[nsa-websocket-message-broker-attributes]] +=== Attributes + +[[nsa-websocket-message-broker-id]] +* **id** A bean identifier, used for referring to the ChannelSecurityInterceptor bean elsewhere in the context. +If specified, Spring Security requires explicit configuration within Spring Messaging. +If not specified, Spring Security will automatically integrate with the messaging infrastructure as described in <> + +[[nsa-websocket-message-broker-same-origin-disabled]] +* **same-origin-disabled** Disables the requirement for CSRF token to be present in the Stomp headers (default false). +Changing the default is useful if it is necessary to allow other origins to make SockJS connections. + +[[nsa-websocket-message-broker-children]] +=== Child Elements of + + +* xref:servlet/appendix/namespace/http.adoc#nsa-expression-handler[expression-handler] +* <> + +[[nsa-intercept-message]] +== + +Defines an authorization rule for a message. + + +[[nsa-intercept-message-parents]] +=== Parent Elements of + + +* <> + + +[[nsa-intercept-message-attributes]] +=== Attributes + +[[nsa-intercept-message-pattern]] +* **pattern** An ant based pattern that matches on the Message destination. +For example, "/**" matches any Message with a destination; "/admin/**" matches any Message that has a destination that starts with "/admin/**". + +[[nsa-intercept-message-type]] +* **type** The type of message to match on. +Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). + +[[nsa-intercept-message-access]] +* **access** The expression used to secure the Message. +For example, "denyAll" will deny access to all of the matching Messages; "permitAll" will grant access to all of the matching Messages; "hasRole('ADMIN') requires the current user to have the role 'ROLE_ADMIN' for the matching Messages. diff --git a/docs/modules/ROOT/pages/servlet/authentication/cas.adoc b/docs/modules/ROOT/pages/servlet/authentication/cas.adoc index 5b6dd7617e..1917e156ae 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/cas.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/cas.adoc @@ -139,7 +139,7 @@ The following beans should be configured to commence the CAS authentication proc ---- For CAS to operate, the `ExceptionTranslationFilter` must have its `authenticationEntryPoint` property set to the `CasAuthenticationEntryPoint` bean. -This can easily be done using xref:servlet/appendix/namespace.adoc#nsa-http-entry-point-ref[entry-point-ref] as is done in the example above. +This can easily be done using xref:servlet/appendix/namespace/http.adoc#nsa-http-entry-point-ref[entry-point-ref] as is done in the example above. The `CasAuthenticationEntryPoint` must refer to the `ServiceProperties` bean (discussed above), which provides the URL to the enterprise's CAS login server. This is where the user's browser will be redirected. diff --git a/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc b/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc index 9e241932be..4b8e960c07 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/jaas.adoc @@ -166,5 +166,5 @@ This means that the `Subject` can be accessed using: Subject subject = Subject.getSubject(AccessController.getContext()); ---- -This integration can easily be configured using the xref:servlet/appendix/namespace.adoc#nsa-http-jaas-api-provision[jaas-api-provision] attribute. +This integration can easily be configured using the xref:servlet/appendix/namespace/http.adoc#nsa-http-jaas-api-provision[jaas-api-provision] attribute. This feature is useful when integrating with legacy or external API's that rely on the JAAS Subject being populated. diff --git a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc index 909f1c3d75..6d0ab11ed2 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc @@ -74,7 +74,7 @@ This is a shortcut for adding a `CookieClearingLogoutHandler` explicitly. [NOTE] ==== Logouts can of course also be configured using the XML Namespace notation. -Please see the documentation for the xref:servlet/appendix/namespace.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section for further details. +Please see the documentation for the xref:servlet/appendix/namespace/http.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section for further details. ==== Generally, in order to customize logout functionality, you can add @@ -145,4 +145,4 @@ If not configured a status code 200 will be returned by default. - xref:servlet/authentication/rememberme.adoc#remember-me-impls[Remember-Me Interfaces and Implementations] - xref:servlet/exploits/csrf.adoc#servlet-considerations-csrf-logout[ Logging Out] in section CSRF Caveats - Section xref:servlet/authentication/cas.adoc#cas-singlelogout[ Single Logout] (CAS protocol) -- Documentation for the xref:servlet/appendix/namespace.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section +- Documentation for the xref:servlet/appendix/namespace/http.adoc#nsa-logout[ logout element] in the Spring Security XML Namespace section diff --git a/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc b/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc index 0168114c1b..f7456927db 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/xml-namespace.adoc @@ -192,7 +192,7 @@ Common problems like incorrect filter ordering are no longer an issue as the fil The `` element creates a `DaoAuthenticationProvider` bean and the `` element creates an `InMemoryDaoImpl`. All `authentication-provider` elements must be children of the `` element, which creates a `ProviderManager` and registers the authentication providers with it. -You can find more detailed information on the beans that are created in the xref:servlet/appendix/namespace.adoc#appendix-namespace[namespace appendix]. +You can find more detailed information on the beans that are created in the xref:servlet/appendix/namespace/index.adoc#appendix-namespace[namespace appendix]. It's worth cross-checking this if you want to start understanding what the important classes in the framework are and how they are used, particularly if you want to customise things later. **** diff --git a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc index fc2d490735..2bbba3b03d 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/csrf.adoc @@ -375,7 +375,7 @@ For details, refer to the <> section. If a token does expire, you might want to customize how it is handled by specifying a custom `AccessDeniedHandler`. The custom `AccessDeniedHandler` can process the `InvalidCsrfTokenException` any way you like. -For an example of how to customize the `AccessDeniedHandler` refer to the provided links for both xref:servlet/appendix/namespace.adoc#nsa-access-denied-handler[xml] and {gh-url}/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java#L64[Java configuration]. +For an example of how to customize the `AccessDeniedHandler` refer to the provided links for both xref:servlet/appendix/namespace/http.adoc#nsa-access-denied-handler[xml] and {gh-url}/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java#L64[Java configuration]. // FIXME: We should add a custom AccessDeniedHandler section in the reference and update the links above diff --git a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc index 6ae9b57a86..bb647abf54 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc @@ -51,7 +51,7 @@ This will ensure that: <2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request. <3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <> -Spring Security also provides xref:servlet/appendix/namespace.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets. +Spring Security also provides xref:servlet/appendix/namespace/websocket.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets. A comparable XML based configuration looks like the following: [source,xml] @@ -132,7 +132,7 @@ This will ensure that: <5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types. <6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages. -Spring Security also provides xref:servlet/appendix/namespace.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets. +Spring Security also provides xref:servlet/appendix/namespace/websocket.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets. A comparable XML based configuration looks like the following: [source,xml] @@ -360,7 +360,7 @@ SockJS may use an https://github.com/sockjs/sockjs-client/tree/v0.3.4[transport By default Spring Security will xref:features/exploits/headers.adoc#headers-frame-options[deny] the site from being framed to prevent Clickjacking attacks. To allow SockJS frame based transports to work, we need to configure Spring Security to allow the same origin to frame the content. -You can customize X-Frame-Options with the xref:servlet/appendix/namespace.adoc#nsa-frame-options[frame-options] element. +You can customize X-Frame-Options with the xref:servlet/appendix/namespace/http.adoc#nsa-frame-options[frame-options] element. For example, the following will instruct Spring Security to use "X-Frame-Options: SAMEORIGIN" which allows iframes within the same domain: [source,xml] @@ -486,7 +486,7 @@ open class WebSecurityConfig : WebSecurityConfigurerAdapter() { ---- ==== -If we are using XML based configuration, we can use the xref:servlet/appendix/namespace.adoc#nsa-csrf-request-matcher-ref[csrf@request-matcher-ref]. +If we are using XML based configuration, we can use the xref:servlet/appendix/namespace/http.adoc#nsa-csrf-request-matcher-ref[csrf@request-matcher-ref]. For example: [source,xml] diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc index c02a4a0bc6..427140c1e7 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc @@ -74,7 +74,7 @@ class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { In addition to the `HttpSecurity.oauth2Client()` DSL, XML configuration is also supported. -The following code shows the complete configuration options available in the xref:servlet/appendix/namespace.adoc#nsa-oauth2-client[ security namespace]: +The following code shows the complete configuration options available in the xref:servlet/appendix/namespace/http.adoc#nsa-oauth2-client[ security namespace]: .OAuth2 Client XML Configuration Options ==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc index db5ee9c506..e30a4e407b 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc @@ -737,7 +737,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { In addition to the `oauth2Login()` DSL, XML configuration is also supported. -The following code shows the complete configuration options available in the xref:servlet/appendix/namespace.adoc#nsa-oauth2-login[ security namespace]: +The following code shows the complete configuration options available in the xref:servlet/appendix/namespace/http.adoc#nsa-oauth2-login[ security namespace]: .OAuth2 Login XML Configuration Options ==== From efa2fab061c084030f0401216bc9fff69975195a Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 3 Nov 2021 15:38:59 -0500 Subject: [PATCH 005/589] Document authentication helper method in WebClient integration Closes gh-10120 --- .../pages/servlet/oauth2/oauth2-client.adoc | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc index 427140c1e7..6d1cfc423d 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc @@ -2212,6 +2212,60 @@ fun index(): String { ==== <1> `clientRegistrationId()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. +The following code shows how to set an `Authentication` as a request attribute: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/") +public String index() { + String resourceUri = ... + + Authentication anonymousAuthentication = new AnonymousAuthenticationToken( + "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + String body = webClient + .get() + .uri(resourceUri) + .attributes(authentication(anonymousAuthentication)) <1> + .retrieve() + .bodyToMono(String.class) + .block(); + + ... + + return "index"; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(): String { + val resourceUri: String = ... + + val anonymousAuthentication: Authentication = AnonymousAuthenticationToken( + "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")) + val body: String = webClient + .get() + .uri(resourceUri) + .attributes(authentication(anonymousAuthentication)) <1> + .retrieve() + .bodyToMono() + .block() + + ... + + return "index" +} +---- +==== +<1> `authentication()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. + +[WARNING] +It is recommended to be cautious with this feature since all HTTP requests will receive an access token bound to the provided principal. + === Defaulting the Authorized Client From e350c8a852c3d98bab9e54ecb9d2707ff56aec74 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 3 Nov 2021 17:01:37 -0500 Subject: [PATCH 006/589] Document parameters converter in oauth2 client servlet docs Closes gh-10467 --- .../pages/servlet/oauth2/oauth2-client.adoc | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc index 6d1cfc423d..fa0c20eac4 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc @@ -929,6 +929,11 @@ If you need to customize the pre-processing of the Token Request, you can provid The default implementation `OAuth2AuthorizationCodeGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-4.1.3[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `OAuth2AuthorizationCodeGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `OAuth2AuthorizationCodeGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. + IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. @@ -1043,6 +1048,11 @@ If you need to customize the pre-processing of the Token Request, you can provid The default implementation `OAuth2RefreshTokenGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-6[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `OAuth2RefreshTokenGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `OAuth2RefreshTokenGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. + IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. @@ -1149,6 +1159,11 @@ If you need to customize the pre-processing of the Token Request, you can provid The default implementation `OAuth2ClientCredentialsGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `OAuth2ClientCredentialsGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `OAuth2ClientCredentialsGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. + IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. @@ -1383,6 +1398,11 @@ If you need to customize the pre-processing of the Token Request, you can provid The default implementation `OAuth2PasswordGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-4.3.2[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the standard Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `OAuth2PasswordGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `OAuth2PasswordGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. + IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. @@ -1661,6 +1681,11 @@ If you need to customize the pre-processing of the Token Request, you can provid The default implementation `JwtBearerGrantRequestEntityConverter` builds a `RequestEntity` representation of a https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[OAuth 2.0 Access Token Request]. However, providing a custom `Converter`, would allow you to extend the Token Request and add custom parameter(s). +To customize only the parameters of the request, you can provide `JwtBearerGrantRequestEntityConverter.setParametersConverter()` with a custom `Converter>` to completely override the parameters sent with the request. This is often simpler than constructing a `RequestEntity` directly. + +[TIP] +If you prefer to only add additional parameters, you can provide `JwtBearerGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. + ==== Customizing the Access Token Response From 82696918ae88011842074e229fa8e9cb37cc4f50 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 4 Nov 2021 11:31:27 -0600 Subject: [PATCH 007/589] Separate OAuth 2.0 Client Servlet Docs Issue gh-10367 --- docs/modules/ROOT/nav.adoc | 6 +- .../pages/features/integrations/jackson.adoc | 2 +- .../ROOT/pages/reactive/oauth2/login.adoc | 6 +- .../servlet/appendix/database-schema.adoc | 2 +- .../servlet/appendix/namespace/http.adoc | 6 +- .../authorization-grants.adoc} | 1153 +---------------- .../oauth2/client/authorized-clients.adoc | 264 ++++ .../oauth2/client/client-authentication.adoc | 165 +++ .../pages/servlet/oauth2/client/core.adoc | 440 +++++++ .../pages/servlet/oauth2/client/index.adoc | 146 +++ .../pages/servlet/oauth2/oauth2-login.adoc | 8 +- .../oauth2/resource-server/bearer-tokens.adoc | 2 +- 12 files changed, 1061 insertions(+), 1139 deletions(-) rename docs/modules/ROOT/pages/servlet/oauth2/{oauth2-client.adoc => client/authorization-grants.adoc} (53%) create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/client/client-authentication.adoc create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index c6f8d0e8fb..f2b3204070 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -56,7 +56,11 @@ *** xref:servlet/authorization/acls.adoc[Domain Object Security ACLs] ** xref:servlet/oauth2/index.adoc[OAuth2] *** xref:servlet/oauth2/oauth2-login.adoc[OAuth2 Log In] -*** xref:servlet/oauth2/oauth2-client.adoc[OAuth2 Client] +*** xref:servlet/oauth2/client/index.adoc[OAuth2 Client] +**** xref:servlet/oauth2/client/core.adoc[Core Interfaces and Classes] +**** xref:servlet/oauth2/client/authorization-grants.adoc[OAuth2 Authorization Grants] +**** xref:servlet/oauth2/client/client-authentication.adoc[OAuth2 Client Authentication] +**** xref:servlet/oauth2/client/authorized-clients.adoc[OAuth2 Authorized Clients] *** xref:servlet/oauth2/resource-server/index.adoc[OAuth2 Resource Server] **** xref:servlet/oauth2/resource-server/jwt.adoc[JWT] **** xref:servlet/oauth2/resource-server/opaque-token.adoc[Opaque Token] diff --git a/docs/modules/ROOT/pages/features/integrations/jackson.adoc b/docs/modules/ROOT/pages/features/integrations/jackson.adoc index e1c407763b..1f67bf98a2 100644 --- a/docs/modules/ROOT/pages/features/integrations/jackson.adoc +++ b/docs/modules/ROOT/pages/features/integrations/jackson.adoc @@ -42,6 +42,6 @@ The following Spring Security modules provide Jackson support: - spring-security-core (`CoreJackson2Module`) - spring-security-web (`WebJackson2Module`, `WebServletJackson2Module`, `WebServerJackson2Module`) -- xref:servlet/oauth2/oauth2-client.adoc#oauth2client[ spring-security-oauth2-client] (`OAuth2ClientJackson2Module`) +- xref:servlet/oauth2/client/index.adoc#oauth2client[ spring-security-oauth2-client] (`OAuth2ClientJackson2Module`) - spring-security-cas (`CasJackson2Module`) ==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login.adoc index a16160c0ff..4aee887bb3 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login.adoc @@ -39,11 +39,11 @@ The redirect URI is the path in the application that the end-user's user-agent i In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. -The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration]. +The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration]. For our example, the `registrationId` is `google`. IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. -Also, see the supported xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. +Also, see the supported xref:servlet/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. [[webflux-oauth2-login-sample-config]] === Configure `application.yml` @@ -68,7 +68,7 @@ spring: .OAuth Client properties ==== <1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. -<2> Following the base property prefix is the ID for the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration], such as google. +<2> Following the base property prefix is the ID for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[`ClientRegistration`], such as google. ==== . Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. diff --git a/docs/modules/ROOT/pages/servlet/appendix/database-schema.adoc b/docs/modules/ROOT/pages/servlet/appendix/database-schema.adoc index f6cf413b68..17cd707fe1 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/database-schema.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/database-schema.adoc @@ -367,7 +367,7 @@ END; [[dbschema-oauth2-client]] == OAuth 2.0 Client Schema -The JDBC implementation of xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-authorized-repo-service[ OAuth2AuthorizedClientService] (`JdbcOAuth2AuthorizedClientService`) requires a table for persisting `OAuth2AuthorizedClient`(s). +The JDBC implementation of xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-repo-service[ OAuth2AuthorizedClientService] (`JdbcOAuth2AuthorizedClientService`) requires a table for persisting `OAuth2AuthorizedClient`(s). You will need to adjust this schema to match the database dialect you are using. [source,ddl] diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index de5e3c6d26..0c23fe39f1 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -947,7 +947,7 @@ Reference to the `JwtDecoderFactory` used by `OidcAuthorizationCodeAuthenticatio [[nsa-oauth2-client]] == -Configures xref:servlet/oauth2/oauth2-client.adoc#oauth2client[OAuth 2.0 Client] support. +Configures xref:servlet/oauth2/client/index.adoc#oauth2client[OAuth 2.0 Client] support. [[nsa-oauth2-client-parents]] @@ -982,7 +982,7 @@ Reference to the `OAuth2AuthorizedClientService`. [[nsa-authorization-code-grant]] == -Configures xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-auth-grant-support[OAuth 2.0 Authorization Code Grant]. +Configures xref:servlet/oauth2/client/authorization-grants.adoc#oauth2Client-auth-grant-support[OAuth 2.0 Authorization Code Grant]. [[nsa-authorization-code-grant-parents]] @@ -1012,7 +1012,7 @@ Reference to the `OAuth2AccessTokenResponseClient`. [[nsa-client-registrations]] == -A container element for client(s) registered (xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration]) with an OAuth 2.0 or OpenID Connect 1.0 Provider. +A container element for client(s) registered (xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration]) with an OAuth 2.0 or OpenID Connect 1.0 Provider. [[nsa-client-registrations-children]] diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc similarity index 53% rename from docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc rename to docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc index fa0c20eac4..b2a60ba35b 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-client.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc @@ -1,630 +1,21 @@ -[[oauth2client]] -= OAuth 2.0 Client - -The OAuth 2.0 Client features provide support for the Client role as defined in the https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Framework]. - -At a high-level, the core features available are: - -.Authorization Grant support -* https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] -* https://tools.ietf.org/html/rfc6749#section-6[Refresh Token] -* https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] -* https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] -* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer] - -.Client Authentication support -* https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] - -.HTTP Client support -* <> (for requesting protected resources) - -The `HttpSecurity.oauth2Client()` DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client. -In addition, `HttpSecurity.oauth2Client().authorizationCodeGrant()` enables the customization of the Authorization Code grant. - -The following code shows the complete configuration options provided by the `HttpSecurity.oauth2Client()` DSL: - -.OAuth2 Client Configuration Options -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .oauth2Client(oauth2 -> oauth2 - .clientRegistrationRepository(this.clientRegistrationRepository()) - .authorizedClientRepository(this.authorizedClientRepository()) - .authorizedClientService(this.authorizedClientService()) - .authorizationCodeGrant(codeGrant -> codeGrant - .authorizationRequestRepository(this.authorizationRequestRepository()) - .authorizationRequestResolver(this.authorizationRequestResolver()) - .accessTokenResponseClient(this.accessTokenResponseClient()) - ) - ); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { - - override fun configure(http: HttpSecurity) { - http { - oauth2Client { - clientRegistrationRepository = clientRegistrationRepository() - authorizedClientRepository = authorizedClientRepository() - authorizedClientService = authorizedClientService() - authorizationCodeGrant { - authorizationRequestRepository = authorizationRequestRepository() - authorizationRequestResolver = authorizationRequestResolver() - accessTokenResponseClient = accessTokenResponseClient() - } - } - } - } -} ----- -==== - -In addition to the `HttpSecurity.oauth2Client()` DSL, XML configuration is also supported. - -The following code shows the complete configuration options available in the xref:servlet/appendix/namespace/http.adoc#nsa-oauth2-client[ security namespace]: - -.OAuth2 Client XML Configuration Options -==== -[source,xml] ----- - - - - - ----- -==== - -The `OAuth2AuthorizedClientManager` is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more `OAuth2AuthorizedClientProvider`(s). - -The following code shows an example of how to register an `OAuth2AuthorizedClientManager` `@Bean` and associate it with an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository, - authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { - val authorizedClientProvider: OAuth2AuthorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build() - val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - -The following sections will go into more detail on the core components used by OAuth 2.0 Client and the configuration options available: - -* <> -** <> -** <> -** <> -** <> -** <> -* <> -** <> -** <> -** <> -** <> -** <> -* <> -** <> -* <> -** <> -* <> - - -[[oauth2Client-core-interface-class]] -== Core Interfaces / Classes - - -[[oauth2Client-client-registration]] -=== ClientRegistration - -`ClientRegistration` is a representation of a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - -A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details. - -`ClientRegistration` and its properties are defined as follows: - -[source,java] ----- -public final class ClientRegistration { - private String registrationId; <1> - private String clientId; <2> - private String clientSecret; <3> - private ClientAuthenticationMethod clientAuthenticationMethod; <4> - private AuthorizationGrantType authorizationGrantType; <5> - private String redirectUri; <6> - private Set scopes; <7> - private ProviderDetails providerDetails; - private String clientName; <8> - - public class ProviderDetails { - private String authorizationUri; <9> - private String tokenUri; <10> - private UserInfoEndpoint userInfoEndpoint; - private String jwkSetUri; <11> - private String issuerUri; <12> - private Map configurationMetadata; <13> - - public class UserInfoEndpoint { - private String uri; <14> - private AuthenticationMethod authenticationMethod; <15> - private String userNameAttributeName; <16> - - } - } -} ----- -<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`. -<2> `clientId`: The client identifier. -<3> `clientSecret`: The client secret. -<4> `clientAuthenticationMethod`: The method used to authenticate the Client with the Provider. -The supported values are *client_secret_basic*, *client_secret_post*, *private_key_jwt*, *client_secret_jwt* and *none* https://tools.ietf.org/html/rfc6749#section-2.1[(public clients)]. -<5> `authorizationGrantType`: The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. - The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. -<6> `redirectUri`: The client's registered redirect URI that the _Authorization Server_ redirects the end-user's user-agent - to after the end-user has authenticated and authorized access to the client. -<7> `scopes`: The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. -<8> `clientName`: A descriptive name used for the client. -The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. -<9> `authorizationUri`: The Authorization Endpoint URI for the Authorization Server. -<10> `tokenUri`: The Token Endpoint URI for the Authorization Server. -<11> `jwkSetUri`: The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (JWK)] Set from the Authorization Server, - which contains the cryptographic key(s) used to verify the https://tools.ietf.org/html/rfc7515[JSON Web Signature (JWS)] of the ID Token and optionally the UserInfo Response. -<12> `issuerUri`: Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server. -<13> `configurationMetadata`: The https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration Information]. - This information will only be available if the Spring Boot 2.x property `spring.security.oauth2.client.provider.[providerId].issuerUri` is configured. -<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. -<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint. -The supported values are *header*, *form* and *query*. -<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. - -A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. - -`ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example: - -==== -.Java -[source,java,role="primary"] ----- -ClientRegistration clientRegistration = - ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() ----- -==== - -The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response. - -As an alternative, you can use `ClientRegistrations.fromOidcIssuerLocation()` to only query the OpenID Connect Provider's Configuration endpoint. - -[[oauth2Client-client-registration-repo]] -=== ClientRegistrationRepository - -The `ClientRegistrationRepository` serves as a repository for OAuth 2.0 / OpenID Connect 1.0 `ClientRegistration`(s). - -[NOTE] -Client registration information is ultimately stored and owned by the associated Authorization Server. -This repository provides the ability to retrieve a sub-set of the primary client registration information, which is stored with the Authorization Server. - -Spring Boot 2.x auto-configuration binds each of the properties under `spring.security.oauth2.client.registration._[registrationId]_` to an instance of `ClientRegistration` and then composes each of the `ClientRegistration` instance(s) within a `ClientRegistrationRepository`. - -[NOTE] -The default implementation of `ClientRegistrationRepository` is `InMemoryClientRegistrationRepository`. - -The auto-configuration also registers the `ClientRegistrationRepository` as a `@Bean` in the `ApplicationContext` so that it is available for dependency-injection, if needed by the application. - -The following listing shows an example: - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @Autowired - private ClientRegistrationRepository clientRegistrationRepository; - - @GetMapping("/") - public String index() { - ClientRegistration oktaRegistration = - this.clientRegistrationRepository.findByRegistrationId("okta"); - - ... - - return "index"; - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - - @Autowired - private lateinit var clientRegistrationRepository: ClientRegistrationRepository - - @GetMapping("/") - fun index(): String { - val oktaRegistration = - this.clientRegistrationRepository.findByRegistrationId("okta") - - //... - - return "index"; - } -} ----- -==== - -[[oauth2Client-authorized-client]] -=== OAuth2AuthorizedClient - -`OAuth2AuthorizedClient` is a representation of an Authorized Client. -A client is considered to be authorized when the end-user (Resource Owner) has granted authorization to the client to access its protected resources. - -`OAuth2AuthorizedClient` serves the purpose of associating an `OAuth2AccessToken` (and optional `OAuth2RefreshToken`) to a `ClientRegistration` (client) and resource owner, who is the `Principal` end-user that granted the authorization. - - -[[oauth2Client-authorized-repo-service]] -=== OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService - -`OAuth2AuthorizedClientRepository` is responsible for persisting `OAuth2AuthorizedClient`(s) between web requests. -Whereas, the primary role of `OAuth2AuthorizedClientService` is to manage `OAuth2AuthorizedClient`(s) at the application-level. - -From a developer perspective, the `OAuth2AuthorizedClientRepository` or `OAuth2AuthorizedClientService` provides the capability to lookup an `OAuth2AccessToken` associated with a client so that it may be used to initiate a protected resource request. - -The following listing shows an example: - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @Autowired - private OAuth2AuthorizedClientService authorizedClientService; - - @GetMapping("/") - public String index(Authentication authentication) { - OAuth2AuthorizedClient authorizedClient = - this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()); - - OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); - - ... - - return "index"; - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - - @Autowired - private lateinit var authorizedClientService: OAuth2AuthorizedClientService - - @GetMapping("/") - fun index(authentication: Authentication): String { - val authorizedClient: OAuth2AuthorizedClient = - this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()); - val accessToken = authorizedClient.accessToken - - ... - - return "index"; - } -} ----- -==== - -[NOTE] -Spring Boot 2.x auto-configuration registers an `OAuth2AuthorizedClientRepository` and/or `OAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`. -However, the application may choose to override and register a custom `OAuth2AuthorizedClientRepository` or `OAuth2AuthorizedClientService` `@Bean`. - -The default implementation of `OAuth2AuthorizedClientService` is `InMemoryOAuth2AuthorizedClientService`, which stores `OAuth2AuthorizedClient`(s) in-memory. - -Alternatively, the JDBC implementation `JdbcOAuth2AuthorizedClientService` may be configured for persisting `OAuth2AuthorizedClient`(s) in a database. - -[NOTE] -`JdbcOAuth2AuthorizedClientService` depends on the table definition described in xref:servlet/appendix/database-schema.adoc#dbschema-oauth2-client[ OAuth 2.0 Client Schema]. - - -[[oauth2Client-authorized-manager-provider]] -=== OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider - -The `OAuth2AuthorizedClientManager` is responsible for the overall management of `OAuth2AuthorizedClient`(s). - -The primary responsibilities include: - -* Authorizing (or re-authorizing) an OAuth 2.0 Client, using an `OAuth2AuthorizedClientProvider`. -* Delegating the persistence of an `OAuth2AuthorizedClient`, typically using an `OAuth2AuthorizedClientService` or `OAuth2AuthorizedClientRepository`. -* Delegating to an `OAuth2AuthorizationSuccessHandler` when an OAuth 2.0 Client has been successfully authorized (or re-authorized). -* Delegating to an `OAuth2AuthorizationFailureHandler` when an OAuth 2.0 Client fails to authorize (or re-authorize). - -An `OAuth2AuthorizedClientProvider` implements a strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. -Implementations will typically implement an authorization grant type, eg. `authorization_code`, `client_credentials`, etc. - -The default implementation of `OAuth2AuthorizedClientManager` is `DefaultOAuth2AuthorizedClientManager`, which is associated with an `OAuth2AuthorizedClientProvider` that may support multiple authorization grant types using a delegation-based composite. -The `OAuth2AuthorizedClientProviderBuilder` may be used to configure and build the delegation-based composite. - -The following code shows an example of how to configure and build an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository, - authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { - val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build() - val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - -When an authorization attempt succeeds, the `DefaultOAuth2AuthorizedClientManager` will delegate to the `OAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `OAuth2AuthorizedClientRepository`. -In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `OAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientOAuth2AuthorizationFailureHandler`. -The default behaviour may be customized via `setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)` and `setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)`. - -The `DefaultOAuth2AuthorizedClientManager` is also associated with a `contextAttributesMapper` of type `Function>`, which is responsible for mapping attribute(s) from the `OAuth2AuthorizeRequest` to a `Map` of attributes to be associated to the `OAuth2AuthorizationContext`. -This can be useful when you need to supply an `OAuth2AuthorizedClientProvider` with required (supported) attribute(s), eg. the `PasswordOAuth2AuthorizedClientProvider` requires the resource owner's `username` and `password` to be available in `OAuth2AuthorizationContext.getAttributes()`. - -The following code shows an example of the `contextAttributesMapper`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .password() - .refreshToken() - .build(); - - DefaultOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, - // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` - authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); - - return authorizedClientManager; -} - -private Function> contextAttributesMapper() { - return authorizeRequest -> { - Map contextAttributes = Collections.emptyMap(); - HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName()); - String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME); - String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD); - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - contextAttributes = new HashMap<>(); - - // `PasswordOAuth2AuthorizedClientProvider` requires both attributes - contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); - contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); - } - return contextAttributes; - }; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository, - authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { - val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .password() - .refreshToken() - .build() - val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - - // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, - // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` - authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) - return authorizedClientManager -} - -private fun contextAttributesMapper(): Function> { - return Function { authorizeRequest -> - var contextAttributes: MutableMap = mutableMapOf() - val servletRequest: HttpServletRequest = authorizeRequest.getAttribute(HttpServletRequest::class.java.name) - val username: String = servletRequest.getParameter(OAuth2ParameterNames.USERNAME) - val password: String = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD) - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - contextAttributes = hashMapOf() - - // `PasswordOAuth2AuthorizedClientProvider` requires both attributes - contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username - contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password - } - contextAttributes - } -} ----- -==== - -The `DefaultOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `HttpServletRequest`. -When operating *_outside_* of a `HttpServletRequest` context, use `AuthorizedClientServiceOAuth2AuthorizedClientManager` instead. - -A _service application_ is a common use case for when to use an `AuthorizedClientServiceOAuth2AuthorizedClientManager`. -Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account. -An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application. - -The following code shows an example of how to configure an `AuthorizedClientServiceOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public OAuth2AuthorizedClientManager authorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientService authorizedClientService) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .clientCredentials() - .build(); - - AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = - new AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientService); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ClientRegistrationRepository, - authorizedClientService: OAuth2AuthorizedClientService): OAuth2AuthorizedClientManager { - val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .clientCredentials() - .build() - val authorizedClientManager = AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientService) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - - [[oauth2Client-auth-grant-support]] -== Authorization Grant Support += Authorization Grant Support [[oauth2Client-auth-code-grant]] -=== Authorization Code +== Authorization Code [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] grant. -==== Obtaining Authorization +=== Obtaining Authorization [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.1.1[Authorization Request/Response] protocol flow for the Authorization Code grant. -==== Initiating the Authorization Request +=== Initiating the Authorization Request The `OAuth2AuthorizationRequestRedirectFilter` uses an `OAuth2AuthorizationRequestResolver` to resolve an `OAuth2AuthorizationRequest` and initiate the Authorization Code grant flow by redirecting the end-user's user-agent to the Authorization Server's Authorization Endpoint. @@ -705,7 +96,7 @@ spring: Configuring the `redirect-uri` with `URI` template variables is especially useful when the OAuth 2.0 Client is running behind a xref:features/exploits/http.adoc#http-proxy-server[Proxy Server]. This ensures that the `X-Forwarded-*` headers are used when expanding the `redirect-uri`. -==== Customizing the Authorization Request +=== Customizing the Authorization Request One of the primary use cases an `OAuth2AuthorizationRequestResolver` can realize is the ability to customize the Authorization Request with additional parameters above the standard parameters defined in the OAuth 2.0 Authorization Framework. @@ -852,7 +243,7 @@ private fun authorizationRequestCustomizer(): Consumer>`. The default implementation `OAuth2AuthorizationCodeGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-4.1.3[OAuth 2.0 Access Token Request]. @@ -937,7 +328,7 @@ If you prefer to only add additional parameters, you can provide `OAuth2Authoriz IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultAuthorizationCodeTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -1026,13 +417,13 @@ class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { [[oauth2Client-refresh-token-grant]] -=== Refresh Token +== Refresh Token [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.5[Refresh Token]. -==== Refreshing an Access Token +=== Refreshing an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-6[Access Token Request/Response] protocol flow for the Refresh Token grant. @@ -1042,7 +433,7 @@ The default implementation of `OAuth2AccessTokenResponseClient` for the Refresh The `DefaultRefreshTokenTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `DefaultRefreshTokenTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. The default implementation `OAuth2RefreshTokenGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-6[OAuth 2.0 Access Token Request]. @@ -1056,7 +447,7 @@ If you prefer to only add additional parameters, you can provide `OAuth2RefreshT IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultRefreshTokenTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -1137,13 +528,13 @@ If the `OAuth2AuthorizedClient.getRefreshToken()` is available and the `OAuth2Au [[oauth2Client-client-creds-grant]] -=== Client Credentials +== Client Credentials [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.4.2[Access Token Request/Response] protocol flow for the Client Credentials grant. @@ -1153,7 +544,7 @@ The default implementation of `OAuth2AccessTokenResponseClient` for the Client C The `DefaultClientCredentialsTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `DefaultClientCredentialsTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. The default implementation `OAuth2ClientCredentialsGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request]. @@ -1167,7 +558,7 @@ If you prefer to only add additional parameters, you can provide `OAuth2ClientCr IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultClientCredentialsTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -1241,7 +632,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) `OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials()` configures a `ClientCredentialsOAuth2AuthorizedClientProvider`, which is an implementation of an `OAuth2AuthorizedClientProvider` for the Client Credentials grant. -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1376,13 +767,13 @@ If not provided, it will default to `ServletRequestAttributes` using `RequestCon [[oauth2Client-password-grant]] -=== Resource Owner Password Credentials +== Resource Owner Password Credentials [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.3.2[Access Token Request/Response] protocol flow for the Resource Owner Password Credentials grant. @@ -1392,7 +783,7 @@ The default implementation of `OAuth2AccessTokenResponseClient` for the Resource The `DefaultPasswordTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `DefaultPasswordTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. The default implementation `OAuth2PasswordGrantRequestEntityConverter` builds a `RequestEntity` representation of a standard https://tools.ietf.org/html/rfc6749#section-4.3.2[OAuth 2.0 Access Token Request]. @@ -1406,7 +797,7 @@ If you prefer to only add additional parameters, you can provide `OAuth2Password IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representation of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultPasswordTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -1481,7 +872,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) `OAuth2AuthorizedClientProviderBuilder.builder().password()` configures a `PasswordOAuth2AuthorizedClientProvider`, which is an implementation of an `OAuth2AuthorizedClientProvider` for the Resource Owner Password Credentials grant. -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1659,13 +1050,13 @@ If not provided, it will default to `ServletRequestAttributes` using `RequestCon [[oauth2Client-jwt-bearer-grant]] -=== JWT Bearer +== JWT Bearer [NOTE] Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on the https://datatracker.ietf.org/doc/html/rfc7523[JWT Bearer] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[Access Token Request/Response] protocol flow for the JWT Bearer grant. @@ -1675,7 +1066,7 @@ The default implementation of `OAuth2AccessTokenResponseClient` for the JWT Bear The `DefaultJwtBearerTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `DefaultJwtBearerTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. The default implementation `JwtBearerGrantRequestEntityConverter` builds a `RequestEntity` representation of a https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[OAuth 2.0 Access Token Request]. @@ -1687,7 +1078,7 @@ To customize only the parameters of the request, you can provide `JwtBearerGrant If you prefer to only add additional parameters, you can provide `JwtBearerGrantRequestEntityConverter.addParametersConverter()` with a custom `Converter>` which constructs an aggregate `Converter`. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultJwtBearerTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: @@ -1763,7 +1154,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) ---- ==== -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1879,491 +1270,3 @@ class OAuth2ResourceServerController { } ---- ==== - - -[[oauth2Client-client-auth-support]] -== Client Authentication Support - - -[[oauth2Client-jwt-bearer-auth]] -=== JWT Bearer - -[NOTE] -Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] Client Authentication. - -The default implementation for JWT Bearer Client Authentication is `NimbusJwtClientAuthenticationParametersConverter`, -which is a `Converter` that customizes the Token Request parameters by adding -a signed JSON Web Token (JWS) in the `client_assertion` parameter. - -The `java.security.PrivateKey` or `javax.crypto.SecretKey` used for signing the JWS -is supplied by the `com.nimbusds.jose.jwk.JWK` resolver associated with `NimbusJwtClientAuthenticationParametersConverter`. - - -==== Authenticate using `private_key_jwt` - -Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-authentication-method: private_key_jwt - authorization-grant-type: authorization_code - ... ----- - -The following example shows how to configure `DefaultAuthorizationCodeTokenResponseClient`: - -==== -.Java -[source,java,role="primary"] ----- -Function jwkResolver = (clientRegistration) -> { - if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { - // Assuming RSA key type - RSAPublicKey publicKey = ... - RSAPrivateKey privateKey = ... - return new RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build(); - } - return null; -}; - -OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = - new OAuth2AuthorizationCodeGrantRequestEntityConverter(); -requestEntityConverter.addParametersConverter( - new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); - -DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = - new DefaultAuthorizationCodeTokenResponseClient(); -tokenResponseClient.setRequestEntityConverter(requestEntityConverter); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val jwkResolver: Function = - Function { clientRegistration -> - if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { - // Assuming RSA key type - var publicKey: RSAPublicKey - var privateKey: RSAPrivateKey - RSAKey.Builder(publicKey) = //... - .privateKey(privateKey) = //... - .keyID(UUID.randomUUID().toString()) - .build() - } - null - } - -val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter() -requestEntityConverter.addParametersConverter( - NimbusJwtClientAuthenticationParametersConverter(jwkResolver) -) - -val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient() -tokenResponseClient.setRequestEntityConverter(requestEntityConverter) ----- -==== - - -==== Authenticate using `client_secret_jwt` - -Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-secret: okta-client-secret - client-authentication-method: client_secret_jwt - authorization-grant-type: client_credentials - ... ----- - -The following example shows how to configure `DefaultClientCredentialsTokenResponseClient`: - -==== -.Java -[source,java,role="primary"] ----- -Function jwkResolver = (clientRegistration) -> { - if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { - SecretKeySpec secretKey = new SecretKeySpec( - clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), - "HmacSHA256"); - return new OctetSequenceKey.Builder(secretKey) - .keyID(UUID.randomUUID().toString()) - .build(); - } - return null; -}; - -OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter = - new OAuth2ClientCredentialsGrantRequestEntityConverter(); -requestEntityConverter.addParametersConverter( - new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); - -DefaultClientCredentialsTokenResponseClient tokenResponseClient = - new DefaultClientCredentialsTokenResponseClient(); -tokenResponseClient.setRequestEntityConverter(requestEntityConverter); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val jwkResolver = Function { clientRegistration: ClientRegistration -> - if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { - val secretKey = SecretKeySpec( - clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), - "HmacSHA256" - ) - OctetSequenceKey.Builder(secretKey) - .keyID(UUID.randomUUID().toString()) - .build() - } - null -} - -val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter() -requestEntityConverter.addParametersConverter( - NimbusJwtClientAuthenticationParametersConverter(jwkResolver) -) - -val tokenResponseClient = DefaultClientCredentialsTokenResponseClient() -tokenResponseClient.setRequestEntityConverter(requestEntityConverter) ----- -==== - - -[[oauth2Client-additional-features]] -== Additional Features - - -[[oauth2Client-registered-authorized-client]] -=== Resolving an Authorized Client - -The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`. -This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `OAuth2AuthorizedClientManager` or `OAuth2AuthorizedClientService`. - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @GetMapping("/") - public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { - OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); - - ... - - return "index"; - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - @GetMapping("/") - fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String { - val accessToken = authorizedClient.accessToken - - ... - - return "index" - } -} ----- -==== - -The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses an <> and therefore inherits it's capabilities. - - -[[oauth2Client-webclient-servlet]] -== WebClient integration for Servlet Environments - -The OAuth 2.0 Client support integrates with `WebClient` using an `ExchangeFilterFunction`. - -The `ServletOAuth2AuthorizedClientExchangeFilterFunction` provides a simple mechanism for requesting protected resources by using an `OAuth2AuthorizedClient` and including the associated `OAuth2AccessToken` as a Bearer Token. -It directly uses an <> and therefore inherits the following capabilities: - -* An `OAuth2AccessToken` will be requested if the client has not yet been authorized. -** `authorization_code` - triggers the Authorization Request redirect to initiate the flow -** `client_credentials` - the access token is obtained directly from the Token Endpoint -** `password` - the access token is obtained directly from the Token Endpoint -* If the `OAuth2AccessToken` is expired, it will be refreshed (or renewed) if an `OAuth2AuthorizedClientProvider` is available to perform the authorization - -The following code shows an example of how to configure `WebClient` with OAuth 2.0 Client support: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { - val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build() -} ----- -==== - -=== Providing the Authorized Client - -The `ServletOAuth2AuthorizedClientExchangeFilterFunction` determines the client to use (for a request) by resolving the `OAuth2AuthorizedClient` from the `ClientRequest.attributes()` (request attributes). - -The following code shows how to set an `OAuth2AuthorizedClient` as a request attribute: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/") -public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { - String resourceUri = ... - - String body = webClient - .get() - .uri(resourceUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) <1> - .retrieve() - .bodyToMono(String.class) - .block(); - - ... - - return "index"; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/") -fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String { - val resourceUri: String = ... - val body: String = webClient - .get() - .uri(resourceUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) <1> - .retrieve() - .bodyToMono() - .block() - - ... - - return "index" -} ----- -==== - -<1> `oauth2AuthorizedClient()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. - -The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/") -public String index() { - String resourceUri = ... - - String body = webClient - .get() - .uri(resourceUri) - .attributes(clientRegistrationId("okta")) <1> - .retrieve() - .bodyToMono(String.class) - .block(); - - ... - - return "index"; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/") -fun index(): String { - val resourceUri: String = ... - - val body: String = webClient - .get() - .uri(resourceUri) - .attributes(clientRegistrationId("okta")) <1> - .retrieve() - .bodyToMono() - .block() - - ... - - return "index" -} ----- -==== -<1> `clientRegistrationId()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. - -The following code shows how to set an `Authentication` as a request attribute: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/") -public String index() { - String resourceUri = ... - - Authentication anonymousAuthentication = new AnonymousAuthenticationToken( - "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); - String body = webClient - .get() - .uri(resourceUri) - .attributes(authentication(anonymousAuthentication)) <1> - .retrieve() - .bodyToMono(String.class) - .block(); - - ... - - return "index"; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/") -fun index(): String { - val resourceUri: String = ... - - val anonymousAuthentication: Authentication = AnonymousAuthenticationToken( - "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")) - val body: String = webClient - .get() - .uri(resourceUri) - .attributes(authentication(anonymousAuthentication)) <1> - .retrieve() - .bodyToMono() - .block() - - ... - - return "index" -} ----- -==== -<1> `authentication()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. - -[WARNING] -It is recommended to be cautious with this feature since all HTTP requests will receive an access token bound to the provided principal. - - -=== Defaulting the Authorized Client - -If neither `OAuth2AuthorizedClient` or `ClientRegistration.getRegistrationId()` is provided as a request attribute, the `ServletOAuth2AuthorizedClientExchangeFilterFunction` can determine the _default_ client to use depending on it's configuration. - -If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authenticated using `HttpSecurity.oauth2Login()`, the `OAuth2AccessToken` associated with the current `OAuth2AuthenticationToken` is used. - -The following code shows the specific configuration: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - oauth2Client.setDefaultOAuth2AuthorizedClient(true); - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { - val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - oauth2Client.setDefaultOAuth2AuthorizedClient(true) - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build() -} ----- -==== - -[WARNING] -It is recommended to be cautious with this feature since all HTTP requests will receive the access token. - -Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a valid `ClientRegistration`, the `OAuth2AccessToken` associated with the `OAuth2AuthorizedClient` is used. - -The following code shows the specific configuration: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { - ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - oauth2Client.setDefaultClientRegistrationId("okta"); - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { - val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - oauth2Client.setDefaultClientRegistrationId("okta") - return WebClient.builder() - .apply(oauth2Client.oauth2Configuration()) - .build() -} ----- -==== - -[WARNING] -It is recommended to be cautious with this feature since all HTTP requests will receive the access token. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc new file mode 100644 index 0000000000..16a626dd73 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/authorized-clients.adoc @@ -0,0 +1,264 @@ +[[oauth2Client-additional-features]] += Authorized Client Features + +[[oauth2Client-registered-authorized-client]] +== Resolving an Authorized Client + +The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`. +This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `OAuth2AuthorizedClientManager` or `OAuth2AuthorizedClientService`. + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @GetMapping("/") + public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + + ... + + return "index"; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + @GetMapping("/") + fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String { + val accessToken = authorizedClient.accessToken + + ... + + return "index" + } +} +---- +==== + +The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses an xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-manager-provider[`OAuth2AuthorizedClientManager`] and therefore inherits it's capabilities. + + +[[oauth2Client-webclient-servlet]] +== WebClient integration for Servlet Environments + +The OAuth 2.0 Client support integrates with `WebClient` using an `ExchangeFilterFunction`. + +The `ServletOAuth2AuthorizedClientExchangeFilterFunction` provides a simple mechanism for requesting protected resources by using an `OAuth2AuthorizedClient` and including the associated `OAuth2AccessToken` as a Bearer Token. +It directly uses an xref:servlet/oauth2/client/core.adoc#oauth2Client-authorized-manager-provider[`OAuth2AuthorizedClientManager`] and therefore inherits the following capabilities: + +* An `OAuth2AccessToken` will be requested if the client has not yet been authorized. +** `authorization_code` - triggers the Authorization Request redirect to initiate the flow +** `client_credentials` - the access token is obtained directly from the Token Endpoint +** `password` - the access token is obtained directly from the Token Endpoint +* If the `OAuth2AccessToken` is expired, it will be refreshed (or renewed) if an `OAuth2AuthorizedClientProvider` is available to perform the authorization + +The following code shows an example of how to configure `WebClient` with OAuth 2.0 Client support: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { + val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build() +} +---- +==== + +=== Providing the Authorized Client + +The `ServletOAuth2AuthorizedClientExchangeFilterFunction` determines the client to use (for a request) by resolving the `OAuth2AuthorizedClient` from the `ClientRequest.attributes()` (request attributes). + +The following code shows how to set an `OAuth2AuthorizedClient` as a request attribute: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/") +public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + String resourceUri = ... + + String body = webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) <1> + .retrieve() + .bodyToMono(String.class) + .block(); + + ... + + return "index"; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String { + val resourceUri: String = ... + val body: String = webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) <1> + .retrieve() + .bodyToMono() + .block() + + ... + + return "index" +} +---- +==== + +<1> `oauth2AuthorizedClient()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. + +The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/") +public String index() { + String resourceUri = ... + + String body = webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) <1> + .retrieve() + .bodyToMono(String.class) + .block(); + + ... + + return "index"; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(): String { + val resourceUri: String = ... + + val body: String = webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) <1> + .retrieve() + .bodyToMono() + .block() + + ... + + return "index" +} +---- +==== +<1> `clientRegistrationId()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. + + +=== Defaulting the Authorized Client + +If neither `OAuth2AuthorizedClient` or `ClientRegistration.getRegistrationId()` is provided as a request attribute, the `ServletOAuth2AuthorizedClientExchangeFilterFunction` can determine the _default_ client to use depending on it's configuration. + +If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authenticated using `HttpSecurity.oauth2Login()`, the `OAuth2AccessToken` associated with the current `OAuth2AuthenticationToken` is used. + +The following code shows the specific configuration: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultOAuth2AuthorizedClient(true); + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { + val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultOAuth2AuthorizedClient(true) + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build() +} +---- +==== + +[WARNING] +It is recommended to be cautious with this feature since all HTTP requests will receive the access token. + +Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a valid `ClientRegistration`, the `OAuth2AccessToken` associated with the `OAuth2AuthorizedClient` is used. + +The following code shows the specific configuration: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultClientRegistrationId("okta"); + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { + val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultClientRegistrationId("okta") + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build() +} +---- +==== + +[WARNING] +It is recommended to be cautious with this feature since all HTTP requests will receive the access token. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/client-authentication.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/client-authentication.adoc new file mode 100644 index 0000000000..630c72b18c --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/client-authentication.adoc @@ -0,0 +1,165 @@ +[[oauth2Client-client-auth-support]] += Client Authentication Support + + +[[oauth2Client-jwt-bearer-auth]] +== JWT Bearer + +[NOTE] +Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] Client Authentication. + +The default implementation for JWT Bearer Client Authentication is `NimbusJwtClientAuthenticationParametersConverter`, +which is a `Converter` that customizes the Token Request parameters by adding +a signed JSON Web Token (JWS) in the `client_assertion` parameter. + +The `java.security.PrivateKey` or `javax.crypto.SecretKey` used for signing the JWS +is supplied by the `com.nimbusds.jose.jwk.JWK` resolver associated with `NimbusJwtClientAuthenticationParametersConverter`. + + +=== Authenticate using `private_key_jwt` + +Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-authentication-method: private_key_jwt + authorization-grant-type: authorization_code + ... +---- + +The following example shows how to configure `DefaultAuthorizationCodeTokenResponseClient`: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + RSAPublicKey publicKey = ... + RSAPrivateKey privateKey = ... + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = + new OAuth2AuthorizationCodeGrantRequestEntityConverter(); +requestEntityConverter.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); + +DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = + new DefaultAuthorizationCodeTokenResponseClient(); +tokenResponseClient.setRequestEntityConverter(requestEntityConverter); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver: Function = + Function { clientRegistration -> + if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + var publicKey: RSAPublicKey + var privateKey: RSAPrivateKey + RSAKey.Builder(publicKey) = //... + .privateKey(privateKey) = //... + .keyID(UUID.randomUUID().toString()) + .build() + } + null + } + +val requestEntityConverter = OAuth2AuthorizationCodeGrantRequestEntityConverter() +requestEntityConverter.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) + +val tokenResponseClient = DefaultAuthorizationCodeTokenResponseClient() +tokenResponseClient.setRequestEntityConverter(requestEntityConverter) +---- +==== + + +=== Authenticate using `client_secret_jwt` + +Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + client-authentication-method: client_secret_jwt + authorization-grant-type: client_credentials + ... +---- + +The following example shows how to configure `DefaultClientCredentialsTokenResponseClient`: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { + SecretKeySpec secretKey = new SecretKeySpec( + clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), + "HmacSHA256"); + return new OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter = + new OAuth2ClientCredentialsGrantRequestEntityConverter(); +requestEntityConverter.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); + +DefaultClientCredentialsTokenResponseClient tokenResponseClient = + new DefaultClientCredentialsTokenResponseClient(); +tokenResponseClient.setRequestEntityConverter(requestEntityConverter); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver = Function { clientRegistration: ClientRegistration -> + if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { + val secretKey = SecretKeySpec( + clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), + "HmacSHA256" + ) + OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build() + } + null +} + +val requestEntityConverter = OAuth2ClientCredentialsGrantRequestEntityConverter() +requestEntityConverter.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) + +val tokenResponseClient = DefaultClientCredentialsTokenResponseClient() +tokenResponseClient.setRequestEntityConverter(requestEntityConverter) +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc new file mode 100644 index 0000000000..e02d387d1f --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/core.adoc @@ -0,0 +1,440 @@ +[[oauth2Client-core-interface-class]] += Core Interfaces / Classes + + +[[oauth2Client-client-registration]] +== ClientRegistration + +`ClientRegistration` is a representation of a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + +A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details. + +`ClientRegistration` and its properties are defined as follows: + +[source,java] +---- +public final class ClientRegistration { + private String registrationId; <1> + private String clientId; <2> + private String clientSecret; <3> + private ClientAuthenticationMethod clientAuthenticationMethod; <4> + private AuthorizationGrantType authorizationGrantType; <5> + private String redirectUri; <6> + private Set scopes; <7> + private ProviderDetails providerDetails; + private String clientName; <8> + + public class ProviderDetails { + private String authorizationUri; <9> + private String tokenUri; <10> + private UserInfoEndpoint userInfoEndpoint; + private String jwkSetUri; <11> + private String issuerUri; <12> + private Map configurationMetadata; <13> + + public class UserInfoEndpoint { + private String uri; <14> + private AuthenticationMethod authenticationMethod; <15> + private String userNameAttributeName; <16> + + } + } +} +---- +<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`. +<2> `clientId`: The client identifier. +<3> `clientSecret`: The client secret. +<4> `clientAuthenticationMethod`: The method used to authenticate the Client with the Provider. +The supported values are *client_secret_basic*, *client_secret_post*, *private_key_jwt*, *client_secret_jwt* and *none* https://tools.ietf.org/html/rfc6749#section-2.1[(public clients)]. +<5> `authorizationGrantType`: The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. + The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. +<6> `redirectUri`: The client's registered redirect URI that the _Authorization Server_ redirects the end-user's user-agent + to after the end-user has authenticated and authorized access to the client. +<7> `scopes`: The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. +<8> `clientName`: A descriptive name used for the client. +The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. +<9> `authorizationUri`: The Authorization Endpoint URI for the Authorization Server. +<10> `tokenUri`: The Token Endpoint URI for the Authorization Server. +<11> `jwkSetUri`: The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (JWK)] Set from the Authorization Server, + which contains the cryptographic key(s) used to verify the https://tools.ietf.org/html/rfc7515[JSON Web Signature (JWS)] of the ID Token and optionally the UserInfo Response. +<12> `issuerUri`: Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server. +<13> `configurationMetadata`: The https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration Information]. + This information will only be available if the Spring Boot 2.x property `spring.security.oauth2.client.provider.[providerId].issuerUri` is configured. +<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. +<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint. +The supported values are *header*, *form* and *query*. +<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. + +A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. + +`ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example: + +==== +.Java +[source,java,role="primary"] +---- +ClientRegistration clientRegistration = + ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() +---- +==== + +The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response. + +As an alternative, you can use `ClientRegistrations.fromOidcIssuerLocation()` to only query the OpenID Connect Provider's Configuration endpoint. + +[[oauth2Client-client-registration-repo]] +== ClientRegistrationRepository + +The `ClientRegistrationRepository` serves as a repository for OAuth 2.0 / OpenID Connect 1.0 `ClientRegistration`(s). + +[NOTE] +Client registration information is ultimately stored and owned by the associated Authorization Server. +This repository provides the ability to retrieve a sub-set of the primary client registration information, which is stored with the Authorization Server. + +Spring Boot 2.x auto-configuration binds each of the properties under `spring.security.oauth2.client.registration._[registrationId]_` to an instance of `ClientRegistration` and then composes each of the `ClientRegistration` instance(s) within a `ClientRegistrationRepository`. + +[NOTE] +The default implementation of `ClientRegistrationRepository` is `InMemoryClientRegistrationRepository`. + +The auto-configuration also registers the `ClientRegistrationRepository` as a `@Bean` in the `ApplicationContext` so that it is available for dependency-injection, if needed by the application. + +The following listing shows an example: + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + @GetMapping("/") + public String index() { + ClientRegistration oktaRegistration = + this.clientRegistrationRepository.findByRegistrationId("okta"); + + ... + + return "index"; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var clientRegistrationRepository: ClientRegistrationRepository + + @GetMapping("/") + fun index(): String { + val oktaRegistration = + this.clientRegistrationRepository.findByRegistrationId("okta") + + //... + + return "index"; + } +} +---- +==== + +[[oauth2Client-authorized-client]] +== OAuth2AuthorizedClient + +`OAuth2AuthorizedClient` is a representation of an Authorized Client. +A client is considered to be authorized when the end-user (Resource Owner) has granted authorization to the client to access its protected resources. + +`OAuth2AuthorizedClient` serves the purpose of associating an `OAuth2AccessToken` (and optional `OAuth2RefreshToken`) to a `ClientRegistration` (client) and resource owner, who is the `Principal` end-user that granted the authorization. + + +[[oauth2Client-authorized-repo-service]] +== OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService + +`OAuth2AuthorizedClientRepository` is responsible for persisting `OAuth2AuthorizedClient`(s) between web requests. +Whereas, the primary role of `OAuth2AuthorizedClientService` is to manage `OAuth2AuthorizedClient`(s) at the application-level. + +From a developer perspective, the `OAuth2AuthorizedClientRepository` or `OAuth2AuthorizedClientService` provides the capability to lookup an `OAuth2AccessToken` associated with a client so that it may be used to initiate a protected resource request. + +The following listing shows an example: + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @Autowired + private OAuth2AuthorizedClientService authorizedClientService; + + @GetMapping("/") + public String index(Authentication authentication) { + OAuth2AuthorizedClient authorizedClient = + this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()); + + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + + ... + + return "index"; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var authorizedClientService: OAuth2AuthorizedClientService + + @GetMapping("/") + fun index(authentication: Authentication): String { + val authorizedClient: OAuth2AuthorizedClient = + this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()); + val accessToken = authorizedClient.accessToken + + ... + + return "index"; + } +} +---- +==== + +[NOTE] +Spring Boot 2.x auto-configuration registers an `OAuth2AuthorizedClientRepository` and/or `OAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`. +However, the application may choose to override and register a custom `OAuth2AuthorizedClientRepository` or `OAuth2AuthorizedClientService` `@Bean`. + +The default implementation of `OAuth2AuthorizedClientService` is `InMemoryOAuth2AuthorizedClientService`, which stores `OAuth2AuthorizedClient`(s) in-memory. + +Alternatively, the JDBC implementation `JdbcOAuth2AuthorizedClientService` may be configured for persisting `OAuth2AuthorizedClient`(s) in a database. + +[NOTE] +`JdbcOAuth2AuthorizedClientService` depends on the table definition described in xref:servlet/appendix/database-schema.adoc#dbschema-oauth2-client[ OAuth 2.0 Client Schema]. + + +[[oauth2Client-authorized-manager-provider]] +== OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider + +The `OAuth2AuthorizedClientManager` is responsible for the overall management of `OAuth2AuthorizedClient`(s). + +The primary responsibilities include: + +* Authorizing (or re-authorizing) an OAuth 2.0 Client, using an `OAuth2AuthorizedClientProvider`. +* Delegating the persistence of an `OAuth2AuthorizedClient`, typically using an `OAuth2AuthorizedClientService` or `OAuth2AuthorizedClientRepository`. +* Delegating to an `OAuth2AuthorizationSuccessHandler` when an OAuth 2.0 Client has been successfully authorized (or re-authorized). +* Delegating to an `OAuth2AuthorizationFailureHandler` when an OAuth 2.0 Client fails to authorize (or re-authorize). + +An `OAuth2AuthorizedClientProvider` implements a strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. +Implementations will typically implement an authorization grant type, eg. `authorization_code`, `client_credentials`, etc. + +The default implementation of `OAuth2AuthorizedClientManager` is `DefaultOAuth2AuthorizedClientManager`, which is associated with an `OAuth2AuthorizedClientProvider` that may support multiple authorization grant types using a delegation-based composite. +The `OAuth2AuthorizedClientProviderBuilder` may be used to configure and build the delegation-based composite. + +The following code shows an example of how to configure and build an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== + +When an authorization attempt succeeds, the `DefaultOAuth2AuthorizedClientManager` will delegate to the `OAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `OAuth2AuthorizedClientRepository`. +In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `OAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientOAuth2AuthorizationFailureHandler`. +The default behaviour may be customized via `setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)` and `setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)`. + +The `DefaultOAuth2AuthorizedClientManager` is also associated with a `contextAttributesMapper` of type `Function>`, which is responsible for mapping attribute(s) from the `OAuth2AuthorizeRequest` to a `Map` of attributes to be associated to the `OAuth2AuthorizationContext`. +This can be useful when you need to supply an `OAuth2AuthorizedClientProvider` with required (supported) attribute(s), eg. the `PasswordOAuth2AuthorizedClientProvider` requires the resource owner's `username` and `password` to be available in `OAuth2AuthorizationContext.getAttributes()`. + +The following code shows an example of the `contextAttributesMapper`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build(); + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, + // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); + + return authorizedClientManager; +} + +private Function> contextAttributesMapper() { + return authorizeRequest -> { + Map contextAttributes = Collections.emptyMap(); + HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName()); + String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME); + String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD); + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = new HashMap<>(); + + // `PasswordOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); + contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); + } + return contextAttributes; + }; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + + // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, + // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) + return authorizedClientManager +} + +private fun contextAttributesMapper(): Function> { + return Function { authorizeRequest -> + var contextAttributes: MutableMap = mutableMapOf() + val servletRequest: HttpServletRequest = authorizeRequest.getAttribute(HttpServletRequest::class.java.name) + val username: String = servletRequest.getParameter(OAuth2ParameterNames.USERNAME) + val password: String = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD) + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = hashMapOf() + + // `PasswordOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username + contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password + } + contextAttributes + } +} +---- +==== + +The `DefaultOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `HttpServletRequest`. +When operating *_outside_* of a `HttpServletRequest` context, use `AuthorizedClientServiceOAuth2AuthorizedClientManager` instead. + +A _service application_ is a common use case for when to use an `AuthorizedClientServiceOAuth2AuthorizedClientManager`. +Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account. +An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application. + +The following code shows an example of how to configure an `AuthorizedClientServiceOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientService authorizedClientService) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); + + AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = + new AuthorizedClientServiceOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientService: OAuth2AuthorizedClientService): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build() + val authorizedClientManager = AuthorizedClientServiceOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc new file mode 100644 index 0000000000..7ed080bab3 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/index.adoc @@ -0,0 +1,146 @@ +[[oauth2client]] += OAuth 2.0 Client +:page-section-summary-toc: 1 + +The OAuth 2.0 Client features provide support for the Client role as defined in the https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Framework]. + +At a high-level, the core features available are: + +.Authorization Grant support +* https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] +* https://tools.ietf.org/html/rfc6749#section-6[Refresh Token] +* https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] +* https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] +* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer] + +.Client Authentication support +* https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] + +.HTTP Client support +* xref:servlet/oauth2/client/authorized-clients.adoc#oauth2Client-webclient-servlet[`WebClient` integration for Servlet Environments] (for requesting protected resources) + +The `HttpSecurity.oauth2Client()` DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client. +In addition, `HttpSecurity.oauth2Client().authorizationCodeGrant()` enables the customization of the Authorization Code grant. + +The following code shows the complete configuration options provided by the `HttpSecurity.oauth2Client()` DSL: + +.OAuth2 Client Configuration Options +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Client(oauth2 -> oauth2 + .clientRegistrationRepository(this.clientRegistrationRepository()) + .authorizedClientRepository(this.authorizedClientRepository()) + .authorizedClientService(this.authorizedClientService()) + .authorizationCodeGrant(codeGrant -> codeGrant + .authorizationRequestRepository(this.authorizationRequestRepository()) + .authorizationRequestResolver(this.authorizationRequestResolver()) + .accessTokenResponseClient(this.accessTokenResponseClient()) + ) + ); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { + + override fun configure(http: HttpSecurity) { + http { + oauth2Client { + clientRegistrationRepository = clientRegistrationRepository() + authorizedClientRepository = authorizedClientRepository() + authorizedClientService = authorizedClientService() + authorizationCodeGrant { + authorizationRequestRepository = authorizationRequestRepository() + authorizationRequestResolver = authorizationRequestResolver() + accessTokenResponseClient = accessTokenResponseClient() + } + } + } + } +} +---- +==== + +In addition to the `HttpSecurity.oauth2Client()` DSL, XML configuration is also supported. + +The following code shows the complete configuration options available in the xref:servlet/appendix/namespace/http.adoc#nsa-oauth2-client[ security namespace]: + +.OAuth2 Client XML Configuration Options +==== +[source,xml] +---- + + + + + +---- +==== + +The `OAuth2AuthorizedClientManager` is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more `OAuth2AuthorizedClientProvider`(s). + +The following code shows an example of how to register an `OAuth2AuthorizedClientManager` `@Bean` and associate it with an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider: OAuth2AuthorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc index e30a4e407b..c80b82b743 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc @@ -40,10 +40,10 @@ The redirect URI is the path in the application that the end-user's user-agent i In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. -The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration]. +The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration]. IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. -Also, see the supported xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. +Also, see the supported xref:servlet/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. [[oauth2login-sample-application-config]] @@ -69,7 +69,7 @@ spring: .OAuth Client properties ==== <1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. -<2> Following the base property prefix is the ID for the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration], such as google. +<2> Following the base property prefix is the ID for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration], such as google. ==== . Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. @@ -93,7 +93,7 @@ At this point, the OAuth Client retrieves your email address and basic profile i [[oauth2login-boot-property-mappings]] == Spring Boot 2.x Property Mappings -The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:servlet/oauth2/oauth2-client.adoc#oauth2Client-client-registration[ClientRegistration] properties. +The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration] properties. |=== |Spring Boot 2.x |ClientRegistration diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc index 9c35a4b87d..a9a1877aff 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/bearer-tokens.adoc @@ -241,7 +241,7 @@ fun rest(): RestTemplate { [NOTE] Unlike the {security-api-url}org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.html[OAuth 2.0 Authorized Client Manager], this filter interceptor makes no attempt to renew the token, should it be expired. -To obtain this level of support, please create an interceptor using the xref:servlet/oauth2/oauth2-client.adoc#oauth2client[OAuth 2.0 Authorized Client Manager]. +To obtain this level of support, please create an interceptor using the xref:servlet/oauth2/client/index.adoc#oauth2client[OAuth 2.0 Authorized Client Manager]. [[oauth2resourceserver-bearertoken-failure]] == Bearer Token Failure From 7708418fae2aac39c74abc38377f8c91fafaa429 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 4 Nov 2021 11:55:53 -0600 Subject: [PATCH 008/589] Separate OAuth 2.0 Login Servlet Docs Issue gh-10367 --- docs/modules/ROOT/nav.adoc | 4 +- .../servlet/appendix/namespace/http.adoc | 2 +- .../pages/servlet/authentication/index.adoc | 2 +- .../advanced.adoc} | 598 +----------------- .../ROOT/pages/servlet/oauth2/login/core.adoc | 567 +++++++++++++++++ .../pages/servlet/oauth2/login/index.adoc | 8 + .../pages/servlet/saml2/login/overview.adoc | 2 +- 7 files changed, 592 insertions(+), 591 deletions(-) rename docs/modules/ROOT/pages/servlet/oauth2/{oauth2-login.adoc => login/advanced.adoc} (58%) create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc create mode 100644 docs/modules/ROOT/pages/servlet/oauth2/login/index.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index f2b3204070..3a01681136 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -55,7 +55,9 @@ *** xref:servlet/authorization/method-security.adoc[Method Security] *** xref:servlet/authorization/acls.adoc[Domain Object Security ACLs] ** xref:servlet/oauth2/index.adoc[OAuth2] -*** xref:servlet/oauth2/oauth2-login.adoc[OAuth2 Log In] +*** xref:servlet/oauth2/login/index.adoc[OAuth2 Log In] +**** xref:servlet/oauth2/login/core.adoc[Core Configuration] +**** xref:servlet/oauth2/login/advanced.adoc[Advanced Configuration] *** xref:servlet/oauth2/client/index.adoc[OAuth2 Client] **** xref:servlet/oauth2/client/core.adoc[Core Interfaces and Classes] **** xref:servlet/oauth2/client/authorization-grants.adoc[OAuth2 Authorization Grants] diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 0c23fe39f1..0c3b995553 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -863,7 +863,7 @@ Maps a `ForwardAuthenticationFailureHandler` to `authenticationFailureHandler` p [[nsa-oauth2-login]] == -The xref:servlet/oauth2/oauth2-login.adoc#oauth2login[OAuth 2.0 Login] feature configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. +The xref:servlet/oauth2/login/index.adoc#oauth2login[OAuth 2.0 Login] feature configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. [[nsa-oauth2-login-parents]] diff --git a/docs/modules/ROOT/pages/servlet/authentication/index.adoc b/docs/modules/ROOT/pages/servlet/authentication/index.adoc index ceb08df6cb..cc232247ca 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/index.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/index.adoc @@ -14,7 +14,7 @@ These sections focus on specific ways you may want to authenticate and point bac // FIXME: brief description * xref:servlet/authentication/passwords/index.adoc#servlet-authentication-unpwd[Username and Password] - how to authenticate with a username/password -* xref:servlet/oauth2/oauth2-login.adoc#oauth2login[OAuth 2.0 Login] - OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub) +* xref:servlet/oauth2/login/index.adoc#oauth2login[OAuth 2.0 Login] - OAuth 2.0 Log In with OpenID Connect and non-standard OAuth 2.0 Login (i.e. GitHub) * xref:servlet/saml2/index.adoc#servlet-saml2[SAML 2.0 Login] - SAML 2.0 Log In * xref:servlet/authentication/cas.adoc#servlet-cas[Central Authentication Server (CAS)] - Central Authentication Server (CAS) Support * xref:servlet/authentication/rememberme.adoc#servlet-rememberme[Remember Me] - how to remember a user past session expiration diff --git a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc similarity index 58% rename from docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc rename to docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc index c80b82b743..5eb68e757f 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/oauth2-login.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc @@ -1,581 +1,5 @@ -[[oauth2login]] -= OAuth 2.0 Login - -The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google). -OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". - -NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework] and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[OpenID Connect Core 1.0]. - - -[[oauth2login-sample-boot]] -== Spring Boot 2.x Sample - -Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login. - -This section shows how to configure the {gh-samples-url}/servlet/spring-boot/java/oauth2/login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: - -* <> -* <> -* <> -* <> - - -[[oauth2login-sample-initial-setup]] -=== Initial setup - -To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials. - -NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified]. - -Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0". - -After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. - - -[[oauth2login-sample-redirect-uri]] -=== Setting the redirect URI - -The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google and have granted access to the OAuth Client _(<>)_ on the Consent page. - -In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. - -TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. -The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration]. - -IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. -Also, see the supported xref:servlet/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. - - -[[oauth2login-sample-application-config]] -=== Configure application.yml - -Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. -To do so: - -. Go to `application.yml` and set the following configuration: -+ -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: <1> - google: <2> - client-id: google-client-id - client-secret: google-client-secret ----- -+ -.OAuth Client properties -==== -<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. -<2> Following the base property prefix is the ID for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration], such as google. -==== - -. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. - - -[[oauth2login-sample-boot-application]] -=== Boot up the application - -Launch the Spring Boot 2.x sample and go to `http://localhost:8080`. -You are then redirected to the default _auto-generated_ login page, which displays a link for Google. - -Click on the Google link, and you are then redirected to Google for authentication. - -After authenticating with your Google account credentials, the next page presented to you is the Consent screen. -The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier. -Click *Allow* to authorize the OAuth Client to access your email address and basic profile information. - -At this point, the OAuth Client retrieves your email address and basic profile information from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session. - - -[[oauth2login-boot-property-mappings]] -== Spring Boot 2.x Property Mappings - -The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration] properties. - -|=== -|Spring Boot 2.x |ClientRegistration - -|`spring.security.oauth2.client.registration._[registrationId]_` -|`registrationId` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-id` -|`clientId` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-secret` -|`clientSecret` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-authentication-method` -|`clientAuthenticationMethod` - -|`spring.security.oauth2.client.registration._[registrationId]_.authorization-grant-type` -|`authorizationGrantType` - -|`spring.security.oauth2.client.registration._[registrationId]_.redirect-uri` -|`redirectUri` - -|`spring.security.oauth2.client.registration._[registrationId]_.scope` -|`scopes` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-name` -|`clientName` - -|`spring.security.oauth2.client.provider._[providerId]_.authorization-uri` -|`providerDetails.authorizationUri` - -|`spring.security.oauth2.client.provider._[providerId]_.token-uri` -|`providerDetails.tokenUri` - -|`spring.security.oauth2.client.provider._[providerId]_.jwk-set-uri` -|`providerDetails.jwkSetUri` - -|`spring.security.oauth2.client.provider._[providerId]_.issuer-uri` -|`providerDetails.issuerUri` - -|`spring.security.oauth2.client.provider._[providerId]_.user-info-uri` -|`providerDetails.userInfoEndpoint.uri` - -|`spring.security.oauth2.client.provider._[providerId]_.user-info-authentication-method` -|`providerDetails.userInfoEndpoint.authenticationMethod` - -|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute` -|`providerDetails.userInfoEndpoint.userNameAttributeName` -|=== - -[TIP] -A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint], by specifying the `spring.security.oauth2.client.provider._[providerId]_.issuer-uri` property. - - -[[oauth2login-common-oauth2-provider]] -== CommonOAuth2Provider - -`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta. - -For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a Provider. -Therefore, it makes sense to provide default values in order to reduce the required configuration. - -As demonstrated previously, when we <>, only the `client-id` and `client-secret` properties are required. - -The following listing shows an example: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - google: - client-id: google-client-id - client-secret: google-client-secret ----- - -[TIP] -The auto-defaulting of client properties works seamlessly here because the `registrationId` (`google`) matches the `GOOGLE` `enum` (case-insensitive) in `CommonOAuth2Provider`. - -For cases where you may want to specify a different `registrationId`, such as `google-login`, you can still leverage auto-defaulting of client properties by configuring the `provider` property. - -The following listing shows an example: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - google-login: <1> - provider: google <2> - client-id: google-client-id - client-secret: google-client-secret ----- -<1> The `registrationId` is set to `google-login`. -<2> The `provider` property is set to `google`, which will leverage the auto-defaulting of client properties set in `CommonOAuth2Provider.GOOGLE.getBuilder()`. - - -[[oauth2login-custom-provider-properties]] -== Configuring Custom Provider Properties - -There are some OAuth 2.0 Providers that support multi-tenancy, which results in different protocol endpoints for each tenant (or sub-domain). - -For example, an OAuth Client registered with Okta is assigned to a specific sub-domain and have their own protocol endpoints. - -For these cases, Spring Boot 2.x provides the following base property for configuring custom provider properties: `spring.security.oauth2.client.provider._[providerId]_`. - -The following listing shows an example: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-secret: okta-client-secret - provider: - okta: <1> - authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize - token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token - user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo - user-name-attribute: sub - jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys ----- - -<1> The base property (`spring.security.oauth2.client.provider.okta`) allows for custom configuration of protocol endpoint locations. - - -[[oauth2login-override-boot-autoconfig]] -== Overriding Spring Boot 2.x Auto-configuration - -The Spring Boot 2.x auto-configuration class for OAuth Client support is `OAuth2ClientAutoConfiguration`. - -It performs the following tasks: - -* Registers a `ClientRegistrationRepository` `@Bean` composed of `ClientRegistration`(s) from the configured OAuth Client properties. -* Provides a `WebSecurityConfigurerAdapter` `@Configuration` and enables OAuth 2.0 Login through `httpSecurity.oauth2Login()`. - -If you need to override the auto-configuration based on your specific requirements, you may do so in the following ways: - -* <> -* <> -* <> - - -[[oauth2login-register-clientregistrationrepository-bean]] -=== Register a ClientRegistrationRepository @Bean - -The following example shows how to register a `ClientRegistrationRepository` `@Bean`: - -==== -.Java -[source,java,role="primary",attrs="-attributes"] ----- -@Configuration -public class OAuth2LoginConfig { - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); - } - - private ClientRegistration googleClientRegistration() { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary",attrs="-attributes"] ----- -@Configuration -class OAuth2LoginConfig { - @Bean - fun clientRegistrationRepository(): ClientRegistrationRepository { - return InMemoryClientRegistrationRepository(googleClientRegistration()) - } - - private fun googleClientRegistration(): ClientRegistration { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build() - } -} ----- -==== - - -[[oauth2login-provide-websecurityconfigureradapter]] -=== Provide a WebSecurityConfigurerAdapter - -The following example shows how to provide a `WebSecurityConfigurerAdapter` with `@EnableWebSecurity` and enable OAuth 2.0 login through `httpSecurity.oauth2Login()`: - -.OAuth2 Login Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebSecurity -public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2Login(withDefaults()); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { - - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2Login { } - } - } -} ----- -==== - - -[[oauth2login-completely-override-autoconfiguration]] -=== Completely Override the Auto-configuration - -The following example shows how to completely override the auto-configuration by registering a `ClientRegistrationRepository` `@Bean` and providing a `WebSecurityConfigurerAdapter`. - -.Overriding the auto-configuration -==== -.Java -[source,java,role="primary",attrs="-attributes"] ----- -@Configuration -public class OAuth2LoginConfig { - - @EnableWebSecurity - public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2Login(withDefaults()); - } - } - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); - } - - private ClientRegistration googleClientRegistration() { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary",attrs="-attributes"] ----- -@Configuration -class OAuth2LoginConfig { - - @EnableWebSecurity - class OAuth2LoginSecurityConfig: WebSecurityConfigurerAdapter() { - - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2Login { } - } - } - } - - @Bean - fun clientRegistrationRepository(): ClientRegistrationRepository { - return InMemoryClientRegistrationRepository(googleClientRegistration()) - } - - private fun googleClientRegistration(): ClientRegistration { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build() - } -} ----- -==== - - -[[oauth2login-javaconfig-wo-boot]] -== Java Configuration without Spring Boot 2.x - -If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in `CommonOAuth2Provider` (for example, Google), apply the following configuration: - -.OAuth2 Login Configuration -==== -.Java -[source,java,role="primary"] ----- -@Configuration -public class OAuth2LoginConfig { - - @EnableWebSecurity - public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2Login(withDefaults()); - } - } - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); - } - - @Bean - public OAuth2AuthorizedClientService authorizedClientService( - ClientRegistrationRepository clientRegistrationRepository) { - return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); - } - - @Bean - public OAuth2AuthorizedClientRepository authorizedClientRepository( - OAuth2AuthorizedClientService authorizedClientService) { - return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); - } - - private ClientRegistration googleClientRegistration() { - return CommonOAuth2Provider.GOOGLE.getBuilder("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Configuration -open class OAuth2LoginConfig { - @EnableWebSecurity - open class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2Login { } - } - } - } - - @Bean - open fun clientRegistrationRepository(): ClientRegistrationRepository { - return InMemoryClientRegistrationRepository(googleClientRegistration()) - } - - @Bean - open fun authorizedClientService( - clientRegistrationRepository: ClientRegistrationRepository? - ): OAuth2AuthorizedClientService { - return InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository) - } - - @Bean - open fun authorizedClientRepository( - authorizedClientService: OAuth2AuthorizedClientService? - ): OAuth2AuthorizedClientRepository { - return AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService) - } - - private fun googleClientRegistration(): ClientRegistration { - return CommonOAuth2Provider.GOOGLE.getBuilder("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .build() - } -} ----- - -.Xml -[source,xml,role="secondary"] ----- - - - - - - - - - - - - - - ----- -==== - - [[oauth2login-advanced]] -== Advanced Configuration += Advanced Configuration `HttpSecurity.oauth2Login()` provides a number of configuration options for customizing OAuth 2.0 Login. The main configuration options are grouped into their protocol endpoint counterparts. @@ -770,7 +194,7 @@ The following sections go into more detail on each of the configuration options [[oauth2login-advanced-login-page]] -=== OAuth 2.0 Login Page +== OAuth 2.0 Login Page By default, the OAuth 2.0 Login Page is auto-generated by the `DefaultLoginPageGeneratingFilter`. The default login page shows each configured OAuth Client with its `ClientRegistration.clientName` as a link, which is capable of initiating the Authorization Request (or OAuth 2.0 Login). @@ -865,7 +289,7 @@ The following line shows an example: [[oauth2login-advanced-redirection-endpoint]] -=== Redirection Endpoint +== Redirection Endpoint The Redirection Endpoint is used by the Authorization Server for returning the Authorization Response (which contains the authorization credentials) to the client via the Resource Owner user-agent. @@ -956,7 +380,7 @@ return CommonOAuth2Provider.GOOGLE.getBuilder("google") [[oauth2login-advanced-userinfo-endpoint]] -=== UserInfo Endpoint +== UserInfo Endpoint The UserInfo Endpoint includes a number of configuration options, as described in the following sub-sections: @@ -966,7 +390,7 @@ The UserInfo Endpoint includes a number of configuration options, as described i [[oauth2login-advanced-map-authorities]] -==== Mapping User Authorities +=== Mapping User Authorities After the user successfully authenticates with the OAuth 2.0 Provider, the `OAuth2User.getAuthorities()` (or `OidcUser.getAuthorities()`) may be mapped to a new set of `GrantedAuthority` instances, which will be supplied to `OAuth2AuthenticationToken` when completing the authentication. @@ -980,7 +404,7 @@ There are a couple of options to choose from when mapping user authorities: [[oauth2login-advanced-map-authorities-grantedauthoritiesmapper]] -===== Using a GrantedAuthoritiesMapper +==== Using a GrantedAuthoritiesMapper Provide an implementation of `GrantedAuthoritiesMapper` and configure it as shown in the following example: @@ -1126,7 +550,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { ==== [[oauth2login-advanced-map-authorities-oauth2userservice]] -===== Delegation-based strategy with OAuth2UserService +==== Delegation-based strategy with OAuth2UserService This strategy is advanced compared to using a `GrantedAuthoritiesMapper`, however, it's also more flexible as it gives you access to the `OAuth2UserRequest` and `OAuth2User` (when using an OAuth 2.0 UserService) or `OidcUserRequest` and `OidcUser` (when using an OpenID Connect 1.0 UserService). @@ -1228,7 +652,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { [[oauth2login-advanced-oauth2-user-service]] -==== OAuth 2.0 UserService +=== OAuth 2.0 UserService `DefaultOAuth2UserService` is an implementation of an `OAuth2UserService` that supports standard OAuth 2.0 Provider's. @@ -1304,7 +728,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { [[oauth2login-advanced-oidc-user-service]] -==== OpenID Connect 1.0 UserService +=== OpenID Connect 1.0 UserService `OidcUserService` is an implementation of an `OAuth2UserService` that supports OpenID Connect 1.0 Provider's. @@ -1364,7 +788,7 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { [[oauth2login-advanced-idtoken-verify]] -=== ID Token Signature Verification +== ID Token Signature Verification OpenID Connect 1.0 Authentication introduces the https://openid.net/specs/openid-connect-core-1_0.html#IDToken[ID Token], which is a security token that contains Claims about the Authentication of an End-User by an Authorization Server when used by a Client. @@ -1409,7 +833,7 @@ If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authe [[oauth2login-advanced-oidc-logout]] -=== OpenID Connect 1.0 Logout +== OpenID Connect 1.0 Logout OpenID Connect Session Management 1.0 allows the ability to log out the End-User at the Provider using the Client. One of the strategies available is https://openid.net/specs/openid-connect-session-1_0.html#RPLogout[RP-Initiated Logout]. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc new file mode 100644 index 0000000000..129a4ae1dc --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc @@ -0,0 +1,567 @@ += Core Configuration + +[[oauth2login-sample-boot]] +== Spring Boot 2.x Sample + +Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login. + +This section shows how to configure the {gh-samples-url}/servlet/spring-boot/java/oauth2/login[*OAuth 2.0 Login sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: + +* <> +* <> +* <> +* <> + + +[[oauth2login-sample-initial-setup]] +=== Initial setup + +To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials. + +NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified]. + +Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0". + +After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. + + +[[oauth2login-sample-redirect-uri]] +=== Setting the redirect URI + +The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google and have granted access to the OAuth Client _(<>)_ on the Consent page. + +In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. + +TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. +The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration]. + +IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. +Also, see the supported xref:servlet/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. + + +[[oauth2login-sample-application-config]] +=== Configure application.yml + +Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. +To do so: + +. Go to `application.yml` and set the following configuration: ++ +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: <1> + google: <2> + client-id: google-client-id + client-secret: google-client-secret +---- ++ +.OAuth Client properties +==== +<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. +<2> Following the base property prefix is the ID for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration], such as google. +==== + +. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. + + +[[oauth2login-sample-boot-application]] +=== Boot up the application + +Launch the Spring Boot 2.x sample and go to `http://localhost:8080`. +You are then redirected to the default _auto-generated_ login page, which displays a link for Google. + +Click on the Google link, and you are then redirected to Google for authentication. + +After authenticating with your Google account credentials, the next page presented to you is the Consent screen. +The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier. +Click *Allow* to authorize the OAuth Client to access your email address and basic profile information. + +At this point, the OAuth Client retrieves your email address and basic profile information from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session. + + +[[oauth2login-boot-property-mappings]] +== Spring Boot 2.x Property Mappings + +The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[ClientRegistration] properties. + +|=== +|Spring Boot 2.x |ClientRegistration + +|`spring.security.oauth2.client.registration._[registrationId]_` +|`registrationId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-id` +|`clientId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-secret` +|`clientSecret` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-authentication-method` +|`clientAuthenticationMethod` + +|`spring.security.oauth2.client.registration._[registrationId]_.authorization-grant-type` +|`authorizationGrantType` + +|`spring.security.oauth2.client.registration._[registrationId]_.redirect-uri` +|`redirectUri` + +|`spring.security.oauth2.client.registration._[registrationId]_.scope` +|`scopes` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-name` +|`clientName` + +|`spring.security.oauth2.client.provider._[providerId]_.authorization-uri` +|`providerDetails.authorizationUri` + +|`spring.security.oauth2.client.provider._[providerId]_.token-uri` +|`providerDetails.tokenUri` + +|`spring.security.oauth2.client.provider._[providerId]_.jwk-set-uri` +|`providerDetails.jwkSetUri` + +|`spring.security.oauth2.client.provider._[providerId]_.issuer-uri` +|`providerDetails.issuerUri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-uri` +|`providerDetails.userInfoEndpoint.uri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-authentication-method` +|`providerDetails.userInfoEndpoint.authenticationMethod` + +|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute` +|`providerDetails.userInfoEndpoint.userNameAttributeName` +|=== + +[TIP] +A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint], by specifying the `spring.security.oauth2.client.provider._[providerId]_.issuer-uri` property. + + +[[oauth2login-common-oauth2-provider]] +== CommonOAuth2Provider + +`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta. + +For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a Provider. +Therefore, it makes sense to provide default values in order to reduce the required configuration. + +As demonstrated previously, when we <>, only the `client-id` and `client-secret` properties are required. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + google: + client-id: google-client-id + client-secret: google-client-secret +---- + +[TIP] +The auto-defaulting of client properties works seamlessly here because the `registrationId` (`google`) matches the `GOOGLE` `enum` (case-insensitive) in `CommonOAuth2Provider`. + +For cases where you may want to specify a different `registrationId`, such as `google-login`, you can still leverage auto-defaulting of client properties by configuring the `provider` property. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + google-login: <1> + provider: google <2> + client-id: google-client-id + client-secret: google-client-secret +---- +<1> The `registrationId` is set to `google-login`. +<2> The `provider` property is set to `google`, which will leverage the auto-defaulting of client properties set in `CommonOAuth2Provider.GOOGLE.getBuilder()`. + + +[[oauth2login-custom-provider-properties]] +== Configuring Custom Provider Properties + +There are some OAuth 2.0 Providers that support multi-tenancy, which results in different protocol endpoints for each tenant (or sub-domain). + +For example, an OAuth Client registered with Okta is assigned to a specific sub-domain and have their own protocol endpoints. + +For these cases, Spring Boot 2.x provides the following base property for configuring custom provider properties: `spring.security.oauth2.client.provider._[providerId]_`. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + provider: + okta: <1> + authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize + token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token + user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo + user-name-attribute: sub + jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys +---- + +<1> The base property (`spring.security.oauth2.client.provider.okta`) allows for custom configuration of protocol endpoint locations. + + +[[oauth2login-override-boot-autoconfig]] +== Overriding Spring Boot 2.x Auto-configuration + +The Spring Boot 2.x auto-configuration class for OAuth Client support is `OAuth2ClientAutoConfiguration`. + +It performs the following tasks: + +* Registers a `ClientRegistrationRepository` `@Bean` composed of `ClientRegistration`(s) from the configured OAuth Client properties. +* Provides a `WebSecurityConfigurerAdapter` `@Configuration` and enables OAuth 2.0 Login through `httpSecurity.oauth2Login()`. + +If you need to override the auto-configuration based on your specific requirements, you may do so in the following ways: + +* <> +* <> +* <> + + +[[oauth2login-register-clientregistrationrepository-bean]] +=== Register a ClientRegistrationRepository @Bean + +The following example shows how to register a `ClientRegistrationRepository` `@Bean`: + +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +@Configuration +public class OAuth2LoginConfig { + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +@Configuration +class OAuth2LoginConfig { + @Bean + fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +---- +==== + + +[[oauth2login-provide-websecurityconfigureradapter]] +=== Provide a WebSecurityConfigurerAdapter + +The following example shows how to provide a `WebSecurityConfigurerAdapter` with `@EnableWebSecurity` and enable OAuth 2.0 login through `httpSecurity.oauth2Login()`: + +.OAuth2 Login Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2Login(withDefaults()); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { } + } + } +} +---- +==== + + +[[oauth2login-completely-override-autoconfiguration]] +=== Completely Override the Auto-configuration + +The following example shows how to completely override the auto-configuration by registering a `ClientRegistrationRepository` `@Bean` and providing a `WebSecurityConfigurerAdapter`. + +.Overriding the auto-configuration +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +@Configuration +public class OAuth2LoginConfig { + + @EnableWebSecurity + public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2Login(withDefaults()); + } + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +@Configuration +class OAuth2LoginConfig { + + @EnableWebSecurity + class OAuth2LoginSecurityConfig: WebSecurityConfigurerAdapter() { + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { } + } + } + } + + @Bean + fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +---- +==== + + +[[oauth2login-javaconfig-wo-boot]] +== Java Configuration without Spring Boot 2.x + +If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in `CommonOAuth2Provider` (for example, Google), apply the following configuration: + +.OAuth2 Login Configuration +==== +.Java +[source,java,role="primary"] +---- +@Configuration +public class OAuth2LoginConfig { + + @EnableWebSecurity + public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .oauth2Login(withDefaults()); + } + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); + } + + @Bean + public OAuth2AuthorizedClientService authorizedClientService( + ClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository( + OAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + } + + private ClientRegistration googleClientRegistration() { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Configuration +open class OAuth2LoginConfig { + @EnableWebSecurity + open class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { } + } + } + } + + @Bean + open fun clientRegistrationRepository(): ClientRegistrationRepository { + return InMemoryClientRegistrationRepository(googleClientRegistration()) + } + + @Bean + open fun authorizedClientService( + clientRegistrationRepository: ClientRegistrationRepository? + ): OAuth2AuthorizedClientService { + return InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository) + } + + @Bean + open fun authorizedClientRepository( + authorizedClientService: OAuth2AuthorizedClientService? + ): OAuth2AuthorizedClientRepository { + return AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService) + } + + private fun googleClientRegistration(): ClientRegistration { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build() + } +} +---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + + + + + + + + + + +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/index.adoc new file mode 100644 index 0000000000..13adc137e5 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/index.adoc @@ -0,0 +1,8 @@ +[[oauth2login]] += OAuth 2.0 Login +:page-section-summary-toc: 1 + +The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google). +OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". + +NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework] and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[OpenID Connect Core 1.0]. diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index e7198c64f9..fc47e68d59 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -3,7 +3,7 @@ :icondir: icons Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security. -First, we see that, like xref:servlet/oauth2/oauth2-login.adoc[OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication. +First, we see that, like xref:servlet/oauth2/login/index.adoc[OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication. It does this through a series of redirects. .Redirecting to Asserting Party Authentication From 4a9637483ac538b8ff636dc851fdb7ed6cf38bb5 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 4 Nov 2021 12:45:39 -0600 Subject: [PATCH 009/589] Separate OAuth 2.0 Client Reactive Docs Issue gh-10367 --- docs/modules/ROOT/nav.adoc | 6 +- .../authorization-grants.adoc} | 1046 +---------------- .../oauth2/client/authorized-clients.adoc | 250 ++++ .../oauth2/client/client-authentication.adoc | 151 +++ .../pages/reactive/oauth2/client/core.adoc | 429 +++++++ .../pages/reactive/oauth2/client/index.adoc | 123 ++ .../ROOT/pages/reactive/oauth2/index.adoc | 2 +- 7 files changed, 992 insertions(+), 1015 deletions(-) rename docs/modules/ROOT/pages/reactive/oauth2/{oauth2-client.adoc => client/authorization-grants.adoc} (52%) create mode 100644 docs/modules/ROOT/pages/reactive/oauth2/client/authorized-clients.adoc create mode 100644 docs/modules/ROOT/pages/reactive/oauth2/client/client-authentication.adoc create mode 100644 docs/modules/ROOT/pages/reactive/oauth2/client/core.adoc create mode 100644 docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 3a01681136..6e5e30a003 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -122,7 +122,11 @@ *** xref:reactive/authorization/method.adoc[EnableReactiveMethodSecurity] ** xref:reactive/oauth2/index.adoc[OAuth2] *** xref:reactive/oauth2/login.adoc[OAuth2 Log In] -*** xref:reactive/oauth2/oauth2-client.adoc[OAuth2 Client] +*** xref:reactive/oauth2/client/index.adoc[OAuth2 Client] +**** xref:reactive/oauth2/client/core.adoc[Core Interfaces and Classes] +**** xref:reactive/oauth2/client/authorization-grants.adoc[OAuth2 Authorization Grants] +**** xref:reactive/oauth2/client/client-authentication.adoc[OAuth2 Client Authentication] +**** xref:reactive/oauth2/client/authorized-clients.adoc[OAuth2 Authorized Clients] *** xref:reactive/oauth2/resource-server/index.adoc[OAuth2 Resource Server] **** xref:reactive/oauth2/resource-server/jwt.adoc[JWT] **** xref:reactive/oauth2/resource-server/opaque-token.adoc[Opaque Token] diff --git a/docs/modules/ROOT/pages/reactive/oauth2/oauth2-client.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc similarity index 52% rename from docs/modules/ROOT/pages/reactive/oauth2/oauth2-client.adoc rename to docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc index a44e3312b3..11fe4d541b 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/oauth2-client.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc @@ -1,596 +1,21 @@ -[[webflux-oauth2-client]] -= OAuth 2.0 Client - -The OAuth 2.0 Client features provide support for the Client role as defined in the https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Framework]. - -At a high-level, the core features available are: - -.Authorization Grant support -* https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] -* https://tools.ietf.org/html/rfc6749#section-6[Refresh Token] -* https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] -* https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] -* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer] - -.Client Authentication support -* https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] - -.HTTP Client support -* <> (for requesting protected resources) - -The `ServerHttpSecurity.oauth2Client()` DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client. - -The following code shows the complete configuration options provided by the `ServerHttpSecurity.oauth2Client()` DSL: - -.OAuth2 Client Configuration Options -==== -.Java -[source,java,role="primary"] ----- -@EnableWebFluxSecurity -public class OAuth2ClientSecurityConfig { - - @Bean - public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - http - .oauth2Client(oauth2 -> oauth2 - .clientRegistrationRepository(this.clientRegistrationRepository()) - .authorizedClientRepository(this.authorizedClientRepository()) - .authorizationRequestRepository(this.authorizationRequestRepository()) - .authenticationConverter(this.authenticationConverter()) - .authenticationManager(this.authenticationManager()) - ); - - return http.build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebFluxSecurity -class OAuth2ClientSecurityConfig { - - @Bean - fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { - oauth2Client { - clientRegistrationRepository = clientRegistrationRepository() - authorizedClientRepository = authorizedClientRepository() - authorizationRequestRepository = authorizedRequestRepository() - authenticationConverter = authenticationConverter() - authenticationManager = authenticationManager() - } - } - - return http.build() - } -} ----- -==== - -The `ReactiveOAuth2AuthorizedClientManager` is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more `ReactiveOAuth2AuthorizedClientProvider`(s). - -The following code shows an example of how to register a `ReactiveOAuth2AuthorizedClientManager` `@Bean` and associate it with a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( - ReactiveClientRegistrationRepository clientRegistrationRepository, - ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { - - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ReactiveClientRegistrationRepository, - authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { - val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build() - val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - -The following sections will go into more detail on the core components used by OAuth 2.0 Client and the configuration options available: - -* <> -** <> -** <> -** <> -** <> -** <> -* <> -** <> -** <> -** <> -** <> -** <> -* <> -** <> -* <> -** <> -* <> - - -[[oauth2Client-core-interface-class]] -== Core Interfaces / Classes - - -[[oauth2Client-client-registration]] -=== ClientRegistration - -`ClientRegistration` is a representation of a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. - -A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details. - -`ClientRegistration` and its properties are defined as follows: - -[source,java] ----- -public final class ClientRegistration { - private String registrationId; <1> - private String clientId; <2> - private String clientSecret; <3> - private ClientAuthenticationMethod clientAuthenticationMethod; <4> - private AuthorizationGrantType authorizationGrantType; <5> - private String redirectUri; <6> - private Set scopes; <7> - private ProviderDetails providerDetails; - private String clientName; <8> - - public class ProviderDetails { - private String authorizationUri; <9> - private String tokenUri; <10> - private UserInfoEndpoint userInfoEndpoint; - private String jwkSetUri; <11> - private String issuerUri; <12> - private Map configurationMetadata; <13> - - public class UserInfoEndpoint { - private String uri; <14> - private AuthenticationMethod authenticationMethod; <15> - private String userNameAttributeName; <16> - - } - } -} ----- -<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`. -<2> `clientId`: The client identifier. -<3> `clientSecret`: The client secret. -<4> `clientAuthenticationMethod`: The method used to authenticate the Client with the Provider. -The supported values are *client_secret_basic*, *client_secret_post*, *private_key_jwt*, *client_secret_jwt* and *none* https://tools.ietf.org/html/rfc6749#section-2.1[(public clients)]. -<5> `authorizationGrantType`: The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. - The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. -<6> `redirectUri`: The client's registered redirect URI that the _Authorization Server_ redirects the end-user's user-agent - to after the end-user has authenticated and authorized access to the client. -<7> `scopes`: The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. -<8> `clientName`: A descriptive name used for the client. -The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. -<9> `authorizationUri`: The Authorization Endpoint URI for the Authorization Server. -<10> `tokenUri`: The Token Endpoint URI for the Authorization Server. -<11> `jwkSetUri`: The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (JWK)] Set from the Authorization Server, - which contains the cryptographic key(s) used to verify the https://tools.ietf.org/html/rfc7515[JSON Web Signature (JWS)] of the ID Token and optionally the UserInfo Response. -<12> `issuerUri`: Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server. -<13> `configurationMetadata`: The https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration Information]. - This information will only be available if the Spring Boot 2.x property `spring.security.oauth2.client.provider.[providerId].issuerUri` is configured. -<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. -<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint. -The supported values are *header*, *form* and *query*. -<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. - -A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. - -`ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example: - -==== -.Java -[source,java,role="primary"] ----- -ClientRegistration clientRegistration = - ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() ----- -==== - -The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response. - -As an alternative, you can use `ClientRegistrations.fromOidcIssuerLocation()` to only query the OpenID Connect Provider's Configuration endpoint. - -[[oauth2Client-client-registration-repo]] -=== ReactiveClientRegistrationRepository - -The `ReactiveClientRegistrationRepository` serves as a repository for OAuth 2.0 / OpenID Connect 1.0 `ClientRegistration`(s). - -[NOTE] -Client registration information is ultimately stored and owned by the associated Authorization Server. -This repository provides the ability to retrieve a sub-set of the primary client registration information, which is stored with the Authorization Server. - -Spring Boot 2.x auto-configuration binds each of the properties under `spring.security.oauth2.client.registration._[registrationId]_` to an instance of `ClientRegistration` and then composes each of the `ClientRegistration` instance(s) within a `ReactiveClientRegistrationRepository`. - -[NOTE] -The default implementation of `ReactiveClientRegistrationRepository` is `InMemoryReactiveClientRegistrationRepository`. - -The auto-configuration also registers the `ReactiveClientRegistrationRepository` as a `@Bean` in the `ApplicationContext` so that it is available for dependency-injection, if needed by the application. - -The following listing shows an example: - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @Autowired - private ReactiveClientRegistrationRepository clientRegistrationRepository; - - @GetMapping("/") - public Mono index() { - return this.clientRegistrationRepository.findByRegistrationId("okta") - ... - .thenReturn("index"); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - - @Autowired - private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository - - @GetMapping("/") - fun index(): Mono { - return this.clientRegistrationRepository.findByRegistrationId("okta") - ... - .thenReturn("index") - } -} ----- -==== - -[[oauth2Client-authorized-client]] -=== OAuth2AuthorizedClient - -`OAuth2AuthorizedClient` is a representation of an Authorized Client. -A client is considered to be authorized when the end-user (Resource Owner) has granted authorization to the client to access its protected resources. - -`OAuth2AuthorizedClient` serves the purpose of associating an `OAuth2AccessToken` (and optional `OAuth2RefreshToken`) to a `ClientRegistration` (client) and resource owner, who is the `Principal` end-user that granted the authorization. - - -[[oauth2Client-authorized-repo-service]] -=== ServerOAuth2AuthorizedClientRepository / ReactiveOAuth2AuthorizedClientService - -`ServerOAuth2AuthorizedClientRepository` is responsible for persisting `OAuth2AuthorizedClient`(s) between web requests. -Whereas, the primary role of `ReactiveOAuth2AuthorizedClientService` is to manage `OAuth2AuthorizedClient`(s) at the application-level. - -From a developer perspective, the `ServerOAuth2AuthorizedClientRepository` or `ReactiveOAuth2AuthorizedClientService` provides the capability to lookup an `OAuth2AccessToken` associated with a client so that it may be used to initiate a protected resource request. - -The following listing shows an example: - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @Autowired - private ReactiveOAuth2AuthorizedClientService authorizedClientService; - - @GetMapping("/") - public Mono index(Authentication authentication) { - return this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()) - .map(OAuth2AuthorizedClient::getAccessToken) - ... - .thenReturn("index"); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - - @Autowired - private lateinit var authorizedClientService: ReactiveOAuth2AuthorizedClientService - - @GetMapping("/") - fun index(authentication: Authentication): Mono { - return this.authorizedClientService.loadAuthorizedClient("okta", authentication.name) - .map { it.accessToken } - ... - .thenReturn("index") - } -} ----- -==== - -[NOTE] -Spring Boot 2.x auto-configuration registers an `ServerOAuth2AuthorizedClientRepository` and/or `ReactiveOAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`. -However, the application may choose to override and register a custom `ServerOAuth2AuthorizedClientRepository` or `ReactiveOAuth2AuthorizedClientService` `@Bean`. - -The default implementation of `ReactiveOAuth2AuthorizedClientService` is `InMemoryReactiveOAuth2AuthorizedClientService`, which stores `OAuth2AuthorizedClient`(s) in-memory. - -Alternatively, the R2DBC implementation `R2dbcReactiveOAuth2AuthorizedClientService` may be configured for persisting `OAuth2AuthorizedClient`(s) in a database. - -[NOTE] -`R2dbcReactiveOAuth2AuthorizedClientService` depends on the table definition described in xref:servlet/appendix/database-schema.adoc#dbschema-oauth2-client[ OAuth 2.0 Client Schema]. - - -[[oauth2Client-authorized-manager-provider]] -=== ReactiveOAuth2AuthorizedClientManager / ReactiveOAuth2AuthorizedClientProvider - -The `ReactiveOAuth2AuthorizedClientManager` is responsible for the overall management of `OAuth2AuthorizedClient`(s). - -The primary responsibilities include: - -* Authorizing (or re-authorizing) an OAuth 2.0 Client, using a `ReactiveOAuth2AuthorizedClientProvider`. -* Delegating the persistence of an `OAuth2AuthorizedClient`, typically using a `ReactiveOAuth2AuthorizedClientService` or `ServerOAuth2AuthorizedClientRepository`. -* Delegating to a `ReactiveOAuth2AuthorizationSuccessHandler` when an OAuth 2.0 Client has been successfully authorized (or re-authorized). -* Delegating to a `ReactiveOAuth2AuthorizationFailureHandler` when an OAuth 2.0 Client fails to authorize (or re-authorize). - -A `ReactiveOAuth2AuthorizedClientProvider` implements a strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. -Implementations will typically implement an authorization grant type, eg. `authorization_code`, `client_credentials`, etc. - -The default implementation of `ReactiveOAuth2AuthorizedClientManager` is `DefaultReactiveOAuth2AuthorizedClientManager`, which is associated with a `ReactiveOAuth2AuthorizedClientProvider` that may support multiple authorization grant types using a delegation-based composite. -The `ReactiveOAuth2AuthorizedClientProviderBuilder` may be used to configure and build the delegation-based composite. - -The following code shows an example of how to configure and build a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( - ReactiveClientRegistrationRepository clientRegistrationRepository, - ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { - - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - - DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ReactiveClientRegistrationRepository, - authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { - val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build() - val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - -When an authorization attempt succeeds, the `DefaultReactiveOAuth2AuthorizedClientManager` will delegate to the `ReactiveOAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `ServerOAuth2AuthorizedClientRepository`. -In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `ServerOAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler`. -The default behaviour may be customized via `setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)` and `setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)`. - -The `DefaultReactiveOAuth2AuthorizedClientManager` is also associated with a `contextAttributesMapper` of type `Function>>`, which is responsible for mapping attribute(s) from the `OAuth2AuthorizeRequest` to a `Map` of attributes to be associated to the `OAuth2AuthorizationContext`. -This can be useful when you need to supply a `ReactiveOAuth2AuthorizedClientProvider` with required (supported) attribute(s), eg. the `PasswordReactiveOAuth2AuthorizedClientProvider` requires the resource owner's `username` and `password` to be available in `OAuth2AuthorizationContext.getAttributes()`. - -The following code shows an example of the `contextAttributesMapper`: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( - ReactiveClientRegistrationRepository clientRegistrationRepository, - ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { - - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .password() - .refreshToken() - .build(); - - DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = - new DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, - // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` - authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); - - return authorizedClientManager; -} - -private Function>> contextAttributesMapper() { - return authorizeRequest -> { - Map contextAttributes = Collections.emptyMap(); - ServerWebExchange exchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName()); - ServerHttpRequest request = exchange.getRequest(); - String username = request.getQueryParams().getFirst(OAuth2ParameterNames.USERNAME); - String password = request.getQueryParams().getFirst(OAuth2ParameterNames.PASSWORD); - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - contextAttributes = new HashMap<>(); - - // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes - contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); - contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); - } - return Mono.just(contextAttributes); - }; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ReactiveClientRegistrationRepository, - authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { - val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .password() - .refreshToken() - .build() - val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - - // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, - // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` - authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) - return authorizedClientManager -} - -private fun contextAttributesMapper(): Function>> { - return Function { authorizeRequest -> - var contextAttributes: MutableMap = mutableMapOf() - val exchange: ServerWebExchange = authorizeRequest.getAttribute(ServerWebExchange::class.java.name)!! - val request: ServerHttpRequest = exchange.request - val username: String? = request.queryParams.getFirst(OAuth2ParameterNames.USERNAME) - val password: String? = request.queryParams.getFirst(OAuth2ParameterNames.PASSWORD) - if (StringUtils.hasText(username) && StringUtils.hasText(password)) { - contextAttributes = hashMapOf() - - // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes - contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username!! - contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password!! - } - Mono.just(contextAttributes) - } -} ----- -==== - -The `DefaultReactiveOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `ServerWebExchange`. -When operating *_outside_* of a `ServerWebExchange` context, use `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` instead. - -A _service application_ is a common use case for when to use an `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`. -Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account. -An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application. - -The following code shows an example of how to configure an `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( - ReactiveClientRegistrationRepository clientRegistrationRepository, - ReactiveOAuth2AuthorizedClientService authorizedClientService) { - - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .clientCredentials() - .build(); - - AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = - new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientService); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun authorizedClientManager( - clientRegistrationRepository: ReactiveClientRegistrationRepository, - authorizedClientService: ReactiveOAuth2AuthorizedClientService): ReactiveOAuth2AuthorizedClientManager { - val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .clientCredentials() - .build() - val authorizedClientManager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientService) - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) - return authorizedClientManager -} ----- -==== - - [[oauth2Client-auth-grant-support]] -== Authorization Grant Support += Authorization Grant Support [[oauth2Client-auth-code-grant]] -=== Authorization Code +== Authorization Code [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] grant. -==== Obtaining Authorization +=== Obtaining Authorization [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.1.1[Authorization Request/Response] protocol flow for the Authorization Code grant. -==== Initiating the Authorization Request +=== Initiating the Authorization Request The `OAuth2AuthorizationRequestRedirectWebFilter` uses a `ServerOAuth2AuthorizationRequestResolver` to resolve an `OAuth2AuthorizationRequest` and initiate the Authorization Code grant flow by redirecting the end-user's user-agent to the Authorization Server's Authorization Endpoint. @@ -671,7 +96,7 @@ spring: Configuring the `redirect-uri` with `URI` template variables is especially useful when the OAuth 2.0 Client is running behind a xref:features/exploits/http.adoc#http-proxy-server[Proxy Server]. This ensures that the `X-Forwarded-*` headers are used when expanding the `redirect-uri`. -==== Customizing the Authorization Request +=== Customizing the Authorization Request One of the primary use cases a `ServerOAuth2AuthorizationRequestResolver` can realize is the ability to customize the Authorization Request with additional parameters above the standard parameters defined in the OAuth 2.0 Authorization Framework. @@ -818,7 +243,7 @@ private fun authorizationRequestCustomizer(): Consumer>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-4.1.3[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the Authorization Code grant are added directly to the body of the request by the `WebClientReactiveAuthorizationCodeTokenResponseClient`. @@ -891,12 +316,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactiveAuthorizationCodeTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactiveAuthorizationCodeTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -959,13 +384,13 @@ class OAuth2ClientSecurityConfig { [[oauth2Client-refresh-token-grant]] -=== Refresh Token +== Refresh Token [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.5[Refresh Token]. -==== Refreshing an Access Token +=== Refreshing an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-6[Access Token Request/Response] protocol flow for the Refresh Token grant. @@ -975,7 +400,7 @@ The default implementation of `ReactiveOAuth2AccessTokenResponseClient` for the The `WebClientReactiveRefreshTokenTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `WebClientReactiveRefreshTokenTokenResponseClient.setParametersConverter()` with a custom `Converter>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-6[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the Refresh Token grant are added directly to the body of the request by the `WebClientReactiveRefreshTokenTokenResponseClient`. @@ -987,12 +412,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactiveRefreshTokenTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactiveRefreshTokenTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -1043,13 +468,13 @@ If the `OAuth2AuthorizedClient.getRefreshToken()` is available and the `OAuth2Au [[oauth2Client-client-creds-grant]] -=== Client Credentials +== Client Credentials [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.4.2[Access Token Request/Response] protocol flow for the Client Credentials grant. @@ -1059,7 +484,7 @@ The default implementation of `ReactiveOAuth2AccessTokenResponseClient` for the The `WebClientReactiveClientCredentialsTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `WebClientReactiveClientCredentialsTokenResponseClient.setParametersConverter()` with a custom `Converter>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the Client Credentials grant are added directly to the body of the request by the `WebClientReactiveClientCredentialsTokenResponseClient`. @@ -1071,12 +496,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactiveClientCredentialsTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactiveClientCredentialsTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -1119,7 +544,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) `ReactiveOAuth2AuthorizedClientProviderBuilder.builder().clientCredentials()` configures a `ClientCredentialsReactiveOAuth2AuthorizedClientProvider`, which is an implementation of a `ReactiveOAuth2AuthorizedClientProvider` for the Client Credentials grant. -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1240,13 +665,13 @@ If not provided, it will be obtained from the https://projectreactor.io/docs/cor [[oauth2Client-password-grant]] -=== Resource Owner Password Credentials +== Resource Owner Password Credentials [NOTE] Please refer to the OAuth 2.0 Authorization Framework for further details on the https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://tools.ietf.org/html/rfc6749#section-4.3.2[Access Token Request/Response] protocol flow for the Resource Owner Password Credentials grant. @@ -1256,7 +681,7 @@ The default implementation of `ReactiveOAuth2AccessTokenResponseClient` for the The `WebClientReactivePasswordTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `WebClientReactivePasswordTokenResponseClient.setParametersConverter()` with a custom `Converter>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the Resource Owner Password Credentials grant are added directly to the body of the request by the `WebClientReactivePasswordTokenResponseClient`. @@ -1268,12 +693,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactivePasswordTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactivePasswordTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -1317,7 +742,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) `ReactiveOAuth2AuthorizedClientProviderBuilder.builder().password()` configures a `PasswordReactiveOAuth2AuthorizedClientProvider`, which is an implementation of a `ReactiveOAuth2AuthorizedClientProvider` for the Resource Owner Password Credentials grant. -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1483,13 +908,13 @@ If not provided, it will be obtained from the https://projectreactor.io/docs/cor [[oauth2Client-jwt-bearer-grant]] -=== JWT Bearer +== JWT Bearer [NOTE] Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on the https://datatracker.ietf.org/doc/html/rfc7523[JWT Bearer] grant. -==== Requesting an Access Token +=== Requesting an Access Token [NOTE] Please refer to the https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[Access Token Request/Response] protocol flow for the JWT Bearer grant. @@ -1499,7 +924,7 @@ The default implementation of `ReactiveOAuth2AccessTokenResponseClient` for the The `WebClientReactiveJwtBearerTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. -==== Customizing the Access Token Request +=== Customizing the Access Token Request If you need to customize the pre-processing of the Token Request, you can provide `WebClientReactiveJwtBearerTokenResponseClient.setParametersConverter()` with a custom `Converter>`. The default implementation builds a `MultiValueMap` containing only the `grant_type` parameter of a standard https://tools.ietf.org/html/rfc6749#section-4.4.2[OAuth 2.0 Access Token Request] which is used to construct the request. Other parameters required by the JWT Bearer grant are added directly to the body of the request by the `WebClientReactiveJwtBearerTokenResponseClient`. @@ -1510,12 +935,12 @@ If you prefer to only add additional parameters, you can instead provide `WebCli IMPORTANT: The custom `Converter` must return valid parameters of an OAuth 2.0 Access Token Request that is understood by the intended OAuth 2.0 Provider. -==== Customizing the Access Token Response +=== Customizing the Access Token Response On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `WebClientReactiveJwtBearerTokenResponseClient.setBodyExtractor()` with a custom configured `BodyExtractor, ReactiveHttpInputMessage>` that is used for converting the OAuth 2.0 Access Token Response to an `OAuth2AccessTokenResponse`. The default implementation provided by `OAuth2BodyExtractors.oauth2AccessTokenResponse()` parses the response and handles errors accordingly. -==== Customizing the `WebClient` +=== Customizing the `WebClient` Alternatively, if your requirements are more advanced, you can take full control of the request/response by simply providing `WebClientReactiveJwtBearerTokenResponseClient.setWebClient()` with a custom configured `WebClient`. @@ -1560,7 +985,7 @@ authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) ---- ==== -==== Using the Access Token +=== Using the Access Token Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: @@ -1673,408 +1098,3 @@ class OAuth2ResourceServerController { } ---- ==== - - -[[oauth2Client-client-auth-support]] -== Client Authentication Support - - -[[oauth2Client-jwt-bearer-auth]] -=== JWT Bearer - -[NOTE] -Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] Client Authentication. - -The default implementation for JWT Bearer Client Authentication is `NimbusJwtClientAuthenticationParametersConverter`, -which is a `Converter` that customizes the Token Request parameters by adding -a signed JSON Web Token (JWS) in the `client_assertion` parameter. - -The `java.security.PrivateKey` or `javax.crypto.SecretKey` used for signing the JWS -is supplied by the `com.nimbusds.jose.jwk.JWK` resolver associated with `NimbusJwtClientAuthenticationParametersConverter`. - - -==== Authenticate using `private_key_jwt` - -Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-authentication-method: private_key_jwt - authorization-grant-type: authorization_code - ... ----- - -The following example shows how to configure `WebClientReactiveAuthorizationCodeTokenResponseClient`: - -==== -.Java -[source,java,role="primary"] ----- -Function jwkResolver = (clientRegistration) -> { - if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { - // Assuming RSA key type - RSAPublicKey publicKey = ... - RSAPrivateKey privateKey = ... - return new RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build(); - } - return null; -}; - -WebClientReactiveAuthorizationCodeTokenResponseClient tokenResponseClient = - new WebClientReactiveAuthorizationCodeTokenResponseClient(); -tokenResponseClient.addParametersConverter( - new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val jwkResolver: Function = - Function { clientRegistration -> - if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { - // Assuming RSA key type - var publicKey: RSAPublicKey = ... - var privateKey: RSAPrivateKey = ... - RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build() - } - null - } - -val tokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient() -tokenResponseClient.addParametersConverter( - NimbusJwtClientAuthenticationParametersConverter(jwkResolver) -) ----- -==== - - -==== Authenticate using `client_secret_jwt` - -Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-secret: okta-client-secret - client-authentication-method: client_secret_jwt - authorization-grant-type: client_credentials - ... ----- - -The following example shows how to configure `WebClientReactiveClientCredentialsTokenResponseClient`: - -==== -.Java -[source,java,role="primary"] ----- -Function jwkResolver = (clientRegistration) -> { - if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { - SecretKeySpec secretKey = new SecretKeySpec( - clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), - "HmacSHA256"); - return new OctetSequenceKey.Builder(secretKey) - .keyID(UUID.randomUUID().toString()) - .build(); - } - return null; -}; - -WebClientReactiveClientCredentialsTokenResponseClient tokenResponseClient = - new WebClientReactiveClientCredentialsTokenResponseClient(); -tokenResponseClient.addParametersConverter( - new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -val jwkResolver = Function { clientRegistration: ClientRegistration -> - if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { - val secretKey = SecretKeySpec( - clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), - "HmacSHA256" - ) - OctetSequenceKey.Builder(secretKey) - .keyID(UUID.randomUUID().toString()) - .build() - } - null -} - -val tokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient() -tokenResponseClient.addParametersConverter( - NimbusJwtClientAuthenticationParametersConverter(jwkResolver) -) ----- -==== - - -[[oauth2Client-additional-features]] -== Additional Features - - -[[oauth2Client-registered-authorized-client]] -=== Resolving an Authorized Client - -The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`. -This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `ReactiveOAuth2AuthorizedClientManager` or `ReactiveOAuth2AuthorizedClientService`. - -==== -.Java -[source,java,role="primary"] ----- -@Controller -public class OAuth2ClientController { - - @GetMapping("/") - public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { - return Mono.just(authorizedClient.getAccessToken()) - ... - .thenReturn("index"); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Controller -class OAuth2ClientController { - @GetMapping("/") - fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { - return Mono.just(authorizedClient.accessToken) - ... - .thenReturn("index") - } -} ----- -==== - -The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses a <> and therefore inherits it's capabilities. - - -[[oauth2Client-webclient-webflux]] -== WebClient integration for Reactive Environments - -The OAuth 2.0 Client support integrates with `WebClient` using an `ExchangeFilterFunction`. - -The `ServerOAuth2AuthorizedClientExchangeFilterFunction` provides a simple mechanism for requesting protected resources by using an `OAuth2AuthorizedClient` and including the associated `OAuth2AccessToken` as a Bearer Token. -It directly uses an <> and therefore inherits the following capabilities: - -* An `OAuth2AccessToken` will be requested if the client has not yet been authorized. -** `authorization_code` - triggers the Authorization Request redirect to initiate the flow -** `client_credentials` - the access token is obtained directly from the Token Endpoint -** `password` - the access token is obtained directly from the Token Endpoint -* If the `OAuth2AccessToken` is expired, it will be refreshed (or renewed) if a `ReactiveOAuth2AuthorizedClientProvider` is available to perform the authorization - -The following code shows an example of how to configure `WebClient` with OAuth 2.0 Client support: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { - ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - return WebClient.builder() - .filter(oauth2Client) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { - val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - return WebClient.builder() - .filter(oauth2Client) - .build() -} ----- -==== - -=== Providing the Authorized Client - -The `ServerOAuth2AuthorizedClientExchangeFilterFunction` determines the client to use (for a request) by resolving the `OAuth2AuthorizedClient` from the `ClientRequest.attributes()` (request attributes). - -The following code shows how to set an `OAuth2AuthorizedClient` as a request attribute: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/") -public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { - String resourceUri = ... - - return webClient - .get() - .uri(resourceUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) <1> - .retrieve() - .bodyToMono(String.class) - ... - .thenReturn("index"); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/") -fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { - val resourceUri: String = ... - - return webClient - .get() - .uri(resourceUri) - .attributes(oauth2AuthorizedClient(authorizedClient)) <1> - .retrieve() - .bodyToMono() - ... - .thenReturn("index") -} ----- -==== - -<1> `oauth2AuthorizedClient()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`. - -The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute: - -==== -.Java -[source,java,role="primary"] ----- -@GetMapping("/") -public Mono index() { - String resourceUri = ... - - return webClient - .get() - .uri(resourceUri) - .attributes(clientRegistrationId("okta")) <1> - .retrieve() - .bodyToMono(String.class) - ... - .thenReturn("index"); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@GetMapping("/") -fun index(): Mono { - val resourceUri: String = ... - - return webClient - .get() - .uri(resourceUri) - .attributes(clientRegistrationId("okta")) <1> - .retrieve() - .bodyToMono() - ... - .thenReturn("index") -} ----- -==== -<1> `clientRegistrationId()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`. - - -=== Defaulting the Authorized Client - -If neither `OAuth2AuthorizedClient` or `ClientRegistration.getRegistrationId()` is provided as a request attribute, the `ServerOAuth2AuthorizedClientExchangeFilterFunction` can determine the _default_ client to use depending on it's configuration. - -If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authenticated using `ServerHttpSecurity.oauth2Login()`, the `OAuth2AccessToken` associated with the current `OAuth2AuthenticationToken` is used. - -The following code shows the specific configuration: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { - ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - oauth2Client.setDefaultOAuth2AuthorizedClient(true); - return WebClient.builder() - .filter(oauth2Client) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { - val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - oauth2Client.setDefaultOAuth2AuthorizedClient(true) - return WebClient.builder() - .filter(oauth2Client) - .build() -} ----- -==== - -[WARNING] -It is recommended to be cautious with this feature since all HTTP requests will receive the access token. - -Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a valid `ClientRegistration`, the `OAuth2AccessToken` associated with the `OAuth2AuthorizedClient` is used. - -The following code shows the specific configuration: - -==== -.Java -[source,java,role="primary"] ----- -@Bean -WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { - ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = - new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); - oauth2Client.setDefaultClientRegistrationId("okta"); - return WebClient.builder() - .filter(oauth2Client) - .build(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { - val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) - oauth2Client.setDefaultClientRegistrationId("okta") - return WebClient.builder() - .filter(oauth2Client) - .build() -} ----- -==== - -[WARNING] -It is recommended to be cautious with this feature since all HTTP requests will receive the access token. diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/authorized-clients.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/authorized-clients.adoc new file mode 100644 index 0000000000..ef42bab6a1 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/authorized-clients.adoc @@ -0,0 +1,250 @@ +[[oauth2Client-additional-features]] += Authorized Clients + + +[[oauth2Client-registered-authorized-client]] +== Resolving an Authorized Client + +The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`. +This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `ReactiveOAuth2AuthorizedClientManager` or `ReactiveOAuth2AuthorizedClientService`. + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @GetMapping("/") + public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + return Mono.just(authorizedClient.getAccessToken()) + ... + .thenReturn("index"); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + @GetMapping("/") + fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { + return Mono.just(authorizedClient.accessToken) + ... + .thenReturn("index") + } +} +---- +==== + +The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses a <> and therefore inherits it's capabilities. + + +[[oauth2Client-webclient-webflux]] +== WebClient integration for Reactive Environments + +The OAuth 2.0 Client support integrates with `WebClient` using an `ExchangeFilterFunction`. + +The `ServerOAuth2AuthorizedClientExchangeFilterFunction` provides a simple mechanism for requesting protected resources by using an `OAuth2AuthorizedClient` and including the associated `OAuth2AccessToken` as a Bearer Token. +It directly uses an <> and therefore inherits the following capabilities: + +* An `OAuth2AccessToken` will be requested if the client has not yet been authorized. +** `authorization_code` - triggers the Authorization Request redirect to initiate the flow +** `client_credentials` - the access token is obtained directly from the Token Endpoint +** `password` - the access token is obtained directly from the Token Endpoint +* If the `OAuth2AccessToken` is expired, it will be refreshed (or renewed) if a `ReactiveOAuth2AuthorizedClientProvider` is available to perform the authorization + +The following code shows an example of how to configure `WebClient` with OAuth 2.0 Client support: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + return WebClient.builder() + .filter(oauth2Client) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { + val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + return WebClient.builder() + .filter(oauth2Client) + .build() +} +---- +==== + +=== Providing the Authorized Client + +The `ServerOAuth2AuthorizedClientExchangeFilterFunction` determines the client to use (for a request) by resolving the `OAuth2AuthorizedClient` from the `ClientRequest.attributes()` (request attributes). + +The following code shows how to set an `OAuth2AuthorizedClient` as a request attribute: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/") +public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + String resourceUri = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) <1> + .retrieve() + .bodyToMono(String.class) + ... + .thenReturn("index"); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { + val resourceUri: String = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) <1> + .retrieve() + .bodyToMono() + ... + .thenReturn("index") +} +---- +==== + +<1> `oauth2AuthorizedClient()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`. + +The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute: + +==== +.Java +[source,java,role="primary"] +---- +@GetMapping("/") +public Mono index() { + String resourceUri = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) <1> + .retrieve() + .bodyToMono(String.class) + ... + .thenReturn("index"); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(): Mono { + val resourceUri: String = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) <1> + .retrieve() + .bodyToMono() + ... + .thenReturn("index") +} +---- +==== +<1> `clientRegistrationId()` is a `static` method in `ServerOAuth2AuthorizedClientExchangeFilterFunction`. + + +=== Defaulting the Authorized Client + +If neither `OAuth2AuthorizedClient` or `ClientRegistration.getRegistrationId()` is provided as a request attribute, the `ServerOAuth2AuthorizedClientExchangeFilterFunction` can determine the _default_ client to use depending on it's configuration. + +If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authenticated using `ServerHttpSecurity.oauth2Login()`, the `OAuth2AccessToken` associated with the current `OAuth2AuthenticationToken` is used. + +The following code shows the specific configuration: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultOAuth2AuthorizedClient(true); + return WebClient.builder() + .filter(oauth2Client) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { + val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultOAuth2AuthorizedClient(true) + return WebClient.builder() + .filter(oauth2Client) + .build() +} +---- +==== + +[WARNING] +It is recommended to be cautious with this feature since all HTTP requests will receive the access token. + +Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a valid `ClientRegistration`, the `OAuth2AccessToken` associated with the `OAuth2AuthorizedClient` is used. + +The following code shows the specific configuration: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultClientRegistrationId("okta"); + return WebClient.builder() + .filter(oauth2Client) + .build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { + val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultClientRegistrationId("okta") + return WebClient.builder() + .filter(oauth2Client) + .build() +} +---- +==== + +[WARNING] +It is recommended to be cautious with this feature since all HTTP requests will receive the access token. diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/client-authentication.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/client-authentication.adoc new file mode 100644 index 0000000000..93bedb5394 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/client-authentication.adoc @@ -0,0 +1,151 @@ +[[oauth2Client-client-auth-support]] += Client Authentication Support + + +[[oauth2Client-jwt-bearer-auth]] +== JWT Bearer + +[NOTE] +Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] Client Authentication. + +The default implementation for JWT Bearer Client Authentication is `NimbusJwtClientAuthenticationParametersConverter`, +which is a `Converter` that customizes the Token Request parameters by adding +a signed JSON Web Token (JWS) in the `client_assertion` parameter. + +The `java.security.PrivateKey` or `javax.crypto.SecretKey` used for signing the JWS +is supplied by the `com.nimbusds.jose.jwk.JWK` resolver associated with `NimbusJwtClientAuthenticationParametersConverter`. + + +=== Authenticate using `private_key_jwt` + +Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-authentication-method: private_key_jwt + authorization-grant-type: authorization_code + ... +---- + +The following example shows how to configure `WebClientReactiveAuthorizationCodeTokenResponseClient`: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + RSAPublicKey publicKey = ... + RSAPrivateKey privateKey = ... + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +WebClientReactiveAuthorizationCodeTokenResponseClient tokenResponseClient = + new WebClientReactiveAuthorizationCodeTokenResponseClient(); +tokenResponseClient.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver: Function = + Function { clientRegistration -> + if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + var publicKey: RSAPublicKey = ... + var privateKey: RSAPrivateKey = ... + RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build() + } + null + } + +val tokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient() +tokenResponseClient.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) +---- +==== + + +=== Authenticate using `client_secret_jwt` + +Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + client-authentication-method: client_secret_jwt + authorization-grant-type: client_credentials + ... +---- + +The following example shows how to configure `WebClientReactiveClientCredentialsTokenResponseClient`: + +==== +.Java +[source,java,role="primary"] +---- +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { + SecretKeySpec secretKey = new SecretKeySpec( + clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), + "HmacSHA256"); + return new OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +WebClientReactiveClientCredentialsTokenResponseClient tokenResponseClient = + new WebClientReactiveClientCredentialsTokenResponseClient(); +tokenResponseClient.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val jwkResolver = Function { clientRegistration: ClientRegistration -> + if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { + val secretKey = SecretKeySpec( + clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), + "HmacSHA256" + ) + OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build() + } + null +} + +val tokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient() +tokenResponseClient.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) +---- +==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/core.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/core.adoc new file mode 100644 index 0000000000..95ce3fd7c6 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/core.adoc @@ -0,0 +1,429 @@ +[[oauth2Client-core-interface-class]] += Core Interfaces / Classes + + +[[oauth2Client-client-registration]] +== ClientRegistration + +`ClientRegistration` is a representation of a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + +A client registration holds information, such as client id, client secret, authorization grant type, redirect URI, scope(s), authorization URI, token URI, and other details. + +`ClientRegistration` and its properties are defined as follows: + +[source,java] +---- +public final class ClientRegistration { + private String registrationId; <1> + private String clientId; <2> + private String clientSecret; <3> + private ClientAuthenticationMethod clientAuthenticationMethod; <4> + private AuthorizationGrantType authorizationGrantType; <5> + private String redirectUri; <6> + private Set scopes; <7> + private ProviderDetails providerDetails; + private String clientName; <8> + + public class ProviderDetails { + private String authorizationUri; <9> + private String tokenUri; <10> + private UserInfoEndpoint userInfoEndpoint; + private String jwkSetUri; <11> + private String issuerUri; <12> + private Map configurationMetadata; <13> + + public class UserInfoEndpoint { + private String uri; <14> + private AuthenticationMethod authenticationMethod; <15> + private String userNameAttributeName; <16> + + } + } +} +---- +<1> `registrationId`: The ID that uniquely identifies the `ClientRegistration`. +<2> `clientId`: The client identifier. +<3> `clientSecret`: The client secret. +<4> `clientAuthenticationMethod`: The method used to authenticate the Client with the Provider. +The supported values are *client_secret_basic*, *client_secret_post*, *private_key_jwt*, *client_secret_jwt* and *none* https://tools.ietf.org/html/rfc6749#section-2.1[(public clients)]. +<5> `authorizationGrantType`: The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. + The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. +<6> `redirectUri`: The client's registered redirect URI that the _Authorization Server_ redirects the end-user's user-agent + to after the end-user has authenticated and authorized access to the client. +<7> `scopes`: The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. +<8> `clientName`: A descriptive name used for the client. +The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. +<9> `authorizationUri`: The Authorization Endpoint URI for the Authorization Server. +<10> `tokenUri`: The Token Endpoint URI for the Authorization Server. +<11> `jwkSetUri`: The URI used to retrieve the https://tools.ietf.org/html/rfc7517[JSON Web Key (JWK)] Set from the Authorization Server, + which contains the cryptographic key(s) used to verify the https://tools.ietf.org/html/rfc7515[JSON Web Signature (JWS)] of the ID Token and optionally the UserInfo Response. +<12> `issuerUri`: Returns the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server. +<13> `configurationMetadata`: The https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration Information]. + This information will only be available if the Spring Boot 2.x property `spring.security.oauth2.client.provider.[providerId].issuerUri` is configured. +<14> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. +<15> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint. +The supported values are *header*, *form* and *query*. +<16> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. + +A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint]. + +`ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example: + +==== +.Java +[source,java,role="primary"] +---- +ClientRegistration clientRegistration = + ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() +---- +==== + +The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response. + +As an alternative, you can use `ClientRegistrations.fromOidcIssuerLocation()` to only query the OpenID Connect Provider's Configuration endpoint. + +[[oauth2Client-client-registration-repo]] +== ReactiveClientRegistrationRepository + +The `ReactiveClientRegistrationRepository` serves as a repository for OAuth 2.0 / OpenID Connect 1.0 `ClientRegistration`(s). + +[NOTE] +Client registration information is ultimately stored and owned by the associated Authorization Server. +This repository provides the ability to retrieve a sub-set of the primary client registration information, which is stored with the Authorization Server. + +Spring Boot 2.x auto-configuration binds each of the properties under `spring.security.oauth2.client.registration._[registrationId]_` to an instance of `ClientRegistration` and then composes each of the `ClientRegistration` instance(s) within a `ReactiveClientRegistrationRepository`. + +[NOTE] +The default implementation of `ReactiveClientRegistrationRepository` is `InMemoryReactiveClientRegistrationRepository`. + +The auto-configuration also registers the `ReactiveClientRegistrationRepository` as a `@Bean` in the `ApplicationContext` so that it is available for dependency-injection, if needed by the application. + +The following listing shows an example: + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @Autowired + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + @GetMapping("/") + public Mono index() { + return this.clientRegistrationRepository.findByRegistrationId("okta") + ... + .thenReturn("index"); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository + + @GetMapping("/") + fun index(): Mono { + return this.clientRegistrationRepository.findByRegistrationId("okta") + ... + .thenReturn("index") + } +} +---- +==== + +[[oauth2Client-authorized-client]] +== OAuth2AuthorizedClient + +`OAuth2AuthorizedClient` is a representation of an Authorized Client. +A client is considered to be authorized when the end-user (Resource Owner) has granted authorization to the client to access its protected resources. + +`OAuth2AuthorizedClient` serves the purpose of associating an `OAuth2AccessToken` (and optional `OAuth2RefreshToken`) to a `ClientRegistration` (client) and resource owner, who is the `Principal` end-user that granted the authorization. + + +[[oauth2Client-authorized-repo-service]] +== ServerOAuth2AuthorizedClientRepository / ReactiveOAuth2AuthorizedClientService + +`ServerOAuth2AuthorizedClientRepository` is responsible for persisting `OAuth2AuthorizedClient`(s) between web requests. +Whereas, the primary role of `ReactiveOAuth2AuthorizedClientService` is to manage `OAuth2AuthorizedClient`(s) at the application-level. + +From a developer perspective, the `ServerOAuth2AuthorizedClientRepository` or `ReactiveOAuth2AuthorizedClientService` provides the capability to lookup an `OAuth2AccessToken` associated with a client so that it may be used to initiate a protected resource request. + +The following listing shows an example: + +==== +.Java +[source,java,role="primary"] +---- +@Controller +public class OAuth2ClientController { + + @Autowired + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + @GetMapping("/") + public Mono index(Authentication authentication) { + return this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()) + .map(OAuth2AuthorizedClient::getAccessToken) + ... + .thenReturn("index"); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var authorizedClientService: ReactiveOAuth2AuthorizedClientService + + @GetMapping("/") + fun index(authentication: Authentication): Mono { + return this.authorizedClientService.loadAuthorizedClient("okta", authentication.name) + .map { it.accessToken } + ... + .thenReturn("index") + } +} +---- +==== + +[NOTE] +Spring Boot 2.x auto-configuration registers an `ServerOAuth2AuthorizedClientRepository` and/or `ReactiveOAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`. +However, the application may choose to override and register a custom `ServerOAuth2AuthorizedClientRepository` or `ReactiveOAuth2AuthorizedClientService` `@Bean`. + +The default implementation of `ReactiveOAuth2AuthorizedClientService` is `InMemoryReactiveOAuth2AuthorizedClientService`, which stores `OAuth2AuthorizedClient`(s) in-memory. + +Alternatively, the R2DBC implementation `R2dbcReactiveOAuth2AuthorizedClientService` may be configured for persisting `OAuth2AuthorizedClient`(s) in a database. + +[NOTE] +`R2dbcReactiveOAuth2AuthorizedClientService` depends on the table definition described in xref:servlet/appendix/database-schema.adoc#dbschema-oauth2-client[ OAuth 2.0 Client Schema]. + + +[[oauth2Client-authorized-manager-provider]] +== ReactiveOAuth2AuthorizedClientManager / ReactiveOAuth2AuthorizedClientProvider + +The `ReactiveOAuth2AuthorizedClientManager` is responsible for the overall management of `OAuth2AuthorizedClient`(s). + +The primary responsibilities include: + +* Authorizing (or re-authorizing) an OAuth 2.0 Client, using a `ReactiveOAuth2AuthorizedClientProvider`. +* Delegating the persistence of an `OAuth2AuthorizedClient`, typically using a `ReactiveOAuth2AuthorizedClientService` or `ServerOAuth2AuthorizedClientRepository`. +* Delegating to a `ReactiveOAuth2AuthorizationSuccessHandler` when an OAuth 2.0 Client has been successfully authorized (or re-authorized). +* Delegating to a `ReactiveOAuth2AuthorizationFailureHandler` when an OAuth 2.0 Client fails to authorize (or re-authorize). + +A `ReactiveOAuth2AuthorizedClientProvider` implements a strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. +Implementations will typically implement an authorization grant type, eg. `authorization_code`, `client_credentials`, etc. + +The default implementation of `ReactiveOAuth2AuthorizedClientManager` is `DefaultReactiveOAuth2AuthorizedClientManager`, which is associated with a `ReactiveOAuth2AuthorizedClientProvider` that may support multiple authorization grant types using a delegation-based composite. +The `ReactiveOAuth2AuthorizedClientProviderBuilder` may be used to configure and build the delegation-based composite. + +The following code shows an example of how to configure and build a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== + +When an authorization attempt succeeds, the `DefaultReactiveOAuth2AuthorizedClientManager` will delegate to the `ReactiveOAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `ServerOAuth2AuthorizedClientRepository`. +In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `ServerOAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler`. +The default behaviour may be customized via `setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)` and `setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)`. + +The `DefaultReactiveOAuth2AuthorizedClientManager` is also associated with a `contextAttributesMapper` of type `Function>>`, which is responsible for mapping attribute(s) from the `OAuth2AuthorizeRequest` to a `Map` of attributes to be associated to the `OAuth2AuthorizationContext`. +This can be useful when you need to supply a `ReactiveOAuth2AuthorizedClientProvider` with required (supported) attribute(s), eg. the `PasswordReactiveOAuth2AuthorizedClientProvider` requires the resource owner's `username` and `password` to be available in `OAuth2AuthorizationContext.getAttributes()`. + +The following code shows an example of the `contextAttributesMapper`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, + // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); + + return authorizedClientManager; +} + +private Function>> contextAttributesMapper() { + return authorizeRequest -> { + Map contextAttributes = Collections.emptyMap(); + ServerWebExchange exchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName()); + ServerHttpRequest request = exchange.getRequest(); + String username = request.getQueryParams().getFirst(OAuth2ParameterNames.USERNAME); + String password = request.getQueryParams().getFirst(OAuth2ParameterNames.PASSWORD); + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = new HashMap<>(); + + // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); + contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); + } + return Mono.just(contextAttributes); + }; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + + // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, + // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) + return authorizedClientManager +} + +private fun contextAttributesMapper(): Function>> { + return Function { authorizeRequest -> + var contextAttributes: MutableMap = mutableMapOf() + val exchange: ServerWebExchange = authorizeRequest.getAttribute(ServerWebExchange::class.java.name)!! + val request: ServerHttpRequest = exchange.request + val username: String? = request.queryParams.getFirst(OAuth2ParameterNames.USERNAME) + val password: String? = request.queryParams.getFirst(OAuth2ParameterNames.PASSWORD) + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = hashMapOf() + + // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username!! + contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password!! + } + Mono.just(contextAttributes) + } +} +---- +==== + +The `DefaultReactiveOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `ServerWebExchange`. +When operating *_outside_* of a `ServerWebExchange` context, use `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` instead. + +A _service application_ is a common use case for when to use an `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`. +Service applications often run in the background, without any user interaction, and typically run under a system-level account instead of a user account. +An OAuth 2.0 Client configured with the `client_credentials` grant type can be considered a type of service application. + +The following code shows an example of how to configure an `AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); + + AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientService: ReactiveOAuth2AuthorizedClientService): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build() + val authorizedClientManager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc new file mode 100644 index 0000000000..b04019a5a4 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc @@ -0,0 +1,123 @@ +[[webflux-oauth2-client]] += OAuth 2.0 Client +:page-section-summary-toc: 1 + +The OAuth 2.0 Client features provide support for the Client role as defined in the https://tools.ietf.org/html/rfc6749#section-1.1[OAuth 2.0 Authorization Framework]. + +At a high-level, the core features available are: + +.Authorization Grant support +* https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] +* https://tools.ietf.org/html/rfc6749#section-6[Refresh Token] +* https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] +* https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] +* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer] + +.Client Authentication support +* https://datatracker.ietf.org/doc/html/rfc7523#section-2.2[JWT Bearer] + +.HTTP Client support +* <> (for requesting protected resources) + +The `ServerHttpSecurity.oauth2Client()` DSL provides a number of configuration options for customizing the core components used by OAuth 2.0 Client. + +The following code shows the complete configuration options provided by the `ServerHttpSecurity.oauth2Client()` DSL: + +.OAuth2 Client Configuration Options +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2ClientSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .oauth2Client(oauth2 -> oauth2 + .clientRegistrationRepository(this.clientRegistrationRepository()) + .authorizedClientRepository(this.authorizedClientRepository()) + .authorizationRequestRepository(this.authorizationRequestRepository()) + .authenticationConverter(this.authenticationConverter()) + .authenticationManager(this.authenticationManager()) + ); + + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2ClientSecurityConfig { + + @Bean + fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + oauth2Client { + clientRegistrationRepository = clientRegistrationRepository() + authorizedClientRepository = authorizedClientRepository() + authorizationRequestRepository = authorizedRequestRepository() + authenticationConverter = authenticationConverter() + authenticationManager = authenticationManager() + } + } + + return http.build() + } +} +---- +==== + +The `ReactiveOAuth2AuthorizedClientManager` is responsible for managing the authorization (or re-authorization) of an OAuth 2.0 Client, in collaboration with one or more `ReactiveOAuth2AuthorizedClientProvider`(s). + +The following code shows an example of how to register a `ReactiveOAuth2AuthorizedClientManager` `@Bean` and associate it with a `ReactiveOAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/index.adoc index af06df5136..95850651fd 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/index.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/index.adoc @@ -4,5 +4,5 @@ Spring Security provides OAuth2 and WebFlux integration for reactive applications. * xref:reactive/oauth2/login.adoc[OAuth2 Log In] - Authenticating with an OAuth2 or OpenID Connect 1.0 Provider -* xref:reactive/oauth2/oauth2-client.adoc[OAuth2 Client] - Making requests to an OAuth2 Resource Server +* xref:reactive/oauth2/client/index.adoc[OAuth2 Client] - Making requests to an OAuth2 Resource Server * xref:reactive/oauth2/resource-server/index.adoc[OAuth2 Resource Server] - Protecting a REST endpoint using OAuth2 From 076c01daef0cbb60904de7738137f591907d9747 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Tue, 9 Nov 2021 14:02:35 -0600 Subject: [PATCH 010/589] Add missing @since 5.6 --- .../oauth2/client/web/OAuth2LoginAuthenticationFilter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java index 9bebb0869c..fa53214d49 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java @@ -222,6 +222,7 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce * authentication result. * @param authenticationResultConverter the converter for * {@link OAuth2AuthenticationToken}'s + * @since 5.6 */ public final void setAuthenticationResultConverter( Converter authenticationResultConverter) { From aabb116a07348ce1dfbe42a69b4221ea2628310c Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 10 Nov 2021 11:05:18 -0300 Subject: [PATCH 011/589] Update to Gradle 7.3 Closes gh-10480 --- buildSrc/gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 59536 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 269 +++++++++++------- 5 files changed, 161 insertions(+), 112 deletions(-) diff --git a/buildSrc/gradle/wrapper/gradle-wrapper.jar b/buildSrc/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18435 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 B8dv}T delta 17845 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 X49Luk)baa85H9$5YCsTPT`SVRWMtMW diff --git a/buildSrc/gradle/wrapper/gradle-wrapper.properties b/buildSrc/gradle/wrapper/gradle-wrapper.properties index ffed3a254e..e750102e09 100644 --- a/buildSrc/gradle/wrapper/gradle-wrapper.properties +++ b/buildSrc/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18435 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 B8dv}T delta 17845 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 X49Luk)baa85H9$5YCsTPT`SVRWMtMW diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a254e..e750102e09 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c81..1b6c787337 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" From f4ddb4e3f4f940c0fe291cc1827959ed2af235cd Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 10 Nov 2021 15:14:01 -0700 Subject: [PATCH 012/589] Update What's New Links --- docs/modules/ROOT/pages/whats-new.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 24ab50b312..0d31e832cf 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -19,9 +19,9 @@ Below are the highlights of the release. * SAML 2.0 Service Provider -** Added https://github.com/spring-projects/spring-security/pull/9483[SAML 2.0 Single Logout Support] -** Added https://github.com/spring-projects/spring-security/pull/10060[Saml2AuthenticationRequestRepository] -** Added https://github.com/spring-projects/spring-security/issues/9486[`RelyingPartyRegistrationResolver`] +** Added xref:servlet/saml2/logout.adoc[SAML 2.0 Single Logout Support] +** Added xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-store-authn-request[Saml2AuthenticationRequestRepository] +** Added xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[`RelyingPartyRegistrationResolver`] ** Improved ``Saml2LoginConfigurer``'s handling of https://github.com/spring-projects/spring-security/issues/10268[`Saml2AuthenticationTokenConverter`] From b60020a40c40064dd3ab51510b7d6efc3edc2b9d Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 10 Nov 2021 15:15:11 -0700 Subject: [PATCH 013/589] Use authorizeHttpRequests in Docs Issue gh-8900 --- .../servlet/authorization/authorize-requests.adoc | 4 ++-- .../servlet/authorization/expression-based.adoc | 4 ++-- .../ROOT/pages/servlet/configuration/java.adoc | 4 ++-- .../modules/ROOT/pages/servlet/integrations/mvc.adoc | 4 ++-- .../ROOT/pages/servlet/integrations/websocket.adoc | 2 +- .../servlet/oauth2/client/authorization-grants.adoc | 2 +- .../ROOT/pages/servlet/oauth2/login/advanced.adoc | 2 +- .../ROOT/pages/servlet/oauth2/login/core.adoc | 6 +++--- .../pages/servlet/oauth2/resource-server/jwt.adoc | 12 ++++++------ .../servlet/oauth2/resource-server/multitenancy.adoc | 6 +++--- .../servlet/oauth2/resource-server/opaque-token.adoc | 10 +++++----- .../pages/servlet/saml2/login/authentication.adoc | 6 +++--- .../ROOT/pages/servlet/saml2/login/overview.adoc | 6 +++--- docs/modules/ROOT/pages/servlet/saml2/logout.adoc | 2 +- 14 files changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc index bbf771a73b..b21c0d096e 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc @@ -32,7 +32,7 @@ The explicit configuration looks like: protected void configure(HttpSecurity http) throws Exception { http // ... - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ); } @@ -71,7 +71,7 @@ We can configure Spring Security to have different rules by adding more rules in protected void configure(HttpSecurity http) throws Exception { http // ... - .authorizeRequests(authorize -> authorize // <1> + .authorizeHttpRequests(authorize -> authorize // <1> .mvcMatchers("/resources/**", "/signup", "/about").permitAll() // <2> .mvcMatchers("/admin/**").hasRole("ADMIN") // <3> .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4> diff --git a/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc b/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc index f5b916d636..218b6dec37 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/expression-based.adoc @@ -144,7 +144,7 @@ You could refer to the method using: [source,java,role="primary"] ---- http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .antMatchers("/user/**").access("@webSecurity.check(authentication,request)") ... ) @@ -210,7 +210,7 @@ You could refer to the method using: [source,java,role="primary",attrs="-attributes"] ---- http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)") ... ); diff --git a/docs/modules/ROOT/pages/servlet/configuration/java.adoc b/docs/modules/ROOT/pages/servlet/configuration/java.adoc index 985a01c516..28837ecfaa 100644 --- a/docs/modules/ROOT/pages/servlet/configuration/java.adoc +++ b/docs/modules/ROOT/pages/servlet/configuration/java.adoc @@ -193,7 +193,7 @@ public class MultiHttpSecurityConfig { protected void configure(HttpSecurity http) throws Exception { http .antMatcher("/api/**") <3> - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().hasRole("ADMIN") ) .httpBasic(withDefaults()); @@ -206,7 +206,7 @@ public class MultiHttpSecurityConfig { @Override protected void configure(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .formLogin(withDefaults()); diff --git a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc index 23bb16ccac..acaa8976d2 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc @@ -138,7 +138,7 @@ If we wanted to restrict access to this controller method to admin users, a deve ---- protected configure(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .antMatchers("/admin").hasRole("ADMIN") ); } @@ -183,7 +183,7 @@ The following configuration will protect the same URLs that Spring MVC will matc ---- protected configure(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/admin").hasRole("ADMIN") ); } diff --git a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc index bb647abf54..d9288df97a 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/websocket.adoc @@ -456,7 +456,7 @@ public class WebSecurityConfig .sameOrigin() ) ) - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize ... ) ... diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc index b2a60ba35b..09a98abbd3 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc @@ -121,7 +121,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc index 5eb68e757f..dc29fc9625 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc @@ -872,7 +872,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2Login(withDefaults()) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc index 129a4ae1dc..a691cd16e4 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/core.adoc @@ -321,7 +321,7 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2Login(withDefaults()); @@ -367,7 +367,7 @@ public class OAuth2LoginConfig { @Override protected void configure(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2Login(withDefaults()); @@ -462,7 +462,7 @@ public class OAuth2LoginConfig { @Override protected void configure(HttpSecurity http) throws Exception { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2Login(withDefaults()); diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc index 64ebfda12f..5a6d0ffee5 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/jwt.adoc @@ -146,7 +146,7 @@ The first is a `WebSecurityConfigurerAdapter` that configures the app as a resou ---- protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); @@ -182,7 +182,7 @@ Replacing this is as simple as exposing the bean within the application: public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyRequest().authenticated() ) @@ -299,7 +299,7 @@ An authorization server's JWK Set Uri can be configured < authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -359,7 +359,7 @@ More powerful than `jwkSetUri()` is `decoder()`, which will completely replace a public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -719,7 +719,7 @@ This means that to protect an endpoint or method with a scope derived from a JWT public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyRequest().authenticated() @@ -926,7 +926,7 @@ static class CustomAuthenticationConverter implements Converter authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc index 07f804d5cc..bfd5394543 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc @@ -53,7 +53,7 @@ And then specify this `AuthenticationManagerResolver` in the DSL: [source,java,role="primary"] ---- http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -109,7 +109,7 @@ JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIs ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -176,7 +176,7 @@ JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get); http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc index 4e0c618599..acccfbc16f 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/opaque-token.adoc @@ -188,7 +188,7 @@ When use Opaque Token, this `WebSecurityConfigurerAdapter` looks like: ---- protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken); @@ -224,7 +224,7 @@ Replacing this is as simple as exposing the bean within the application: public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read") .anyRequest().authenticated() ) @@ -338,7 +338,7 @@ An authorization server's Introspection Uri can be configured < authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -400,7 +400,7 @@ More powerful than `introspectionUri()` is `introspector()`, which will complete public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 @@ -479,7 +479,7 @@ This means that to protect an endpoint or method with a scope derived from an Op public class MappedAuthorities extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http - .authorizeRequests(authorizeRequests -> authorizeRequests + .authorizeHttpRequests(authorizeRequests -> authorizeRequests .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts") .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages") .anyRequest().authenticated() diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc index 2d6efa7ab2..65edf60069 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc @@ -38,7 +38,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { ); http - .authorizeRequests(authz -> authz + .authorizeHttpRequests(authz -> authz .anyRequest().authenticated() ) .saml2Login(saml2 -> saml2 @@ -106,7 +106,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { }); http - .authorizeRequests(authz -> authz + .authorizeHttpRequests(authz -> authz .anyRequest().authenticated() ) .saml2Login(saml2 -> saml2 @@ -310,7 +310,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...); http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .saml2Login(saml2 -> saml2 diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index fc47e68d59..f7b91a3880 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -289,7 +289,7 @@ When including `spring-security-saml2-service-provider`, the `WebSecurityConfigu ---- protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated() ) .saml2Login(withDefaults()); @@ -323,7 +323,7 @@ You can replace this by exposing the bean within the application: public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/messages/**").hasAuthority("ROLE_USER") .anyRequest().authenticated() ) @@ -471,7 +471,7 @@ Alternatively, you can directly wire up the repository using the DSL, which will public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) { http - .authorizeRequests(authorize -> authorize + .authorizeHttpRequests(authorize -> authorize .mvcMatchers("/messages/**").hasAuthority("ROLE_USER") .anyRequest().authenticated() ) diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index 0d1a886753..04c6ed0f94 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -43,7 +43,7 @@ RelyingPartyRegistrationRepository registrations() { @Bean SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception { http - .authorizeRequests((authorize) -> authorize + .authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated() ) .saml2Login(withDefaults()) From 310a50587c713cda1e2eab53436af7ef66a3acdc Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 10 Nov 2021 15:38:29 -0700 Subject: [PATCH 014/589] Port Missing Integration Docs Closes gh-10465 --- docs/modules/ROOT/nav.adoc | 4 + .../servlet/integrations/concurrency.adoc | 166 ++++++++++++++++++ .../ROOT/pages/servlet/integrations/data.adoc | 45 +++++ .../pages/servlet/integrations/jackson.adoc | 30 ++++ .../servlet/integrations/localization.adoc | 36 ++++ 5 files changed, 281 insertions(+) create mode 100644 docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc create mode 100644 docs/modules/ROOT/pages/servlet/integrations/data.adoc create mode 100644 docs/modules/ROOT/pages/servlet/integrations/jackson.adoc create mode 100644 docs/modules/ROOT/pages/servlet/integrations/localization.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 6e5e30a003..b1f65c4d56 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -81,7 +81,11 @@ *** xref:servlet/exploits/http.adoc[] *** xref:servlet/exploits/firewall.adoc[] ** xref:servlet/integrations/index.adoc[Integrations] +*** xref:servlet/integrations/concurrency.adoc[Concurrency] +*** xref:servlet/integrations/jackson.adoc[Jackson] +*** xref:servlet/integrations/localization.adoc[Localization] *** xref:servlet/integrations/servlet-api.adoc[Servlet APIs] +*** xref:servlet/integrations/data.adoc[Spring Data] *** xref:servlet/integrations/mvc.adoc[Spring MVC] *** xref:servlet/integrations/websocket.adoc[WebSocket] *** xref:servlet/integrations/cors.adoc[Spring's CORS Support] diff --git a/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc b/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc new file mode 100644 index 0000000000..3d8036baf8 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/integrations/concurrency.adoc @@ -0,0 +1,166 @@ +[[concurrency]] += Concurrency Support + +In most environments, Security is stored on a per `Thread` basis. +This means that when work is done on a new `Thread`, the `SecurityContext` is lost. +Spring Security provides some infrastructure to help make this much easier for users. +Spring Security provides low level abstractions for working with Spring Security in multi-threaded environments. +In fact, this is what Spring Security builds on to integration with xref:servlet/integrations/servlet-api.adoc#servletapi-start-runnable[`AsyncContext.start(Runnable)`] and xref:servlet/integrations/mvc.adoc#mvc-async[Spring MVC Async Integration]. + +== DelegatingSecurityContextRunnable + +One of the most fundamental building blocks within Spring Security's concurrency support is the `DelegatingSecurityContextRunnable`. +It wraps a delegate `Runnable` in order to initialize the `SecurityContextHolder` with a specified `SecurityContext` for the delegate. +It then invokes the delegate Runnable ensuring to clear the `SecurityContextHolder` afterwards. +The `DelegatingSecurityContextRunnable` looks something like this: + +[source,java] +---- +public void run() { +try { + SecurityContextHolder.setContext(securityContext); + delegate.run(); +} finally { + SecurityContextHolder.clearContext(); +} +} +---- + +While very simple, it makes it seamless to transfer the SecurityContext from one Thread to another. +This is important since, in most cases, the SecurityContextHolder acts on a per Thread basis. +For example, you might have used Spring Security's xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[``] support to secure one of your services. +You can now easily transfer the `SecurityContext` of the current `Thread` to the `Thread` that invokes the secured service. +An example of how you might do this can be found below: + +[source,java] +---- +Runnable originalRunnable = new Runnable() { +public void run() { + // invoke secured service +} +}; + +SecurityContext context = SecurityContextHolder.getContext(); +DelegatingSecurityContextRunnable wrappedRunnable = + new DelegatingSecurityContextRunnable(originalRunnable, context); + +new Thread(wrappedRunnable).start(); +---- + +The code above performs the following steps: + +* Creates a `Runnable` that will be invoking our secured service. +Notice that it is not aware of Spring Security +* Obtains the `SecurityContext` that we wish to use from the `SecurityContextHolder` and initializes the `DelegatingSecurityContextRunnable` +* Use the `DelegatingSecurityContextRunnable` to create a Thread +* Start the Thread we created + +Since it is quite common to create a `DelegatingSecurityContextRunnable` with the `SecurityContext` from the `SecurityContextHolder` there is a shortcut constructor for it. +The following code is the same as the code above: + + +[source,java] +---- +Runnable originalRunnable = new Runnable() { +public void run() { + // invoke secured service +} +}; + +DelegatingSecurityContextRunnable wrappedRunnable = + new DelegatingSecurityContextRunnable(originalRunnable); + +new Thread(wrappedRunnable).start(); +---- + +The code we have is simple to use, but it still requires knowledge that we are using Spring Security. +In the next section we will take a look at how we can utilize `DelegatingSecurityContextExecutor` to hide the fact that we are using Spring Security. + +== DelegatingSecurityContextExecutor + +In the previous section we found that it was easy to use the `DelegatingSecurityContextRunnable`, but it was not ideal since we had to be aware of Spring Security in order to use it. +Let's take a look at how `DelegatingSecurityContextExecutor` can shield our code from any knowledge that we are using Spring Security. + +The design of `DelegatingSecurityContextExecutor` is very similar to that of `DelegatingSecurityContextRunnable` except it accepts a delegate `Executor` instead of a delegate `Runnable`. +You can see an example of how it might be used below: + + +[source,java] +---- +SecurityContext context = SecurityContextHolder.createEmptyContext(); +Authentication authentication = + new UsernamePasswordAuthenticationToken("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER")); +context.setAuthentication(authentication); + +SimpleAsyncTaskExecutor delegateExecutor = + new SimpleAsyncTaskExecutor(); +DelegatingSecurityContextExecutor executor = + new DelegatingSecurityContextExecutor(delegateExecutor, context); + +Runnable originalRunnable = new Runnable() { +public void run() { + // invoke secured service +} +}; + +executor.execute(originalRunnable); +---- + +The code performs the following steps: + +* Creates the `SecurityContext` to be used for our `DelegatingSecurityContextExecutor`. +Note that in this example we simply create the `SecurityContext` by hand. +However, it does not matter where or how we get the `SecurityContext` (i.e. we could obtain it from the `SecurityContextHolder` if we wanted). +* Creates a delegateExecutor that is in charge of executing submitted ``Runnable``s +* Finally we create a `DelegatingSecurityContextExecutor` which is in charge of wrapping any Runnable that is passed into the execute method with a `DelegatingSecurityContextRunnable`. +It then passes the wrapped Runnable to the delegateExecutor. +In this instance, the same `SecurityContext` will be used for every Runnable submitted to our `DelegatingSecurityContextExecutor`. +This is nice if we are running background tasks that need to be run by a user with elevated privileges. +* At this point you may be asking yourself "How does this shield my code of any knowledge of Spring Security?" Instead of creating the `SecurityContext` and the `DelegatingSecurityContextExecutor` in our own code, we can inject an already initialized instance of `DelegatingSecurityContextExecutor`. + +[source,java] +---- +@Autowired +private Executor executor; // becomes an instance of our DelegatingSecurityContextExecutor + +public void submitRunnable() { +Runnable originalRunnable = new Runnable() { + public void run() { + // invoke secured service + } +}; +executor.execute(originalRunnable); +} +---- + +Now our code is unaware that the `SecurityContext` is being propagated to the `Thread`, then the `originalRunnable` is run, and then the `SecurityContextHolder` is cleared out. +In this example, the same user is being used to run each thread. +What if we wanted to use the user from `SecurityContextHolder` at the time we invoked `executor.execute(Runnable)` (i.e. the currently logged in user) to process ``originalRunnable``? +This can be done by removing the `SecurityContext` argument from our `DelegatingSecurityContextExecutor` constructor. +For example: + + +[source,java] +---- +SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor(); +DelegatingSecurityContextExecutor executor = + new DelegatingSecurityContextExecutor(delegateExecutor); +---- + +Now anytime `executor.execute(Runnable)` is executed the `SecurityContext` is first obtained by the `SecurityContextHolder` and then that `SecurityContext` is used to create our `DelegatingSecurityContextRunnable`. +This means that we are running our `Runnable` with the same user that was used to invoke the `executor.execute(Runnable)` code. + +== Spring Security Concurrency Classes + +Refer to the Javadoc for additional integrations with both the Java concurrent APIs and the Spring Task abstractions. +They are quite self-explanatory once you understand the previous code. + +* `DelegatingSecurityContextCallable` +* `DelegatingSecurityContextExecutor` +* `DelegatingSecurityContextExecutorService` +* `DelegatingSecurityContextRunnable` +* `DelegatingSecurityContextScheduledExecutorService` +* `DelegatingSecurityContextSchedulingTaskExecutor` +* `DelegatingSecurityContextAsyncTaskExecutor` +* `DelegatingSecurityContextTaskExecutor` +* `DelegatingSecurityContextTaskScheduler` diff --git a/docs/modules/ROOT/pages/servlet/integrations/data.adoc b/docs/modules/ROOT/pages/servlet/integrations/data.adoc new file mode 100644 index 0000000000..35f89ec78c --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/integrations/data.adoc @@ -0,0 +1,45 @@ +[[data]] += Spring Data Integration + +Spring Security provides Spring Data integration that allows referring to the current user within your queries. +It is not only useful but necessary to include the user in the queries to support paged results since filtering the results afterwards would not scale. + +[[data-configuration]] +== Spring Data & Spring Security Configuration + +To use this support, add `org.springframework.security:spring-security-data` dependency and provide a bean of type `SecurityEvaluationContextExtension`. +In Java Configuration, this would look like: + +[source,java] +---- +@Bean +public SecurityEvaluationContextExtension securityEvaluationContextExtension() { + return new SecurityEvaluationContextExtension(); +} +---- + +In XML Configuration, this would look like: + +[source,xml] +---- + +---- + +[[data-query]] +== Security Expressions within @Query + +Now Spring Security can be used within your queries. +For example: + +[source,java] +---- +@Repository +public interface MessageRepository extends PagingAndSortingRepository { + @Query("select m from Message m where m.to.id = ?#{ principal?.id }") + Page findInbox(Pageable pageable); +} +---- + +This checks to see if the `Authentication.getPrincipal().getId()` is equal to the recipient of the `Message`. +Note that this example assumes you have customized the principal to be an Object that has an id property. +By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/expression-based.adoc#common-expressions[Common Security Expressions] are available within the Query. diff --git a/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc b/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc new file mode 100644 index 0000000000..ee17f37c69 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc @@ -0,0 +1,30 @@ +[[jackson]] += Jackson Support + +Spring Security provides Jackson support for persisting Spring Security related classes. +This can improve the performance of serializing Spring Security related classes when working with distributed sessions (i.e. session replication, Spring Session, etc). + +To use it, register the `SecurityJackson2Modules.getModules(ClassLoader)` with `ObjectMapper` (https://github.com/FasterXML/jackson-databind[jackson-databind]): + +[source,java] +---- +ObjectMapper mapper = new ObjectMapper(); +ClassLoader loader = getClass().getClassLoader(); +List modules = SecurityJackson2Modules.getModules(loader); +mapper.registerModules(modules); + +// ... use ObjectMapper as normally ... +SecurityContext context = new SecurityContextImpl(); +// ... +String json = mapper.writeValueAsString(context); +---- + +[NOTE] +==== +The following Spring Security modules provide Jackson support: + +- spring-security-core (`CoreJackson2Module`) +- spring-security-web (`WebJackson2Module`, `WebServletJackson2Module`, `WebServerJackson2Module`) +- <> (`OAuth2ClientJackson2Module`) +- spring-security-cas (`CasJackson2Module`) +==== diff --git a/docs/modules/ROOT/pages/servlet/integrations/localization.adoc b/docs/modules/ROOT/pages/servlet/integrations/localization.adoc new file mode 100644 index 0000000000..e1fc22b9a2 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/integrations/localization.adoc @@ -0,0 +1,36 @@ +[[localization]] += Localization +Spring Security supports localization of exception messages that end users are likely to see. +If your application is designed for English-speaking users, you don't need to do anything as by default all Security messages are in English. +If you need to support other locales, everything you need to know is contained in this section. + +All exception messages can be localized, including messages related to authentication failures and access being denied (authorization failures). +Exceptions and logging messages that are focused on developers or system deplopers (including incorrect attributes, interface contract violations, using incorrect constructors, startup time validation, debug-level logging) are not localized and instead are hard-coded in English within Spring Security's code. + +Shipping in the `spring-security-core-xx.jar` you will find an `org.springframework.security` package that in turn contains a `messages.properties` file, as well as localized versions for some common languages. +This should be referred to by your `ApplicationContext`, as Spring Security classes implement Spring's `MessageSourceAware` interface and expect the message resolver to be dependency injected at application context startup time. +Usually all you need to do is register a bean inside your application context to refer to the messages. +An example is shown below: + +[source,xml] +---- + + + +---- + +The `messages.properties` is named in accordance with standard resource bundles and represents the default language supported by Spring Security messages. +This default file is in English. + +If you wish to customize the `messages.properties` file, or support other languages, you should copy the file, rename it accordingly, and register it inside the above bean definition. +There are not a large number of message keys inside this file, so localization should not be considered a major initiative. +If you do perform localization of this file, please consider sharing your work with the community by logging a JIRA task and attaching your appropriately-named localized version of `messages.properties`. + +Spring Security relies on Spring's localization support in order to actually lookup the appropriate message. +In order for this to work, you have to make sure that the locale from the incoming request is stored in Spring's `org.springframework.context.i18n.LocaleContextHolder`. +Spring MVC's `DispatcherServlet` does this for your application automatically, but since Spring Security's filters are invoked before this, the `LocaleContextHolder` needs to be set up to contain the correct `Locale` before the filters are called. +You can either do this in a filter yourself (which must come before the Spring Security filters in `web.xml`) or you can use Spring's `RequestContextFilter`. +Please refer to the Spring Framework documentation for further details on using localization with Spring. + +The "contacts" sample application is set up to use localized messages. From 538541bf40d0170439e4c18f0ea8c28755c518ee Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 10 Nov 2021 17:33:03 -0700 Subject: [PATCH 015/589] Don't Cache ReactiveJwtDecoders Errors Closes gh-10444 --- ...ReactiveAuthenticationManagerResolver.java | 3 +- ...uerAuthenticationManagerResolverTests.java | 34 +++++++++++++++++++ ...iveAuthenticationManagerResolverTests.java | 28 +++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java index afbfb38f9c..26aeca6d08 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolver.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.server.resource.authentication; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -186,7 +187,7 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver return this.authenticationManagers.computeIfAbsent(issuer, (k) -> Mono.fromCallable(() -> new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation(k))) .subscribeOn(Schedulers.boundedElastic()) - .cache() + .cache((manager) -> Duration.ofMillis(Long.MAX_VALUE), (ex) -> Duration.ZERO, () -> Duration.ZERO) ); // @formatter:on } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java index 99ab3933b9..8bc9573eda 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolverTests.java @@ -96,6 +96,40 @@ public class JwtIssuerAuthenticationManagerResolverTests { } } + @Test + public void resolveWhednUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.start(); + String issuer = server.url("").toString(); + // @formatter:off + server.enqueue(new MockResponse().setResponseCode(500) + .setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)) + ); + server.enqueue(new MockResponse().setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)) + ); + server.enqueue(new MockResponse().setResponseCode(200) + .setHeader("Content-Type", "application/json") + .setBody(JWK_SET) + ); + // @formatter:on + JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), + new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); + jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); + JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver( + issuer); + Authentication token = withBearerToken(jws.serialize()); + AuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null); + assertThat(authenticationManager).isNotNull(); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> authenticationManager.authenticate(token)); + Authentication authentication = authenticationManager.authenticate(token); + assertThat(authentication.isAuthenticated()).isTrue(); + } + } + @Test public void resolveWhenUsingSameIssuerThenReturnsSameAuthenticationManager() throws Exception { try (MockWebServer server = new MockWebServer()) { diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java index c13eac86f8..357d95423d 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerReactiveAuthenticationManagerResolverTests.java @@ -95,6 +95,34 @@ public class JwtIssuerReactiveAuthenticationManagerResolverTests { } } + // gh-10444 + @Test + public void resolveWhednUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception { + try (MockWebServer server = new MockWebServer()) { + String issuer = server.url("").toString(); + // @formatter:off + server.enqueue(new MockResponse().setResponseCode(500).setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer))); + server.enqueue(new MockResponse().setResponseCode(200).setHeader("Content-Type", "application/json") + .setBody(JWK_SET)); + // @formatter:on + JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256), + new Payload(new JSONObject(Collections.singletonMap(JwtClaimNames.ISS, issuer)))); + jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY)); + JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver( + issuer); + ReactiveAuthenticationManager authenticationManager = authenticationManagerResolver.resolve(null).block(); + assertThat(authenticationManager).isNotNull(); + Authentication token = withBearerToken(jws.serialize()); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> authenticationManager.authenticate(token).block()); + Authentication authentication = authenticationManager.authenticate(token).block(); + assertThat(authentication.isAuthenticated()).isTrue(); + } + } + @Test public void resolveWhenUsingSameIssuerThenReturnsSameAuthenticationManager() throws Exception { try (MockWebServer server = new MockWebServer()) { From 127e10e60706cb85aea363a7026e77c0fd34f3ee Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 8 Nov 2021 13:35:57 -0600 Subject: [PATCH 016/589] Antora Playbook --- .github/actions/algolia-config.json | 20 ++ .github/actions/algolia-deploy.sh | 20 ++ .github/actions/algolia-docsearch-scraper.sh | 21 ++ .github/actions/dispatch.sh | 4 +- .github/workflows/algolia-index.yml | 16 ++ ...uild-reference.yml => antora-generate.yml} | 4 +- .github/workflows/deploy-reference.yml | 33 +++ docs/antora-playbook.yml | 26 +++ docs/antora.yml | 2 +- docs/antora/extensions/major-minor-segment.js | 200 ++++++++++++++++++ docs/antora/extensions/root-component-name.js | 40 ++++ docs/local-antora-playbook.yml | 26 +++ docs/spring-security-docs.gradle | 17 ++ 13 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 .github/actions/algolia-config.json create mode 100755 .github/actions/algolia-deploy.sh create mode 100755 .github/actions/algolia-docsearch-scraper.sh create mode 100644 .github/workflows/algolia-index.yml rename .github/workflows/{build-reference.yml => antora-generate.yml} (91%) create mode 100644 .github/workflows/deploy-reference.yml create mode 100644 docs/antora-playbook.yml create mode 100644 docs/antora/extensions/major-minor-segment.js create mode 100644 docs/antora/extensions/root-component-name.js create mode 100644 docs/local-antora-playbook.yml diff --git a/.github/actions/algolia-config.json b/.github/actions/algolia-config.json new file mode 100644 index 0000000000..09d30d486e --- /dev/null +++ b/.github/actions/algolia-config.json @@ -0,0 +1,20 @@ +{ + "index_name": "security-docs", + "start_urls": [ + "https://docs.spring.io/spring-security/reference/" + ], + "selectors": { + "lvl0": { + "selector": "//nav[@class='crumbs']//li[@class='crumb'][last()-1]", + "type": "xpath", + "global": true, + "default_value": "Home" + }, + "lvl1": ".doc h1", + "lvl2": ".doc h2", + "lvl3": ".doc h3", + "lvl4": ".doc h4", + "text": ".doc p, .doc td.content, .doc th.tableblock" + } +} + diff --git a/.github/actions/algolia-deploy.sh b/.github/actions/algolia-deploy.sh new file mode 100755 index 0000000000..994dfee9ac --- /dev/null +++ b/.github/actions/algolia-deploy.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +HOST="$1" +HOST_PATH="$2" +SSH_PRIVATE_KEY="$3" +SSH_KNOWN_HOST="$4" + + +if [ "$#" -ne 4 ]; then + echo -e "not enough arguments USAGE:\n\n$0 \$HOST \$HOST_PATH \$SSH_PRIVATE_KEY \$SSH_KNOWN_HOSTS \n\n" >&2 + exit 1 +fi + +# Use a non-default path to avoid overriding when testing locally +SSH_PRIVATE_KEY_PATH=~/.ssh/github-actions-docs +install -m 600 -D /dev/null "$SSH_PRIVATE_KEY_PATH" +echo "$SSH_PRIVATE_KEY" > "$SSH_PRIVATE_KEY_PATH" +echo "$SSH_KNOWN_HOST" > ~/.ssh/known_hosts +rsync --delete -avze "ssh -i $SSH_PRIVATE_KEY_PATH" docs/build/site/ "$HOST:$HOST_PATH" +rm -f "$SSH_PRIVATE_KEY_PATH" diff --git a/.github/actions/algolia-docsearch-scraper.sh b/.github/actions/algolia-docsearch-scraper.sh new file mode 100755 index 0000000000..2bb9ce178a --- /dev/null +++ b/.github/actions/algolia-docsearch-scraper.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +### +# Docs +# config.json https://docsearch.algolia.com/docs/config-file +# Run the crawler https://docsearch.algolia.com/docs/run-your-own/#run-the-crawl-from-the-docker-image + +### USAGE +if [ "$#" -ne 3 ]; then + echo -e "not enough arguments USAGE:\n\n$0 \$ALGOLIA_APPLICATION_ID \$ALGOLIA_API_KEY \$CONFIG_FILE\n\n" >&2 + exit 1 +fi + +# Script Parameters +APPLICATION_ID=$1 +API_KEY=$2 +CONFIG_FILE=$3 + +#### Script +script_dir=$(dirname $0) +docker run -e "APPLICATION_ID=$APPLICATION_ID" -e "API_KEY=$API_KEY" -e "CONFIG=$(cat $CONFIG_FILE | jq -r tostring)" algolia/docsearch-scraper diff --git a/.github/actions/dispatch.sh b/.github/actions/dispatch.sh index d6c2a37794..955e9cbbee 100755 --- a/.github/actions/dispatch.sh +++ b/.github/actions/dispatch.sh @@ -1,5 +1,5 @@ REPOSITORY_REF="$1" TOKEN="$2" -curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${TOKEN}" --request POST --data '{"event_type": "request-build"}' https://api.github.com/repos/${REPOSITORY_REF}/dispatches -echo "Requested Build for $REPOSITORY_REF" \ No newline at end of file +curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${TOKEN}" --request POST --data '{"event_type": "request-build-reference"}' https://api.github.com/repos/${REPOSITORY_REF}/dispatches +echo "Requested Build for $REPOSITORY_REF" diff --git a/.github/workflows/algolia-index.yml b/.github/workflows/algolia-index.yml new file mode 100644 index 0000000000..dfc2295af3 --- /dev/null +++ b/.github/workflows/algolia-index.yml @@ -0,0 +1,16 @@ +name: Update Algolia Index + +on: + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: # Manual trigger + +jobs: + update: + name: Update Algolia Index + runs-on: ubuntu-latest + steps: + - name: Checkout Source + uses: actions/checkout@v2 + - name: Update Index + run: ${GITHUB_WORKSPACE}/.github/actions/algolia-docsearch-scraper.sh "${{ secrets.ALGOLIA_APPLICATION_ID }}" "${{ secrets.ALGOLIA_WRITE_API_KEY }}" "${GITHUB_WORKSPACE}/.github/actions/algolia-config.json" diff --git a/.github/workflows/build-reference.yml b/.github/workflows/antora-generate.yml similarity index 91% rename from .github/workflows/build-reference.yml rename to .github/workflows/antora-generate.yml index 7387f4a040..f5cd25cfbf 100644 --- a/.github/workflows/build-reference.yml +++ b/.github/workflows/antora-generate.yml @@ -1,4 +1,4 @@ -name: reference +name: Generate Antora Files and Request Build on: push: @@ -27,4 +27,4 @@ jobs: repository-name: "spring-io/spring-generated-docs" token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - name: Dispatch Build Request - run: ${GITHUB_WORKSPACE}/.github/actions/dispatch.sh 'spring-io/spring-reference' "$GH_ACTIONS_REPO_TOKEN" + run: ${GITHUB_WORKSPACE}/.github/actions/dispatch.sh 'spring-projects/spring-security' "$GH_ACTIONS_REPO_TOKEN" diff --git a/.github/workflows/deploy-reference.yml b/.github/workflows/deploy-reference.yml new file mode 100644 index 0000000000..a0033b926b --- /dev/null +++ b/.github/workflows/deploy-reference.yml @@ -0,0 +1,33 @@ +name: Build & Deploy Reference + +on: + repository_dispatch: + types: request-build-reference + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: # Manual trigger + +jobs: + deploy: + name: deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + cache: gradle + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Build with Gradle + run: ./gradlew :spring-security-docs:antora --stacktrace + - name: Cleanup Gradle Cache + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties + - name: Deploy + run: ${GITHUB_WORKSPACE}/.github/actions/algolia-deploy.sh "${{ secrets.DOCS_USERNAME }}@${{ secrets.DOCS_HOST }}" "/opt/www/domains/spring.io/docs/htdocs/spring-security/reference/" "${{ secrets.DOCS_SSH_KEY }}" "${{ secrets.DOCS_SSH_HOST_KEY }}" diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml new file mode 100644 index 0000000000..519d53d195 --- /dev/null +++ b/docs/antora-playbook.yml @@ -0,0 +1,26 @@ +site: + title: Spring Security + url: https://docs.spring.io/spring-security/reference/ +asciidoc: + attributes: + page-pagination: true +content: + sources: + - url: https://github.com/spring-io/spring-generated-docs + branches: [spring-projects/spring-security/*] + - url: https://github.com/spring-projects/spring-security + branches: [main,5.6.x] + start_path: docs +urls: + latest_version_segment_strategy: redirect:to + latest_version_segment: '' + redirect_facility: httpd +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip + snapshot: true + +pipeline: + extensions: + - require: ./antora/extensions/major-minor-segment.js + - require: ./antora/extensions/root-component-name.js diff --git a/docs/antora.yml b/docs/antora.yml index 45645cd5b4..0ae5b21792 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,2 @@ name: ROOT -version: 5.6 +version: '5.6' diff --git a/docs/antora/extensions/major-minor-segment.js b/docs/antora/extensions/major-minor-segment.js new file mode 100644 index 0000000000..eec0764544 --- /dev/null +++ b/docs/antora/extensions/major-minor-segment.js @@ -0,0 +1,200 @@ +// https://gitlab.com/antora/antora/-/issues/132#note_712132072 +'use strict' + +const { posix: path } = require('path') + +module.exports.register = (pipeline, { config }) => { + pipeline.on('contentClassified', ({ contentCatalog }) => { + contentCatalog.getComponents().forEach(component => { + const componentName = component.name; + const generationToVersion = new Map(); + component.versions.forEach(version => { + const generation = getGeneration(version.version); + const original = generationToVersion.get(generation); + if (original === undefined || (original.prerelease && !version.prerelease)) { + generationToVersion.set(generation, version); + } + }); + + const versionToGeneration = Array.from(generationToVersion.entries()).reduce((acc, entry) => { + const [ generation, version ] = entry; + acc.set(version.version, generation); + return acc; + }, new Map()); + + contentCatalog.findBy({ component: componentName }).forEach((file) => { + const candidateVersion = file.src.version; + if (versionToGeneration.has(candidateVersion)) { + const generation = versionToGeneration.get(candidateVersion); + if (file.out) { + if (file.out) { + file.out.dirname = file.out.dirname.replace(candidateVersion, generation) + file.out.path = file.out.path.replace(candidateVersion, generation); + } + } + if (file.pub) { + file.pub.url = file.pub.url.replace(candidateVersion, generation) + } + } + }); + versionToGeneration.forEach((generation, mappedVersion) => { + contentCatalog.getComponent(componentName).versions.filter(version => version.version === mappedVersion).forEach((version) => { + version.url = version.url.replace(mappedVersion, generation); + }) + const symbolicVersionAlias = createSymbolicVersionAlias( + componentName, + mappedVersion, + generation, + 'redirect:to' + ) + symbolicVersionAlias.src.version = generation; + contentCatalog.addFile(symbolicVersionAlias); + }); + }) + }) +} + +function createSymbolicVersionAlias (component, version, symbolicVersionSegment, strategy) { + if (symbolicVersionSegment == null || symbolicVersionSegment === version) return + const family = 'alias' + const baseVersionAliasSrc = { component, module: 'ROOT', family, relative: '', basename: '', stem: '', extname: '' } + const symbolicVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version: symbolicVersionSegment }) + const symbolicVersionAlias = { + src: symbolicVersionAliasSrc, + pub: computePub( + symbolicVersionAliasSrc, + computeOut(symbolicVersionAliasSrc, family, symbolicVersionSegment), + family + ), + } + const originalVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version }) + const originalVersionSegment = computeVersionSegment(component, version, 'original') + const originalVersionAlias = { + src: originalVersionAliasSrc, + pub: computePub( + originalVersionAliasSrc, + computeOut(originalVersionAliasSrc, family, originalVersionSegment), + family + ), + } + if (strategy === 'redirect:to') { + originalVersionAlias.out = undefined + originalVersionAlias.rel = symbolicVersionAlias + return originalVersionAlias + } else { + symbolicVersionAlias.out = undefined + symbolicVersionAlias.rel = originalVersionAlias + return symbolicVersionAlias + } +} + + +function computeOut (src, family, version, htmlUrlExtensionStyle) { + let { component, module: module_, basename, extname, relative, stem } = src + if (module_ === 'ROOT') module_ = '' + let indexifyPathSegment = '' + let familyPathSegment = '' + + if (family === 'page') { + if (stem !== 'index' && htmlUrlExtensionStyle === 'indexify') { + basename = 'index.html' + indexifyPathSegment = stem + } else if (extname === '.adoc') { + basename = stem + '.html' + } + } else if (family === 'image') { + familyPathSegment = '_images' + } else if (family === 'attachment') { + familyPathSegment = '_attachments' + } + const modulePath = path.join(component, version, module_) + const dirname = path.join(modulePath, familyPathSegment, path.dirname(relative), indexifyPathSegment) + const path_ = path.join(dirname, basename) + const moduleRootPath = path.relative(dirname, modulePath) || '.' + const rootPath = path.relative(dirname, '') || '.' + + return { dirname, basename, path: path_, moduleRootPath, rootPath } +} + +function computePub (src, out, family, version, htmlUrlExtensionStyle) { + const pub = {} + let url + if (family === 'nav') { + const urlSegments = version ? [src.component, version] : [src.component] + if (src.module && src.module !== 'ROOT') urlSegments.push(src.module) + // an artificial URL used for resolving page references in navigation model + url = '/' + urlSegments.join('/') + '/' + pub.moduleRootPath = '.' + } else if (family === 'page') { + const urlSegments = out.path.split('/') + const lastUrlSegmentIdx = urlSegments.length - 1 + if (htmlUrlExtensionStyle === 'drop') { + // drop just the .html extension or, if the filename is index.html, the whole segment + const lastUrlSegment = urlSegments[lastUrlSegmentIdx] + urlSegments[lastUrlSegmentIdx] = + lastUrlSegment === 'index.html' ? '' : lastUrlSegment.substr(0, lastUrlSegment.length - 5) + } else if (htmlUrlExtensionStyle === 'indexify') { + urlSegments[lastUrlSegmentIdx] = '' + } + url = '/' + urlSegments.join('/') + } else { + url = '/' + out.path + if (family === 'alias' && !src.relative.length) pub.splat = true + } + + pub.url = ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url + + if (out) { + pub.moduleRootPath = out.moduleRootPath + pub.rootPath = out.rootPath + } + + return pub +} + +function computeVersionSegment (name, version, mode) { + if (mode === 'original') return !version || version === 'master' ? '' : version + const strategy = this.latestVersionUrlSegmentStrategy + // NOTE: special exception; revisit in Antora 3 + if (!version || version === 'master') { + if (mode !== 'alias') return '' + if (strategy === 'redirect:to') return + } + if (strategy === 'redirect:to' || strategy === (mode === 'alias' ? 'redirect:from' : 'replace')) { + const component = this.getComponent(name) + const componentVersion = component && this.getComponentVersion(component, version) + if (componentVersion) { + const segment = + componentVersion === component.latest + ? this.latestVersionUrlSegment + : componentVersion === component.latestPrerelease + ? this.latestPrereleaseVersionUrlSegment + : undefined + return segment == null ? version : segment + } + } + return version +} + +function getGeneration(version) { + if (!version) return version; + const firstIndex = version.indexOf('.') + if (firstIndex < 0) { + return version; + } + const secondIndex = version.indexOf('.', firstIndex + 1); + const result = version.substr(0, secondIndex); + return result; +} + +function out(args) { + console.log(JSON.stringify(args, no_data, 2)); +} + + +function no_data(key, value) { + if (key == "data" || key == "files") { + return value ? "__data__" : value; + } + return value; +} \ No newline at end of file diff --git a/docs/antora/extensions/root-component-name.js b/docs/antora/extensions/root-component-name.js new file mode 100644 index 0000000000..dcc8dc482c --- /dev/null +++ b/docs/antora/extensions/root-component-name.js @@ -0,0 +1,40 @@ +// https://gitlab.com/antora/antora/-/issues/132#note_712132072 +'use strict' + +const { posix: path } = require('path') + +module.exports.register = (pipeline, { config }) => { + pipeline.on('contentClassified', ({ contentCatalog }) => { + const rootComponentName = config.rootComponentName || 'ROOT' + const rootComponentNameLength = rootComponentName.length + contentCatalog.findBy({ component: rootComponentName }).forEach((file) => { + if (file.out) { + file.out.dirname = file.out.dirname.substr(rootComponentNameLength) + file.out.path = file.out.path.substr(rootComponentNameLength + 1) + file.out.rootPath = fixPath(file.out.rootPath) + } + if (file.pub) { + file.pub.url = file.pub.url.substr(rootComponentNameLength + 1) + if (file.pub.rootPath) { + file.pub.rootPath = fixPath(file.pub.rootPath) + } + } + if (file.rel) { + if (file.rel.pub) { + file.rel.pub.url = file.rel.pub.url.substr(rootComponentNameLength + 1) + file.rel.pub.rootPath = fixPath(file.rel.pub.rootPath); + } + } + }) + const rootComponent = contentCatalog.getComponent(rootComponentName) + rootComponent?.versions?.forEach((version) => { + version.url = version.url.substr(rootComponentName.length + 1) + }) + // const siteStartPage = contentCatalog.getById({ component: '', version: '', module: '', family: 'alias', relative: 'index.adoc' }) + // if (siteStartPage) delete siteStartPage.out + }) + + function fixPath(path) { + return path.split('/').slice(1).join('/') || '.' + } +} \ No newline at end of file diff --git a/docs/local-antora-playbook.yml b/docs/local-antora-playbook.yml new file mode 100644 index 0000000000..8e2678cb29 --- /dev/null +++ b/docs/local-antora-playbook.yml @@ -0,0 +1,26 @@ +site: + title: Spring Security + url: https://docs.spring.io/spring-security/reference/ +asciidoc: + attributes: + page-pagination: true +content: + sources: + - url: ../../spring-io/spring-generated-docs + branches: [spring-projects/spring-security/*] + - url: ../../spring-projects/spring-security + branches: [main,5.6.x] + start_path: docs +urls: + latest_version_segment_strategy: redirect:to + latest_version_segment: '' + redirect_facility: httpd +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip + snapshot: true + +pipeline: + extensions: + - require: ./antora/extensions/major-minor-segment.js + - require: ./antora/extensions/root-component-name.js diff --git a/docs/spring-security-docs.gradle b/docs/spring-security-docs.gradle index 0c4747cd59..a28a20c5b3 100644 --- a/docs/spring-security-docs.gradle +++ b/docs/spring-security-docs.gradle @@ -1,6 +1,23 @@ +plugins { + id "io.github.rwinch.antora" version "0.0.2" +} + apply plugin: 'io.spring.convention.docs' apply plugin: 'java' +antora { + antoraVersion = "3.0.0-alpha.8" + arguments = ["--fetch"] +} + +tasks.antora { + environment = [ + "ALGOLIA_API_KEY" : "82c7ead946afbac3cf98c32446154691", + "ALGOLIA_APP_ID" : "244V8V9FGG", + "ALGOLIA_INDEX_NAME" : "security-docs" + ] +} + tasks.register("generateAntora") { group = "Documentation" description = "Generates the antora.yml for dynamic properties" From 08dc83c781aeadda48db420b9957415e2783ff98 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 11 Nov 2021 13:35:34 -0600 Subject: [PATCH 017/589] Fix Antora Versions --- docs/antora.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/antora.yml b/docs/antora.yml index 0ae5b21792..fe004e38a4 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ name: ROOT -version: '5.6' +version: '5.6.0' +prerelease: '-SNAPSHOT' From 1246d5839de67533db651a51cb640b32e6d3d4f7 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 5 Nov 2021 10:44:16 -0500 Subject: [PATCH 018/589] Revamp OAuth 2.0 Login Reactive documentation Related gh-8174 --- .../ROOT/pages/reactive/oauth2/login.adoc | 1262 +++++++++++++++-- 1 file changed, 1165 insertions(+), 97 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login.adoc index 4aee887bb3..b03e5ca1d0 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login.adoc @@ -1,16 +1,16 @@ [[webflux-oauth2-login]] = OAuth 2.0 Login -The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. -GitHub) or OpenID Connect 1.0 Provider (such as Google). +The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google). OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework] and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[OpenID Connect Core 1.0]. -[[webflux-oauth2-login-sample]] -== Spring Boot 2.0 Sample -Spring Boot 2.0 brings full auto-configuration capabilities for OAuth 2.0 Login. +[[webflux-oauth2-login-sample]] +== Spring Boot 2.x Sample + +Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login. This section shows how to configure the {gh-samples-url}/reactive/webflux/java/oauth2/login[*OAuth 2.0 Login WebFlux sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: @@ -31,6 +31,7 @@ Follow the instructions on the https://developers.google.com/identity/protocols/ After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. + [[webflux-oauth2-login-sample-redirect]] === Setting the redirect URI @@ -39,11 +40,12 @@ The redirect URI is the path in the application that the end-user's user-agent i In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. -The *_registrationId_* is a unique identifier for the xref:servlet/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration]. +The *_registrationId_* is a unique identifier for the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration]. For our example, the `registrationId` is `google`. IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. -Also, see the supported xref:servlet/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. +Also, see the supported xref:reactive/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. + [[webflux-oauth2-login-sample-config]] === Configure `application.yml` @@ -68,7 +70,7 @@ spring: .OAuth Client properties ==== <1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. -<2> Following the base property prefix is the ID for the xref:servlet/oauth2/client/index.adoc#oauth2Client-client-registration[`ClientRegistration`], such as google. +<2> Following the base property prefix is the ID for the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[`ClientRegistration`], such as google. ==== . Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. @@ -77,7 +79,7 @@ spring: [[webflux-oauth2-login-sample-start]] === Boot up the application -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +Launch the Spring Boot 2.x sample and go to `http://localhost:8080`. You are then redirected to the default _auto-generated_ login page, which displays a link for Google. Click on the Google link, and you are then redirected to Google for authentication. @@ -88,142 +90,1103 @@ Click *Allow* to authorize the OAuth Client to access your email address and bas At this point, the OAuth Client retrieves your email address and basic profile information from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session. -[[webflux-oauth2-login-openid-provider-configuration]] -== Using OpenID Provider Configuration -For well known providers, Spring Security provides the necessary defaults for the OAuth Authorization Provider's configuration. -If you are working with your own Authorization Provider that supports https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration] or https://tools.ietf.org/html/rfc8414#section-3[Authorization Server Metadata], the https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse[OpenID Provider Configuration Response]'s `issuer-uri` can be used to configure the application. +[[oauth2login-boot-property-mappings]] +== Spring Boot 2.x Property Mappings -[source,yml] +The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration] properties. + +|=== +|Spring Boot 2.x |ClientRegistration + +|`spring.security.oauth2.client.registration._[registrationId]_` +|`registrationId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-id` +|`clientId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-secret` +|`clientSecret` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-authentication-method` +|`clientAuthenticationMethod` + +|`spring.security.oauth2.client.registration._[registrationId]_.authorization-grant-type` +|`authorizationGrantType` + +|`spring.security.oauth2.client.registration._[registrationId]_.redirect-uri` +|`redirectUri` + +|`spring.security.oauth2.client.registration._[registrationId]_.scope` +|`scopes` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-name` +|`clientName` + +|`spring.security.oauth2.client.provider._[providerId]_.authorization-uri` +|`providerDetails.authorizationUri` + +|`spring.security.oauth2.client.provider._[providerId]_.token-uri` +|`providerDetails.tokenUri` + +|`spring.security.oauth2.client.provider._[providerId]_.jwk-set-uri` +|`providerDetails.jwkSetUri` + +|`spring.security.oauth2.client.provider._[providerId]_.issuer-uri` +|`providerDetails.issuerUri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-uri` +|`providerDetails.userInfoEndpoint.uri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-authentication-method` +|`providerDetails.userInfoEndpoint.authenticationMethod` + +|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute` +|`providerDetails.userInfoEndpoint.userNameAttributeName` +|=== + +[TIP] +A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint], by specifying the `spring.security.oauth2.client.provider._[providerId]_.issuer-uri` property. + + +[[webflux-oauth2-login-common-oauth2-provider]] +== CommonOAuth2Provider + +`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta. + +For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a Provider. +Therefore, it makes sense to provide default values in order to reduce the required configuration. + +As demonstrated previously, when we <>, only the `client-id` and `client-secret` properties are required. + +The following listing shows an example: + +[source,yaml] ---- spring: security: oauth2: client: - provider: - keycloak: - issuer-uri: https://idp.example.com/auth/realms/demo registration: - keycloak: - client-id: spring-security - client-secret: 6cea952f-10d0-4d00-ac79-cc865820dc2c + google: + client-id: google-client-id + client-secret: google-client-secret ---- -The `issuer-uri` instructs Spring Security to query in series the endpoints `https://idp.example.com/auth/realms/demo/.well-known/openid-configuration`, `https://idp.example.com/.well-known/openid-configuration/auth/realms/demo`, or `https://idp.example.com/.well-known/oauth-authorization-server/auth/realms/demo` to discover the configuration. +[TIP] +The auto-defaulting of client properties works seamlessly here because the `registrationId` (`google`) matches the `GOOGLE` `enum` (case-insensitive) in `CommonOAuth2Provider`. -[NOTE] -Spring Security will query the endpoints one at a time, stopping at the first that gives a 200 response. +For cases where you may want to specify a different `registrationId`, such as `google-login`, you can still leverage auto-defaulting of client properties by configuring the `provider` property. -The `client-id` and `client-secret` are linked to the provider because `keycloak` is used for both the provider and the registration. +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + google-login: <1> + provider: google <2> + client-id: google-client-id + client-secret: google-client-secret +---- +<1> The `registrationId` is set to `google-login`. +<2> The `provider` property is set to `google`, which will leverage the auto-defaulting of client properties set in `CommonOAuth2Provider.GOOGLE.getBuilder()`. -[[webflux-oauth2-login-explicit]] -== Explicit OAuth2 Login Configuration +[[webflux-oauth2-login-custom-provider-properties]] +== Configuring Custom Provider Properties -A minimal OAuth2 Login configuration is shown below: +There are some OAuth 2.0 Providers that support multi-tenancy, which results in different protocol endpoints for each tenant (or sub-domain). + +For example, an OAuth Client registered with Okta is assigned to a specific sub-domain and have their own protocol endpoints. + +For these cases, Spring Boot 2.x provides the following base property for configuring custom provider properties: `spring.security.oauth2.client.provider._[providerId]_`. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + provider: + okta: <1> + authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize + token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token + user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo + user-name-attribute: sub + jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys +---- + +<1> The base property (`spring.security.oauth2.client.provider.okta`) allows for custom configuration of protocol endpoint locations. + + +[[webflux-oauth2-login-override-boot-autoconfig]] +== Overriding Spring Boot 2.x Auto-configuration + +The Spring Boot 2.x auto-configuration class for OAuth Client support is `ReactiveOAuth2ClientAutoConfiguration`. + +It performs the following tasks: + +* Registers a `ReactiveClientRegistrationRepository` `@Bean` composed of `ClientRegistration`(s) from the configured OAuth Client properties. +* Registers a `SecurityWebFilterChain` `@Bean` and enables OAuth 2.0 Login through `serverHttpSecurity.oauth2Login()`. + +If you need to override the auto-configuration based on your specific requirements, you may do so in the following ways: + +* <> +* <> +* <> + + +[[webflux-oauth2-login-register-reactiveclientregistrationrepository-bean]] +=== Register a ReactiveClientRegistrationRepository @Bean + +The following example shows how to register a `ReactiveClientRegistrationRepository` `@Bean`: -.Minimal OAuth2 Login ==== .Java -[source,java,role="primary"] +[source,java,role="primary",attrs="-attributes"] ---- -@Bean -ReactiveClientRegistrationRepository clientRegistrations() { - ClientRegistration clientRegistration = ClientRegistrations - .fromIssuerLocation("https://idp.example.com/auth/realms/demo") - .clientId("spring-security") - .clientSecret("6cea952f-10d0-4d00-ac79-cc865820dc2c") - .build(); - return new InMemoryReactiveClientRegistrationRepository(clientRegistration); -} +@Configuration +public class OAuth2LoginConfig { -@Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - // ... - .oauth2Login(withDefaults()); - return http.build(); + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } } ---- .Kotlin -[source,kotlin,role="secondary"] +[source,kotlin,role="secondary",attrs="-attributes"] ---- -@Bean -fun clientRegistrations(): ReactiveClientRegistrationRepository { - val clientRegistration: ClientRegistration = ClientRegistrations - .fromIssuerLocation("https://idp.example.com/auth/realms/demo") - .clientId("spring-security") - .clientSecret("6cea952f-10d0-4d00-ac79-cc865820dc2c") - .build() - return InMemoryReactiveClientRegistrationRepository(clientRegistration) -} +@Configuration +class OAuth2LoginConfig { -@Bean -fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - return http { - oauth2Login { } + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() } } ---- ==== -Additional configuration options can be seen below: -.Advanced OAuth2 Login +[[webflux-oauth2-login-register-securitywebfilterchain-bean]] +=== Register a SecurityWebFilterChain @Bean + +The following example shows how to register a `SecurityWebFilterChain` `@Bean` with `@EnableWebFluxSecurity` and enable OAuth 2.0 login through `serverHttpSecurity.oauth2Login()`: + +.OAuth2 Login Configuration ==== .Java [source,java,role="primary"] ---- -@Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - // ... - .oauth2Login(oauth2 -> oauth2 - .authenticationConverter(converter) - .authenticationManager(manager) - .authorizedClientRepository(authorizedClients) - .clientRegistrationRepository(clientRegistrations) - ); - return http.build(); +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } } ---- .Kotlin [source,kotlin,role="secondary"] ---- -@Bean -fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - return http { - oauth2Login { - authenticationConverter = converter - authenticationManager = manager - authorizedClientRepository = authorizedClients - clientRegistrationRepository = clientRegistration +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + + return http.build() + } +} +---- +==== + + +[[webflux-oauth2-login-completely-override-autoconfiguration]] +=== Completely Override the Auto-configuration + +The following example shows how to completely override the auto-configuration by registering a `ReactiveClientRegistrationRepository` `@Bean` and a `SecurityWebFilterChain` `@Bean`. + +.Overriding the auto-configuration +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +@EnableWebFluxSecurity +class OAuth2LoginConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + + return http.build() + } + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +---- +==== + + +[[webflux-oauth2-login-javaconfig-wo-boot]] +== Java Configuration without Spring Boot 2.x + +If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in `CommonOAuth2Provider` (for example, Google), apply the following configuration: + +.OAuth2 Login Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + @Bean + public ReactiveOAuth2AuthorizedClientService authorizedClientService( + ReactiveClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository); + } + + @Bean + public ServerOAuth2AuthorizedClientRepository authorizedClientRepository( + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService); + } + + private ClientRegistration googleClientRegistration() { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + + return http.build() + } + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + @Bean + fun authorizedClientService( + clientRegistrationRepository: ReactiveClientRegistrationRepository + ): ReactiveOAuth2AuthorizedClientService { + return InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository) + } + + @Bean + fun authorizedClientRepository( + authorizedClientService: ReactiveOAuth2AuthorizedClientService + ): ServerOAuth2AuthorizedClientRepository { + return AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService) + } + + private fun googleClientRegistration(): ClientRegistration { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build() + } +} +---- +==== + + +[[webflux-oauth2-login-advanced]] +== Advanced Configuration + +The OAuth 2.0 Authorization Framework defines the https://tools.ietf.org/html/rfc6749#section-3[Protocol Endpoints] as follows: + +The authorization process utilizes two authorization server endpoints (HTTP resources): + +* Authorization Endpoint: Used by the client to obtain authorization from the resource owner via user-agent redirection. +* Token Endpoint: Used by the client to exchange an authorization grant for an access token, typically with client authentication. + +As well as one client endpoint: + +* Redirection Endpoint: Used by the authorization server to return responses containing authorization credentials to the client via the resource owner user-agent. + +The OpenID Connect Core 1.0 specification defines the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] as follows: + +The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns claims about the authenticated end-user. +To obtain the requested claims about the end-user, the client makes a request to the UserInfo Endpoint by using an access token obtained through OpenID Connect Authentication. +These claims are normally represented by a JSON object that contains a collection of name-value pairs for the claims. + +`ServerHttpSecurity.oauth2Login()` provides a number of configuration options for customizing OAuth 2.0 Login. + +The following code shows the complete configuration options available for the `oauth2Login()` DSL: + +.OAuth2 Login Configuration Options +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .oauth2Login(oauth2 -> oauth2 + .authenticationConverter(this.authenticationConverter()) + .authenticationMatcher(this.authenticationMatcher()) + .authenticationManager(this.authenticationManager()) + .authenticationSuccessHandler(this.authenticationSuccessHandler()) + .authenticationFailureHandler(this.authenticationFailureHandler()) + .clientRegistrationRepository(this.clientRegistrationRepository()) + .authorizedClientRepository(this.authorizedClientRepository()) + .authorizedClientService(this.authorizedClientService()) + .authorizationRequestResolver(this.authorizationRequestResolver()) + .authorizationRequestRepository(this.authorizationRequestRepository()) + .securityContextRepository(this.securityContextRepository()) + ); + + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + oauth2Login { + authenticationConverter = authenticationConverter() + authenticationMatcher = authenticationMatcher() + authenticationManager = authenticationManager() + authenticationSuccessHandler = authenticationSuccessHandler() + authenticationFailureHandler = authenticationFailureHandler() + clientRegistrationRepository = clientRegistrationRepository() + authorizedClientRepository = authorizedClientRepository() + authorizedClientService = authorizedClientService() + authorizationRequestResolver = authorizationRequestResolver() + authorizationRequestRepository = authorizationRequestRepository() + securityContextRepository = securityContextRepository() + } + } + + return http.build() + } +} +---- +==== + +The following sections go into more detail on each of the configuration options available: + +* <> +* <> +* <> +* <> +* <> + + +[[webflux-oauth2-login-advanced-login-page]] +=== OAuth 2.0 Login Page + +By default, the OAuth 2.0 Login Page is auto-generated by the `LoginPageGeneratingWebFilter`. +The default login page shows each configured OAuth Client with its `ClientRegistration.clientName` as a link, which is capable of initiating the Authorization Request (or OAuth 2.0 Login). + +[NOTE] +In order for `LoginPageGeneratingWebFilter` to show links for configured OAuth Clients, the registered `ReactiveClientRegistrationRepository` needs to also implement `Iterable`. +See `InMemoryReactiveClientRegistrationRepository` for reference. + +The link's destination for each OAuth Client defaults to the following: + +`+"/oauth2/authorization/{registrationId}"+` + +The following line shows an example: + +[source,html] +---- +Google +---- + +To override the default login page, configure the `exceptionHandling().authenticationEntryPoint()` and (optionally) `oauth2Login().authorizationRequestResolver()`. + +The following listing shows an example: + +.OAuth2 Login Page Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/login/oauth2")) + ) + .oauth2Login(oauth2 -> oauth2 + .authorizationRequestResolver(this.authorizationRequestResolver()) + ); + + return http.build(); + } + + private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() { + ServerWebExchangeMatcher authorizationRequestMatcher = + new PathPatternParserServerWebExchangeMatcher( + "/login/oauth2/authorization/{registrationId}"); + + return new DefaultServerOAuth2AuthorizationRequestResolver( + this.clientRegistrationRepository(), authorizationRequestMatcher); + } + + ... +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + exceptionHandling { + authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/login/oauth2") + } + oauth2Login { + authorizationRequestResolver = authorizationRequestResolver() + } + } + + return http.build() + } + + private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver { + val authorizationRequestMatcher: ServerWebExchangeMatcher = PathPatternParserServerWebExchangeMatcher( + "/login/oauth2/authorization/{registrationId}" + ) + + return DefaultServerOAuth2AuthorizationRequestResolver( + clientRegistrationRepository(), authorizationRequestMatcher + ) + } + + ... +} +---- +==== + +[IMPORTANT] +You need to provide a `@Controller` with a `@RequestMapping("/login/oauth2")` that is capable of rendering the custom login page. + +[TIP] +==== +As noted earlier, configuring `oauth2Login().authorizationRequestResolver()` is optional. +However, if you choose to customize it, ensure the link to each OAuth Client matches the pattern provided through the `ServerWebExchangeMatcher`. + +The following line shows an example: + +[source,html] +---- +Google +---- +==== + + +[[webflux-oauth2-login-advanced-redirection-endpoint]] +=== Redirection Endpoint + +The Redirection Endpoint is used by the Authorization Server for returning the Authorization Response (which contains the authorization credentials) to the client via the Resource Owner user-agent. + +[TIP] +OAuth 2.0 Login leverages the Authorization Code Grant. +Therefore, the authorization credential is the authorization code. + +The default Authorization Response redirection endpoint is `/login/oauth2/code/{registrationId}`. + +If you would like to customize the Authorization Response redirection endpoint, configure it as shown in the following example: + +.Redirection Endpoint Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .oauth2Login(oauth2 -> oauth2 + .authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}")) + ); + + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + oauth2Login { + authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}") + } + } + + return http.build() + } +} +---- +==== + +[IMPORTANT] +==== +You also need to ensure the `ClientRegistration.redirectUri` matches the custom Authorization Response redirection endpoint. + +The following listing shows an example: + +.Java +[source,java,role="primary",attrs="-attributes"] +---- +return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}") + .build(); +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}") + .build() +---- +==== + + +[[webflux-oauth2-login-advanced-userinfo-endpoint]] +=== UserInfo Endpoint + +The UserInfo Endpoint includes a number of configuration options, as described in the following sub-sections: + +* <> +* <> +* <> + + +[[webflux-oauth2-login-advanced-map-authorities]] +==== Mapping User Authorities + +After the user successfully authenticates with the OAuth 2.0 Provider, the `OAuth2User.getAuthorities()` (or `OidcUser.getAuthorities()`) may be mapped to a new set of `GrantedAuthority` instances, which will be supplied to `OAuth2AuthenticationToken` when completing the authentication. + +[TIP] +`OAuth2AuthenticationToken.getAuthorities()` is used for authorizing requests, such as in `hasRole('USER')` or `hasRole('ADMIN')`. + +There are a couple of options to choose from when mapping user authorities: + +* <> +* <> + + +[[webflux-oauth2-login-advanced-map-authorities-grantedauthoritiesmapper]] +===== Using a GrantedAuthoritiesMapper + +Register a `GrantedAuthoritiesMapper` `@Bean` to have it automatically applied to the configuration, as shown in the following example: + +.Granted Authorities Mapper Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public GrantedAuthoritiesMapper userAuthoritiesMapper() { + return (authorities) -> { + Set mappedAuthorities = new HashSet<>(); + + authorities.forEach(authority -> { + if (OidcUserAuthority.class.isInstance(authority)) { + OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority; + + OidcIdToken idToken = oidcUserAuthority.getIdToken(); + OidcUserInfo userInfo = oidcUserAuthority.getUserInfo(); + + // Map the claims found in idToken and/or userInfo + // to one or more GrantedAuthority's and add it to mappedAuthorities + + } else if (OAuth2UserAuthority.class.isInstance(authority)) { + OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority; + + Map userAttributes = oauth2UserAuthority.getAttributes(); + + // Map the attributes found in userAttributes + // to one or more GrantedAuthority's and add it to mappedAuthorities + + } + }); + + return mappedAuthorities; + }; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + oauth2Login { } + } + + return http.build() + } + + @Bean + fun userAuthoritiesMapper(): GrantedAuthoritiesMapper = GrantedAuthoritiesMapper { authorities: Collection -> + val mappedAuthorities = emptySet() + + authorities.forEach { authority -> + if (authority is OidcUserAuthority) { + val idToken = authority.idToken + val userInfo = authority.userInfo + // Map the claims found in idToken and/or userInfo + // to one or more GrantedAuthority's and add it to mappedAuthorities + } else if (authority is OAuth2UserAuthority) { + val userAttributes = authority.attributes + // Map the attributes found in userAttributes + // to one or more GrantedAuthority's and add it to mappedAuthorities + } + } + + mappedAuthorities + } +} +---- +==== + +[[webflux-oauth2-login-advanced-map-authorities-reactiveoauth2userservice]] +===== Delegation-based strategy with ReactiveOAuth2UserService + +This strategy is advanced compared to using a `GrantedAuthoritiesMapper`, however, it's also more flexible as it gives you access to the `OAuth2UserRequest` and `OAuth2User` (when using an OAuth 2.0 UserService) or `OidcUserRequest` and `OidcUser` (when using an OpenID Connect 1.0 UserService). + +The `OAuth2UserRequest` (and `OidcUserRequest`) provides you access to the associated `OAuth2AccessToken`, which is very useful in the cases where the _delegator_ needs to fetch authority information from a protected resource before it can map the custom authorities for the user. + +The following example shows how to implement and configure a delegation-based strategy using an OpenID Connect 1.0 UserService: + +.ReactiveOAuth2UserService Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveOAuth2UserService oidcUserService() { + final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService(); + + return (userRequest) -> { + // Delegate to the default implementation for loading a user + return delegate.loadUser(userRequest) + .flatMap((oidcUser) -> { + OAuth2AccessToken accessToken = userRequest.getAccessToken(); + Set mappedAuthorities = new HashSet<>(); + + // TODO + // 1) Fetch the authority information from the protected resource using accessToken + // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities + + // 3) Create a copy of oidcUser but use the mappedAuthorities instead + oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo()); + + return Mono.just(oidcUser); + }); + }; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + oauth2Login { } + } + + return http.build() + } + + @Bean + fun oidcUserService(): ReactiveOAuth2UserService { + val delegate = OidcReactiveOAuth2UserService() + + return ReactiveOAuth2UserService { userRequest -> + // Delegate to the default implementation for loading a user + delegate.loadUser(userRequest) + .flatMap { oidcUser -> + val accessToken = userRequest.accessToken + val mappedAuthorities = mutableSetOf() + + // TODO + // 1) Fetch the authority information from the protected resource using accessToken + // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities + // 3) Create a copy of oidcUser but use the mappedAuthorities instead + val mappedOidcUser = DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo) + + Mono.just(mappedOidcUser) + } } } } ---- ==== -You may register a `GrantedAuthoritiesMapper` `@Bean` to have it automatically applied to the default configuration, as shown in the following example: -.GrantedAuthoritiesMapper Bean +[[webflux-oauth2-login-advanced-oauth2-user-service]] +==== OAuth 2.0 UserService + +`DefaultReactiveOAuth2UserService` is an implementation of a `ReactiveOAuth2UserService` that supports standard OAuth 2.0 Provider's. + +[NOTE] +`ReactiveOAuth2UserService` obtains the user attributes of the end-user (the resource owner) from the UserInfo Endpoint (by using the access token granted to the client during the authorization flow) and returns an `AuthenticatedPrincipal` in the form of an `OAuth2User`. + +`DefaultReactiveOAuth2UserService` uses a `WebClient` when requesting the user attributes at the UserInfo Endpoint. + +If you need to customize the pre-processing of the UserInfo Request and/or the post-handling of the UserInfo Response, you will need to provide `DefaultReactiveOAuth2UserService.setWebClient()` with a custom configured `WebClient`. + +Whether you customize `DefaultReactiveOAuth2UserService` or provide your own implementation of `ReactiveOAuth2UserService`, you'll need to configure it as shown in the following example: + +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveOAuth2UserService oauth2UserService() { + ... + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + oauth2Login { } + } + + return http.build() + } + + @Bean + fun oauth2UserService(): ReactiveOAuth2UserService { + // ... + } +} +---- +==== + + +[[webflux-oauth2-login-advanced-oidc-user-service]] +==== OpenID Connect 1.0 UserService + +`OidcReactiveOAuth2UserService` is an implementation of a `ReactiveOAuth2UserService` that supports OpenID Connect 1.0 Provider's. + +The `OidcReactiveOAuth2UserService` leverages the `DefaultReactiveOAuth2UserService` when requesting the user attributes at the UserInfo Endpoint. + +If you need to customize the pre-processing of the UserInfo Request and/or the post-handling of the UserInfo Response, you will need to provide `OidcReactiveOAuth2UserService.setOauth2UserService()` with a custom configured `ReactiveOAuth2UserService`. + +Whether you customize `OidcReactiveOAuth2UserService` or provide your own implementation of `ReactiveOAuth2UserService` for OpenID Connect 1.0 Provider's, you'll need to configure it as shown in the following example: + +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveOAuth2UserService oidcUserService() { + ... + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + oauth2Login { } + } + + return http.build() + } + + @Bean + fun oidcUserService(): ReactiveOAuth2UserService { + // ... + } +} +---- +==== + + +[[webflux-oauth2-login-advanced-idtoken-verify]] +=== ID Token Signature Verification + +OpenID Connect 1.0 Authentication introduces the https://openid.net/specs/openid-connect-core-1_0.html#IDToken[ID Token], which is a security token that contains Claims about the Authentication of an End-User by an Authorization Server when used by a Client. + +The ID Token is represented as a https://tools.ietf.org/html/rfc7519[JSON Web Token] (JWT) and MUST be signed using https://tools.ietf.org/html/rfc7515[JSON Web Signature] (JWS). + +The `ReactiveOidcIdTokenDecoderFactory` provides a `ReactiveJwtDecoder` used for `OidcIdToken` signature verification. The default algorithm is `RS256` but may be different when assigned during client registration. +For these cases, a resolver may be configured to return the expected JWS algorithm assigned for a specific client. + +The JWS algorithm resolver is a `Function` that accepts a `ClientRegistration` and returns the expected `JwsAlgorithm` for the client, eg. `SignatureAlgorithm.RS256` or `MacAlgorithm.HS256` + +The following code shows how to configure the `OidcIdTokenDecoderFactory` `@Bean` to default to `MacAlgorithm.HS256` for all `ClientRegistration`: + ==== .Java [source,java,role="primary"] ---- @Bean -public GrantedAuthoritiesMapper userAuthoritiesMapper() { - ... -} - -@Bean -SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - http - // ... - .oauth2Login(withDefaults()); - return http.build(); +public ReactiveJwtDecoderFactory idTokenDecoderFactory() { + ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory(); + idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256); + return idTokenDecoderFactory; } ---- @@ -231,15 +1194,120 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { [source,kotlin,role="secondary"] ---- @Bean -fun userAuthoritiesMapper(): GrantedAuthoritiesMapper { - // ... +fun idTokenDecoderFactory(): ReactiveJwtDecoderFactory { + val idTokenDecoderFactory = ReactiveOidcIdTokenDecoderFactory() + idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 } + return idTokenDecoderFactory } +---- +==== -@Bean -fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - return http { - oauth2Login { } +[NOTE] +For MAC based algorithms such as `HS256`, `HS384` or `HS512`, the `client-secret` corresponding to the `client-id` is used as the symmetric key for signature verification. + +[TIP] +If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authentication, the JWS algorithm resolver may evaluate the provided `ClientRegistration` to determine which algorithm to return. + + +[[webflux-oauth2-login-advanced-oidc-logout]] +=== OpenID Connect 1.0 Logout + +OpenID Connect Session Management 1.0 allows the ability to log out the End-User at the Provider using the Client. +One of the strategies available is https://openid.net/specs/openid-connect-session-1_0.html#RPLogout[RP-Initiated Logout]. + +If the OpenID Provider supports both Session Management and https://openid.net/specs/openid-connect-discovery-1_0.html[Discovery], the client may obtain the `end_session_endpoint` `URL` from the OpenID Provider's https://openid.net/specs/openid-connect-session-1_0.html#OPMetadata[Discovery Metadata]. +This can be achieved by configuring the `ClientRegistration` with the `issuer-uri`, as in the following example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + ... + provider: + okta: + issuer-uri: https://dev-1234.oktapreview.com +---- + +...and the `OidcClientInitiatedServerLogoutSuccessHandler`, which implements RP-Initiated Logout, may be configured as follows: + +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Autowired + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()) + .logout(logout -> logout + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + ); + + return http.build(); + } + + private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() { + OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler = + new OidcClientInitiatedServerLogoutSuccessHandler(this.clientRegistrationRepository); + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}"); + + return oidcLogoutSuccessHandler; + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Autowired + private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + logout { + logoutSuccessHandler = oidcLogoutSuccessHandler() + } + } + + return http.build() + } + + private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler { + val oidcLogoutSuccessHandler = OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository) + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}") + return oidcLogoutSuccessHandler } } ---- ==== + +NOTE: `OidcClientInitiatedServerLogoutSuccessHandler` supports the `{baseUrl}` placeholder. +If used, the application's base URL, like `https://app.example.org`, will replace it at request time. From 7d806b668f76bd7cbde4607c06e312d4cbe4c576 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Tue, 9 Nov 2021 11:38:51 -0600 Subject: [PATCH 019/589] Separate OAuth 2.0 Client Reactive Docs Related gh-10367 --- docs/modules/ROOT/nav.adoc | 4 +- .../ROOT/pages/reactive/oauth2/index.adoc | 2 +- .../{login.adoc => login/advanced.adoc} | 574 +----------------- .../pages/reactive/oauth2/login/core.adoc | 545 +++++++++++++++++ .../pages/reactive/oauth2/login/index.adoc | 8 + 5 files changed, 567 insertions(+), 566 deletions(-) rename docs/modules/ROOT/pages/reactive/oauth2/{login.adoc => login/advanced.adoc} (54%) create mode 100644 docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc create mode 100644 docs/modules/ROOT/pages/reactive/oauth2/login/index.adoc diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index b1f65c4d56..e9c683a2df 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -125,7 +125,9 @@ ** Authorization *** xref:reactive/authorization/method.adoc[EnableReactiveMethodSecurity] ** xref:reactive/oauth2/index.adoc[OAuth2] -*** xref:reactive/oauth2/login.adoc[OAuth2 Log In] +*** xref:reactive/oauth2/login/index.adoc[OAuth2 Log In] +**** xref:reactive/oauth2/login/core.adoc[Core Configuration] +**** xref:reactive/oauth2/login/advanced.adoc[Advanced Configuration] *** xref:reactive/oauth2/client/index.adoc[OAuth2 Client] **** xref:reactive/oauth2/client/core.adoc[Core Interfaces and Classes] **** xref:reactive/oauth2/client/authorization-grants.adoc[OAuth2 Authorization Grants] diff --git a/docs/modules/ROOT/pages/reactive/oauth2/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/index.adoc index 95850651fd..592bc9fbab 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/index.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/index.adoc @@ -3,6 +3,6 @@ Spring Security provides OAuth2 and WebFlux integration for reactive applications. -* xref:reactive/oauth2/login.adoc[OAuth2 Log In] - Authenticating with an OAuth2 or OpenID Connect 1.0 Provider +* xref:reactive/oauth2/login/index.adoc[OAuth2 Log In] - Authenticating with an OAuth2 or OpenID Connect 1.0 Provider * xref:reactive/oauth2/client/index.adoc[OAuth2 Client] - Making requests to an OAuth2 Resource Server * xref:reactive/oauth2/resource-server/index.adoc[OAuth2 Resource Server] - Protecting a REST endpoint using OAuth2 diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc similarity index 54% rename from docs/modules/ROOT/pages/reactive/oauth2/login.adoc rename to docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc index b03e5ca1d0..4bc38316ef 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc @@ -1,559 +1,5 @@ -[[webflux-oauth2-login]] -= OAuth 2.0 Login - -The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google). -OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". - -NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework] and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[OpenID Connect Core 1.0]. - - -[[webflux-oauth2-login-sample]] -== Spring Boot 2.x Sample - -Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login. - -This section shows how to configure the {gh-samples-url}/reactive/webflux/java/oauth2/login[*OAuth 2.0 Login WebFlux sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: - -* <> -* <> -* <> -* <> - - -[[webflux-oauth2-login-sample-setup]] -=== Initial setup - -To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials. - -NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified]. - -Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0". - -After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. - - -[[webflux-oauth2-login-sample-redirect]] -=== Setting the redirect URI - -The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google and have granted access to the OAuth Client _(<>)_ on the Consent page. - -In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. - -TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. -The *_registrationId_* is a unique identifier for the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration]. -For our example, the `registrationId` is `google`. - -IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. -Also, see the supported xref:reactive/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. - - -[[webflux-oauth2-login-sample-config]] -=== Configure `application.yml` - -Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. -To do so: - -. Go to `application.yml` and set the following configuration: -+ -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: <1> - google: <2> - client-id: google-client-id - client-secret: google-client-secret ----- -+ -.OAuth Client properties -==== -<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. -<2> Following the base property prefix is the ID for the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[`ClientRegistration`], such as google. -==== - -. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. - - -[[webflux-oauth2-login-sample-start]] -=== Boot up the application - -Launch the Spring Boot 2.x sample and go to `http://localhost:8080`. -You are then redirected to the default _auto-generated_ login page, which displays a link for Google. - -Click on the Google link, and you are then redirected to Google for authentication. - -After authenticating with your Google account credentials, the next page presented to you is the Consent screen. -The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier. -Click *Allow* to authorize the OAuth Client to access your email address and basic profile information. - -At this point, the OAuth Client retrieves your email address and basic profile information from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session. - - -[[oauth2login-boot-property-mappings]] -== Spring Boot 2.x Property Mappings - -The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration] properties. - -|=== -|Spring Boot 2.x |ClientRegistration - -|`spring.security.oauth2.client.registration._[registrationId]_` -|`registrationId` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-id` -|`clientId` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-secret` -|`clientSecret` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-authentication-method` -|`clientAuthenticationMethod` - -|`spring.security.oauth2.client.registration._[registrationId]_.authorization-grant-type` -|`authorizationGrantType` - -|`spring.security.oauth2.client.registration._[registrationId]_.redirect-uri` -|`redirectUri` - -|`spring.security.oauth2.client.registration._[registrationId]_.scope` -|`scopes` - -|`spring.security.oauth2.client.registration._[registrationId]_.client-name` -|`clientName` - -|`spring.security.oauth2.client.provider._[providerId]_.authorization-uri` -|`providerDetails.authorizationUri` - -|`spring.security.oauth2.client.provider._[providerId]_.token-uri` -|`providerDetails.tokenUri` - -|`spring.security.oauth2.client.provider._[providerId]_.jwk-set-uri` -|`providerDetails.jwkSetUri` - -|`spring.security.oauth2.client.provider._[providerId]_.issuer-uri` -|`providerDetails.issuerUri` - -|`spring.security.oauth2.client.provider._[providerId]_.user-info-uri` -|`providerDetails.userInfoEndpoint.uri` - -|`spring.security.oauth2.client.provider._[providerId]_.user-info-authentication-method` -|`providerDetails.userInfoEndpoint.authenticationMethod` - -|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute` -|`providerDetails.userInfoEndpoint.userNameAttributeName` -|=== - -[TIP] -A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint], by specifying the `spring.security.oauth2.client.provider._[providerId]_.issuer-uri` property. - - -[[webflux-oauth2-login-common-oauth2-provider]] -== CommonOAuth2Provider - -`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta. - -For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a Provider. -Therefore, it makes sense to provide default values in order to reduce the required configuration. - -As demonstrated previously, when we <>, only the `client-id` and `client-secret` properties are required. - -The following listing shows an example: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - google: - client-id: google-client-id - client-secret: google-client-secret ----- - -[TIP] -The auto-defaulting of client properties works seamlessly here because the `registrationId` (`google`) matches the `GOOGLE` `enum` (case-insensitive) in `CommonOAuth2Provider`. - -For cases where you may want to specify a different `registrationId`, such as `google-login`, you can still leverage auto-defaulting of client properties by configuring the `provider` property. - -The following listing shows an example: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - google-login: <1> - provider: google <2> - client-id: google-client-id - client-secret: google-client-secret ----- -<1> The `registrationId` is set to `google-login`. -<2> The `provider` property is set to `google`, which will leverage the auto-defaulting of client properties set in `CommonOAuth2Provider.GOOGLE.getBuilder()`. - - -[[webflux-oauth2-login-custom-provider-properties]] -== Configuring Custom Provider Properties - -There are some OAuth 2.0 Providers that support multi-tenancy, which results in different protocol endpoints for each tenant (or sub-domain). - -For example, an OAuth Client registered with Okta is assigned to a specific sub-domain and have their own protocol endpoints. - -For these cases, Spring Boot 2.x provides the following base property for configuring custom provider properties: `spring.security.oauth2.client.provider._[providerId]_`. - -The following listing shows an example: - -[source,yaml] ----- -spring: - security: - oauth2: - client: - registration: - okta: - client-id: okta-client-id - client-secret: okta-client-secret - provider: - okta: <1> - authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize - token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token - user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo - user-name-attribute: sub - jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys ----- - -<1> The base property (`spring.security.oauth2.client.provider.okta`) allows for custom configuration of protocol endpoint locations. - - -[[webflux-oauth2-login-override-boot-autoconfig]] -== Overriding Spring Boot 2.x Auto-configuration - -The Spring Boot 2.x auto-configuration class for OAuth Client support is `ReactiveOAuth2ClientAutoConfiguration`. - -It performs the following tasks: - -* Registers a `ReactiveClientRegistrationRepository` `@Bean` composed of `ClientRegistration`(s) from the configured OAuth Client properties. -* Registers a `SecurityWebFilterChain` `@Bean` and enables OAuth 2.0 Login through `serverHttpSecurity.oauth2Login()`. - -If you need to override the auto-configuration based on your specific requirements, you may do so in the following ways: - -* <> -* <> -* <> - - -[[webflux-oauth2-login-register-reactiveclientregistrationrepository-bean]] -=== Register a ReactiveClientRegistrationRepository @Bean - -The following example shows how to register a `ReactiveClientRegistrationRepository` `@Bean`: - -==== -.Java -[source,java,role="primary",attrs="-attributes"] ----- -@Configuration -public class OAuth2LoginConfig { - - @Bean - public ReactiveClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); - } - - private ClientRegistration googleClientRegistration() { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary",attrs="-attributes"] ----- -@Configuration -class OAuth2LoginConfig { - - @Bean - fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { - return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) - } - - private fun googleClientRegistration(): ClientRegistration { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build() - } -} ----- -==== - - -[[webflux-oauth2-login-register-securitywebfilterchain-bean]] -=== Register a SecurityWebFilterChain @Bean - -The following example shows how to register a `SecurityWebFilterChain` `@Bean` with `@EnableWebFluxSecurity` and enable OAuth 2.0 login through `serverHttpSecurity.oauth2Login()`: - -.OAuth2 Login Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebFluxSecurity -public class OAuth2LoginSecurityConfig { - - @Bean - public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - http - .authorizeExchange(authorize -> authorize - .anyExchange().authenticated() - ) - .oauth2Login(withDefaults()); - - return http.build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebFluxSecurity -class OAuth2LoginSecurityConfig { - - @Bean - fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { - authorizeExchange { - authorize(anyExchange, authenticated) - } - oauth2Login { } - } - - return http.build() - } -} ----- -==== - - -[[webflux-oauth2-login-completely-override-autoconfiguration]] -=== Completely Override the Auto-configuration - -The following example shows how to completely override the auto-configuration by registering a `ReactiveClientRegistrationRepository` `@Bean` and a `SecurityWebFilterChain` `@Bean`. - -.Overriding the auto-configuration -==== -.Java -[source,java,role="primary",attrs="-attributes"] ----- -@EnableWebFluxSecurity -public class OAuth2LoginConfig { - - @Bean - public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - http - .authorizeExchange(authorize -> authorize - .anyExchange().authenticated() - ) - .oauth2Login(withDefaults()); - - return http.build(); - } - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); - } - - private ClientRegistration googleClientRegistration() { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary",attrs="-attributes"] ----- -@EnableWebFluxSecurity -class OAuth2LoginConfig { - - @Bean - fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { - authorizeExchange { - authorize(anyExchange, authenticated) - } - oauth2Login { } - } - - return http.build() - } - - @Bean - fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { - return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) - } - - private fun googleClientRegistration(): ClientRegistration { - return ClientRegistration.withRegistrationId("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") - .scope("openid", "profile", "email", "address", "phone") - .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") - .tokenUri("https://www.googleapis.com/oauth2/v4/token") - .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") - .userNameAttributeName(IdTokenClaimNames.SUB) - .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") - .clientName("Google") - .build() - } -} ----- -==== - - -[[webflux-oauth2-login-javaconfig-wo-boot]] -== Java Configuration without Spring Boot 2.x - -If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in `CommonOAuth2Provider` (for example, Google), apply the following configuration: - -.OAuth2 Login Configuration -==== -.Java -[source,java,role="primary"] ----- -@EnableWebFluxSecurity -public class OAuth2LoginConfig { - - @Bean - public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { - http - .authorizeExchange(authorize -> authorize - .anyExchange().authenticated() - ) - .oauth2Login(withDefaults()); - - return http.build(); - } - - @Bean - public ReactiveClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); - } - - @Bean - public ReactiveOAuth2AuthorizedClientService authorizedClientService( - ReactiveClientRegistrationRepository clientRegistrationRepository) { - return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository); - } - - @Bean - public ServerOAuth2AuthorizedClientRepository authorizedClientRepository( - ReactiveOAuth2AuthorizedClientService authorizedClientService) { - return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService); - } - - private ClientRegistration googleClientRegistration() { - return CommonOAuth2Provider.GOOGLE.getBuilder("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .build(); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebFluxSecurity -class OAuth2LoginConfig { - - @Bean - fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { - authorizeExchange { - authorize(anyExchange, authenticated) - } - oauth2Login { } - } - - return http.build() - } - - @Bean - fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { - return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) - } - - @Bean - fun authorizedClientService( - clientRegistrationRepository: ReactiveClientRegistrationRepository - ): ReactiveOAuth2AuthorizedClientService { - return InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository) - } - - @Bean - fun authorizedClientRepository( - authorizedClientService: ReactiveOAuth2AuthorizedClientService - ): ServerOAuth2AuthorizedClientRepository { - return AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService) - } - - private fun googleClientRegistration(): ClientRegistration { - return CommonOAuth2Provider.GOOGLE.getBuilder("google") - .clientId("google-client-id") - .clientSecret("google-client-secret") - .build() - } -} ----- -==== - - [[webflux-oauth2-login-advanced]] -== Advanced Configuration += Advanced Configuration The OAuth 2.0 Authorization Framework defines the https://tools.ietf.org/html/rfc6749#section-3[Protocol Endpoints] as follows: @@ -646,7 +92,7 @@ The following sections go into more detail on each of the configuration options [[webflux-oauth2-login-advanced-login-page]] -=== OAuth 2.0 Login Page +== OAuth 2.0 Login Page By default, the OAuth 2.0 Login Page is auto-generated by the `LoginPageGeneratingWebFilter`. The default login page shows each configured OAuth Client with its `ClientRegistration.clientName` as a link, which is capable of initiating the Authorization Request (or OAuth 2.0 Login). @@ -757,7 +203,7 @@ The following line shows an example: [[webflux-oauth2-login-advanced-redirection-endpoint]] -=== Redirection Endpoint +== Redirection Endpoint The Redirection Endpoint is used by the Authorization Server for returning the Authorization Response (which contains the authorization credentials) to the client via the Resource Owner user-agent. @@ -838,7 +284,7 @@ return CommonOAuth2Provider.GOOGLE.getBuilder("google") [[webflux-oauth2-login-advanced-userinfo-endpoint]] -=== UserInfo Endpoint +== UserInfo Endpoint The UserInfo Endpoint includes a number of configuration options, as described in the following sub-sections: @@ -848,7 +294,7 @@ The UserInfo Endpoint includes a number of configuration options, as described i [[webflux-oauth2-login-advanced-map-authorities]] -==== Mapping User Authorities +=== Mapping User Authorities After the user successfully authenticates with the OAuth 2.0 Provider, the `OAuth2User.getAuthorities()` (or `OidcUser.getAuthorities()`) may be mapped to a new set of `GrantedAuthority` instances, which will be supplied to `OAuth2AuthenticationToken` when completing the authentication. @@ -862,7 +308,7 @@ There are a couple of options to choose from when mapping user authorities: [[webflux-oauth2-login-advanced-map-authorities-grantedauthoritiesmapper]] -===== Using a GrantedAuthoritiesMapper +==== Using a GrantedAuthoritiesMapper Register a `GrantedAuthoritiesMapper` `@Bean` to have it automatically applied to the configuration, as shown in the following example: @@ -954,7 +400,7 @@ class OAuth2LoginSecurityConfig { ==== [[webflux-oauth2-login-advanced-map-authorities-reactiveoauth2userservice]] -===== Delegation-based strategy with ReactiveOAuth2UserService +==== Delegation-based strategy with ReactiveOAuth2UserService This strategy is advanced compared to using a `GrantedAuthoritiesMapper`, however, it's also more flexible as it gives you access to the `OAuth2UserRequest` and `OAuth2User` (when using an OAuth 2.0 UserService) or `OidcUserRequest` and `OidcUser` (when using an OpenID Connect 1.0 UserService). @@ -1046,7 +492,7 @@ class OAuth2LoginSecurityConfig { [[webflux-oauth2-login-advanced-oauth2-user-service]] -==== OAuth 2.0 UserService +=== OAuth 2.0 UserService `DefaultReactiveOAuth2UserService` is an implementation of a `ReactiveOAuth2UserService` that supports standard OAuth 2.0 Provider's. @@ -1107,7 +553,7 @@ class OAuth2LoginSecurityConfig { [[webflux-oauth2-login-advanced-oidc-user-service]] -==== OpenID Connect 1.0 UserService +=== OpenID Connect 1.0 UserService `OidcReactiveOAuth2UserService` is an implementation of a `ReactiveOAuth2UserService` that supports OpenID Connect 1.0 Provider's. @@ -1165,7 +611,7 @@ class OAuth2LoginSecurityConfig { [[webflux-oauth2-login-advanced-idtoken-verify]] -=== ID Token Signature Verification +== ID Token Signature Verification OpenID Connect 1.0 Authentication introduces the https://openid.net/specs/openid-connect-core-1_0.html#IDToken[ID Token], which is a security token that contains Claims about the Authentication of an End-User by an Authorization Server when used by a Client. diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc new file mode 100644 index 0000000000..f876a775f7 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc @@ -0,0 +1,545 @@ += Core Configuration + +[[webflux-oauth2-login-sample]] +== Spring Boot 2.x Sample + +Spring Boot 2.x brings full auto-configuration capabilities for OAuth 2.0 Login. + +This section shows how to configure the {gh-samples-url}/reactive/webflux/java/oauth2/login[*OAuth 2.0 Login WebFlux sample*] using _Google_ as the _Authentication Provider_ and covers the following topics: + +* <> +* <> +* <> +* <> + + +[[webflux-oauth2-login-sample-setup]] +=== Initial setup + +To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials. + +NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified]. + +Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0". + +After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret. + + +[[webflux-oauth2-login-sample-redirect]] +=== Setting the redirect URI + +The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google and have granted access to the OAuth Client _(<>)_ on the Consent page. + +In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:8080/login/oauth2/code/google`. + +TIP: The default redirect URI template is `+{baseUrl}/login/oauth2/code/{registrationId}+`. +The *_registrationId_* is a unique identifier for the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration]. +For our example, the `registrationId` is `google`. + +IMPORTANT: If the OAuth Client is running behind a proxy server, it is recommended to check xref:features/exploits/http.adoc#http-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. +Also, see the supported xref:reactive/oauth2/client/authorization-grants.adoc#oauth2Client-auth-code-redirect-uri[ `URI` template variables] for `redirect-uri`. + + +[[webflux-oauth2-login-sample-config]] +=== Configure `application.yml` + +Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. +To do so: + +. Go to `application.yml` and set the following configuration: ++ +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: <1> + google: <2> + client-id: google-client-id + client-secret: google-client-secret +---- ++ +.OAuth Client properties +==== +<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties. +<2> Following the base property prefix is the ID for the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[`ClientRegistration`], such as google. +==== + +. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier. + + +[[webflux-oauth2-login-sample-start]] +=== Boot up the application + +Launch the Spring Boot 2.x sample and go to `http://localhost:8080`. +You are then redirected to the default _auto-generated_ login page, which displays a link for Google. + +Click on the Google link, and you are then redirected to Google for authentication. + +After authenticating with your Google account credentials, the next page presented to you is the Consent screen. +The Consent screen asks you to either allow or deny access to the OAuth Client you created earlier. +Click *Allow* to authorize the OAuth Client to access your email address and basic profile information. + +At this point, the OAuth Client retrieves your email address and basic profile information from the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint] and establishes an authenticated session. + + +[[oauth2login-boot-property-mappings]] +== Spring Boot 2.x Property Mappings + +The following table outlines the mapping of the Spring Boot 2.x OAuth Client properties to the xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration[ClientRegistration] properties. + +|=== +|Spring Boot 2.x |ClientRegistration + +|`spring.security.oauth2.client.registration._[registrationId]_` +|`registrationId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-id` +|`clientId` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-secret` +|`clientSecret` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-authentication-method` +|`clientAuthenticationMethod` + +|`spring.security.oauth2.client.registration._[registrationId]_.authorization-grant-type` +|`authorizationGrantType` + +|`spring.security.oauth2.client.registration._[registrationId]_.redirect-uri` +|`redirectUri` + +|`spring.security.oauth2.client.registration._[registrationId]_.scope` +|`scopes` + +|`spring.security.oauth2.client.registration._[registrationId]_.client-name` +|`clientName` + +|`spring.security.oauth2.client.provider._[providerId]_.authorization-uri` +|`providerDetails.authorizationUri` + +|`spring.security.oauth2.client.provider._[providerId]_.token-uri` +|`providerDetails.tokenUri` + +|`spring.security.oauth2.client.provider._[providerId]_.jwk-set-uri` +|`providerDetails.jwkSetUri` + +|`spring.security.oauth2.client.provider._[providerId]_.issuer-uri` +|`providerDetails.issuerUri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-uri` +|`providerDetails.userInfoEndpoint.uri` + +|`spring.security.oauth2.client.provider._[providerId]_.user-info-authentication-method` +|`providerDetails.userInfoEndpoint.authenticationMethod` + +|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute` +|`providerDetails.userInfoEndpoint.userNameAttributeName` +|=== + +[TIP] +A `ClientRegistration` can be initially configured using discovery of an OpenID Connect Provider's https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[Configuration endpoint] or an Authorization Server's https://tools.ietf.org/html/rfc8414#section-3[Metadata endpoint], by specifying the `spring.security.oauth2.client.provider._[providerId]_.issuer-uri` property. + + +[[webflux-oauth2-login-common-oauth2-provider]] +== CommonOAuth2Provider + +`CommonOAuth2Provider` pre-defines a set of default client properties for a number of well known providers: Google, GitHub, Facebook, and Okta. + +For example, the `authorization-uri`, `token-uri`, and `user-info-uri` do not change often for a Provider. +Therefore, it makes sense to provide default values in order to reduce the required configuration. + +As demonstrated previously, when we <>, only the `client-id` and `client-secret` properties are required. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + google: + client-id: google-client-id + client-secret: google-client-secret +---- + +[TIP] +The auto-defaulting of client properties works seamlessly here because the `registrationId` (`google`) matches the `GOOGLE` `enum` (case-insensitive) in `CommonOAuth2Provider`. + +For cases where you may want to specify a different `registrationId`, such as `google-login`, you can still leverage auto-defaulting of client properties by configuring the `provider` property. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + google-login: <1> + provider: google <2> + client-id: google-client-id + client-secret: google-client-secret +---- +<1> The `registrationId` is set to `google-login`. +<2> The `provider` property is set to `google`, which will leverage the auto-defaulting of client properties set in `CommonOAuth2Provider.GOOGLE.getBuilder()`. + + +[[webflux-oauth2-login-custom-provider-properties]] +== Configuring Custom Provider Properties + +There are some OAuth 2.0 Providers that support multi-tenancy, which results in different protocol endpoints for each tenant (or sub-domain). + +For example, an OAuth Client registered with Okta is assigned to a specific sub-domain and have their own protocol endpoints. + +For these cases, Spring Boot 2.x provides the following base property for configuring custom provider properties: `spring.security.oauth2.client.provider._[providerId]_`. + +The following listing shows an example: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + provider: + okta: <1> + authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize + token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token + user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo + user-name-attribute: sub + jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys +---- + +<1> The base property (`spring.security.oauth2.client.provider.okta`) allows for custom configuration of protocol endpoint locations. + + +[[webflux-oauth2-login-override-boot-autoconfig]] +== Overriding Spring Boot 2.x Auto-configuration + +The Spring Boot 2.x auto-configuration class for OAuth Client support is `ReactiveOAuth2ClientAutoConfiguration`. + +It performs the following tasks: + +* Registers a `ReactiveClientRegistrationRepository` `@Bean` composed of `ClientRegistration`(s) from the configured OAuth Client properties. +* Registers a `SecurityWebFilterChain` `@Bean` and enables OAuth 2.0 Login through `serverHttpSecurity.oauth2Login()`. + +If you need to override the auto-configuration based on your specific requirements, you may do so in the following ways: + +* <> +* <> +* <> + + +[[webflux-oauth2-login-register-reactiveclientregistrationrepository-bean]] +=== Register a ReactiveClientRegistrationRepository @Bean + +The following example shows how to register a `ReactiveClientRegistrationRepository` `@Bean`: + +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +@Configuration +public class OAuth2LoginConfig { + + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +@Configuration +class OAuth2LoginConfig { + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +---- +==== + + +[[webflux-oauth2-login-register-securitywebfilterchain-bean]] +=== Register a SecurityWebFilterChain @Bean + +The following example shows how to register a `SecurityWebFilterChain` `@Bean` with `@EnableWebFluxSecurity` and enable OAuth 2.0 login through `serverHttpSecurity.oauth2Login()`: + +.OAuth2 Login Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + + return http.build() + } +} +---- +==== + + +[[webflux-oauth2-login-completely-override-autoconfiguration]] +=== Completely Override the Auto-configuration + +The following example shows how to completely override the auto-configuration by registering a `ReactiveClientRegistrationRepository` `@Bean` and a `SecurityWebFilterChain` `@Bean`. + +.Overriding the auto-configuration +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +@EnableWebFluxSecurity +class OAuth2LoginConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + + return http.build() + } + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +---- +==== + + +[[webflux-oauth2-login-javaconfig-wo-boot]] +== Java Configuration without Spring Boot 2.x + +If you are not able to use Spring Boot 2.x and would like to configure one of the pre-defined providers in `CommonOAuth2Provider` (for example, Google), apply the following configuration: + +.OAuth2 Login Configuration +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +public class OAuth2LoginConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + @Bean + public ReactiveOAuth2AuthorizedClientService authorizedClientService( + ReactiveClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository); + } + + @Bean + public ServerOAuth2AuthorizedClientRepository authorizedClientRepository( + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService); + } + + private ClientRegistration googleClientRegistration() { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build(); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +class OAuth2LoginConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + + return http.build() + } + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + @Bean + fun authorizedClientService( + clientRegistrationRepository: ReactiveClientRegistrationRepository + ): ReactiveOAuth2AuthorizedClientService { + return InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository) + } + + @Bean + fun authorizedClientRepository( + authorizedClientService: ReactiveOAuth2AuthorizedClientService + ): ServerOAuth2AuthorizedClientRepository { + return AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService) + } + + private fun googleClientRegistration(): ClientRegistration { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build() + } +} +---- +==== diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/index.adoc new file mode 100644 index 0000000000..878398ef90 --- /dev/null +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/index.adoc @@ -0,0 +1,8 @@ +[[webflux-oauth2-login]] += OAuth 2.0 Login +:page-section-summary-toc: 1 + +The OAuth 2.0 Login feature provides an application with the capability to have users log in to the application by using their existing account at an OAuth 2.0 Provider (e.g. GitHub) or OpenID Connect 1.0 Provider (such as Google). +OAuth 2.0 Login implements the use cases: "Login with Google" or "Login with GitHub". + +NOTE: OAuth 2.0 Login is implemented by using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework] and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[OpenID Connect Core 1.0]. From 73e1506e5e1c7c76cb7ba17816ea4365179f0002 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 11 Nov 2021 09:23:14 -0600 Subject: [PATCH 020/589] Consistency update for servlet docs --- .../ROOT/pages/servlet/oauth2/login/advanced.adoc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc index dc29fc9625..bd1d49e7fe 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/login/advanced.adoc @@ -191,6 +191,8 @@ The following sections go into more detail on each of the configuration options * <> * <> * <> +* <> +* <> [[oauth2login-advanced-login-page]] @@ -892,9 +894,6 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { return oidcLogoutSuccessHandler; } } - -NOTE: `OidcClientInitiatedLogoutSuccessHandler` supports the `{baseUrl}` placeholder. -If used, the application's base URL, like `https://app.example.org`, will replace it at request time. ---- .Kotlin @@ -926,8 +925,8 @@ class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() { return oidcLogoutSuccessHandler } } +---- +==== NOTE: `OidcClientInitiatedLogoutSuccessHandler` supports the `{baseUrl}` placeholder. If used, the application's base URL, like `https://app.example.org`, will replace it at request time. ----- -==== From 0e6722800da297a253efd70c4e64b6bc28d3af50 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 11 Nov 2021 14:05:55 -0600 Subject: [PATCH 021/589] Polish gh-10479 --- docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc index 4bc38316ef..a9aace4190 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc @@ -656,7 +656,7 @@ If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authe [[webflux-oauth2-login-advanced-oidc-logout]] -=== OpenID Connect 1.0 Logout +== OpenID Connect 1.0 Logout OpenID Connect Session Management 1.0 allows the ability to log out the End-User at the Provider using the Client. One of the strategies available is https://openid.net/specs/openid-connect-session-1_0.html#RPLogout[RP-Initiated Logout]. From 0bdaa21867e88190c0f8eb29c6ca032d3ed6fe9c Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 11 Nov 2021 14:51:40 -0600 Subject: [PATCH 022/589] Update What's New for 5.6 --- docs/modules/ROOT/pages/whats-new.adoc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 0d31e832cf..aec0de8599 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -43,6 +43,10 @@ Below are the highlights of the release. [[whats-new-webflux]] == WebFlux +* OAuth 2.0 Login + +** Improved xref:reactive/oauth2/login/index.adoc[Reactive OAuth 2.0 Login Documentation] + * OAuth 2.0 Client ** Improved https://github.com/spring-projects/spring-security/pull/9791[Client Credentials encoding] @@ -50,4 +54,4 @@ Below are the highlights of the release. ** Added https://github.com/spring-projects/spring-security/pull/10269[custom response parsing] for Access Token Requests ** Added https://github.com/spring-projects/spring-security/pull/10327[jwt-bearer Grant Type support] for Access Token Requests ** Added https://github.com/spring-projects/spring-security/pull/10336[JWT Client Authentication support] for Access Token Requests -** Improved https://github.com/spring-projects/spring-security/pull/10373[Reactive OAuth 2.0 Client Documentation] +** Improved xref:reactive/oauth2/client/index.adoc[Reactive OAuth 2.0 Client Documentation] From 23e517762408799dfba7bb3c7132a7a93baec671 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 11 Nov 2021 16:55:44 -0600 Subject: [PATCH 023/589] Update logback-classic to 1.2.7 Closes gh-10490 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index b2cc1be458..aa539d4259 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -16,7 +16,7 @@ dependencies { api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.0") constraints { - api "ch.qos.logback:logback-classic:1.2.6" + api "ch.qos.logback:logback-classic:1.2.7" api "com.google.inject:guice:3.0" api "com.nimbusds:nimbus-jose-jwt:9.14" api "com.nimbusds:oauth2-oidc-sdk:9.18" From 98a88ffdf8eeed9c7b3c47e1faef8e9249ac4a55 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 11 Nov 2021 16:55:46 -0600 Subject: [PATCH 024/589] Update com.nimbusds to 9.19 Closes gh-10491 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index aa539d4259..70c528677d 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -19,7 +19,7 @@ dependencies { api "ch.qos.logback:logback-classic:1.2.7" api "com.google.inject:guice:3.0" api "com.nimbusds:nimbus-jose-jwt:9.14" - api "com.nimbusds:oauth2-oidc-sdk:9.18" + api "com.nimbusds:oauth2-oidc-sdk:9.19" api "com.squareup.okhttp3:mockwebserver:3.14.9" api "com.squareup.okhttp3:okhttp:3.14.9" api "com.unboundid:unboundid-ldapsdk:4.0.14" From 4b23949ebd2c50018997af0360bfb32a44619943 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 11 Nov 2021 16:55:50 -0600 Subject: [PATCH 025/589] Update io.projectreactor to 2020.0.13 Closes gh-10493 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 18001d9e6a..7b8183995f 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -76,7 +76,7 @@ dependencies { implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' - implementation 'io.projectreactor:reactor-core:3.4.11' + implementation 'io.projectreactor:reactor-core:3.4.12' implementation 'gradle.plugin.org.gretty:gretty:3.0.1' implementation 'com.apollographql.apollo:apollo-runtime:2.4.5' implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 70c528677d..4a546ff68f 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -8,7 +8,7 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") - api platform("io.projectreactor:reactor-bom:2020.0.12") + api platform("io.projectreactor:reactor-bom:2020.0.13") api platform("io.rsocket:rsocket-bom:1.1.1") api platform("org.junit:junit-bom:5.8.1") api platform("org.springframework.data:spring-data-bom:2021.1.0-M1") From a5b1d68350c9e2081fa43aec29bdb5cfbc48f882 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 11 Nov 2021 16:55:54 -0600 Subject: [PATCH 026/589] Update hibernate-entitymanager to 5.6.1.Final Closes gh-10495 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 4a546ff68f..c0b7ed4123 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -53,7 +53,7 @@ dependencies { api "org.eclipse.jetty:jetty-servlet:9.4.44.v20210927" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" - api "org.hibernate:hibernate-entitymanager:5.6.0.Final" + api "org.hibernate:hibernate-entitymanager:5.6.1.Final" api "org.hsqldb:hsqldb:2.6.0" api "org.jasig.cas.client:cas-client-core:3.6.2" api "org.mockito:mockito-core:3.12.4" From 6959456cab3d9a6f7281d39ee934979d82669efe Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 11 Nov 2021 16:55:56 -0600 Subject: [PATCH 027/589] Update hsqldb to 2.6.1 Closes gh-10496 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index c0b7ed4123..fc2d963ea6 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -54,7 +54,7 @@ dependencies { api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" api "org.hibernate:hibernate-entitymanager:5.6.1.Final" - api "org.hsqldb:hsqldb:2.6.0" + api "org.hsqldb:hsqldb:2.6.1" api "org.jasig.cas.client:cas-client-core:3.6.2" api "org.mockito:mockito-core:3.12.4" api "org.mockito:mockito-inline:3.12.4" From f0da370b1a8c80184c28d1352315df2e2574ae2c Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 11 Nov 2021 16:55:59 -0600 Subject: [PATCH 028/589] Update org.springframework to 5.3.13 Closes gh-10497 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9864734612..88b3ee4849 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ aspectjVersion=1.9.7 springJavaformatVersion=0.0.29 springBootVersion=2.4.2 -springFrameworkVersion=5.3.11 +springFrameworkVersion=5.3.13 openSamlVersion=3.4.6 version=5.6.0-SNAPSHOT kotlinVersion=1.5.31 From 4f185724a3d0737ccf6f69a3666e0cd398d689e5 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 12 Nov 2021 15:08:11 -0500 Subject: [PATCH 029/589] Polish gh-10479 --- docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc | 2 +- docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc index a9aace4190..a9b0a23e65 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc @@ -127,7 +127,7 @@ public class OAuth2LoginSecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { http - .exceptionHandling(exceptions -> exceptions + .exceptionHandling(exceptionHandling -> exceptionHandling .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/login/oauth2")) ) .oauth2Login(oauth2 -> oauth2 diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc index f876a775f7..9c6a47f752 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc @@ -380,8 +380,8 @@ public class OAuth2LoginConfig { } @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(this.googleClientRegistration()); + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); } private ClientRegistration googleClientRegistration() { From f100877c58dd1bc7828edde97ffd8c0fd2a6eda0 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 15 Nov 2021 09:26:01 -0600 Subject: [PATCH 030/589] Update to spring-data-bom:2021.1.0 Closes gh-10503 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index fc2d963ea6..6529e68893 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -11,7 +11,7 @@ dependencies { api platform("io.projectreactor:reactor-bom:2020.0.13") api platform("io.rsocket:rsocket-bom:1.1.1") api platform("org.junit:junit-bom:5.8.1") - api platform("org.springframework.data:spring-data-bom:2021.1.0-M1") + api platform("org.springframework.data:spring-data-bom:2021.1.0") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.0") From fa628f7491277c02c820eda6f8d13a98566dd6fa Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 15 Nov 2021 08:34:07 -0600 Subject: [PATCH 031/589] Release 5.6.0 --- docs/antora.yml | 1 - gradle.properties | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index fe004e38a4..2398ca17a3 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,2 @@ name: ROOT version: '5.6.0' -prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index 88b3ee4849..39cf8038e1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.29 springBootVersion=2.4.2 springFrameworkVersion=5.3.13 openSamlVersion=3.4.6 -version=5.6.0-SNAPSHOT +version=5.6.0 kotlinVersion=1.5.31 samplesBranch=main org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 29a4b2bc9b3e4d58933120f7175fd3609d70d172 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 15 Nov 2021 10:29:26 -0600 Subject: [PATCH 032/589] Next Development Version --- docs/antora.yml | 3 ++- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 2398ca17a3..40f6866738 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,2 +1,3 @@ name: ROOT -version: '5.6.0' +version: '5.6.1' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index 39cf8038e1..9fa3da5b60 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.29 springBootVersion=2.4.2 springFrameworkVersion=5.3.13 openSamlVersion=3.4.6 -version=5.6.0 +version=5.6.1-SNAPSHOT kotlinVersion=1.5.31 samplesBranch=main org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 6b6f473a1b6531b52449a83abd861a4b3437bc7f Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 15 Nov 2021 13:17:23 -0700 Subject: [PATCH 033/589] Added authorizeHttpRequests Docs Closes gh-10442 --- .../authorization/authorizationfilter.odg | Bin 0 -> 16489 bytes .../authorization/authorizationfilter.png | Bin 0 -> 121956 bytes .../authorization/authorizationhierarchy.odg | Bin 0 -> 16083 bytes .../authorization/authorizationhierarchy.png | Bin 0 -> 123877 bytes docs/modules/ROOT/nav.adoc | 3 +- .../servlet/authorization/architecture.adoc | 255 ++++++++++++++---- .../authorize-http-requests.adoc | 171 ++++++++++++ .../authorization/authorize-requests.adoc | 11 +- 8 files changed, 381 insertions(+), 59 deletions(-) create mode 100644 docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.odg create mode 100644 docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.png create mode 100644 docs/modules/ROOT/assets/images/servlet/authorization/authorizationhierarchy.odg create mode 100644 docs/modules/ROOT/assets/images/servlet/authorization/authorizationhierarchy.png create mode 100644 docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc diff --git a/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.odg b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.odg new file mode 100644 index 0000000000000000000000000000000000000000..5ef95428f950eef9600730026ad497d46b183376 GIT binary patch literal 16489 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 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.png b/docs/modules/ROOT/assets/images/servlet/authorization/authorizationfilter.png new file mode 100644 index 0000000000000000000000000000000000000000..8118785797c109cf96ea2137ffb9c99b9b323664 GIT binary patch literal 121956 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

XPu~acKn*p`WB(%ge1# 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

K2(aauip1hQEiCO^U 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ʀC^_e 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 VbL%*(_yqVTCL}Fb!msP~e*h?f9_9c5 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index e9c683a2df..875cd62ca9 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -49,7 +49,8 @@ *** xref:servlet/authentication/events.adoc[Authentication Events] ** xref:servlet/authorization/index.adoc[Authorization] *** xref:servlet/authorization/architecture.adoc[Authorization Architecture] -*** xref:servlet/authorization/authorize-requests.adoc[Authorize HTTP Requests] +*** xref:servlet/authorization/authorize-http-requests.adoc[Authorize HTTP Requests] +*** xref:servlet/authorization/authorize-requests.adoc[Authorize HTTP Requests with FilterSecurityInterceptor] *** xref:servlet/authorization/expression-based.adoc[Expression-Based Access Control] *** xref:servlet/authorization/secure-objects.adoc[Secure Object Implementations] *** xref:servlet/authorization/method-security.adoc[Method Security] diff --git a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc index dd4ad3ee19..80b8905618 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc @@ -8,7 +8,7 @@ == Authorities xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`], discusses how all `Authentication` implementations store a list of `GrantedAuthority` objects. These represent the authorities that have been granted to the principal. -The `GrantedAuthority` objects are inserted into the `Authentication` object by the `AuthenticationManager` and are later read by ``AccessDecisionManager``s when making authorization decisions. +The `GrantedAuthority` objects are inserted into the `Authentication` object by the `AuthenticationManager` and are later read by either the `AuthorizationManager` when making authorization decisions. `GrantedAuthority` is an interface with only one method: @@ -19,25 +19,219 @@ String getAuthority(); ---- -This method allows -``AccessDecisionManager``s to obtain a precise `String` representation of the `GrantedAuthority`. -By returning a representation as a `String`, a `GrantedAuthority` can be easily "read" by most ``AccessDecisionManager``s. +This method allows ``AuthorizationManager``s to obtain a precise `String` representation of the `GrantedAuthority`. +By returning a representation as a `String`, a `GrantedAuthority` can be easily "read" by most ``AuthorizationManager``s and ``AccessDecisionManager``s. If a `GrantedAuthority` cannot be precisely represented as a `String`, the `GrantedAuthority` is considered "complex" and `getAuthority()` must return `null`. An example of a "complex" `GrantedAuthority` would be an implementation that stores a list of operations and authority thresholds that apply to different customer account numbers. Representing this complex `GrantedAuthority` as a `String` would be quite difficult, and as a result the `getAuthority()` method should return `null`. -This will indicate to any `AccessDecisionManager` that it will need to specifically support the `GrantedAuthority` implementation in order to understand its contents. +This will indicate to any `AuthorizationManager` that it will need to specifically support the `GrantedAuthority` implementation in order to understand its contents. Spring Security includes one concrete `GrantedAuthority` implementation, `SimpleGrantedAuthority`. This allows any user-specified `String` to be converted into a `GrantedAuthority`. All ``AuthenticationProvider``s included with the security architecture use `SimpleGrantedAuthority` to populate the `Authentication` object. - [[authz-pre-invocation]] == Pre-Invocation Handling Spring Security provides interceptors which control access to secure objects such as method invocations or web requests. A pre-invocation decision on whether the invocation is allowed to proceed is made by the `AccessDecisionManager`. +=== The AuthorizationManager +`AuthorizationManager` supersedes both <>. + +Applications that customize an `AccessDecisionManager` or `AccessDecisionVoter` are encouraged to <>. + +``AuthorizationManager``s are called by the xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`] and are responsible for making final access control decisions. +The `AuthorizationManager` interface contains two methods: + +[source,java] +---- +AuthorizationDecision check(Supplier authentication, Object secureObject); + +default AuthorizationDecision verify(Supplier authentication, Object secureObject) + throws AccessDeniedException { + // ... +} +---- + +The ``AuthorizationManager``'s `check` method is passed all the relevant information it needs in order to make an authorization decision. +In particular, passing the secure `Object` enables those arguments contained in the actual secure object invocation to be inspected. +For example, let's assume the secure object was a `MethodInvocation`. +It would be easy to query the `MethodInvocation` for any `Customer` argument, and then implement some sort of security logic in the `AuthorizationManager` to ensure the principal is permitted to operate on that customer. +Implementations are expected to return a positive `AuthorizationDecision` if access is granted, negative `AuthorizationDecision` if access is denied, and a null `AuthorizationDecision` when abstaining from making a decision. + +`verify` calls `check` and subsequently throws an `AccessDeniedException` in the case of a negative `AuthorizationDecision`. + +[[authz-delegate-authorization-manager]] +=== Delegate-based AuthorizationManager Implementations +Whilst users can implement their own `AuthorizationManager` to control all aspects of authorization, Spring Security ships with a delegating `AuthorizationManager` that can collaborate with individual ``AuthorizationManager``s. + +`RequestMatcherDelegatingAuthorizationManager` will match the request with the most appropriate delegate `AuthorizationManager`. +For method security, you can use `AuthorizationManagerBeforeMethodInterceptor` and `AuthorizationManagerAfterMethodInterceptor`. + +<> illustrates the relevant classes. + +[[authz-authorization-manager-implementations]] +.Authorization Manager Implementations +image::{figures}/authorizationhierarchy.png[] + +Using this approach, a composition of `AuthorizationManager` implementations can be polled on an authorization decision. + +[[authz-authority-authorization-manager]] +==== AuthorityAuthorizationManager +The most common `AuthorizationManager` provided with Spring Security is `AuthorityAuthorizationManager`. +It is configured with a given set of authorities to look for on the current `Authentication`. +It will return positive `AuthorizationDecision` should the `Authentication` contain any of the configured authorities. +It will return a negative `AuthorizationDecision` otherwise. + +[[authz-authenticated-authorization-manager]] +==== AuthenticatedAuthorizationManager +Another manager is the `AuthenticatedAuthorizationManager`. +It can be used to differentiate between anonymous, fully-authenticated and remember-me authenticated users. +Many sites allow certain limited access under remember-me authentication, but require a user to confirm their identity by logging in for full access. + +[[authz-custom-authorization-manager]] +==== Custom Authorization Managers +Obviously, you can also implement a custom `AuthorizationManager` and you can put just about any access-control logic you want in it. +It might be specific to your application (business-logic related) or it might implement some security administration logic. +For example, you can create an implementation that can query Open Policy Agent or your own authorization database. + +[TIP] +You'll find a https://spring.io/blog/2009/01/03/spring-security-customization-part-2-adjusting-secured-session-in-real-time[blog article] on the Spring web site which describes how to use the legacy `AccessDecisionVoter` to deny access in real-time to users whose accounts have been suspended. +You can achieve the same outcome by implementing `AuthorizationManager` instead. + +[[authz-voter-adaptation]] +== Adapting AccessDecisionManager and AccessDecisionVoters + +Previous to `AuthorizationManager`, Spring Security published <>. + +In some cases, like migrating an older application, it may be desirable to introduce an `AuthorizationManager` that invokes an `AccessDecisionManager` or `AccessDecisionVoter`. + +To call an existing `AccessDecisionManager`, you can do: + +.Adapting an AccessDecisionManager +==== +.Java +[source,java,role="primary"] +---- +@Component +public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager { + private final AccessDecisionManager accessDecisionManager; + private final SecurityMetadataSource securityMetadataSource; + + @Override + public AuthorizationDecision check(Supplier authentication, Object object) { + try { + Collection attributes = this.securityMetadataSource.getAttributes(object); + this.accessDecisionManager.decide(authentication.get(), object, attributes); + return new AuthorizationDecision(true); + } catch (AccessDeniedException ex) { + return new AuthorizationDecision(false); + } + } + + @Override + public void verify(Supplier authentication, Object object) { + Collection attributes = this.securityMetadataSource.getAttributes(object); + this.accessDecisionManager.decide(authentication.get(), object, attributes); + } +} +---- +==== + +And then wire it into your `SecurityFilterChain`. + +Or to only call an `AccessDecisionVoter`, you can do: + +.Adapting an AccessDecisionVoter +==== +.Java +[source,java,role="primary"] +---- +@Component +public class AccessDecisionVoterAuthorizationManagerAdapter implements AuthorizationManager { + private final AccessDecisionVoter accessDecisionVoter; + private final SecurityMetadataSource securityMetadataSource; + + @Override + public AuthorizationDecision check(Supplier authentication, Object object) { + Collection attributes = this.securityMetadataSource.getAttributes(object); + int decision = this.accessDecisionVoter.vote(authentication.get(), object, attributes); + switch (decision) { + case ACCESS_GRANTED: + return new AuthorizationDecision(true); + case ACCESS_DENIED: + return new AuthorizationDecision(false); + } + return null; + } +} +---- +==== + +And then wire it into your `SecurityFilterChain`. + +[[authz-hierarchical-roles]] +== Hierarchical Roles +It is a common requirement that a particular role in an application should automatically "include" other roles. +For example, in an application which has the concept of an "admin" and a "user" role, you may want an admin to be able to do everything a normal user can. +To achieve this, you can either make sure that all admin users are also assigned the "user" role. +Alternatively, you can modify every access constraint which requires the "user" role to also include the "admin" role. +This can get quite complicated if you have a lot of different roles in your application. + +The use of a role-hierarchy allows you to configure which roles (or authorities) should include others. +An extended version of Spring Security's `RoleVoter`, `RoleHierarchyVoter`, is configured with a `RoleHierarchy`, from which it obtains all the "reachable authorities" which the user is assigned. +A typical configuration might look like this: + +.Hierarchical Roles Configuration +==== +.Java +[source,java,role="primary"] +---- +@Bean +AccessDecisionVoter hierarchyVoter() { + RoleHierarchy hierarchy = new RoleHierarchyImpl(); + hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" + + "ROLE_STAFF > ROLE_USER\n" + + "ROLE_USER > ROLE_GUEST"); + return new RoleHierarcyVoter(hierarchy); +} +---- + +.Xml +[source,java,role="secondary"] +---- + + + + + + + + ROLE_ADMIN > ROLE_STAFF + ROLE_STAFF > ROLE_USER + ROLE_USER > ROLE_GUEST + + + +---- +==== + +Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`. +A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AuthorizationManager` adapted to call the above `RoleHierarchyVoter`. +The `>` symbol can be thought of as meaning "includes". + +Role hierarchies offer a convenient means of simplifying the access-control configuration data for your application and/or reducing the number of authorities which you need to assign to a user. +For more complex requirements you may wish to define a logical mapping between the specific access-rights your application requires and the roles that are assigned to users, translating between the two when loading the user information. + +[[authz-legacy-note]] +== Legacy Authorization Components + +[NOTE] +Spring Security contains some legacy components. +Since they are not yet removed, documentation is included for historical purposes. +Their recommended replacements are above. [[authz-access-decision-manager]] === The AccessDecisionManager @@ -72,8 +266,6 @@ Whilst users can implement their own `AccessDecisionManager` to control all aspe .Voting Decision Manager image::{figures}/access-decision-voting.png[] - - Using this approach, a series of `AccessDecisionVoter` implementations are polled on an authorization decision. The `AccessDecisionManager` then decides whether or not to throw an `AccessDeniedException` based on its assessment of the votes. @@ -104,7 +296,6 @@ Like the other implementations, there is a parameter that controls the behaviour It is possible to implement a custom `AccessDecisionManager` that tallies votes differently. For example, votes from a particular `AccessDecisionVoter` might receive additional weighting, whilst a deny vote from a particular voter may have a veto effect. - [[authz-role-voter]] ==== RoleVoter The most commonly used `AccessDecisionVoter` provided with Spring Security is the simple `RoleVoter`, which treats configuration attributes as simple role names and votes to grant access if the user has been assigned that role. @@ -130,14 +321,6 @@ Obviously, you can also implement a custom `AccessDecisionVoter` and you can put It might be specific to your application (business-logic related) or it might implement some security administration logic. For example, you'll find a https://spring.io/blog/2009/01/03/spring-security-customization-part-2-adjusting-secured-session-in-real-time[blog article] on the Spring web site which describes how to use a voter to deny access in real-time to users whose accounts have been suspended. - -[[authz-after-invocation-handling]] -== After Invocation Handling -Whilst the `AccessDecisionManager` is called by the `AbstractSecurityInterceptor` before proceeding with the secure object invocation, some applications need a way of modifying the object actually returned by the secure object invocation. -Whilst you could easily implement your own AOP concern to achieve this, Spring Security provides a convenient hook that has several concrete implementations that integrate with its ACL capabilities. - -<> illustrates Spring Security's `AfterInvocationManager` and its concrete implementations. - [[authz-after-invocation]] .After Invocation Implementation image::{figures}/after-invocation.png[] @@ -151,41 +334,3 @@ If you're using the typical Spring Security included `AccessDecisionManager` imp In turn, if the `AccessDecisionManager` property "`allowIfAllAbstainDecisions`" is `false`, an `AccessDeniedException` will be thrown. You may avoid this potential issue by either (i) setting "`allowIfAllAbstainDecisions`" to `true` (although this is generally not recommended) or (ii) simply ensure that there is at least one configuration attribute that an `AccessDecisionVoter` will vote to grant access for. This latter (recommended) approach is usually achieved through a `ROLE_USER` or `ROLE_AUTHENTICATED` configuration attribute. - - -[[authz-hierarchical-roles]] -== Hierarchical Roles -It is a common requirement that a particular role in an application should automatically "include" other roles. -For example, in an application which has the concept of an "admin" and a "user" role, you may want an admin to be able to do everything a normal user can. -To achieve this, you can either make sure that all admin users are also assigned the "user" role. -Alternatively, you can modify every access constraint which requires the "user" role to also include the "admin" role. -This can get quite complicated if you have a lot of different roles in your application. - -The use of a role-hierarchy allows you to configure which roles (or authorities) should include others. -An extended version of Spring Security's <>, `RoleHierarchyVoter`, is configured with a `RoleHierarchy`, from which it obtains all the "reachable authorities" which the user is assigned. -A typical configuration might look like this: - -[source,xml] ----- - - - - - - - - ROLE_ADMIN > ROLE_STAFF - ROLE_STAFF > ROLE_USER - ROLE_USER > ROLE_GUEST - - - ----- - -Here we have four roles in a hierarchy `ROLE_ADMIN => ROLE_STAFF => ROLE_USER => ROLE_GUEST`. -A user who is authenticated with `ROLE_ADMIN`, will behave as if they have all four roles when security constraints are evaluated against an `AccessDecisionManager` configured with the above `RoleHierarchyVoter`. -The `>` symbol can be thought of as meaning "includes". - -Role hierarchies offer a convenient means of simplifying the access-control configuration data for your application and/or reducing the number of authorities which you need to assign to a user. -For more complex requirements you may wish to define a logical mapping between the specific access-rights your application requires and the roles that are assigned to users, translating between the two when loading the user information. diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc new file mode 100644 index 0000000000..0b02433caf --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-http-requests.adoc @@ -0,0 +1,171 @@ +[[servlet-authorization-authorizationfilter]] += Authorize HttpServletRequests with AuthorizationFilter +:figures: servlet/authorization + +This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works within Servlet-based applications. + +[NOTE] +`AuthorizationFilter` supersedes xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`]. +To remain backward compatible, `FilterSecurityInterceptor` remains the default. +This section discusses how `AuthorizationFilter` works and how to override the default configuration. + +The {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] provides xref:servlet/authorization/index.adoc#servlet-authorization[authorization] for ``HttpServletRequest``s. +It is inserted into the xref:servlet/architecture.adoc#servlet-filterchainproxy[FilterChainProxy] as one of the xref:servlet/architecture.adoc#servlet-security-filters[Security Filters]. + +You can override the default when you declare a `SecurityFilterChain`. +Instead of using xref:servlet/authorization/authorize-http-requests.adoc#servlet-authorize-requests-defaults[`authorizeRequests`], use `authorizeHttpRequests`, like so: + +.Use authorizeHttpRequests +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws AuthenticationException { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated(); + ) + // ... + + return http.build(); +} +---- +==== + +This improves on `authorizeRequests` in a number of ways: + +1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters. +This simplifies reuse and customization. +2. Delays `Authentication` lookup. +Instead of the authentication needing to be looked up for every request, it will only look it up in requests where an authorization decision requires authentication. +3. Bean-based configuration support. + +When `authorizeHttpRequests` is used instead of `authorizeRequests`, then {security-api-url}org/springframework/security/web/access/intercept/AuthorizationFilter.html[`AuthorizationFilter`] is used instead of xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`]. + +.Authorize HttpServletRequest +image::{figures}/authorizationfilter.png[] + +* image:{icondir}/number_1.png[] First, the `AuthorizationFilter` obtains an xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] from the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. +It wraps this in an `Supplier` in order to delay lookup. +* image:{icondir}/number_2.png[] Second, `AuthorizationFilter` creates a {security-api-url}org/springframework/security/web/FilterInvocation.html[`FilterInvocation`] from the `HttpServletRequest`, `HttpServletResponse`, and `FilterChain`. +// FIXME: link to FilterInvocation +* image:{icondir}/number_3.png[] Next, it passes the `Supplier` and `FilterInvocation` to the xref:servlet/architecture.adoc#authz-authorization-manager[`AuthorizationManager`]. +** image:{icondir}/number_4.png[] If authorization is denied, an `AccessDeniedException` is thrown. +In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`. +** image:{icondir}/number_5.png[] If access is granted, `AuthorizationFilter` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally. + +We can configure Spring Security to have different rules by adding more rules in order of precedence. + +.Authorize Requests +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + // ... + .authorizeHttpRequests(authorize -> authorize // <1> + .mvcMatchers("/resources/**", "/signup", "/about").permitAll() // <2> + .mvcMatchers("/admin/**").hasRole("ADMIN") // <3> + .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4> + .anyRequest().denyAll() // <5> + ); + + return http.build(); +} +---- +==== +<1> There are multiple authorization rules specified. +Each rule is considered in the order they were declared. +<2> We specified multiple URL patterns that any user can access. +Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about". +<3> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN". +You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix. +<4> Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA". +You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix. +<5> Any URL that has not already been matched on is denied access. +This is a good strategy if you do not want to accidentally forget to update your authorization rules. + +You can take a bean-based approach by constructing your own xref:servlet/authorization/architecture.adoc#authz-delegate-authorization-manager[`RequestMatcherDelegatingAuthorizationManager`] like so: + +.Configure RequestMatcherDelegatingAuthorizationManager +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http, AuthorizationManager access) + throws AuthenticationException { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().access(access) + ) + // ... + + return http.build(); +} + +@Bean +AuthorizationManager requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) { + RequestMatcher permitAll = + new AndRequestMatcher( + new MvcRequestMatcher(introspector, "/resources/**"), + new MvcRequestMatcher(introspector, "/signup"), + new MvcRequestMatcher(introspector, "/about")); + RequestMatcher admin = new MvcRequestMatcher(introspector, "/admin/**"); + RequestMatcher db = new MvcRequestMatcher(introspector, "/db/**"); + RequestMatcher any = AnyRequestMatcher.INSTANCE; + AuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .add(permitAll, (context) -> new AuthorizationDecision(true)) + .add(admin, AuthorityAuthorizationManager.hasRole("ADMIN")) + .add(db, AuthorityAuthorizationManager.hasRole("DBA")) + .add(any, new AuthenticatedAuthorizationManager()) + .build(); + return (context) -> manager.check(context.getRequest()); +} +---- +==== + +You can also wire xref:servlet/authorization/architecture.adoc#authz-custom-authorization-manager[your own custom authorization managers] for any request matcher. + +Here is an example of mapping a custom authorization manager to the `my/authorized/endpoint`: + +.Custom Authorization Manager +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .mvcMatchers("/my/authorized/endpoint").access(new CustomAuthorizationManager()); + ) + // ... + + return http.build(); +} +---- +==== + +Or you can provide it for all requests as seen below: + +.Custom Authorization Manager for All Requests +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest.access(new CustomAuthorizationManager()); + ) + // ... + + return http.build(); +} +---- +==== diff --git a/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc b/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc index b21c0d096e..57bcea0bf3 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/authorize-requests.adoc @@ -2,6 +2,10 @@ = Authorize HttpServletRequest with FilterSecurityInterceptor :figures: servlet/authorization +[NOTE] +`FilterSecurityInterceptor` is in the process of being replaced by xref:servlet/authorization/authorize-http-requests.adoc[`AuthorizationFilter`]. +Consider using that instead. + This section builds on xref:servlet/architecture.adoc#servlet-architecture[Servlet Architecture and Implementation] by digging deeper into how xref:servlet/authorization/index.adoc#servlet-authorization[authorization] works within Servlet based applications. The {security-api-url}org/springframework/security/web/access/intercept/FilterSecurityInterceptor.html[`FilterSecurityInterceptor`] provides xref:servlet/authorization/index.adoc#servlet-authorization[authorization] for ``HttpServletRequest``s. @@ -14,7 +18,7 @@ image::{figures}/filtersecurityinterceptor.png[] * image:{icondir}/number_2.png[] Second, `FilterSecurityInterceptor` creates a {security-api-url}org/springframework/security/web/FilterInvocation.html[`FilterInvocation`] from the `HttpServletRequest`, `HttpServletResponse`, and `FilterChain` that are passed into the `FilterSecurityInterceptor`. // FIXME: link to FilterInvocation * image:{icondir}/number_3.png[] Next, it passes the `FilterInvocation` to `SecurityMetadataSource` to get the ``ConfigAttribute``s. -* image:{icondir}/number_4.png[] Finally, it passes the `Authentication`, `FilterInvocation`, and ``ConfigAttribute``s to the `AccessDecisionManager`. +* image:{icondir}/number_4.png[] Finally, it passes the `Authentication`, `FilterInvocation`, and ``ConfigAttribute``s to the xref:servlet/authorization.adoc#authz-access-decision-manager`AccessDecisionManager`. ** image:{icondir}/number_5.png[] If authorization is denied, an `AccessDeniedException` is thrown. In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] handles the `AccessDeniedException`. ** image:{icondir}/number_6.png[] If access is granted, `FilterSecurityInterceptor` continues with the xref:servlet/architecture.adoc#servlet-filters-review[FilterChain] which allows the application to process normally. @@ -24,6 +28,7 @@ In this case the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilt By default, Spring Security's authorization will require all requests to be authenticated. The explicit configuration looks like: +[[servlet-authorize-requests-defaults]] .Every Request Must be Authenticated ==== .Java @@ -32,7 +37,7 @@ The explicit configuration looks like: protected void configure(HttpSecurity http) throws Exception { http // ... - .authorizeHttpRequests(authorize -> authorize + .authorizeRequests(authorize -> authorize .anyRequest().authenticated() ); } @@ -71,7 +76,7 @@ We can configure Spring Security to have different rules by adding more rules in protected void configure(HttpSecurity http) throws Exception { http // ... - .authorizeHttpRequests(authorize -> authorize // <1> + .authorizeRequests(authorize -> authorize // <1> .mvcMatchers("/resources/**", "/signup", "/about").permitAll() // <2> .mvcMatchers("/admin/**").hasRole("ADMIN") // <3> .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4> From 7b1509857009440f8517688773ef59fcf2c421b2 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 15 Nov 2021 16:45:36 -0700 Subject: [PATCH 034/589] Update Spring Security to 5.7 Closes gh-10509 --- .../config/SecurityNamespaceHandler.java | 4 +- .../main/resources/META-INF/spring.schemas | 6 +- .../security/config/spring-security-5.7.rnc | 1130 ++++++ .../security/config/spring-security-5.7.xsd | 3284 +++++++++++++++++ .../config/doc/XsdDocumentedTests.java | 6 +- .../core/SpringSecurityCoreVersion.java | 2 +- docs/antora-playbook.yml | 2 +- docs/antora.yml | 2 +- docs/local-antora-playbook.yml | 2 +- .../servlet/appendix/namespace/index.adoc | 2 +- docs/modules/ROOT/pages/whats-new.adoc | 56 +- gradle.properties | 2 +- .../src/main/resources/META-INF/security.tld | 2 +- 13 files changed, 4432 insertions(+), 68 deletions(-) create mode 100644 config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc create mode 100644 config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd diff --git a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index d3dd04a7d9..98ec11e35c 100644 --- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -94,7 +94,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler { if (!namespaceMatchesVersion(element)) { pc.getReaderContext().fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or " + "spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema " - + "with Spring Security 5.6. Please update your schema declarations to the 5.6 schema.", element); + + "with Spring Security 5.7. Please update your schema declarations to the 5.7 schema.", element); } String name = pc.getDelegate().getLocalName(element); BeanDefinitionParser parser = this.parsers.get(name); @@ -215,7 +215,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler { private boolean matchesVersionInternal(Element element) { String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation"); - return schemaLocation.matches("(?m).*spring-security-5\\.6.*.xsd.*") + return schemaLocation.matches("(?m).*spring-security-5\\.7.*.xsd.*") || schemaLocation.matches("(?m).*spring-security.xsd.*") || !schemaLocation.matches("(?m).*spring-security.*"); } diff --git a/config/src/main/resources/META-INF/spring.schemas b/config/src/main/resources/META-INF/spring.schemas index a67252965d..3759792198 100644 --- a/config/src/main/resources/META-INF/spring.schemas +++ b/config/src/main/resources/META-INF/spring.schemas @@ -1,4 +1,5 @@ -http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.6.xsd +http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.7.xsd +http\://www.springframework.org/schema/security/spring-security-5.7.xsd=org/springframework/security/config/spring-security-5.7.xsd http\://www.springframework.org/schema/security/spring-security-5.6.xsd=org/springframework/security/config/spring-security-5.6.xsd http\://www.springframework.org/schema/security/spring-security-5.5.xsd=org/springframework/security/config/spring-security-5.5.xsd http\://www.springframework.org/schema/security/spring-security-5.4.xsd=org/springframework/security/config/spring-security-5.4.xsd @@ -17,7 +18,8 @@ http\://www.springframework.org/schema/security/spring-security-2.0.xsd=org/spri http\://www.springframework.org/schema/security/spring-security-2.0.1.xsd=org/springframework/security/config/spring-security-2.0.1.xsd http\://www.springframework.org/schema/security/spring-security-2.0.2.xsd=org/springframework/security/config/spring-security-2.0.2.xsd http\://www.springframework.org/schema/security/spring-security-2.0.4.xsd=org/springframework/security/config/spring-security-2.0.4.xsd -https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.6.xsd +https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.7.xsd +https\://www.springframework.org/schema/security/spring-security-5.7.xsd=org/springframework/security/config/spring-security-5.7.xsd https\://www.springframework.org/schema/security/spring-security-5.6.xsd=org/springframework/security/config/spring-security-5.6.xsd https\://www.springframework.org/schema/security/spring-security-5.5.xsd=org/springframework/security/config/spring-security-5.5.xsd https\://www.springframework.org/schema/security/spring-security-5.4.xsd=org/springframework/security/config/spring-security-5.4.xsd diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc new file mode 100644 index 0000000000..f8c3f8ab13 --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc @@ -0,0 +1,1130 @@ +namespace a = "https://relaxng.org/ns/compatibility/annotations/1.0" +datatypes xsd = "http://www.w3.org/2001/XMLSchema-datatypes" + +default namespace = "http://www.springframework.org/schema/security" + +start = http | ldap-server | authentication-provider | ldap-authentication-provider | any-user-service | ldap-server | ldap-authentication-provider + +hash = + ## Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + attribute hash {"bcrypt"} +base64 = + ## Whether a string should be base64 encoded + attribute base64 {xsd:boolean} +request-matcher = + ## Defines the strategy use for matching incoming requests. Currently the options are 'mvc' (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions. + attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"} +port = + ## Specifies an IP port number. Used to configure an embedded LDAP server, for example. + attribute port { xsd:nonNegativeInteger } +url = + ## Specifies a URL. + attribute url { xsd:token } +id = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute id {xsd:token} +name = + ## A bean identifier, used for referring to the bean elsewhere in the context. + attribute name {xsd:token} +ref = + ## Defines a reference to a Spring bean Id. + attribute ref {xsd:token} + +cache-ref = + ## Defines a reference to a cache for use with a UserDetailsService. + attribute cache-ref {xsd:token} + +user-service-ref = + ## A reference to a user-service (or UserDetailsService bean) Id + attribute user-service-ref {xsd:token} + +authentication-manager-ref = + ## A reference to an AuthenticationManager bean + attribute authentication-manager-ref {xsd:token} + +data-source-ref = + ## A reference to a DataSource bean + attribute data-source-ref {xsd:token} + + + +debug = + ## Enables Spring Security debugging infrastructure. This will provide human-readable (multi-line) debugging information to monitor requests coming into the security filters. This may include sensitive information, such as request parameters or headers, and should only be used in a development environment. + element debug {empty} + +password-encoder = + ## element which defines a password encoding strategy. Used by an authentication provider to convert submitted passwords to hashed versions, for example. + element password-encoder {password-encoder.attlist} +password-encoder.attlist &= + ref | (hash) + +role-prefix = + ## A non-empty string prefix that will be added to role strings loaded from persistent storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is non-empty. + attribute role-prefix {xsd:token} + +use-expressions = + ## Enables the use of expressions in the 'access' attributes in elements rather than the traditional list of configuration attributes. Defaults to 'true'. If enabled, each attribute should contain a single boolean expression. If the expression evaluates to 'true', access will be granted. + attribute use-expressions {xsd:boolean} + +ldap-server = + ## Defines an LDAP server location or starts an embedded server. The url indicates the location of a remote server. If no url is given, an embedded server will be started, listening on the supplied port number. The port is optional and defaults to 33389. A Spring LDAP ContextSource bean will be registered for the server with the id supplied. + element ldap-server {ldap-server.attlist} +ldap-server.attlist &= id? +ldap-server.attlist &= (url | port)? +ldap-server.attlist &= + ## Username (DN) of the "manager" user identity which will be used to authenticate to a (non-embedded) LDAP server. If omitted, anonymous access will be used. + attribute manager-dn {xsd:string}? +ldap-server.attlist &= + ## The password for the manager DN. This is required if the manager-dn is specified. + attribute manager-password {xsd:string}? +ldap-server.attlist &= + ## Explicitly specifies an ldif file resource to load into an embedded LDAP server. The default is classpath*:*.ldiff + attribute ldif { xsd:string }? +ldap-server.attlist &= + ## Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + attribute root { xsd:string }? +ldap-server.attlist &= + ## Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and 'unboundid'. By default, it will depends if the library is available in the classpath. + attribute mode { "apacheds" | "unboundid" }? + +ldap-server-ref-attribute = + ## The optional server to use. If omitted, and a default LDAP server is registered (using with no Id), that server will be used. + attribute server-ref {xsd:token} + + +group-search-filter-attribute = + ## Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN of the user. + attribute group-search-filter {xsd:token} +group-search-base-attribute = + ## Search base for group membership searches. Defaults to "" (searching from the root). + attribute group-search-base {xsd:token} +user-search-filter-attribute = + ## The LDAP filter used to search for users (optional). For example "(uid={0})". The substituted parameter is the user's login name. + attribute user-search-filter {xsd:token} +user-search-base-attribute = + ## Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + attribute user-search-base {xsd:token} +group-role-attribute-attribute = + ## The LDAP attribute name which contains the role name which will be used within Spring Security. Defaults to "cn". + attribute group-role-attribute {xsd:token} +user-details-class-attribute = + ## Allows the objectClass of the user entry to be specified. If set, the framework will attempt to load standard attributes for the defined class into the returned UserDetails object + attribute user-details-class {"person" | "inetOrgPerson"} +user-context-mapper-attribute = + ## Allows explicit customization of the loaded user object by specifying a UserDetailsContextMapper bean which will be called with the context information from the user's directory entry + attribute user-context-mapper-ref {xsd:token} + + +ldap-user-service = + ## This element configures a LdapUserDetailsService which is a combination of a FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + element ldap-user-service {ldap-us.attlist} +ldap-us.attlist &= id? +ldap-us.attlist &= + ldap-server-ref-attribute? +ldap-us.attlist &= + user-search-filter-attribute? +ldap-us.attlist &= + user-search-base-attribute? +ldap-us.attlist &= + group-search-filter-attribute? +ldap-us.attlist &= + group-search-base-attribute? +ldap-us.attlist &= + group-role-attribute-attribute? +ldap-us.attlist &= + cache-ref? +ldap-us.attlist &= + role-prefix? +ldap-us.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +ldap-authentication-provider = + ## Sets up an ldap authentication provider + element ldap-authentication-provider {ldap-ap.attlist, password-compare-element?} +ldap-ap.attlist &= + ldap-server-ref-attribute? +ldap-ap.attlist &= + user-search-base-attribute? +ldap-ap.attlist &= + user-search-filter-attribute? +ldap-ap.attlist &= + group-search-base-attribute? +ldap-ap.attlist &= + group-search-filter-attribute? +ldap-ap.attlist &= + group-role-attribute-attribute? +ldap-ap.attlist &= + ## A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present and will be substituted with the username. + attribute user-dn-pattern {xsd:token}? +ldap-ap.attlist &= + role-prefix? +ldap-ap.attlist &= + (user-details-class-attribute | user-context-mapper-attribute)? + +password-compare-element = + ## Specifies that an LDAP provider should use an LDAP compare operation of the user's password to authenticate the user + element password-compare {password-compare.attlist, password-encoder?} + +password-compare.attlist &= + ## The attribute in the directory which contains the user password. Defaults to "userPassword". + attribute password-attribute {xsd:token}? +password-compare.attlist &= + hash? + +intercept-methods = + ## Can be used inside a bean definition to add a security interceptor to the bean and set up access configuration attributes for the bean's methods + element intercept-methods {intercept-methods.attlist, protect+} +intercept-methods.attlist &= + ## Optional AccessDecisionManager bean ID to be used by the created method security interceptor. + attribute access-decision-manager-ref {xsd:token}? + + +protect = + ## Defines a protected method and the access control configuration attributes that apply to it. We strongly advise you NOT to mix "protect" declarations with any services provided "global-method-security". + element protect {protect.attlist, empty} +protect.attlist &= + ## A method name + attribute method {xsd:token} +protect.attlist &= + ## Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + attribute access {xsd:token} + +method-security-metadata-source = + ## Creates a MethodSecurityMetadataSource instance + element method-security-metadata-source {msmds.attlist, protect+} +msmds.attlist &= id? + +msmds.attlist &= use-expressions? + +method-security = + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with Spring Security annotations. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. Interceptors are invoked in the order specified in AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. + element method-security {method-security.attlist, expression-handler?} +method-security.attlist &= + ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "true". + attribute pre-post-enabled {xsd:boolean}? +method-security.attlist &= + ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "false". + attribute secured-enabled {xsd:boolean}? +method-security.attlist &= + ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "false". + attribute jsr250-enabled {xsd:boolean}? +method-security.attlist &= + ## If true, class-based proxying will be used instead of interface-based proxying. + attribute proxy-target-class {xsd:boolean}? + +global-method-security = + ## Provides method security for all beans registered in the Spring application context. Specifically, beans will be scanned for matches with the ordered list of "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a match, the beans will automatically be proxied and security authorization applied to the methods accordingly. If you use and enable all four sources of method security metadata (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 security annotations), the metadata sources will be queried in that order. In practical terms, this enables you to use XML to override method security metadata expressed in annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize etc.), @Secured and finally JSR-250. + element global-method-security {global-method-security.attlist, (pre-post-annotation-handling | expression-handler)?, protect-pointcut*, after-invocation-provider*} +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this application context. Defaults to "disabled". + attribute pre-post-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether the use of Spring Security's @Secured annotations should be enabled for this application context. Defaults to "disabled". + attribute secured-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). This will require the javax.annotation.security classes on the classpath. Defaults to "disabled". + attribute jsr250-annotations {"disabled" | "enabled" }? +global-method-security.attlist &= + ## Optional AccessDecisionManager bean ID to override the default used for method security. + attribute access-decision-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Optional RunAsmanager implementation which will be used by the configured MethodSecurityInterceptor + attribute run-as-manager-ref {xsd:token}? +global-method-security.attlist &= + ## Allows the advice "order" to be set for the method security interceptor. + attribute order {xsd:token}? +global-method-security.attlist &= + ## If true, class based proxying will be used instead of interface based proxying. + attribute proxy-target-class {xsd:boolean}? +global-method-security.attlist &= + ## Can be used to specify that AspectJ should be used instead of the default Spring AOP. If set, secured classes must be woven with the AnnotationSecurityAspect from the spring-security-aspects module. + attribute mode {"aspectj"}? +global-method-security.attlist &= + ## An external MethodSecurityMetadataSource instance can be supplied which will take priority over other sources (such as the default annotations). + attribute metadata-source-ref {xsd:token}? +global-method-security.attlist &= + authentication-manager-ref? + + +after-invocation-provider = + ## Allows addition of extra AfterInvocationProvider beans which should be called by the MethodSecurityInterceptor created by global-method-security. + element after-invocation-provider {ref} + +pre-post-annotation-handling = + ## Allows the default expression-based mechanism for handling Spring Security's pre and post invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be replace entirely. Only applies if these annotations are enabled. + element pre-post-annotation-handling {invocation-attribute-factory, pre-invocation-advice, post-invocation-advice} + +invocation-attribute-factory = + ## Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and post invocation metadata from the annotated methods. + element invocation-attribute-factory {ref} + +pre-invocation-advice = + ## Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the PreInvocationAuthorizationAdviceVoter for the element. + element pre-invocation-advice {ref} + +post-invocation-advice = + ## Customizes the PostInvocationAdviceProvider with the ref as the PostInvocationAuthorizationAdvice for the element. + element post-invocation-advice {ref} + + +expression-handler = + ## Defines the SecurityExpressionHandler instance which will be used if expression-based access-control is enabled. A default implementation (with no ACL support) will be used if not supplied. + element expression-handler {ref} + +protect-pointcut = + ## Defines a protected pointcut and the access control configuration attributes that apply to it. Every bean registered in the Spring application context that provides a method that matches the pointcut will receive security authorization. + element protect-pointcut {protect-pointcut.attlist, empty} +protect-pointcut.attlist &= + ## An AspectJ expression, including the 'execution' keyword. For example, 'execution(int com.foo.TargetObject.countLength(String))' (without the quotes). + attribute expression {xsd:string} +protect-pointcut.attlist &= + ## Access configuration attributes list that applies to all methods matching the pointcut, e.g. "ROLE_A,ROLE_B" + attribute access {xsd:token} + +websocket-message-broker = + ## Allows securing a Message Broker. There are two modes. If no id is specified: ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that can be manually registered with the clientInboundChannel. + element websocket-message-broker { websocket-message-broker.attrlist, (intercept-message* & expression-handler?) } + +websocket-message-broker.attrlist &= + ## A bean identifier, used for referring to the bean elsewhere in the context. If specified, explicit configuration within clientInboundChannel is required. If not specified, ensures that any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures that the SecurityContextChannelInterceptor is automatically registered for the clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the clientInboundChannel. + attribute id {xsd:token}? +websocket-message-broker.attrlist &= + ## Disables the requirement for CSRF token to be present in the Stomp headers (default false). Changing the default is useful if it is necessary to allow other origins to make SockJS connections. + attribute same-origin-disabled {xsd:boolean}? + +intercept-message = + ## Creates an authorization rule for a websocket message. + element intercept-message {intercept-message.attrlist} + +intercept-message.attrlist &= + ## The destination ant pattern which will be mapped to the access attribute. For example, /** matches any message with a destination, /admin/** matches any message that has a destination that starts with admin. + attribute pattern {xsd:token}? +intercept-message.attrlist &= + ## The access configuration attributes that apply for the configured message. For example, permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role 'ROLE_ADMIN'. + attribute access {xsd:token}? +intercept-message.attrlist &= + ## The type of message to match on. Valid values are defined in SimpMessageType (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, DISCONNECT_ACK, OTHER). + attribute type {"CONNECT" | "CONNECT_ACK" | "HEARTBEAT" | "MESSAGE" | "SUBSCRIBE"| "UNSUBSCRIBE" | "DISCONNECT" | "DISCONNECT_ACK" | "OTHER"}? + +http-firewall = + ## Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created by the namespace. + element http-firewall {ref} + +http = + ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none". + element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & oauth2-resource-server? & openid-login? & x509? & jee? & http-basic? & logout? & password-management? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) } +http.attlist &= + ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. + attribute pattern {xsd:token}? +http.attlist &= + ## When set to 'none', requests matching the pattern attribute will be ignored by Spring Security. No security filters will be applied and no SecurityContext will be available. If set, the element must be empty, with no children. + attribute security {"none"}? +http.attlist &= + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref { xsd:token }? +http.attlist &= + ## A legacy attribute which automatically registers a login form, BASIC authentication and a logout URL and logout services. If unspecified, defaults to "false". We'd recommend you avoid using this and instead explicitly configure the services you require. + attribute auto-config {xsd:boolean}? +http.attlist &= + use-expressions? +http.attlist &= + ## Controls the eagerness with which an HTTP session is created by Spring Security classes. If not set, defaults to "ifRequired". If "stateless" is used, this implies that the application guarantees that it will not create a session. This differs from the use of "never" which means that Spring Security will not create a session, but will make use of one if the application does. + attribute create-session {"ifRequired" | "always" | "never" | "stateless"}? +http.attlist &= + ## A reference to a SecurityContextRepository bean. This can be used to customize how the SecurityContext is stored between requests. + attribute security-context-repository-ref {xsd:token}? +http.attlist &= + request-matcher? +http.attlist &= + ## Provides versions of HttpServletRequest security methods such as isUserInRole() and getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to "true". + attribute servlet-api-provision {xsd:boolean}? +http.attlist &= + ## If available, runs the request as the Subject acquired from the JaasAuthenticationToken. Defaults to "false". + attribute jaas-api-provision {xsd:boolean}? +http.attlist &= + ## Optional attribute specifying the ID of the AccessDecisionManager implementation which should be used for authorizing HTTP requests. + attribute access-decision-manager-ref {xsd:token}? +http.attlist &= + ## Optional attribute specifying the realm name that will be used for all authentication features that require a realm name (eg BASIC and Digest authentication). If unspecified, defaults to "Spring Security Application". + attribute realm {xsd:token}? +http.attlist &= + ## Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + attribute entry-point-ref {xsd:token}? +http.attlist &= + ## Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults to "true" + attribute once-per-request {xsd:boolean}? +http.attlist &= + ## Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" (rewriting is disabled). + attribute disable-url-rewriting {xsd:boolean}? +http.attlist &= + ## Exposes the list of filters defined by this configuration under this bean name in the application context. + name? +http.attlist &= + authentication-manager-ref? + +access-denied-handler = + ## Defines the access-denied strategy that should be used. An access denied page can be defined or a reference to an AccessDeniedHandler instance. + element access-denied-handler {access-denied-handler.attlist, empty} +access-denied-handler.attlist &= (ref | access-denied-handler-page) + +access-denied-handler-page = + ## The access denied page that an authenticated user will be redirected to if they request a page which they don't have the authority to access. + attribute error-page {xsd:token} + +intercept-url = + ## Specifies the access attributes and/or filter list for a particular set of URLs. + element intercept-url {intercept-url.attlist, empty} +intercept-url.attlist &= + (pattern | request-matcher-ref) +intercept-url.attlist &= + ## The access configuration attributes that apply for the configured path. + attribute access {xsd:token}? +intercept-url.attlist &= + ## The HTTP Method for which the access configuration attributes should apply. If not specified, the attributes will apply to any method. + attribute method {"GET" | "DELETE" | "HEAD" | "OPTIONS" | "POST" | "PUT" | "PATCH" | "TRACE"}? + +intercept-url.attlist &= + ## Used to specify that a URL must be accessed over http or https, or that there is no preference. The value should be "http", "https" or "any", respectively. + attribute requires-channel {xsd:token}? +intercept-url.attlist &= + ## The path to the servlet. This attribute is only applicable when 'request-matcher' is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more HttpServlet's registered in the ServletContext that have mappings starting with '/' and are different; 2) The pattern starts with the same value of a registered HttpServlet path, excluding the default (root) HttpServlet '/'. + attribute servlet-path {xsd:token}? + +logout = + ## Incorporates a logout processing filter. Most web applications require a logout filter, although you may not require one if you write a controller to provider similar logic. + element logout {logout.attlist, empty} +logout.attlist &= + ## Specifies the URL that will cause a logout. Spring Security will initialize a filter that responds to this particular URL. Defaults to /logout if unspecified. + attribute logout-url {xsd:token}? +logout.attlist &= + ## Specifies the URL to display once the user has logged out. If not specified, defaults to /?logout (i.e. /login?logout). + attribute logout-success-url {xsd:token}? +logout.attlist &= + ## Specifies whether a logout also causes HttpSession invalidation, which is generally desirable. If unspecified, defaults to true. + attribute invalidate-session {xsd:boolean}? +logout.attlist &= + ## A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out. + attribute success-handler-ref {xsd:token}? +logout.attlist &= + ## A comma-separated list of the names of cookies which should be deleted when the user logs out + attribute delete-cookies {xsd:token}? + +request-cache = + ## Allow the RequestCache used for saving requests during the login process to be set + element request-cache {ref} + +form-login = + ## Sets up a form login configuration for authentication with a username and password + element form-login {form-login.attlist, empty} +form-login.attlist &= + ## The URL that the login form is posted to. If unspecified, it defaults to /login. + attribute login-processing-url {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the username. Defaults to 'username'. + attribute username-parameter {xsd:token}? +form-login.attlist &= + ## The name of the request parameter which contains the password. Defaults to 'password'. + attribute password-parameter {xsd:token}? +form-login.attlist &= + ## The URL that will be redirected to after successful authentication, if the user's previous action could not be resumed. This generally happens if the user visits a login page without having first requested a secured operation that triggers authentication. If unspecified, defaults to the root of the application. + attribute default-target-url {xsd:token}? +form-login.attlist &= + ## Whether the user should always be redirected to the default-target-url after login. + attribute always-use-default-target {xsd:boolean}? +form-login.attlist &= + ## The URL for the login page. If no login URL is specified, Spring Security will automatically create a login URL at GET /login and a corresponding filter to render that login URL when requested. + attribute login-page {xsd:token}? +form-login.attlist &= + ## The URL for the login failure page. If no login failure URL is specified, Spring Security will automatically create a failure login URL at /login?error and a corresponding filter to render that login failure URL when requested. + attribute authentication-failure-url {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful authentication request. Should not be used in combination with default-target-url (or always-use-default-target-url) as the implementation should always deal with navigation to the subsequent destination + attribute authentication-success-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationFailureHandler bean which should be used to handle a failed authentication request. Should not be used in combination with authentication-failure-url as the implementation should always deal with navigation to the subsequent destination + attribute authentication-failure-handler-ref {xsd:token}? +form-login.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationFailureHandler + attribute authentication-failure-forward-url {xsd:token}? +form-login.attlist &= + ## The URL for the ForwardAuthenticationSuccessHandler + attribute authentication-success-forward-url {xsd:token}? + +oauth2-login = + ## Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + element oauth2-login {oauth2-login.attlist} +oauth2-login.attlist &= + ## Reference to the ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the GrantedAuthoritiesMapper + attribute user-authorities-mapper-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OAuth2UserService + attribute user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the OpenID Connect OAuth2UserService + attribute oidc-user-service-ref {xsd:token}? +oauth2-login.attlist &= + ## The URI where the filter processes authentication requests + attribute login-processing-url {xsd:token}? +oauth2-login.attlist &= + ## The URI to send users to login + attribute login-page {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthenticationSuccessHandler + attribute authentication-success-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the AuthenticationFailureHandler + attribute authentication-failure-handler-ref {xsd:token}? +oauth2-login.attlist &= + ## Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider + attribute jwt-decoder-factory-ref {xsd:token}? + +oauth2-client = + ## Configures OAuth 2.0 Client support. + element oauth2-client {oauth2-client.attlist, (authorization-code-grant?) } +oauth2-client.attlist &= + ## Reference to the ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? + +authorization-code-grant = + ## Configures OAuth 2.0 Authorization Code Grant. + element authorization-code-grant {authorization-code-grant.attlist, empty} +authorization-code-grant.attlist &= + ## Reference to the AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? + +client-registrations = + ## Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + element client-registrations {client-registration+, provider*} + +client-registration = + ## Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + element client-registration {client-registration.attlist} +client-registration.attlist &= + ## The ID that uniquely identifies the client registration. + attribute registration-id {xsd:token} +client-registration.attlist &= + ## The client identifier. + attribute client-id {xsd:token} +client-registration.attlist &= + ## The client secret. + attribute client-secret {xsd:token}? +client-registration.attlist &= + ## The method used to authenticate the client with the provider. The supported values are client_secret_basic, client_secret_post and none (public clients). + attribute client-authentication-method {"client_secret_basic" | "basic" | "client_secret_post" | "post" | "none"}? +client-registration.attlist &= + ## The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The supported values are authorization_code, client_credentials, password and implicit. + attribute authorization-grant-type {"authorization_code" | "client_credentials" | "password" | "implicit"}? +client-registration.attlist &= + ## The client’s registered redirect URI that the Authorization Server redirects the end-user’s user-agent to after the end-user has authenticated and authorized access to the client. + attribute redirect-uri {xsd:token}? +client-registration.attlist &= + ## A comma-separated list of scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. + attribute scope {xsd:token}? +client-registration.attlist &= + ## A descriptive name used for the client. The name may be used in certain scenarios, such as when displaying the name of the client in the auto-generated login page. + attribute client-name {xsd:token}? +client-registration.attlist &= + ## A reference to the associated provider. May reference a 'provider' element or use one of the common providers (google, github, facebook, okta). + attribute provider-id {xsd:token} + +provider = + ## The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + element provider {provider.attlist} +provider.attlist &= + ## The ID that uniquely identifies the provider. + attribute provider-id {xsd:token} +provider.attlist &= + ## The Authorization Endpoint URI for the Authorization Server. + attribute authorization-uri {xsd:token}? +provider.attlist &= + ## The Token Endpoint URI for the Authorization Server. + attribute token-uri {xsd:token}? +provider.attlist &= + ## The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. + attribute user-info-uri {xsd:token}? +provider.attlist &= + ## The authentication method used when sending the access token to the UserInfo Endpoint. The supported values are header, form and query. + attribute user-info-authentication-method {"header" | "form" | "query"}? +provider.attlist &= + ## The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. + attribute user-info-user-name-attribute {xsd:token}? +provider.attlist &= + ## The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID Token and optionally the UserInfo Response. + attribute jwk-set-uri {xsd:token}? +provider.attlist &= + ## The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + attribute issuer-uri {xsd:token}? + +oauth2-resource-server = + ## Configures authentication support as an OAuth 2.0 Resource Server. + element oauth2-resource-server {oauth2-resource-server.attlist, (jwt? & opaque-token?)} +oauth2-resource-server.attlist &= + ## Reference to an AuthenticationManagerResolver + attribute authentication-manager-resolver-ref {xsd:token}? +oauth2-resource-server.attlist &= + ## Reference to a BearerTokenResolver + attribute bearer-token-resolver-ref {xsd:token}? +oauth2-resource-server.attlist &= + ## Reference to a AuthenticationEntryPoint + attribute entry-point-ref {xsd:token}? + +jwt = + ## Configures JWT authentication + element jwt {jwt.attlist} +jwt.attlist &= + ## The URI to use to collect the JWK Set for verifying JWTs + attribute jwk-set-uri {xsd:token}? +jwt.attlist &= + ## Reference to a JwtDecoder + attribute decoder-ref {xsd:token}? +jwt.attlist &= + ## Reference to a Converter + attribute jwt-authentication-converter-ref {xsd:token}? + +opaque-token = + ## Configuration Opaque Token authentication + element opaque-token {opaque-token.attlist} +opaque-token.attlist &= + ## The URI to use to introspect opaque token attributes + attribute introspection-uri {xsd:token}? +opaque-token.attlist &= + ## The Client ID to use to authenticate the introspection request + attribute client-id {xsd:token}? +opaque-token.attlist &= + ## The Client secret to use to authenticate the introspection request + attribute client-secret {xsd:token}? +opaque-token.attlist &= + ## Reference to an OpaqueTokenIntrospector + attribute introspector-ref {xsd:token}? + +openid-login = + ## Sets up form login for authentication with an Open ID identity. NOTE: The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. + element openid-login {form-login.attlist, user-service-ref?, attribute-exchange*} + +attribute-exchange = + ## Sets up an attribute exchange configuration to request specified attributes from the OpenID identity provider. When multiple elements are used, each must have an identifier-attribute attribute. Each configuration will be matched in turn against the supplied login identifier until a match is found. + element attribute-exchange {attribute-exchange.attlist, openid-attribute+} + +attribute-exchange.attlist &= + ## A regular expression which will be compared against the claimed identity, when deciding which attribute-exchange configuration to use during authentication. + attribute identifier-match {xsd:token}? + +openid-attribute = + ## Attributes used when making an OpenID AX Fetch Request. NOTE: The OpenID 1.0 and 2.0 protocols have been deprecated and users are encouraged to migrate to OpenID Connect, which is supported by spring-security-oauth2. + element openid-attribute {openid-attribute.attlist} + +openid-attribute.attlist &= + ## Specifies the name of the attribute that you wish to get back. For example, email. + attribute name {xsd:token} +openid-attribute.attlist &= + ## Specifies the attribute type. For example, https://axschema.org/contact/email. See your OP's documentation for valid attribute types. + attribute type {xsd:token} +openid-attribute.attlist &= + ## Specifies if this attribute is required to the OP, but does not error out if the OP does not return the attribute. Default is false. + attribute required {xsd:boolean}? +openid-attribute.attlist &= + ## Specifies the number of attributes that you wish to get back. For example, return 3 emails. The default value is 1. + attribute count {xsd:int}? + + +filter-chain-map = + ## Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + element filter-chain-map {filter-chain-map.attlist, filter-chain+} +filter-chain-map.attlist &= + request-matcher? + +filter-chain = + ## Used within to define a specific URL pattern and the list of filters which apply to the URLs matching that pattern. When multiple filter-chain elements are assembled in a list in order to configure a FilterChainProxy, the most specific patterns must be placed at the top of the list, with most general ones at the bottom. + element filter-chain {filter-chain.attlist, empty} +filter-chain.attlist &= + (pattern | request-matcher-ref) +filter-chain.attlist &= + ## A comma separated list of bean names that implement Filter that should be processed for this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + attribute filters {xsd:token} + +pattern = + ## The request URL pattern which will be mapped to the FilterChain. + attribute pattern {xsd:token} +request-matcher-ref = + ## Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + attribute request-matcher-ref {xsd:token} + +filter-security-metadata-source = + ## Used to explicitly configure a FilterSecurityMetadataSource bean for use with a FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy explicitly, rather than using the element. The intercept-url elements used should only contain pattern, method and access attributes. Any others will result in a configuration error. + element filter-security-metadata-source {fsmds.attlist, intercept-url+} +fsmds.attlist &= + use-expressions? +fsmds.attlist &= + id? +fsmds.attlist &= + request-matcher? + +http-basic = + ## Adds support for basic authentication + element http-basic {http-basic.attlist, empty} + +http-basic.attlist &= + ## Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + attribute entry-point-ref {xsd:token}? +http-basic.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +password-management = + ## Adds support for the password management. + element password-management {password-management.attlist, empty} + +password-management.attlist &= + ## The change password page. Defaults to "/change-password". + attribute change-password-page {xsd:string}? + +session-management = + ## Session-management related functionality is implemented by the addition of a SessionManagementFilter to the filter stack. + element session-management {session-management.attlist, concurrency-control?} + +session-management.attlist &= + ## Indicates how session fixation protection will be applied when a user authenticates. If set to "none", no protection will be applied. "newSession" will create a new empty session, with only Spring Security-related attributes migrated. "migrateSession" will create a new session and copy all session attributes to the new session. In Servlet 3.1 (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing session and use the container-supplied session fixation protection (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and newer containers, "migrateSession" in older containers. Throws an exception if "changeSessionId" is used in older containers. + attribute session-fixation-protection {"none" | "newSession" | "migrateSession" | "changeSessionId" }? +session-management.attlist &= + ## The URL to which a user will be redirected if they submit an invalid session indentifier. Typically used to detect session timeouts. + attribute invalid-session-url {xsd:token}? +session-management.attlist &= + ## Allows injection of the InvalidSessionStrategy instance used by the SessionManagementFilter + attribute invalid-session-strategy-ref {xsd:token}? +session-management.attlist &= + ## Allows injection of the SessionAuthenticationStrategy instance used by the SessionManagementFilter + attribute session-authentication-strategy-ref {xsd:token}? +session-management.attlist &= + ## Defines the URL of the error page which should be shown when the SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error code will be returned to the client. Note that this attribute doesn't apply if the error occurs during a form-based login, where the URL for authentication failure will take precedence. + attribute session-authentication-error-url {xsd:token}? + + +concurrency-control = + ## Enables concurrent session control, limiting the number of authenticated sessions a user may have at the same time. + element concurrency-control {concurrency-control.attlist, empty} + +concurrency-control.attlist &= + ## The maximum number of sessions a single authenticated user can have open at the same time. Defaults to "1". A negative value denotes unlimited sessions. + attribute max-sessions {xsd:token}? +concurrency-control.attlist &= + ## The URL a user will be redirected to if they attempt to use a session which has been "expired" because they have logged in again. + attribute expired-url {xsd:token}? +concurrency-control.attlist &= + ## Allows injection of the SessionInformationExpiredStrategy instance used by the ConcurrentSessionFilter + attribute expired-session-strategy-ref {xsd:token}? +concurrency-control.attlist &= + ## Specifies that an unauthorized error should be reported when a user attempts to login when they already have the maximum configured sessions open. The default behaviour is to expire the original session. If the session-authentication-error-url attribute is set on the session-management URL, the user will be redirected to this URL. + attribute error-if-maximum-exceeded {xsd:boolean}? +concurrency-control.attlist &= + ## Allows you to define an alias for the SessionRegistry bean in order to access it in your own configuration. + attribute session-registry-alias {xsd:token}? +concurrency-control.attlist &= + ## Allows you to define an external SessionRegistry bean to be used by the concurrency control setup. + attribute session-registry-ref {xsd:token}? + + +remember-me = + ## Sets up remember-me authentication. If used with the "key" attribute (or no attributes) the cookie-only implementation will be used. Specifying "token-repository-ref" or "remember-me-data-source-ref" will use the more secure, persisten token approach. + element remember-me {remember-me.attlist} +remember-me.attlist &= + ## The "key" used to identify cookies from a specific token-based remember-me application. You should set this to a unique value for your application. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? + +remember-me.attlist &= + (token-repository-ref | remember-me-data-source-ref | remember-me-services-ref) + +remember-me.attlist &= + user-service-ref? + +remember-me.attlist &= + ## Exports the internally defined RememberMeServices as a bean alias, allowing it to be used by other beans in the application context. + attribute services-alias {xsd:token}? + +remember-me.attlist &= + ## Determines whether the "secure" flag will be set on the remember-me cookie. If set to true, the cookie will only be submitted over HTTPS (recommended). By default, secure cookies will be used if the request is made on a secure connection. + attribute use-secure-cookie {xsd:boolean}? + +remember-me.attlist &= + ## The period (in seconds) for which the remember-me cookie should be valid. + attribute token-validity-seconds {xsd:string}? + +remember-me.attlist &= + ## Reference to an AuthenticationSuccessHandler bean which should be used to handle a successful remember-me authentication. + attribute authentication-success-handler-ref {xsd:token}? +remember-me.attlist &= + ## The name of the request parameter which toggles remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-parameter {xsd:token}? +remember-me.attlist &= + ## The name of cookie which store the token for remember-me authentication. Defaults to 'remember-me'. + attribute remember-me-cookie {xsd:token}? + +token-repository-ref = + ## Reference to a PersistentTokenRepository bean for use with the persistent token remember-me implementation. + attribute token-repository-ref {xsd:token} +remember-me-services-ref = + ## Allows a custom implementation of RememberMeServices to be used. Note that this implementation should return RememberMeAuthenticationToken instances with the same "key" value as specified in the remember-me element. Alternatively it should register its own AuthenticationProvider. It should also implement the LogoutHandler interface, which will be invoked when a user logs out. Typically the remember-me cookie would be removed on logout. + attribute services-ref {xsd:token}? +remember-me-data-source-ref = + ## DataSource bean for the database that contains the token repository schema. + data-source-ref + +anonymous = + ## Adds support for automatically granting all anonymous web requests a particular principal identity and a corresponding granted authority. + element anonymous {anonymous.attlist} +anonymous.attlist &= + ## The key shared between the provider and filter. This generally does not need to be set. If unset, it will default to a random value generated by SecureRandom. + attribute key {xsd:token}? +anonymous.attlist &= + ## The username that should be assigned to the anonymous request. This allows the principal to be identified, which may be important for logging and auditing. if unset, defaults to "anonymousUser". + attribute username {xsd:token}? +anonymous.attlist &= + ## The granted authority that should be assigned to the anonymous request. Commonly this is used to assign the anonymous request particular roles, which can subsequently be used in authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + attribute granted-authority {xsd:token}? +anonymous.attlist &= + ## With the default namespace setup, the anonymous "authentication" facility is automatically enabled. You can disable it using this property. + attribute enabled {xsd:boolean}? + + +port-mappings = + ## Defines the list of mappings between http and https ports for use in redirects + element port-mappings {port-mappings.attlist, port-mapping+} + +port-mappings.attlist &= empty + +port-mapping = + ## Provides a method to map http ports to https ports when forcing a redirect. + element port-mapping {http-port, https-port} + +http-port = + ## The http port to use. + attribute http {xsd:token} + +https-port = + ## The https port to use. + attribute https {xsd:token} + + +x509 = + ## Adds support for X.509 client authentication. + element x509 {x509.attlist} +x509.attlist &= + ## The regular expression used to obtain the username from the certificate's subject. Defaults to matching on the common name using the pattern "CN=(.*?),". + attribute subject-principal-regex {xsd:token}? +x509.attlist &= + ## Explicitly specifies which user-service should be used to load user data for X.509 authenticated clients. If ommitted, the default user-service will be used. + user-service-ref? +x509.attlist &= + ## Reference to an AuthenticationDetailsSource which will be used by the authentication filter + attribute authentication-details-source-ref {xsd:token}? + +jee = + ## Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration with container authentication. + element jee {jee.attlist} +jee.attlist &= + ## A comma-separate list of roles to look for in the incoming HttpServletRequest. + attribute mappable-roles {xsd:token} +jee.attlist &= + ## Explicitly specifies which user-service should be used to load user data for container authenticated clients. If ommitted, the set of mappable-roles will be used to construct the authorities for the user. + user-service-ref? + +authentication-manager = + ## Registers the AuthenticationManager instance and allows its list of AuthenticationProviders to be defined. Also allows you to define an alias to allow you to reference the AuthenticationManager in your own beans. + element authentication-manager {authman.attlist & authentication-provider* & ldap-authentication-provider*} +authman.attlist &= + id? +authman.attlist &= + ## An alias you wish to use for the AuthenticationManager bean (not required it you are using a specific id) + attribute alias {xsd:token}? +authman.attlist &= + ## If set to true, the AuthenticationManger will attempt to clear any credentials data in the returned Authentication object, once the user has been authenticated. + attribute erase-credentials {xsd:boolean}? + +authentication-provider = + ## Indicates that the contained user-service should be used as an authentication source. + element authentication-provider {ap.attlist & any-user-service & password-encoder?} +ap.attlist &= + ## Specifies a reference to a separately configured AuthenticationProvider instance which should be registered within the AuthenticationManager. + ref? +ap.attlist &= + ## Specifies a reference to a separately configured UserDetailsService from which to obtain authentication data. + user-service-ref? + +user-service = + ## Creates an in-memory UserDetailsService from a properties file or a list of "user" child elements. Usernames are converted to lower-case internally to allow for case-insensitive lookups, so this should not be used if case-sensitivity is required. + element user-service {id? & (properties-file | (user*))} +properties-file = + ## The location of a Properties file where each line is in the format of username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + attribute properties {xsd:token}? + +user = + ## Represents a user in the application. + element user {user.attlist, empty} +user.attlist &= + ## The username assigned to the user. + attribute name {xsd:token} +user.attlist &= + ## The password assigned to the user. This may be hashed if the corresponding authentication provider supports hashing (remember to set the "hash" attribute of the "user-service" element). This attribute be omitted in the case where the data will not be used for authentication, but only for accessing authorities. If omitted, the namespace will generate a random value, preventing its accidental use for authentication. Cannot be empty. + attribute password {xsd:string}? +user.attlist &= + ## One of more authorities granted to the user. Separate authorities with a comma (but no space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + attribute authorities {xsd:token} +user.attlist &= + ## Can be set to "true" to mark an account as locked and unusable. + attribute locked {xsd:boolean}? +user.attlist &= + ## Can be set to "true" to mark an account as disabled and unusable. + attribute disabled {xsd:boolean}? + +jdbc-user-service = + ## Causes creation of a JDBC-based UserDetailsService. + element jdbc-user-service {id? & jdbc-user-service.attlist} +jdbc-user-service.attlist &= + ## The bean ID of the DataSource which provides the required tables. + attribute data-source-ref {xsd:token} +jdbc-user-service.attlist &= + cache-ref? +jdbc-user-service.attlist &= + ## An SQL statement to query a username, password, and enabled status given a username. Default is "select username,password,enabled from users where username = ?" + attribute users-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query for a user's granted authorities given a username. The default is "select username, authority from authorities where username = ?" + attribute authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + ## An SQL statement to query user's group authorities given a username. The default is "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + attribute group-authorities-by-username-query {xsd:token}? +jdbc-user-service.attlist &= + role-prefix? + +csrf = +## Element for configuration of the CsrfFilter for protection against CSRF. It also updates the default RequestCache to only replay "GET" requests. + element csrf {csrf-options.attlist} +csrf-options.attlist &= + ## Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is enabled). + attribute disabled {xsd:boolean}? +csrf-options.attlist &= + ## The RequestMatcher instance to be used to determine if CSRF should be applied. Default is any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + attribute request-matcher-ref { xsd:token }? +csrf-options.attlist &= + ## The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by LazyCsrfTokenRepository. + attribute token-repository-ref { xsd:token }? + +headers = +## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & header*)} +headers-options.attlist &= + ## Specifies if the default headers should be disabled. Default false. + attribute defaults-disabled {xsd:token}? +headers-options.attlist &= + ## Specifies if headers should be disabled. Default false. + attribute disabled {xsd:token}? +hsts = + ## Adds support for HTTP Strict Transport Security (HSTS) + element hsts {hsts-options.attlist} +hsts-options.attlist &= + ## Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hsts-options.attlist &= + ## Specifies if subdomains should be included. Default true. + attribute include-subdomains {xsd:boolean}? +hsts-options.attlist &= + ## Specifies the maximum amount of time the host should be considered a Known HSTS Host. Default one year. + attribute max-age-seconds {xsd:integer}? +hsts-options.attlist &= + ## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true. + attribute request-matcher-ref { xsd:token }? +hsts-options.attlist &= + ## Specifies if preload should be included. Default false. + attribute preload {xsd:boolean}? + +cors = +## Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is specified a HandlerMappingIntrospector is used as the CorsConfigurationSource +element cors { cors-options.attlist } +cors-options.attlist &= + ref? +cors-options.attlist &= + ## Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to use + attribute configuration-source-ref {xsd:token}? + +hpkp = + ## Adds support for HTTP Public Key Pinning (HPKP). + element hpkp {hpkp.pins,hpkp.attlist} +hpkp.pins = + ## The list with pins + element pins {hpkp.pin+} +hpkp.pin = + ## A pin is specified using the base64-encoded SPKI fingerprint as value and the cryptographic hash algorithm as attribute + element pin { + ## The cryptographic hash algorithm + attribute algorithm { xsd:string }?, + text + } +hpkp.attlist &= + ## Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + attribute disabled {xsd:boolean}? +hpkp.attlist &= + ## Specifies if subdomains should be included. Default false. + attribute include-subdomains {xsd:boolean}? +hpkp.attlist &= + ## Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + attribute max-age-seconds {xsd:integer}? +hpkp.attlist &= + ## Specifies if the browser should only report pin validation failures. Default true. + attribute report-only {xsd:boolean}? +hpkp.attlist &= + ## Specifies the URI to which the browser should report pin validation failures. + attribute report-uri {xsd:string}? + +content-security-policy = + ## Adds support for Content Security Policy (CSP) + element content-security-policy {csp-options.attlist} +csp-options.attlist &= + ## The security policy directive(s) for the Content-Security-Policy header or if report-only is set to true, then the Content-Security-Policy-Report-Only header is used. + attribute policy-directives {xsd:token}? +csp-options.attlist &= + ## Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy violations only. Defaults to false. + attribute report-only {xsd:boolean}? + +referrer-policy = + ## Adds support for Referrer Policy + element referrer-policy {referrer-options.attlist} +referrer-options.attlist &= + ## The policies for the Referrer-Policy header. + attribute policy {"no-referrer","no-referrer-when-downgrade","same-origin","origin","strict-origin","origin-when-cross-origin","strict-origin-when-cross-origin","unsafe-url"}? + +feature-policy = + ## Adds support for Feature Policy + element feature-policy {feature-options.attlist} +feature-options.attlist &= + ## The security policy directive(s) for the Feature-Policy header. + attribute policy-directives {xsd:token}? + +permissions-policy = + ## Adds support for Permissions Policy + element permissions-policy {permissions-options.attlist} +permissions-options.attlist &= + ## The policies for the Permissions-Policy header. + attribute policy {xsd:token}? + +cache-control = + ## Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for every request + element cache-control {cache-control.attlist} +cache-control.attlist &= + ## Specifies if Cache Control should be disabled. Default false. + attribute disabled {xsd:boolean}? + +frame-options = + ## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header. + element frame-options {frame-options.attlist,empty} +frame-options.attlist &= + ## If disabled, the X-Frame-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? +frame-options.attlist &= + ## Specify the policy to use for the X-Frame-Options-Header. + attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}? +frame-options.attlist &= + ## Specify the strategy to use when ALLOW-FROM is chosen. + attribute strategy {"static","whitelist","regexp"}? +frame-options.attlist &= + ## Specify a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen. + ref? +frame-options.attlist &= + ## Specify a value to use for the chosen strategy. + attribute value {xsd:string}? +frame-options.attlist &= + ## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'. + ## Deprecated ALLOW-FROM is an obsolete directive that no longer works in modern browsers. Instead use + ## Content-Security-Policy with the + ## frame-ancestors + ## directive. + attribute from-parameter {xsd:string}? + + +xss-protection = + ## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header. + element xss-protection {xss-protection.attlist,empty} +xss-protection.attlist &= + ## disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + attribute disabled {xsd:boolean}? +xss-protection.attlist &= + ## specify that XSS Protection should be explicitly enabled or disabled. Default is 'true' meaning it is enabled. + attribute enabled {xsd:boolean}? +xss-protection.attlist &= + ## Add mode=block to the header or not, default is on. + attribute block {xsd:boolean}? + +content-type-options = + ## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + element content-type-options {content-type-options.attlist, empty} +content-type-options.attlist &= + ## If disabled, the X-Content-Type-Options header will not be included. Default false. + attribute disabled {xsd:boolean}? + +header= + ## Add additional headers to the response. + element header {header.attlist} +header.attlist &= + ## The name of the header to add. + attribute name {xsd:token}? +header.attlist &= + ## The value for the header. + attribute value {xsd:token}? +header.attlist &= + ## Reference to a custom HeaderWriter implementation. + ref? + +any-user-service = user-service | jdbc-user-service | ldap-user-service + +custom-filter = + ## Used to indicate that a filter bean declaration should be incorporated into the security filter chain. + element custom-filter {custom-filter.attlist} + +custom-filter.attlist &= + ref + +custom-filter.attlist &= + (after | before | position) + +after = + ## The filter immediately after which the custom-filter should be placed in the chain. This feature will only be needed by advanced users who wish to mix their own filters into the security filter chain and have some knowledge of the standard Spring Security filters. The filter names map to specific Spring Security implementation filters. + attribute after {named-security-filter} +before = + ## The filter immediately before which the custom-filter should be placed in the chain + attribute before {named-security-filter} +position = + ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. + attribute position {named-security-filter} + +named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER" | "WELL_KNOWN_CHANGE_PASSWORD_REDIRECT_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd new file mode 100644 index 0000000000..0297b1bafe --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd @@ -0,0 +1,3284 @@ + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + Whether a string should be base64 encoded + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + + + Specifies a URL. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + A reference to an AuthenticationManager bean + + + + + + + + A reference to a DataSource bean + + + + + + + Enables Spring Security debugging infrastructure. This will provide human-readable + (multi-line) debugging information to monitor requests coming into the security filters. + This may include sensitive information, such as request parameters or headers, and should + only be used in a development environment. + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Defines an LDAP server location or starts an embedded server. The url indicates the + location of a remote server. If no url is given, an embedded server will be started, + listening on the supplied port number. The port is optional and defaults to 33389. A + Spring LDAP ContextSource bean will be registered for the server with the id supplied. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Specifies a URL. + + + + + + Specifies an IP port number. Used to configure an embedded LDAP server, for example. + + + + + + Username (DN) of the "manager" user identity which will be used to authenticate to a + (non-embedded) LDAP server. If omitted, anonymous access will be used. + + + + + + The password for the manager DN. This is required if the manager-dn is specified. + + + + + + Explicitly specifies an ldif file resource to load into an embedded LDAP server. The + default is classpath*:*.ldiff + + + + + + Optional root suffix for the embedded LDAP server. Default is "dc=springframework,dc=org" + + + + + + Explicitly specifies which embedded ldap server should use. Values are 'apacheds' and + 'unboundid'. By default, it will depends if the library is available in the classpath. + + + + + + + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + This element configures a LdapUserDetailsService which is a combination of a + FilterBasedLdapUserSearch and a DefaultLdapAuthoritiesPopulator. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The optional server to use. If omitted, and a default LDAP server is registered (using + <ldap-server> with no Id), that server will be used. + + + + + + Search base for user searches. Defaults to "". Only used with a 'user-search-filter'. + + + + + + The LDAP filter used to search for users (optional). For example "(uid={0})". The + substituted parameter is the user's login name. + + + + + + Search base for group membership searches. Defaults to "" (searching from the root). + + + + + + Group search filter. Defaults to (uniqueMember={0}). The substituted parameter is the DN + of the user. + + + + + + The LDAP attribute name which contains the role name which will be used within Spring + Security. Defaults to "cn". + + + + + + A specific pattern used to build the user's DN, for example "uid={0},ou=people". The key + "{0}" must be present and will be substituted with the username. + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + Allows the objectClass of the user entry to be specified. If set, the framework will + attempt to load standard attributes for the defined class into the returned UserDetails + object + + + + + + + + + + + + Allows explicit customization of the loaded user object by specifying a + UserDetailsContextMapper bean which will be called with the context information from the + user's directory entry + + + + + + + + + The attribute in the directory which contains the user password. Defaults to + "userPassword". + + + + + + Defines the hashing algorithm used on user passwords. Bcrypt is recommended. + + + + + + + + + + + + Can be used inside a bean definition to add a security interceptor to the bean and set up + access configuration attributes for the bean's methods + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + Optional AccessDecisionManager bean ID to be used by the created method security + interceptor. + + + + + + + + + A method name + + + + + + Access configuration attributes list that applies to the method, e.g. "ROLE_A,ROLE_B". + + + + + + + Creates a MethodSecurityMetadataSource instance + + + + + + + Defines a protected method and the access control configuration attributes that apply to + it. We strongly advise you NOT to mix "protect" declarations with any services provided + "global-method-security". + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + + Provides method security for all beans registered in the Spring application context. + Specifically, beans will be scanned for matches with Spring Security annotations. Where + there is a match, the beans will automatically be proxied and security authorization + applied to the methods accordingly. Interceptors are invoked in the order specified in + AuthorizationInterceptorsOrder. Use can create your own interceptors using Spring AOP. + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + Specifies whether the use of Spring Security's pre and post invocation annotations + (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this + application context. Defaults to "true". + + + + + + Specifies whether the use of Spring Security's @Secured annotations should be enabled for + this application context. Defaults to "false". + + + + + + Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). + This will require the javax.annotation.security classes on the classpath. Defaults to + "false". + + + + + + If true, class-based proxying will be used instead of interface-based proxying. + + + + + + + Provides method security for all beans registered in the Spring application context. + Specifically, beans will be scanned for matches with the ordered list of + "protect-pointcut" sub-elements, Spring Security annotations and/or. Where there is a + match, the beans will automatically be proxied and security authorization applied to the + methods accordingly. If you use and enable all four sources of method security metadata + (ie "protect-pointcut" declarations, expression annotations, @Secured and also JSR250 + security annotations), the metadata sources will be queried in that order. In practical + terms, this enables you to use XML to override method security metadata expressed in + annotations. If using annotations, the order of precedence is EL-based (@PreAuthorize + etc.), @Secured and finally JSR-250. + + + + + + + + Allows the default expression-based mechanism for handling Spring Security's pre and post + invocation annotations (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) to be + replace entirely. Only applies if these annotations are enabled. + + + + + + + Defines the PrePostInvocationAttributeFactory instance which is used to generate pre and + post invocation metadata from the annotated methods. + + + + + + + + + Customizes the PreInvocationAuthorizationAdviceVoter with the ref as the + PreInvocationAuthorizationAdviceVoter for the <pre-post-annotation-handling> element. + + + + + + + + + Customizes the PostInvocationAdviceProvider with the ref as the + PostInvocationAuthorizationAdvice for the <pre-post-annotation-handling> element. + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + Defines a protected pointcut and the access control configuration attributes that apply to + it. Every bean registered in the Spring application context that provides a method that + matches the pointcut will receive security authorization. + + + + + + + + + Allows addition of extra AfterInvocationProvider beans which should be called by the + MethodSecurityInterceptor created by global-method-security. + + + + + + + + + + + + + + Specifies whether the use of Spring Security's pre and post invocation annotations + (@PreFilter, @PreAuthorize, @PostFilter, @PostAuthorize) should be enabled for this + application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether the use of Spring Security's @Secured annotations should be enabled for + this application context. Defaults to "disabled". + + + + + + + + + + + + Specifies whether JSR-250 style attributes are to be used (for example "RolesAllowed"). + This will require the javax.annotation.security classes on the classpath. Defaults to + "disabled". + + + + + + + + + + + + Optional AccessDecisionManager bean ID to override the default used for method security. + + + + + + Optional RunAsmanager implementation which will be used by the configured + MethodSecurityInterceptor + + + + + + Allows the advice "order" to be set for the method security interceptor. + + + + + + If true, class based proxying will be used instead of interface based proxying. + + + + + + Can be used to specify that AspectJ should be used instead of the default Spring AOP. If + set, secured classes must be woven with the AnnotationSecurityAspect from the + spring-security-aspects module. + + + + + + + + + + + An external MethodSecurityMetadataSource instance can be supplied which will take priority + over other sources (such as the default annotations). + + + + + + A reference to an AuthenticationManager bean + + + + + + + + + + + + + + + An AspectJ expression, including the 'execution' keyword. For example, 'execution(int + com.foo.TargetObject.countLength(String))' (without the quotes). + + + + + + Access configuration attributes list that applies to all methods matching the pointcut, + e.g. "ROLE_A,ROLE_B" + + + + + + + Allows securing a Message Broker. There are two modes. If no id is specified: ensures that + any SimpAnnotationMethodMessageHandler has the AuthenticationPrincipalArgumentResolver + registered as a custom argument resolver; ensures that the + SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. If the id is specified, creates a ChannelSecurityInterceptor that + can be manually registered with the clientInboundChannel. + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. If specified, + explicit configuration within clientInboundChannel is required. If not specified, ensures + that any SimpAnnotationMethodMessageHandler has the + AuthenticationPrincipalArgumentResolver registered as a custom argument resolver; ensures + that the SecurityContextChannelInterceptor is automatically registered for the + clientInboundChannel; and that a ChannelSecurityInterceptor is registered with the + clientInboundChannel. + + + + + + Disables the requirement for CSRF token to be present in the Stomp headers (default + false). Changing the default is useful if it is necessary to allow other origins to make + SockJS connections. + + + + + + + Creates an authorization rule for a websocket message. + + + + + + + + + + The destination ant pattern which will be mapped to the access attribute. For example, /** + matches any message with a destination, /admin/** matches any message that has a + destination that starts with admin. + + + + + + The access configuration attributes that apply for the configured message. For example, + permitAll grants access to anyone, hasRole('ROLE_ADMIN') requires the user have the role + 'ROLE_ADMIN'. + + + + + + The type of message to match on. Valid values are defined in SimpMessageType (i.e. + CONNECT, CONNECT_ACK, HEARTBEAT, MESSAGE, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT, + DISCONNECT_ACK, OTHER). + + + + + + + + + + + + + + + + + + + + Allows a custom instance of HttpFirewall to be injected into the FilterChainProxy created + by the namespace. + + + + + + + + + Container element for HTTP security configuration. Multiple elements can now be defined, + each with a specific pattern to which the enclosed security configuration applies. A + pattern can also be configured to bypass Spring Security's filters completely by setting + the "security" attribute to "none". + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + Defines the access-denied strategy that should be used. An access denied page can be + defined or a reference to an AccessDeniedHandler instance. + + + + + + + + + Sets up a form login configuration for authentication with a username and password + + + + + + + + + + + + Sets up form login for authentication with an Open ID identity. NOTE: The OpenID 1.0 and + 2.0 protocols have been deprecated and users are <a + href="https://openid.net/specs/openid-connect-migration-1_0.html">encouraged to + migrate</a> to <a href="https://openid.net/connect/">OpenID Connect</a>, which is + supported by <code>spring-security-oauth2</code>. + + + + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + + Adds support for X.509 client authentication. + + + + + + + + + + Adds support for basic authentication + + + + + + + + + Incorporates a logout processing filter. Most web applications require a logout filter, + although you may not require one if you write a controller to provider similar logic. + + + + + + + + + + Session-management related functionality is implemented by the addition of a + SessionManagementFilter to the filter stack. + + + + + + + Enables concurrent session control, limiting the number of authenticated sessions a user + may have at the same time. + + + + + + + + + + + + + Sets up remember-me authentication. If used with the "key" attribute (or no attributes) + the cookie-only implementation will be used. Specifying "token-repository-ref" or + "remember-me-data-source-ref" will use the more secure, persisten token approach. + + + + + + + + + Adds support for automatically granting all anonymous web requests a particular principal + identity and a corresponding granted authority. + + + + + + + + + Defines the list of mappings between http and https ports for use in redirects + + + + + + + Provides a method to map http ports to https ports when forcing a redirect. + + + + + + + + + + + + + + + Defines the SecurityExpressionHandler instance which will be used if expression-based + access-control is enabled. A default implementation (with no ACL support) will be used if + not supplied. + + + + + + + + + + + + + + + + + The request URL pattern which will be mapped to the filter chain created by this <http> + element. If omitted, the filter chain will match all requests. + + + + + + When set to 'none', requests matching the pattern attribute will be ignored by Spring + Security. No security filters will be applied and no SecurityContext will be available. If + set, the <http> element must be empty, with no children. + + + + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A legacy attribute which automatically registers a login form, BASIC authentication and a + logout URL and logout services. If unspecified, defaults to "false". We'd recommend you + avoid using this and instead explicitly configure the services you require. + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + Controls the eagerness with which an HTTP session is created by Spring Security classes. + If not set, defaults to "ifRequired". If "stateless" is used, this implies that the + application guarantees that it will not create a session. This differs from the use of + "never" which means that Spring Security will not create a session, but will make use of + one if the application does. + + + + + + + + + + + + + + A reference to a SecurityContextRepository bean. This can be used to customize how the + SecurityContext is stored between requests. + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + Provides versions of HttpServletRequest security methods such as isUserInRole() and + getPrincipal() which are implemented by accessing the Spring SecurityContext. Defaults to + "true". + + + + + + If available, runs the request as the Subject acquired from the JaasAuthenticationToken. + Defaults to "false". + + + + + + Optional attribute specifying the ID of the AccessDecisionManager implementation which + should be used for authorizing HTTP requests. + + + + + + Optional attribute specifying the realm name that will be used for all authentication + features that require a realm name (eg BASIC and Digest authentication). If unspecified, + defaults to "Spring Security Application". + + + + + + Allows a customized AuthenticationEntryPoint to be set on the ExceptionTranslationFilter. + + + + + + Corresponds to the observeOncePerRequest property of FilterSecurityInterceptor. Defaults + to "true" + + + + + + Prevents the jsessionid parameter from being added to rendered URLs. Defaults to "true" + (rewriting is disabled). + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + A reference to an AuthenticationManager bean + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + The access denied page that an authenticated user will be redirected to if they request a + page which they don't have the authority to access. + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + The access configuration attributes that apply for the configured path. + + + + + + The HTTP Method for which the access configuration attributes should apply. If not + specified, the attributes will apply to any method. + + + + + + + + + + + + + + + + + + Used to specify that a URL must be accessed over http or https, or that there is no + preference. The value should be "http", "https" or "any", respectively. + + + + + + The path to the servlet. This attribute is only applicable when 'request-matcher' is + 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are + 2 or more HttpServlet's registered in the ServletContext that have mappings starting with + '/' and are different; 2) The pattern starts with the same value of a registered + HttpServlet path, excluding the default (root) HttpServlet '/'. + + + + + + + + + Specifies the URL that will cause a logout. Spring Security will initialize a filter that + responds to this particular URL. Defaults to /logout if unspecified. + + + + + + Specifies the URL to display once the user has logged out. If not specified, defaults to + <form-login-login-page>/?logout (i.e. /login?logout). + + + + + + Specifies whether a logout also causes HttpSession invalidation, which is generally + desirable. If unspecified, defaults to true. + + + + + + A reference to a LogoutSuccessHandler implementation which will be used to determine the + destination to which the user is taken after logging out. + + + + + + A comma-separated list of the names of cookies which should be deleted when the user logs + out + + + + + + + Allow the RequestCache used for saving requests during the login process to be set + + + + + + + + + + + The URL that the login form is posted to. If unspecified, it defaults to /login. + + + + + + The name of the request parameter which contains the username. Defaults to 'username'. + + + + + + The name of the request parameter which contains the password. Defaults to 'password'. + + + + + + The URL that will be redirected to after successful authentication, if the user's previous + action could not be resumed. This generally happens if the user visits a login page + without having first requested a secured operation that triggers authentication. If + unspecified, defaults to the root of the application. + + + + + + Whether the user should always be redirected to the default-target-url after login. + + + + + + The URL for the login page. If no login URL is specified, Spring Security will + automatically create a login URL at GET /login and a corresponding filter to render that + login URL when requested. + + + + + + The URL for the login failure page. If no login failure URL is specified, Spring Security + will automatically create a failure login URL at /login?error and a corresponding filter + to render that login failure URL when requested. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful authentication request. Should not be used in combination with + default-target-url (or always-use-default-target-url) as the implementation should always + deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationFailureHandler bean which should be used to handle a failed + authentication request. Should not be used in combination with authentication-failure-url + as the implementation should always deal with navigation to the subsequent destination + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + The URL for the ForwardAuthenticationFailureHandler + + + + + + The URL for the ForwardAuthenticationSuccessHandler + + + + + + + Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + + + + + + + + + + Reference to the ClientRegistrationRepository + + + + + + Reference to the OAuth2AuthorizedClientRepository + + + + + + Reference to the OAuth2AuthorizedClientService + + + + + + Reference to the AuthorizationRequestRepository + + + + + + Reference to the OAuth2AuthorizationRequestResolver + + + + + + Reference to the OAuth2AccessTokenResponseClient + + + + + + Reference to the GrantedAuthoritiesMapper + + + + + + Reference to the OAuth2UserService + + + + + + Reference to the OpenID Connect OAuth2UserService + + + + + + The URI where the filter processes authentication requests + + + + + + The URI to send users to login + + + + + + Reference to the AuthenticationSuccessHandler + + + + + + Reference to the AuthenticationFailureHandler + + + + + + Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider + + + + + + + Configures OAuth 2.0 Client support. + + + + + + + + + + + + + Reference to the ClientRegistrationRepository + + + + + + Reference to the OAuth2AuthorizedClientRepository + + + + + + Reference to the OAuth2AuthorizedClientService + + + + + + + Configures OAuth 2.0 Authorization Code Grant. + + + + + + + + + + Reference to the AuthorizationRequestRepository + + + + + + Reference to the OAuth2AuthorizationRequestResolver + + + + + + Reference to the OAuth2AccessTokenResponseClient + + + + + + + Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 + Provider. + + + + + + + + + + + + Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. + + + + + + + + + + The ID that uniquely identifies the client registration. + + + + + + The client identifier. + + + + + + The client secret. + + + + + + The method used to authenticate the client with the provider. The supported values are + client_secret_basic, client_secret_post and none (public clients). + + + + + + + + + + + + + + + The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The + supported values are authorization_code, client_credentials, password and implicit. + + + + + + + + + + + + + + The client’s registered redirect URI that the Authorization Server redirects the + end-user’s user-agent to after the end-user has authenticated and authorized access to the + client. + + + + + + A comma-separated list of scope(s) requested by the client during the Authorization + Request flow, such as openid, email, or profile. + + + + + + A descriptive name used for the client. The name may be used in certain scenarios, such as + when displaying the name of the client in the auto-generated login page. + + + + + + A reference to the associated provider. May reference a 'provider' element or use one of + the common providers (google, github, facebook, okta). + + + + + + + The configuration information for an OAuth 2.0 or OpenID Connect 1.0 Provider. + + + + + + + + + + The ID that uniquely identifies the provider. + + + + + + The Authorization Endpoint URI for the Authorization Server. + + + + + + The Token Endpoint URI for the Authorization Server. + + + + + + The UserInfo Endpoint URI used to access the claims/attributes of the authenticated + end-user. + + + + + + The authentication method used when sending the access token to the UserInfo Endpoint. The + supported values are header, form and query. + + + + + + + + + + + + + The name of the attribute returned in the UserInfo Response that references the Name or + Identifier of the end-user. + + + + + + The URI used to retrieve the JSON Web Key (JWK) Set from the Authorization Server, which + contains the cryptographic key(s) used to verify the JSON Web Signature (JWS) of the ID + Token and optionally the UserInfo Response. + + + + + + The URI used to discover the configuration information for an OAuth 2.0 or OpenID Connect + 1.0 Provider. + + + + + + + Configures authentication support as an OAuth 2.0 Resource Server. + + + + + + + + + + + + + + Reference to an AuthenticationManagerResolver + + + + + + Reference to a BearerTokenResolver + + + + + + Reference to a AuthenticationEntryPoint + + + + + + + Configures JWT authentication + + + + + + + + + + The URI to use to collect the JWK Set for verifying JWTs + + + + + + Reference to a JwtDecoder + + + + + + Reference to a Converter<Jwt, AbstractAuthenticationToken> + + + + + + + Configuration Opaque Token authentication + + + + + + + + + + The URI to use to introspect opaque token attributes + + + + + + The Client ID to use to authenticate the introspection request + + + + + + The Client secret to use to authenticate the introspection request + + + + + + Reference to an OpaqueTokenIntrospector + + + + + + + + Sets up an attribute exchange configuration to request specified attributes from the + OpenID identity provider. When multiple elements are used, each must have an + identifier-attribute attribute. Each configuration will be matched in turn against the + supplied login identifier until a match is found. + + + + + + + + + + + + + A regular expression which will be compared against the claimed identity, when deciding + which attribute-exchange configuration to use during authentication. + + + + + + + Attributes used when making an OpenID AX Fetch Request. NOTE: The OpenID 1.0 and 2.0 + protocols have been deprecated and users are <a + href="https://openid.net/specs/openid-connect-migration-1_0.html">encouraged to + migrate</a> to <a href="https://openid.net/connect/">OpenID Connect</a>, which is + supported by <code>spring-security-oauth2</code>. + + + + + + + + + + Specifies the name of the attribute that you wish to get back. For example, email. + + + + + + Specifies the attribute type. For example, https://axschema.org/contact/email. See your + OP's documentation for valid attribute types. + + + + + + Specifies if this attribute is required to the OP, but does not error out if the OP does + not return the attribute. Default is false. + + + + + + Specifies the number of attributes that you wish to get back. For example, return 3 + emails. The default value is 1. + + + + + + + Used to explicitly configure a FilterChainProxy instance with a FilterChainMap + + + + + + + + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + Used within to define a specific URL pattern and the list of filters which apply to the + URLs matching that pattern. When multiple filter-chain elements are assembled in a list in + order to configure a FilterChainProxy, the most specific patterns must be placed at the + top of the list, with most general ones at the bottom. + + + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + A comma separated list of bean names that implement Filter that should be processed for + this FilterChain. If the value is none, then no Filters will be used for this FilterChain. + + + + + + + + The request URL pattern which will be mapped to the FilterChain. + + + + + + + + Allows a RequestMatcher instance to be used, as an alternative to pattern-matching. + + + + + + + Used to explicitly configure a FilterSecurityMetadataSource bean for use with a + FilterSecurityInterceptor. Usually only needed if you are configuring a FilterChainProxy + explicitly, rather than using the <http> element. The intercept-url elements used should + only contain pattern, method and access attributes. Any others will result in a + configuration error. + + + + + + + Specifies the access attributes and/or filter list for a particular set of URLs. + + + + + + + + + + + + + + Enables the use of expressions in the 'access' attributes in <intercept-url> elements + rather than the traditional list of configuration attributes. Defaults to 'true'. If + enabled, each attribute should contain a single boolean expression. If the expression + evaluates to 'true', access will be granted. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + + + + + + + + + + + + + + + + + Sets the AuthenticationEntryPoint which is used by the BasicAuthenticationFilter. + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + Adds support for the password management. + + + + + + + + + + The change password page. Defaults to "/change-password". + + + + + + + + + Indicates how session fixation protection will be applied when a user authenticates. If + set to "none", no protection will be applied. "newSession" will create a new empty + session, with only Spring Security-related attributes migrated. "migrateSession" will + create a new session and copy all session attributes to the new session. In Servlet 3.1 + (Java EE 7) and newer containers, specifying "changeSessionId" will keep the existing + session and use the container-supplied session fixation protection + (HttpServletRequest#changeSessionId()). Defaults to "changeSessionId" in Servlet 3.1 and + newer containers, "migrateSession" in older containers. Throws an exception if + "changeSessionId" is used in older containers. + + + + + + + + + + + + + + The URL to which a user will be redirected if they submit an invalid session indentifier. + Typically used to detect session timeouts. + + + + + + Allows injection of the InvalidSessionStrategy instance used by the + SessionManagementFilter + + + + + + Allows injection of the SessionAuthenticationStrategy instance used by the + SessionManagementFilter + + + + + + Defines the URL of the error page which should be shown when the + SessionAuthenticationStrategy raises an exception. If not set, an unauthorized (401) error + code will be returned to the client. Note that this attribute doesn't apply if the error + occurs during a form-based login, where the URL for authentication failure will take + precedence. + + + + + + + + + The maximum number of sessions a single authenticated user can have open at the same time. + Defaults to "1". A negative value denotes unlimited sessions. + + + + + + The URL a user will be redirected to if they attempt to use a session which has been + "expired" because they have logged in again. + + + + + + Allows injection of the SessionInformationExpiredStrategy instance used by the + ConcurrentSessionFilter + + + + + + Specifies that an unauthorized error should be reported when a user attempts to login when + they already have the maximum configured sessions open. The default behaviour is to expire + the original session. If the session-authentication-error-url attribute is set on the + session-management URL, the user will be redirected to this URL. + + + + + + Allows you to define an alias for the SessionRegistry bean in order to access it in your + own configuration. + + + + + + Allows you to define an external SessionRegistry bean to be used by the concurrency + control setup. + + + + + + + + + The "key" used to identify cookies from a specific token-based remember-me application. + You should set this to a unique value for your application. If unset, it will default to a + random value generated by SecureRandom. + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + A reference to a DataSource bean + + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Exports the internally defined RememberMeServices as a bean alias, allowing it to be used + by other beans in the application context. + + + + + + Determines whether the "secure" flag will be set on the remember-me cookie. If set to + true, the cookie will only be submitted over HTTPS (recommended). By default, secure + cookies will be used if the request is made on a secure connection. + + + + + + The period (in seconds) for which the remember-me cookie should be valid. + + + + + + Reference to an AuthenticationSuccessHandler bean which should be used to handle a + successful remember-me authentication. + + + + + + The name of the request parameter which toggles remember-me authentication. Defaults to + 'remember-me'. + + + + + + The name of cookie which store the token for remember-me authentication. Defaults to + 'remember-me'. + + + + + + + + Reference to a PersistentTokenRepository bean for use with the persistent token + remember-me implementation. + + + + + + + + Allows a custom implementation of RememberMeServices to be used. Note that this + implementation should return RememberMeAuthenticationToken instances with the same "key" + value as specified in the remember-me element. Alternatively it should register its own + AuthenticationProvider. It should also implement the LogoutHandler interface, which will + be invoked when a user logs out. Typically the remember-me cookie would be removed on + logout. + + + + + + + + + + + + The key shared between the provider and filter. This generally does not need to be set. If + unset, it will default to a random value generated by SecureRandom. + + + + + + The username that should be assigned to the anonymous request. This allows the principal + to be identified, which may be important for logging and auditing. if unset, defaults to + "anonymousUser". + + + + + + The granted authority that should be assigned to the anonymous request. Commonly this is + used to assign the anonymous request particular roles, which can subsequently be used in + authorization decisions. If unset, defaults to "ROLE_ANONYMOUS". + + + + + + With the default namespace setup, the anonymous "authentication" facility is automatically + enabled. You can disable it using this property. + + + + + + + + + + The http port to use. + + + + + + + + The https port to use. + + + + + + + + + The regular expression used to obtain the username from the certificate's subject. + Defaults to matching on the common name using the pattern "CN=(.*?),". + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + Reference to an AuthenticationDetailsSource which will be used by the authentication + filter + + + + + + + Adds a J2eePreAuthenticatedProcessingFilter to the filter chain to provide integration + with container authentication. + + + + + + + + + + A comma-separate list of roles to look for in the incoming HttpServletRequest. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Registers the AuthenticationManager instance and allows its list of + AuthenticationProviders to be defined. Also allows you to define an alias to allow you to + reference the AuthenticationManager in your own beans. + + + + + + + Indicates that the contained user-service should be used as an authentication source. + + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + Sets up an ldap authentication provider + + + + + + + Specifies that an LDAP provider should use an LDAP compare operation of the user's + password to authenticate the user + + + + + + + element which defines a password encoding strategy. Used by an authentication provider to + convert submitted passwords to hashed versions, for example. + + + + + + + + + + + + + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + An alias you wish to use for the AuthenticationManager bean (not required it you are using + a specific id) + + + + + + If set to true, the AuthenticationManger will attempt to clear any credentials data in the + returned Authentication object, once the user has been authenticated. + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + A reference to a user-service (or UserDetailsService bean) Id + + + + + + + Creates an in-memory UserDetailsService from a properties file or a list of "user" child + elements. Usernames are converted to lower-case internally to allow for case-insensitive + lookups, so this should not be used if case-sensitivity is required. + + + + + + + Represents a user in the application. + + + + + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The location of a Properties file where each line is in the format of + username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + + + + + + + + + The username assigned to the user. + + + + + + The password assigned to the user. This may be hashed if the corresponding authentication + provider supports hashing (remember to set the "hash" attribute of the "user-service" + element). This attribute be omitted in the case where the data will not be used for + authentication, but only for accessing authorities. If omitted, the namespace will + generate a random value, preventing its accidental use for authentication. Cannot be + empty. + + + + + + One of more authorities granted to the user. Separate authorities with a comma (but no + space). For example, "ROLE_USER,ROLE_ADMINISTRATOR" + + + + + + Can be set to "true" to mark an account as locked and unusable. + + + + + + Can be set to "true" to mark an account as disabled and unusable. + + + + + + + Causes creation of a JDBC-based UserDetailsService. + + + + + + A bean identifier, used for referring to the bean elsewhere in the context. + + + + + + + + + + The bean ID of the DataSource which provides the required tables. + + + + + + Defines a reference to a cache for use with a UserDetailsService. + + + + + + An SQL statement to query a username, password, and enabled status given a username. + Default is "select username,password,enabled from users where username = ?" + + + + + + An SQL statement to query for a user's granted authorities given a username. The default + is "select username, authority from authorities where username = ?" + + + + + + An SQL statement to query user's group authorities given a username. The default is + "select g.id, g.group_name, ga.authority from groups g, group_members gm, + group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" + + + + + + A non-empty string prefix that will be added to role strings loaded from persistent + storage (e.g. "ROLE_"). Use the value "none" for no prefix in cases where the default is + non-empty. + + + + + + + Element for configuration of the CsrfFilter for protection against CSRF. It also updates + the default RequestCache to only replay "GET" requests. + + + + + + + + + + Specifies if csrf protection should be disabled. Default false (i.e. CSRF protection is + enabled). + + + + + + The RequestMatcher instance to be used to determine if CSRF should be applied. Default is + any HTTP method except "GET", "TRACE", "HEAD", "OPTIONS" + + + + + + The CsrfTokenRepository to use. The default is HttpSessionCsrfTokenRepository wrapped by + LazyCsrfTokenRepository. + + + + + + + Element for configuration of the HeaderWritersFilter. Enables easy setting for the + X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. + + + + + + + + + + + + + + + + + + + + + + + Specifies if the default headers should be disabled. Default false. + + + + + + Specifies if headers should be disabled. Default false. + + + + + + + Adds support for HTTP Strict Transport Security (HSTS) + + + + + + + + + + Specifies if HTTP Strict Transport Security (HSTS) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default true. + + + + + + Specifies the maximum amount of time the host should be considered a Known HSTS Host. + Default one year. + + + + + + The RequestMatcher instance to be used to determine if the header should be set. Default + is if HttpServletRequest.isSecure() is true. + + + + + + Specifies if preload should be included. Default false. + + + + + + + Element for configuration of CorsFilter. If no CorsFilter or CorsConfigurationSource is + specified a HandlerMappingIntrospector is used as the CorsConfigurationSource + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specifies a bean id that is a CorsConfigurationSource used to construct the CorsFilter to + use + + + + + + + Adds support for HTTP Public Key Pinning (HPKP). + + + + + + + + + + + + + + + + + + The list with pins + + + + + + + + + + + A pin is specified using the base64-encoded SPKI fingerprint as value and the + cryptographic hash algorithm as attribute + + + + + + The cryptographic hash algorithm + + + + + + + + + Specifies if HTTP Public Key Pinning (HPKP) should be disabled. Default false. + + + + + + Specifies if subdomains should be included. Default false. + + + + + + Sets the value for the max-age directive of the Public-Key-Pins header. Default 60 days. + + + + + + Specifies if the browser should only report pin validation failures. Default true. + + + + + + Specifies the URI to which the browser should report pin validation failures. + + + + + + + Adds support for Content Security Policy (CSP) + + + + + + + + + + The security policy directive(s) for the Content-Security-Policy header or if report-only + is set to true, then the Content-Security-Policy-Report-Only header is used. + + + + + + Set to true, to enable the Content-Security-Policy-Report-Only header for reporting policy + violations only. Defaults to false. + + + + + + + Adds support for Referrer Policy + + + + + + + + + + The policies for the Referrer-Policy header. + + + + + + + + + + + + + + + + + + + Adds support for Feature Policy + + + + + + + + + + The security policy directive(s) for the Feature-Policy header. + + + + + + + Adds support for Permissions Policy + + + + + + + + + + The policies for the Permissions-Policy header. + + + + + + + Adds Cache-Control no-cache, no-store, must-revalidate, Pragma no-cache, and Expires 0 for + every request + + + + + + + + + + Specifies if Cache Control should be disabled. Default false. + + + + + + + Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options + header. + + + + + + + + + + If disabled, the X-Frame-Options header will not be included. Default false. + + + + + + Specify the policy to use for the X-Frame-Options-Header. + + + + + + + + + + + + + Specify the strategy to use when ALLOW-FROM is chosen. + + + + + + + + + + + + + Defines a reference to a Spring bean Id. + + + + + + Specify a value to use for the chosen strategy. + + + + + + Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' + based strategy. Default is 'from'. Deprecated ALLOW-FROM is an obsolete directive that no + longer works in modern browsers. Instead use Content-Security-Policy with the <a + href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors">frame-ancestors</a> + directive. + + + + + + + Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the + X-XSS-Protection header. + + + + + + + + + + disable the X-XSS-Protection header. Default is 'false' meaning it is enabled. + + + + + + specify that XSS Protection should be explicitly enabled or disabled. Default is 'true' + meaning it is enabled. + + + + + + Add mode=block to the header or not, default is on. + + + + + + + Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'. + + + + + + + + + + If disabled, the X-Content-Type-Options header will not be included. Default false. + + + + + + + Add additional headers to the response. + + + + + + + + + + The name of the header to add. + + + + + + The value for the header. + + + + + + Defines a reference to a Spring bean Id. + + + + + + + + Used to indicate that a filter bean declaration should be incorporated into the security + filter chain. + + + + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + The filter immediately after which the custom-filter should be placed in the chain. This + feature will only be needed by advanced users who wish to mix their own filters into the + security filter chain and have some knowledge of the standard Spring Security filters. The + filter names map to specific Spring Security implementation filters. + + + + + + + + The filter immediately before which the custom-filter should be placed in the chain + + + + + + + + The explicit position at which the custom-filter should be placed in the chain. Use if you + are replacing a standard filter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java index e8782a3b84..6e1e4bbe6f 100644 --- a/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java +++ b/config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java @@ -65,7 +65,7 @@ public class XsdDocumentedTests { String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd"; - String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.6.xsd"; + String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.7.xsd"; XmlSupport xml = new XmlSupport(); @@ -150,8 +150,8 @@ public class XsdDocumentedTests { .getParentFile() .list((dir, name) -> name.endsWith(".xsd")); // @formatter:on - assertThat(schemas.length).isEqualTo(18) - .withFailMessage("the count is equal to 18, if not then schemaDocument needs updating"); + assertThat(schemas.length).isEqualTo(19) + .withFailMessage("the count is equal to 19, if not then schemaDocument needs updating"); } /** diff --git a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java index 2b13e62677..478f564943 100644 --- a/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java +++ b/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java @@ -43,7 +43,7 @@ public final class SpringSecurityCoreVersion { * N.B. Classes are not intended to be serializable between different versions. See * SEC-1709 for why we still need a serial version. */ - public static final long SERIAL_VERSION_UID = 560L; + public static final long SERIAL_VERSION_UID = 570L; static final String MIN_SPRING_VERSION = getSpringVersion(); diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml index 519d53d195..c584e63f6f 100644 --- a/docs/antora-playbook.yml +++ b/docs/antora-playbook.yml @@ -9,7 +9,7 @@ content: - url: https://github.com/spring-io/spring-generated-docs branches: [spring-projects/spring-security/*] - url: https://github.com/spring-projects/spring-security - branches: [main,5.6.x] + branches: [main,5.6.x,5.7.x] start_path: docs urls: latest_version_segment_strategy: redirect:to diff --git a/docs/antora.yml b/docs/antora.yml index 40f6866738..c306b92d0e 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,3 @@ name: ROOT -version: '5.6.1' +version: '5.7.0' prerelease: '-SNAPSHOT' diff --git a/docs/local-antora-playbook.yml b/docs/local-antora-playbook.yml index 8e2678cb29..724e43819e 100644 --- a/docs/local-antora-playbook.yml +++ b/docs/local-antora-playbook.yml @@ -9,7 +9,7 @@ content: - url: ../../spring-io/spring-generated-docs branches: [spring-projects/spring-security/*] - url: ../../spring-projects/spring-security - branches: [main,5.6.x] + branches: [main,5.6.x,5.7.x] start_path: docs urls: latest_version_segment_strategy: redirect:to diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc index 4f36c2c2cb..f569cd1abf 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/index.adoc @@ -6,4 +6,4 @@ This appendix provides a reference to the elements available in the security nam If you haven't used the namespace before, please read the xref:servlet/configuration/xml-namespace.adoc#ns-config[introductory chapter] on namespace configuration, as this is intended as a supplement to the information there. Using a good quality XML editor while editing a configuration based on the schema is recommended as this will provide contextual information on which elements and attributes are available as well as comments explaining their purpose. The namespace is written in https://relaxng.org/[RELAX NG] Compact format and later converted into an XSD schema. -If you are familiar with this format, you may wish to examine the https://raw.githubusercontent.com/spring-projects/spring-security/main/config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc[schema file] directly. +If you are familiar with this format, you may wish to examine the https://raw.githubusercontent.com/spring-projects/spring-security/main/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc[schema file] directly. diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index aec0de8599..10be0bc0d5 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -1,57 +1,5 @@ [[new]] -= What's New in Spring Security 5.6 += What's New in Spring Security 5.7 -Spring Security 5.6 provides a number of new features. +Spring Security 5.7 provides a number of new features. Below are the highlights of the release. - -* All new https://antora.org/[Antora] based https://docs.spring.io/spring-security/[documentation]. - -[[whats-new-servlet]] -== Servlet -* Core - -** Introduced https://github.com/spring-projects/spring-security/issues/10226[`SecurityContextChangedListener`] -** Improved https://github.com/spring-projects/spring-security/pull/10279[Method Security Logging] - -* Configuration - -** Introduced https://github.com/spring-projects/spring-security/pull/9630[`AuthorizationManager`] for method security - -* SAML 2.0 Service Provider - -** Added xref:servlet/saml2/logout.adoc[SAML 2.0 Single Logout Support] -** Added xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-store-authn-request[Saml2AuthenticationRequestRepository] -** Added xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[`RelyingPartyRegistrationResolver`] -** Improved ``Saml2LoginConfigurer``'s handling of https://github.com/spring-projects/spring-security/issues/10268[`Saml2AuthenticationTokenConverter`] - - -* OAuth 2.0 Login - -** Added https://github.com/spring-projects/spring-security/pull/10041[`Converter` for `Authentication` result] - -* OAuth 2.0 Client - -** Improved https://github.com/spring-projects/spring-security/pull/9791[Client Credentials encoding] -** Improved https://github.com/spring-projects/spring-security/pull/9779[Access Token Response parsing] -** Added https://github.com/spring-projects/spring-security/pull/10155[custom grant types support] for Authorization Requests -** Introduced https://github.com/spring-projects/spring-security/pull/9208[JwtEncoder] - -* Testing - -** Added support to https://github.com/spring-projects/spring-security/pull/9737[propagate the TestSecurityContextHolder to SecurityContextHolder] - -[[whats-new-webflux]] -== WebFlux - -* OAuth 2.0 Login - -** Improved xref:reactive/oauth2/login/index.adoc[Reactive OAuth 2.0 Login Documentation] - -* OAuth 2.0 Client - -** Improved https://github.com/spring-projects/spring-security/pull/9791[Client Credentials encoding] -** Added https://github.com/spring-projects/spring-security/pull/10131[custom headers support] for Access Token Requests -** Added https://github.com/spring-projects/spring-security/pull/10269[custom response parsing] for Access Token Requests -** Added https://github.com/spring-projects/spring-security/pull/10327[jwt-bearer Grant Type support] for Access Token Requests -** Added https://github.com/spring-projects/spring-security/pull/10336[JWT Client Authentication support] for Access Token Requests -** Improved xref:reactive/oauth2/client/index.adoc[Reactive OAuth 2.0 Client Documentation] diff --git a/gradle.properties b/gradle.properties index 9fa3da5b60..09d81058b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.29 springBootVersion=2.4.2 springFrameworkVersion=5.3.13 openSamlVersion=3.4.6 -version=5.6.1-SNAPSHOT +version=5.7.0-SNAPSHOT kotlinVersion=1.5.31 samplesBranch=main org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError diff --git a/taglibs/src/main/resources/META-INF/security.tld b/taglibs/src/main/resources/META-INF/security.tld index b14bac5ab4..10640f3798 100644 --- a/taglibs/src/main/resources/META-INF/security.tld +++ b/taglibs/src/main/resources/META-INF/security.tld @@ -20,7 +20,7 @@ version="2.0"> Spring Security Authorization Tag Library - 5.6 + 5.7 security http://www.springframework.org/security/tags From ec8912aa4721ac1ba2dcede80dc91006603c8e73 Mon Sep 17 00:00:00 2001 From: Lars Grefer Date: Fri, 12 Nov 2021 00:02:15 +0100 Subject: [PATCH 035/589] Update aspectj-plugin to 6.3.0 Version 6.3.0 aligns with the used Gradle 7.3 --- aspects/spring-security-aspects.gradle | 4 ---- build.gradle | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/aspects/spring-security-aspects.gradle b/aspects/spring-security-aspects.gradle index 3a58595619..d3bbe9cbc0 100644 --- a/aspects/spring-security-aspects.gradle +++ b/aspects/spring-security-aspects.gradle @@ -27,7 +27,3 @@ sourceSets.test.aspectj.srcDir "src/test/java" sourceSets.test.java.srcDirs = files() compileAspectj.ajcOptions.outxmlfile = "META-INF/aop.xml" - -aspectj { - version = aspectjVersion -} diff --git a/build.gradle b/build.gradle index 20f17eab4f..86306c7518 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { dependencies { classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion" classpath 'io.spring.nohttp:nohttp-gradle:0.0.10' - classpath "io.freefair.gradle:aspectj-plugin:6.2.0" + classpath "io.freefair.gradle:aspectj-plugin:6.3.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "com.netflix.nebula:nebula-project-plugin:8.2.0" } From 399cf2e59d9325ddc1b6678805af520a5ad47ec5 Mon Sep 17 00:00:00 2001 From: heowc Date: Fri, 17 Sep 2021 15:26:13 +0900 Subject: [PATCH 036/589] Support for changing prefix and suffix in `DelegatingPasswordEncoder` Closes gh-10273 --- .../password/DelegatingPasswordEncoder.java | 53 ++++++++++++---- .../DelegatingPasswordEncoderTests.java | 62 +++++++++++++++++++ 2 files changed, 103 insertions(+), 12 deletions(-) diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java index d83dec021d..fae9877512 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java @@ -116,14 +116,19 @@ import java.util.Map; * * @author Rob Winch * @author Michael Simons + * @author heowc * @since 5.0 * @see org.springframework.security.crypto.factory.PasswordEncoderFactories */ public class DelegatingPasswordEncoder implements PasswordEncoder { - private static final String PREFIX = "{"; + private static final String DEFAULT_PREFIX = "{"; - private static final String SUFFIX = "}"; + private static final String DEFAULT_SUFFIX = "}"; + + private final String prefix; + + private final String suffix; private final String idForEncode; @@ -142,9 +147,31 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { * {@link #matches(CharSequence, String)} */ public DelegatingPasswordEncoder(String idForEncode, Map idToPasswordEncoder) { + this(idForEncode, idToPasswordEncoder, DEFAULT_PREFIX, DEFAULT_SUFFIX); + } + + /** + * Creates a new instance + * @param idForEncode the id used to lookup which {@link PasswordEncoder} should be + * used for {@link #encode(CharSequence)} + * @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine + * which {@link PasswordEncoder} should be used for + * @param prefix the prefix that denotes the start of an {@code idForEncode} + * @param suffix the suffix that denotes the end of an {@code idForEncode} + * {@link #matches(CharSequence, String)} + */ + public DelegatingPasswordEncoder(String idForEncode, Map idToPasswordEncoder, + String prefix, String suffix) { if (idForEncode == null) { throw new IllegalArgumentException("idForEncode cannot be null"); } + if (prefix == null) { + throw new IllegalArgumentException("prefix cannot be null"); + } + if (suffix == null || suffix.isEmpty()) { + throw new IllegalArgumentException("suffix cannot be empty"); + } + if (!idToPasswordEncoder.containsKey(idForEncode)) { throw new IllegalArgumentException( "idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder); @@ -153,16 +180,18 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { if (id == null) { continue; } - if (id.contains(PREFIX)) { - throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX); + if (!prefix.isEmpty() && id.contains(prefix)) { + throw new IllegalArgumentException("id " + id + " cannot contain " + prefix); } - if (id.contains(SUFFIX)) { - throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX); + if (id.contains(suffix)) { + throw new IllegalArgumentException("id " + id + " cannot contain " + suffix); } } this.idForEncode = idForEncode; this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode); this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder); + this.prefix = prefix; + this.suffix = suffix; } /** @@ -188,7 +217,7 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { - return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword); + return this.prefix + this.idForEncode + this.suffix + this.passwordEncoderForEncode.encode(rawPassword); } @Override @@ -209,15 +238,15 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { if (prefixEncodedPassword == null) { return null; } - int start = prefixEncodedPassword.indexOf(PREFIX); + int start = prefixEncodedPassword.indexOf(this.prefix); if (start != 0) { return null; } - int end = prefixEncodedPassword.indexOf(SUFFIX, start); + int end = prefixEncodedPassword.indexOf(this.suffix, start); if (end < 0) { return null; } - return prefixEncodedPassword.substring(start + 1, end); + return prefixEncodedPassword.substring(start + this.prefix.length(), end); } @Override @@ -233,8 +262,8 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { } private String extractEncodedPassword(String prefixEncodedPassword) { - int start = prefixEncodedPassword.indexOf(SUFFIX); - return prefixEncodedPassword.substring(start + 1); + int start = prefixEncodedPassword.indexOf(this.suffix); + return prefixEncodedPassword.substring(start + this.suffix.length()); } /** diff --git a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java index 9f3c3f18ac..dca1bc8b06 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/password/DelegatingPasswordEncoderTests.java @@ -36,6 +36,7 @@ import static org.mockito.Mockito.verifyZeroInteractions; /** * @author Rob Winch * @author Michael Simons + * @author heowc * @since 5.0 */ @ExtendWith(MockitoExtension.class) @@ -64,12 +65,16 @@ public class DelegatingPasswordEncoderTests { private DelegatingPasswordEncoder passwordEncoder; + private DelegatingPasswordEncoder onlySuffixPasswordEncoder; + @BeforeEach public void setup() { this.delegates = new HashMap<>(); this.delegates.put(this.bcryptId, this.bcrypt); this.delegates.put("noop", this.noop); this.passwordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates); + + this.onlySuffixPasswordEncoder = new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$"); } @Test @@ -83,6 +88,49 @@ public class DelegatingPasswordEncoderTests { .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId + "INVALID", this.delegates)); } + @Test + public void constructorWhenPrefixIsNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, null, "$")); + } + + @Test + public void constructorWhenSuffixIsNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "$", null)); + } + + @Test + public void constructorWhenPrefixIsEmpty() { + assertThat(new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$")).isNotNull(); + } + + @Test + public void constructorWhenSuffixIsEmpty() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "$", "")); + } + + @Test + public void constructorWhenPrefixAndSuffixAreEmpty() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "")); + } + + @Test + public void constructorWhenIdContainsPrefixThenIllegalArgumentException() { + this.delegates.put('$' + this.bcryptId, this.bcrypt); + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "$", "$")); + } + + @Test + public void constructorWhenIdContainsSuffixThenIllegalArgumentException() { + this.delegates.put(this.bcryptId + '$', this.bcrypt); + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingPasswordEncoder(this.bcryptId, this.delegates, "", "$")); + } + @Test public void setDefaultPasswordEncoderForMatchesWhenNullThenIllegalArgumentException() { assertThatIllegalArgumentException() @@ -104,6 +152,12 @@ public class DelegatingPasswordEncoderTests { assertThat(this.passwordEncoder.encode(this.rawPassword)).isEqualTo(this.bcryptEncodedPassword); } + @Test + public void encodeWhenValidBySpecifyDelegatingPasswordEncoderThenUsesIdForEncode() { + given(this.bcrypt.encode(this.rawPassword)).willReturn(this.encodedPassword); + assertThat(this.onlySuffixPasswordEncoder.encode(this.rawPassword)).isEqualTo("bcrypt$" + this.encodedPassword); + } + @Test public void matchesWhenBCryptThenDelegatesToBCrypt() { given(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).willReturn(true); @@ -112,6 +166,14 @@ public class DelegatingPasswordEncoderTests { verifyZeroInteractions(this.noop); } + @Test + public void matchesWhenBCryptBySpecifyDelegatingPasswordEncoderThenDelegatesToBCrypt() { + given(this.bcrypt.matches(this.rawPassword, this.encodedPassword)).willReturn(true); + assertThat(this.onlySuffixPasswordEncoder.matches(this.rawPassword, "bcrypt$" + this.encodedPassword)).isTrue(); + verify(this.bcrypt).matches(this.rawPassword, this.encodedPassword); + verifyZeroInteractions(this.noop); + } + @Test public void matchesWhenNoopThenDelegatesToNoop() { given(this.noop.matches(this.rawPassword, this.encodedPassword)).willReturn(true); From 582629c08768d6d72d508f04b06509ed436cece6 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 16 Nov 2021 13:15:45 -0600 Subject: [PATCH 037/589] Rename prefix/suffix in DelegatingPasswordEncoder Issue gh-10273 --- .../password/DelegatingPasswordEncoder.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java index fae9877512..4623bc9b02 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java @@ -53,7 +53,8 @@ import java.util.Map; * Such that "id" is an identifier used to look up which {@link PasswordEncoder} should be * used and "encodedPassword" is the original encoded password for the selected * {@link PasswordEncoder}. The "id" must be at the beginning of the password, start with - * "{" and end with "}". If the "id" cannot be found, the "id" will be null. + * "{" (id prefix) and end with "}" (id suffix). Both id prefix and id suffix can be customized via + * {@link #DelegatingPasswordEncoder(String, Map, String, String)}. If the "id" cannot be found, the "id" will be null. * * For example, the following might be a list of passwords encoded using different "id". * All of the original passwords are "password". @@ -122,13 +123,13 @@ import java.util.Map; */ public class DelegatingPasswordEncoder implements PasswordEncoder { - private static final String DEFAULT_PREFIX = "{"; + private static final String DEFAULT_ID_PREFIX = "{"; - private static final String DEFAULT_SUFFIX = "}"; + private static final String DEFAULT_ID_SUFFIX = "}"; - private final String prefix; + private final String idPrefix; - private final String suffix; + private final String idSuffix; private final String idForEncode; @@ -147,7 +148,7 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { * {@link #matches(CharSequence, String)} */ public DelegatingPasswordEncoder(String idForEncode, Map idToPasswordEncoder) { - this(idForEncode, idToPasswordEncoder, DEFAULT_PREFIX, DEFAULT_SUFFIX); + this(idForEncode, idToPasswordEncoder, DEFAULT_ID_PREFIX, DEFAULT_ID_SUFFIX); } /** @@ -156,19 +157,19 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { * used for {@link #encode(CharSequence)} * @param idToPasswordEncoder a Map of id to {@link PasswordEncoder} used to determine * which {@link PasswordEncoder} should be used for - * @param prefix the prefix that denotes the start of an {@code idForEncode} - * @param suffix the suffix that denotes the end of an {@code idForEncode} + * @param idPrefix the prefix that denotes the start of the id in the encoded results + * @param idSuffix the suffix that denotes the end of an id in the encoded results * {@link #matches(CharSequence, String)} */ public DelegatingPasswordEncoder(String idForEncode, Map idToPasswordEncoder, - String prefix, String suffix) { + String idPrefix, String idSuffix) { if (idForEncode == null) { throw new IllegalArgumentException("idForEncode cannot be null"); } - if (prefix == null) { + if (idPrefix == null) { throw new IllegalArgumentException("prefix cannot be null"); } - if (suffix == null || suffix.isEmpty()) { + if (idSuffix == null || idSuffix.isEmpty()) { throw new IllegalArgumentException("suffix cannot be empty"); } @@ -180,18 +181,18 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { if (id == null) { continue; } - if (!prefix.isEmpty() && id.contains(prefix)) { - throw new IllegalArgumentException("id " + id + " cannot contain " + prefix); + if (!idPrefix.isEmpty() && id.contains(idPrefix)) { + throw new IllegalArgumentException("id " + id + " cannot contain " + idPrefix); } - if (id.contains(suffix)) { - throw new IllegalArgumentException("id " + id + " cannot contain " + suffix); + if (id.contains(idSuffix)) { + throw new IllegalArgumentException("id " + id + " cannot contain " + idSuffix); } } this.idForEncode = idForEncode; this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode); this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder); - this.prefix = prefix; - this.suffix = suffix; + this.idPrefix = idPrefix; + this.idSuffix = idSuffix; } /** @@ -217,7 +218,7 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { - return this.prefix + this.idForEncode + this.suffix + this.passwordEncoderForEncode.encode(rawPassword); + return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword); } @Override @@ -238,15 +239,15 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { if (prefixEncodedPassword == null) { return null; } - int start = prefixEncodedPassword.indexOf(this.prefix); + int start = prefixEncodedPassword.indexOf(this.idPrefix); if (start != 0) { return null; } - int end = prefixEncodedPassword.indexOf(this.suffix, start); + int end = prefixEncodedPassword.indexOf(this.idSuffix, start); if (end < 0) { return null; } - return prefixEncodedPassword.substring(start + this.prefix.length(), end); + return prefixEncodedPassword.substring(start + this.idPrefix.length(), end); } @Override @@ -262,8 +263,8 @@ public class DelegatingPasswordEncoder implements PasswordEncoder { } private String extractEncodedPassword(String prefixEncodedPassword) { - int start = prefixEncodedPassword.indexOf(this.suffix); - return prefixEncodedPassword.substring(start + this.suffix.length()); + int start = prefixEncodedPassword.indexOf(this.idSuffix); + return prefixEncodedPassword.substring(start + this.idSuffix.length()); } /** From 0c201565fcded508cabb7f9c7c42598f9b1a3511 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 16 Nov 2021 13:32:15 -0600 Subject: [PATCH 038/589] Fix format DelegatingPasswordEncoder --- .../security/crypto/password/DelegatingPasswordEncoder.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java index 4623bc9b02..811c558155 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/DelegatingPasswordEncoder.java @@ -53,8 +53,9 @@ import java.util.Map; * Such that "id" is an identifier used to look up which {@link PasswordEncoder} should be * used and "encodedPassword" is the original encoded password for the selected * {@link PasswordEncoder}. The "id" must be at the beginning of the password, start with - * "{" (id prefix) and end with "}" (id suffix). Both id prefix and id suffix can be customized via - * {@link #DelegatingPasswordEncoder(String, Map, String, String)}. If the "id" cannot be found, the "id" will be null. + * "{" (id prefix) and end with "}" (id suffix). Both id prefix and id suffix can be + * customized via {@link #DelegatingPasswordEncoder(String, Map, String, String)}. If the + * "id" cannot be found, the "id" will be null. * * For example, the following might be a list of passwords encoded using different "id". * All of the original passwords are "password". From aa0f788f592baae5ce649b262f2faead9989c939 Mon Sep 17 00:00:00 2001 From: Onur Kagan Ozcan Date: Thu, 12 Aug 2021 11:20:36 +0300 Subject: [PATCH 039/589] Add RedirectStrategy customization to ChannelSecurityConfigurer for RetryWith classes --- .../ChannelSecurityConfigurer.java | 20 +++++++- .../ChannelSecurityConfigurerTests.java | 47 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java index 19fc8ae0ca..979084ce8c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,9 @@ import org.springframework.security.config.annotation.SecurityBuilder; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.PortMapper; +import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl; import org.springframework.security.web.access.channel.ChannelProcessingFilter; import org.springframework.security.web.access.channel.ChannelProcessor; @@ -75,6 +77,7 @@ import org.springframework.security.web.util.matcher.RequestMatcher; * * @param the type of {@link HttpSecurityBuilder} that is being configured * @author Rob Winch + * @author Onur Kagan Ozcan * @since 3.2 */ public final class ChannelSecurityConfigurer> @@ -86,6 +89,8 @@ public final class ChannelSecurityConfigurer> private List channelProcessors; + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + private final ChannelRequestMatcherRegistry REGISTRY; /** @@ -123,9 +128,11 @@ public final class ChannelSecurityConfigurer> if (portMapper != null) { RetryWithHttpEntryPoint httpEntryPoint = new RetryWithHttpEntryPoint(); httpEntryPoint.setPortMapper(portMapper); + httpEntryPoint.setRedirectStrategy(this.redirectStrategy); insecureChannelProcessor.setEntryPoint(httpEntryPoint); RetryWithHttpsEntryPoint httpsEntryPoint = new RetryWithHttpsEntryPoint(); httpsEntryPoint.setPortMapper(portMapper); + httpsEntryPoint.setRedirectStrategy(this.redirectStrategy); secureChannelProcessor.setEntryPoint(httpsEntryPoint); } insecureChannelProcessor = postProcess(insecureChannelProcessor); @@ -185,6 +192,17 @@ public final class ChannelSecurityConfigurer> return this; } + /** + * Sets the {@link RedirectStrategy} instances to use in + * {@link RetryWithHttpEntryPoint} and {@link RetryWithHttpsEntryPoint} + * @param redirectStrategy + * @return the {@link ChannelSecurityConfigurer} for further customizations + */ + public ChannelRequestMatcherRegistry redirectStrategy(RedirectStrategy redirectStrategy) { + ChannelSecurityConfigurer.this.redirectStrategy = redirectStrategy; + return this; + } + /** * Return the {@link SecurityBuilder} when done using the * {@link SecurityConfigurer}. This is useful for method chaining. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java index 041419e50d..83b2045a84 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,11 @@ package org.springframework.security.config.annotation.web.configurers; +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -27,6 +32,8 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.PortMapperImpl; +import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl; import org.springframework.security.web.access.channel.ChannelProcessingFilter; import org.springframework.security.web.access.channel.InsecureChannelProcessor; @@ -44,6 +51,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * * @author Rob Winch * @author Eleftheria Stein + * @author Onur Kagan Ozcan */ @ExtendWith(SpringTestContextExtension.class) public class ChannelSecurityConfigurerTests { @@ -93,6 +101,12 @@ public class ChannelSecurityConfigurerTests { this.mvc.perform(get("/")).andExpect(redirectedUrl("https://localhost/")); } + @Test + public void requestWhenRequiresChannelConfiguredWithUrlRedirectThenRedirectsToUrlWithHttps() throws Exception { + this.spring.register(RequiresChannelWithTestUrlRedirectStrategy.class).autowire(); + this.mvc.perform(get("/")).andExpect(redirectedUrl("https://localhost/test")); + } + @EnableWebSecurity static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter { @@ -155,4 +169,35 @@ public class ChannelSecurityConfigurerTests { } + @EnableWebSecurity + static class RequiresChannelWithTestUrlRedirectStrategy extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .portMapper() + .portMapper(new PortMapperImpl()) + .and() + .requiresChannel() + .redirectStrategy(new TestUrlRedirectStrategy()) + .anyRequest() + .requiresSecure(); + // @formatter:on + } + + } + + static class TestUrlRedirectStrategy implements RedirectStrategy { + + @Override + public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) + throws IOException { + String redirectUrl = url + "test"; + redirectUrl = response.encodeRedirectURL(redirectUrl); + response.sendRedirect(redirectUrl); + } + + } + } From 61ee4e5a763f53b1f3fde6f1b04ae03f4379612f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 14 Jul 2021 16:26:28 +0200 Subject: [PATCH 040/589] Avoid using SpEL to change the meaning of the injection point This commit removes the use of SpEL expression and replaces it with an explicit call to the underlying method. --- .../configuration/WebSecurityConfiguration.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java index 45bf5e5f1e..a608d0110b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import javax.servlet.Filter; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.Bean; @@ -143,19 +142,20 @@ public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAwa * instances used to create the web configuration. * @param objectPostProcessor the {@link ObjectPostProcessor} used to create a * {@link WebSecurity} instance - * @param webSecurityConfigurers the + * @param beanFactory the bean factory to use to retrieve the relevant * {@code } instances used to * create the web configuration * @throws Exception */ @Autowired(required = false) public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor objectPostProcessor, - @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List> webSecurityConfigurers) - throws Exception { + ConfigurableListableBeanFactory beanFactory) throws Exception { this.webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor)); if (this.debugEnabled != null) { this.webSecurity.debug(this.debugEnabled); } + List> webSecurityConfigurers = new AutowiredWebSecurityConfigurersIgnoreParents( + beanFactory).getWebSecurityConfigurers(); webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null; Object previousConfig = null; @@ -189,12 +189,6 @@ public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAwa return new RsaKeyConversionServicePostProcessor(); } - @Bean - public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents( - ConfigurableListableBeanFactory beanFactory) { - return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory); - } - @Override public void setImportMetadata(AnnotationMetadata importMetadata) { Map enableWebSecurityAttrMap = importMetadata From 4318a519711802c1cf06779670ec890d900fe9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=AB=C2=A0Christophe?= Date: Tue, 3 Aug 2021 11:20:03 +0200 Subject: [PATCH 041/589] Fix CsrfConfigurer default AccessDeniedHandler consistency Fix when AccessDeniedHandler is specified per RequestMatcher on ExceptionHandlingConfigurer. This introduces evolutions on : - CsrfConfigurer#getDefaultAccessDeniedHandler, to retrieve an AccessDeniedHandler similar to the one used by ExceptionHandlingConfigurer. - OAuth2ResourceServerConfigurer#accessDeniedHandler, to continue to handle CsrfException with the default AccessDeniedHandler implementation Fixes: gh-6511 --- .../web/configurers/CsrfConfigurer.java | 6 ++-- .../OAuth2ResourceServerConfigurer.java | 9 +++++- .../web/configurers/CsrfConfigurerTests.java | 31 ++++++++++++++++++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java index c0a3cd62ee..b9c5cc7c63 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -237,8 +237,8 @@ public final class CsrfConfigurer> /** * Gets the default {@link AccessDeniedHandler} from the - * {@link ExceptionHandlingConfigurer#getAccessDeniedHandler()} or create a - * {@link AccessDeniedHandlerImpl} if not available. + * {@link ExceptionHandlingConfigurer#getAccessDeniedHandler(HttpSecurityBuilder)} or + * create a {@link AccessDeniedHandlerImpl} if not available. * @param http the {@link HttpSecurityBuilder} * @return the {@link AccessDeniedHandler} */ @@ -247,7 +247,7 @@ public final class CsrfConfigurer> ExceptionHandlingConfigurer exceptionConfig = http.getConfigurer(ExceptionHandlingConfigurer.class); AccessDeniedHandler handler = null; if (exceptionConfig != null) { - handler = exceptionConfig.getAccessDeniedHandler(); + handler = exceptionConfig.getAccessDeniedHandler(http); } if (handler == null) { handler = new AccessDeniedHandlerImpl(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 02a99fdb2a..ca7ef5720d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -18,6 +18,8 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.se import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; @@ -51,6 +53,9 @@ import org.springframework.security.oauth2.server.resource.web.DefaultBearerToke import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.AccessDeniedHandlerImpl; +import org.springframework.security.web.access.DelegatingAccessDeniedHandler; +import org.springframework.security.web.csrf.CsrfException; import org.springframework.security.web.util.matcher.AndRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; @@ -153,7 +158,9 @@ public final class OAuth2ResourceServerConfigurer(Map.of(CsrfException.class, new AccessDeniedHandlerImpl())), + new BearerTokenAccessDeniedHandler()); private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java index 750609bf14..bc431b6afb 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -320,6 +320,17 @@ public class CsrfConfigurerTests { any(HttpServletResponse.class), any()); } + @Test + public void getWhenCustomDefaultAccessDeniedHandlerForThenHandlerIsUsed() throws Exception { + DefaultAccessDeniedHandlerForConfig.DENIED_HANDLER = mock(AccessDeniedHandler.class); + DefaultAccessDeniedHandlerForConfig.MATCHER = mock(RequestMatcher.class); + given(DefaultAccessDeniedHandlerForConfig.MATCHER.matches(any())).willReturn(true); + this.spring.register(DefaultAccessDeniedHandlerForConfig.class, BasicController.class).autowire(); + this.mvc.perform(post("/")).andExpect(status().isOk()); + verify(DefaultAccessDeniedHandlerForConfig.DENIED_HANDLER).handle(any(HttpServletRequest.class), + any(HttpServletResponse.class), any()); + } + @Test public void loginWhenNoCsrfTokenThenRespondsWithForbidden() throws Exception { this.spring.register(FormLoginConfig.class).autowire(); @@ -608,6 +619,24 @@ public class CsrfConfigurerTests { } + @EnableWebSecurity + static class DefaultAccessDeniedHandlerForConfig extends WebSecurityConfigurerAdapter { + + static AccessDeniedHandler DENIED_HANDLER; + + static RequestMatcher MATCHER; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .exceptionHandling() + .defaultAccessDeniedHandlerFor(DENIED_HANDLER, MATCHER); + // @formatter:on + } + + } + @EnableWebSecurity static class FormLoginConfig extends WebSecurityConfigurerAdapter { From 96a6fef820948601f14408d7385d889fbc3dbaf2 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 23 Jun 2021 14:27:28 -0500 Subject: [PATCH 042/589] Prevent Save @Transient Authentication with existing HttpSession Previously, @Transient Authentication would get saved if an existing HttpSession existed but it shouldn't. This commit always prevents @Transient Authentication from being saved. Closes gh-9992 --- .../HttpSessionSecurityContextRepository.java | 13 ++++++++----- ...ttpSessionSecurityContextRepositoryTests.java | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java index 756db58a56..5811887f9d 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java @@ -233,6 +233,9 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo } private boolean isTransientAuthentication(Authentication authentication) { + if (authentication == null) { + return false; + } return AnnotationUtils.getAnnotation(authentication.getClass(), Transient.class) != null; } @@ -327,6 +330,9 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo @Override protected void saveContext(SecurityContext context) { final Authentication authentication = context.getAuthentication(); + if (isTransientAuthentication(authentication)) { + return; + } HttpSession httpSession = this.request.getSession(false); String springSecurityContextKey = HttpSessionSecurityContextRepository.this.springSecurityContextKey; // See SEC-776 @@ -348,7 +354,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo } return; } - httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context, authentication); + httpSession = (httpSession != null) ? httpSession : createNewSessionIfAllowed(context); // If HttpSession exists, store current SecurityContext but only if it has // actually changed in this thread (see SEC-37, SEC-1307, SEC-1528) if (httpSession != null) { @@ -369,10 +375,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo || context.getAuthentication() != this.authBeforeExecution; } - private HttpSession createNewSessionIfAllowed(SecurityContext context, Authentication authentication) { - if (isTransientAuthentication(authentication)) { - return null; - } + private HttpSession createNewSessionIfAllowed(SecurityContext context) { if (this.httpSessionExistedAtStartOfRequest) { this.logger.debug("HttpSession is now null, but was not null at start of request; " + "session was invalidated, so do not create a new session"); diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index cfbe0f332f..2ab0d4754c 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java @@ -21,6 +21,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Collections; import javax.servlet.Filter; import javax.servlet.ServletException; @@ -614,6 +615,21 @@ public class HttpSessionSecurityContextRepositoryTests { assertThat(session).isNull(); } + @Test + public void saveContextWhenTransientAuthenticationAndSessionExistsThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.getSession(); // ensure the session exists + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContext context = repo.loadContext(holder); + SomeTransientAuthentication authentication = new SomeTransientAuthentication(); + context.setAuthentication(authentication); + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(Collections.list(session.getAttributeNames())).isEmpty(); + } + @Test public void saveContextWhenTransientAuthenticationWithCustomAnnotationThenSkipped() { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); From 00fafd878cad4c3f8741aecb6d6603de4e30c5c4 Mon Sep 17 00:00:00 2001 From: Khaled Hamlaoui Date: Thu, 21 Oct 2021 16:11:40 +0200 Subject: [PATCH 043/589] Allow custom OAuth2ErrorHttpMessageConverter with OAuth2ErrorResponseErrorHandler Closes gh-10425 --- .../http/OAuth2ErrorResponseErrorHandler.java | 15 ++++++++++- .../OAuth2ErrorResponseErrorHandlerTests.java | 27 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandler.java index 3f23c7d583..5e14fb66a1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandler.java @@ -23,10 +23,12 @@ import com.nimbusds.oauth2.sdk.token.BearerTokenError; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.ResponseErrorHandler; @@ -41,7 +43,7 @@ import org.springframework.web.client.ResponseErrorHandler; */ public class OAuth2ErrorResponseErrorHandler implements ResponseErrorHandler { - private final OAuth2ErrorHttpMessageConverter oauth2ErrorConverter = new OAuth2ErrorHttpMessageConverter(); + private HttpMessageConverter oauth2ErrorConverter = new OAuth2ErrorHttpMessageConverter(); private final ResponseErrorHandler defaultErrorHandler = new DefaultResponseErrorHandler(); @@ -89,4 +91,15 @@ public class OAuth2ErrorResponseErrorHandler implements ResponseErrorHandler { } } + /** + * Sets the {@link HttpMessageConverter} for an OAuth 2.0 Error. + * @param oauth2ErrorConverter A {@link HttpMessageConverter} for an + * {@link OAuth2Error OAuth 2.0 Error}. + * @since 5.7 + */ + public final void setErrorConverter(HttpMessageConverter oauth2ErrorConverter) { + Assert.notNull(oauth2ErrorConverter, "oauth2ErrorConverter cannot be null"); + this.oauth2ErrorConverter = oauth2ErrorConverter; + } + } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandlerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandlerTests.java index f5a8483494..aed0aad7fa 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandlerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandlerTests.java @@ -23,12 +23,19 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.HttpMessageConverter; import org.springframework.mock.http.MockHttpInputMessage; import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.web.client.UnknownHttpStatusCodeException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link OAuth2ErrorResponseErrorHandler}. @@ -53,6 +60,26 @@ public class OAuth2ErrorResponseErrorHandlerTests { .withMessage("[unauthorized_client] The client is not authorized"); } + @Test + public void handleErrorWhenOAuth2ErrorConverterSetThenCalled() throws IOException { + HttpMessageConverter oauth2ErrorConverter = mock(HttpMessageConverter.class); + this.errorHandler.setErrorConverter(oauth2ErrorConverter); + // @formatter:off + String errorResponse = "{\n" + + " \"errorCode\": \"unauthorized_client\",\n" + + " \"errorSummary\": \"The client is not authorized\"\n" + + "}\n"; + // @formatter:on + MockClientHttpResponse response = new MockClientHttpResponse(errorResponse.getBytes(), HttpStatus.BAD_REQUEST); + given(oauth2ErrorConverter.read(any(), any())) + .willReturn(new OAuth2Error("unauthorized_client", "The client is not authorized", null)); + + assertThatExceptionOfType(OAuth2AuthorizationException.class) + .isThrownBy(() -> this.errorHandler.handleError(response)) + .withMessage("[unauthorized_client] The client is not authorized"); + verify(oauth2ErrorConverter).read(eq(OAuth2Error.class), eq(response)); + } + @Test public void handleErrorWhenErrorResponseWwwAuthenticateHeaderThenHandled() { String wwwAuthenticateHeader = "Bearer realm=\"auth-realm\" error=\"insufficient_scope\" error_description=\"The access token expired\""; From 3fb1565cc062938c78ed9890f9394be0a22e360a Mon Sep 17 00:00:00 2001 From: Jeff Maxwell Date: Mon, 15 Nov 2021 17:42:13 -0600 Subject: [PATCH 044/589] Fix jwtDecoder Documentation Usage Closes gh-10505 --- .../pages/servlet/oauth2/resource-server/multitenancy.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc index bfd5394543..76d53a0151 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc @@ -416,9 +416,9 @@ Now that we have a tenant-aware processor and a tenant-aware validator, we can p ---- @Bean JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator jwtValidator) { - NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor); + NimbusJwtDecoder decoder = new NimbusJwtDecoder(jwtProcessor); OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<> - (JwtValidators.createDefault(), this.jwtValidator); + (JwtValidators.createDefault(), jwtValidator); decoder.setJwtValidator(validator); return decoder; } From b7cc667d210e127cd085fd2f021784adab015217 Mon Sep 17 00:00:00 2001 From: Jeff Maxwell Date: Mon, 15 Nov 2021 17:36:41 -0600 Subject: [PATCH 045/589] Fix setJWTClaimSetJWSKeySelector Typo Closes gh-10504 --- .../ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc index 76d53a0151..8bd734b29d 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/multitenancy.adoc @@ -323,7 +323,7 @@ Next, we can construct a `JWTProcessor`: JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) { ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor(); - jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector); + jwtProcessor.setJWTClaimsSetAwareJWSKeySelector(keySelector); return jwtProcessor; } ---- From 02cd1dd3c4ebcfe16db924a983c3d474dbbaefd7 Mon Sep 17 00:00:00 2001 From: Norbert Nowak Date: Mon, 1 Nov 2021 12:42:57 +0100 Subject: [PATCH 046/589] Fix AuthnRequestConverter Sample Typos Closes gh-10364 --- .../saml2/login/authentication-requests.adoc | 43 ++++++++----------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc index ba394250a4..3a299c96b9 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -187,7 +187,7 @@ But, if you do need something from the request, then you can use create a custom ---- @Component public class AuthnRequestConverter implements - Converter { + Converter { private final AuthnRequestBuilder authnRequestBuilder; private final IssuerBuilder issuerBuilder; @@ -195,18 +195,17 @@ public class AuthnRequestConverter implements // ... constructor public AuthnRequest convert(Saml2AuthenticationRequestContext context) { - MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context; Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(myContext.getIssuer()); + issuer.setValue(context.getIssuer()); AuthnRequest authnRequest = authnRequestBuilder.buildObject(); authnRequest.setIssuer(issuer); - authnRequest.setDestination(myContext.getDestination()); - authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl()); + authnRequest.setDestination(context.getDestination()); + authnRequest.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl()); // ... additional settings - authRequest.setForceAuthn(myContext.getForceAuthn()); + authRequest.setForceAuthn(context.getForceAuthn()); return authnRequest; } } @@ -216,22 +215,21 @@ public class AuthnRequestConverter implements [source,kotlin,role="secondary"] ---- @Component -class AuthnRequestConverter : Converter { +class AuthnRequestConverter : Converter { private val authnRequestBuilder: AuthnRequestBuilder? = null private val issuerBuilder: IssuerBuilder? = null // ... constructor override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest { - val myContext: MySaml2AuthenticationRequestContext = context val issuer: Issuer = issuerBuilder.buildObject() - issuer.value = myContext.getIssuer() + issuer.value = context.getIssuer() val authnRequest: AuthnRequest = authnRequestBuilder.buildObject() authnRequest.issuer = issuer - authnRequest.destination = myContext.getDestination() - authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl() + authnRequest.destination = context.getDestination() + authnRequest.assertionConsumerServiceURL = context.getAssertionConsumerServiceUrl() // ... additional settings - authRequest.setForceAuthn(myContext.getForceAuthn()) + authRequest.setForceAuthn(context.getForceAuthn()) return authnRequest } } @@ -246,12 +244,11 @@ Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and ---- @Bean Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() { - Saml2AuthenticationRequestContextResolver resolver = - new DefaultSaml2AuthenticationRequestContextResolver(); - return request -> { - Saml2AuthenticationRequestContext context = resolver.resolve(request); - return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null); - }; + Saml2AuthenticationRequestContextResolver resolver = new DefaultSaml2AuthenticationRequestContextResolver(relyingPartyRegistrationResolver); + return request -> { + Saml2AuthenticationRequestContext context = resolver.resolve(request); + return context; + }; } @Bean @@ -270,13 +267,9 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory( ---- @Bean open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver { - val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver() - return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest -> - val context = resolver.resolve(request) - MySaml2AuthenticationRequestContext( - context, - request.getParameter("force") != null - ) + val resolver = DefaultSaml2AuthenticationRequestContextResolver(relyingPartyRegistrationResolver) + return Saml2AuthenticationRequestContextResolver { request -> + resolver.resolve(request) } } From 739cdc1a4ca160ca36b2e231b0ec2228129fb9aa Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 18 Nov 2021 13:30:23 -0700 Subject: [PATCH 047/589] Polish AuthRequestConverter Sample Doc Issue gh-10364 --- .../saml2/login/authentication-requests.adoc | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc index 3a299c96b9..ba512b5a4a 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -195,17 +195,18 @@ public class AuthnRequestConverter implements // ... constructor public AuthnRequest convert(Saml2AuthenticationRequestContext context) { + MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context; Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(context.getIssuer()); + issuer.setValue(myContext.getIssuer()); AuthnRequest authnRequest = authnRequestBuilder.buildObject(); authnRequest.setIssuer(issuer); - authnRequest.setDestination(context.getDestination()); - authnRequest.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl()); + authnRequest.setDestination(myContext.getDestination()); + authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl()); // ... additional settings - authRequest.setForceAuthn(context.getForceAuthn()); + authRequest.setForceAuthn(myContext.getForceAuthn()); return authnRequest; } } @@ -220,16 +221,17 @@ class AuthnRequestConverter : Converter { - Saml2AuthenticationRequestContext context = resolver.resolve(request); - return context; - }; + Saml2AuthenticationRequestContextResolver resolver = + new DefaultSaml2AuthenticationRequestContextResolver(); + return request -> { + Saml2AuthenticationRequestContext context = resolver.resolve(request); + return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null); + }; } @Bean @@ -267,9 +270,13 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory( ---- @Bean open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver { - val resolver = DefaultSaml2AuthenticationRequestContextResolver(relyingPartyRegistrationResolver) - return Saml2AuthenticationRequestContextResolver { request -> - resolver.resolve(request) + val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver() + return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest -> + val context = resolver.resolve(request) + MySaml2AuthenticationRequestContext( + context, + request.getParameter("force") != null + ) } } From 17e28fa7aa9fef1a12ce430cf2eb904b8735d741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Kov=C3=A1=C4=8D?= Date: Sat, 9 Oct 2021 13:56:25 +0200 Subject: [PATCH 048/589] Update clockSkew javadoc according to implementation Closes gh-10174 --- ...OAuth2AuthorizedClientProviderBuilder.java | 20 ++++++++++++------- ...OAuth2AuthorizedClientProviderBuilder.java | 20 ++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java index fa109dd2aa..10a048f185 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -204,10 +204,12 @@ public final class OAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link PasswordGrantBuilder} + * @see PasswordOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public PasswordGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; @@ -275,10 +277,12 @@ public final class OAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link ClientCredentialsGrantBuilder} + * @see ClientCredentialsOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; @@ -365,10 +369,12 @@ public final class OAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link RefreshTokenGrantBuilder} + * @see RefreshTokenOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java index 7b0580571d..c9483fa16b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ReactiveOAuth2AuthorizedClientProviderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -225,10 +225,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link ClientCredentialsGrantBuilder} + * @see ClientCredentialsReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; @@ -297,10 +299,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link PasswordGrantBuilder} + * @see PasswordReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public PasswordGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; @@ -368,10 +372,12 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder { /** * Sets the maximum acceptable clock skew, which is used when checking the access - * token expiry. An access token is considered expired if it's before - * {@code Instant.now(this.clock) - clockSkew}. + * token expiry. An access token is considered expired if + * {@code OAuth2Token#getExpiresAt() - clockSkew} is before the current time + * {@code clock#instant()}. * @param clockSkew the maximum acceptable clock skew * @return the {@link RefreshTokenGrantBuilder} + * @see RefreshTokenReactiveOAuth2AuthorizedClientProvider#setClockSkew(Duration) */ public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) { this.clockSkew = clockSkew; From cf95d3f91e414dbc2fd6ede849f54c91100a27c2 Mon Sep 17 00:00:00 2001 From: Lars Grefer Date: Sat, 20 Nov 2021 02:21:46 +0100 Subject: [PATCH 049/589] Fix Gradle Deprecation Warnings --- .../main/groovy/io/spring/gradle/convention/DocsPlugin.groovy | 2 +- settings.gradle | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy index d0a64ab85b..c62fef79b1 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/DocsPlugin.groovy @@ -25,7 +25,7 @@ public class DocsPlugin implements Plugin { group = 'Distribution' archiveBaseName = project.rootProject.name archiveClassifier = 'docs' - description = "Builds -${classifier} archive containing all " + + description = "Builds -${archiveClassifier.get()} archive containing all " + "Docs for deployment at docs.spring.io" from(project.tasks.api.outputs) { diff --git a/settings.gradle b/settings.gradle index a73b597502..63a863b982 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,8 +10,6 @@ plugins { id "io.spring.ge.conventions" version "0.0.7" } -enableFeaturePreview("VERSION_ORDERING_V2") - dependencyResolutionManagement { repositories { mavenCentral() From d736a2b3586024605dcc185e6365e2ac71ce5f61 Mon Sep 17 00:00:00 2001 From: Lars Grefer Date: Sat, 20 Nov 2021 02:22:13 +0100 Subject: [PATCH 050/589] Remove usages of Gradle's jcenter() repository Closes gh-10253 --- buildSrc/build.gradle | 1 - .../RepositoryConventionPlugin.groovy | 5 ---- .../RepositoryConventionPluginTests.java | 30 ++++++++----------- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 7b8183995f..3b1e9cf196 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -9,7 +9,6 @@ plugins { sourceCompatibility = 1.8 repositories { - jcenter() gradlePluginPortal() mavenCentral() maven { url 'https://repo.spring.io/plugins-release/' } diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy index c242081429..407163d82a 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/RepositoryConventionPlugin.groovy @@ -35,11 +35,6 @@ class RepositoryConventionPlugin implements Plugin { mavenLocal() } mavenCentral() - jcenter() { - content { - includeGroup "org.gretty" - } - } if (isSnapshot) { maven { name = 'artifactory-snapshot' diff --git a/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java b/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java index 2bad49c8a3..f1048dbbab 100644 --- a/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java +++ b/buildSrc/src/test/java/io/spring/gradle/convention/RepositoryConventionPluginTests.java @@ -107,7 +107,7 @@ public class RepositoryConventionPluginTests { this.project.getPluginManager().apply(RepositoryConventionPlugin.class); RepositoryHandler repositories = this.project.getRepositories(); - assertThat(repositories).hasSize(5); + assertThat(repositories).hasSize(4); assertThat((repositories.get(0)).getName()).isEqualTo("MavenLocal"); } @@ -119,39 +119,33 @@ public class RepositoryConventionPluginTests { this.project.getPluginManager().apply(RepositoryConventionPlugin.class); RepositoryHandler repositories = this.project.getRepositories(); - assertThat(repositories).hasSize(6); + assertThat(repositories).hasSize(5); assertThat((repositories.get(0)).getName()).isEqualTo("MavenLocal"); } private void assertSnapshotRepository(RepositoryHandler repositories) { - assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(6); - assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString()) - .isEqualTo("https://repo.maven.apache.org/maven2/"); - assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString()) - .isEqualTo("https://jcenter.bintray.com/"); - assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString()) - .isEqualTo("https://repo.spring.io/snapshot/"); - assertThat(((MavenArtifactRepository) repositories.get(3)).getUrl().toString()) - .isEqualTo("https://repo.spring.io/milestone/"); - } - - private void assertMilestoneRepository(RepositoryHandler repositories) { assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(5); assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString()) .isEqualTo("https://repo.maven.apache.org/maven2/"); assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString()) - .isEqualTo("https://jcenter.bintray.com/"); + .isEqualTo("https://repo.spring.io/snapshot/"); assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString()) .isEqualTo("https://repo.spring.io/milestone/"); } - private void assertReleaseRepository(RepositoryHandler repositories) { + private void assertMilestoneRepository(RepositoryHandler repositories) { assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(4); assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString()) .isEqualTo("https://repo.maven.apache.org/maven2/"); assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString()) - .isEqualTo("https://jcenter.bintray.com/"); - assertThat(((MavenArtifactRepository) repositories.get(2)).getUrl().toString()) + .isEqualTo("https://repo.spring.io/milestone/"); + } + + private void assertReleaseRepository(RepositoryHandler repositories) { + assertThat(repositories).extracting(ArtifactRepository::getName).hasSize(3); + assertThat(((MavenArtifactRepository) repositories.get(0)).getUrl().toString()) + .isEqualTo("https://repo.maven.apache.org/maven2/"); + assertThat(((MavenArtifactRepository) repositories.get(1)).getUrl().toString()) .isEqualTo("https://repo.spring.io/release/"); } From bb99d7d95ac2d31874b93e84bb584c3f0cf52cb6 Mon Sep 17 00:00:00 2001 From: Henning Poettker Date: Tue, 23 Nov 2021 14:10:10 +0100 Subject: [PATCH 051/589] Fix return type for NoOpPasswordEncoder bean in documentation --- .../ROOT/pages/features/authentication/password-storage.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc index dcbf3ab8ad..4800f29a1d 100644 --- a/docs/modules/ROOT/pages/features/authentication/password-storage.adoc +++ b/docs/modules/ROOT/pages/features/authentication/password-storage.adoc @@ -463,7 +463,7 @@ You should instead migrate to using `DelegatingPasswordEncoder` to support secur [source,java,role="primary"] ---- @Bean -public static NoOpPasswordEncoder passwordEncoder() { +public static PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } ---- From 3c1827812394eff66bb75c153c6a45ea30ed6bb4 Mon Sep 17 00:00:00 2001 From: Markus Heiden Date: Fri, 4 Dec 2020 21:27:52 +0100 Subject: [PATCH 052/589] Start with LDAP Jackson2 mixins Issue gh-9263 --- .../jackson2/SecurityJackson2Modules.java | 5 + ldap/spring-security-ldap.gradle | 2 + .../ldap/jackson2/InetOrgPersonMixin.java | 46 +++++++++ .../ldap/jackson2/LdapAuthorityMixin.java | 62 ++++++++++++ .../ldap/jackson2/LdapJackson2Module.java | 60 ++++++++++++ .../jackson2/LdapUserDetailsImplMixin.java | 47 +++++++++ .../security/ldap/jackson2/PersonMixin.java | 46 +++++++++ .../jackson2/InetOrgPersonMixinTests.java | 95 +++++++++++++++++++ .../jackson2/LdapAuthorityMixinTests.java | 25 +++++ .../LdapUserDetailsImplMixinTests.java | 25 +++++ .../ldap/jackson2/PersonMixinTests.java | 59 ++++++++++++ 11 files changed, 472 insertions(+) create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java diff --git a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java index febd2b755c..ceaed240ab 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java @@ -84,6 +84,8 @@ public final class SecurityJackson2Modules { private static final String javaTimeJackson2ModuleClass = "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"; + private static final String ldapJackson2ModuleClass = "org.springframework.security.ldap.jackson2.LdapJackson2Module"; + private SecurityJackson2Modules() { } @@ -129,6 +131,9 @@ public final class SecurityJackson2Modules { if (ClassUtils.isPresent(javaTimeJackson2ModuleClass, loader)) { addToModulesList(loader, modules, javaTimeJackson2ModuleClass); } + if (ClassUtils.isPresent(ldapJackson2ModuleClass, loader)) { + addToModulesList(loader, modules, ldapJackson2ModuleClass); + } return modules; } diff --git a/ldap/spring-security-ldap.gradle b/ldap/spring-security-ldap.gradle index e16802ea45..f1c8074af4 100644 --- a/ldap/spring-security-ldap.gradle +++ b/ldap/spring-security-ldap.gradle @@ -8,6 +8,7 @@ dependencies { api 'org.springframework:spring-core' api 'org.springframework:spring-tx' + optional 'com.fasterxml.jackson.core:jackson-databind' optional 'ldapsdk:ldapsdk' optional "com.unboundid:unboundid-ldapsdk" optional "org.apache.directory.server:apacheds-core" @@ -34,6 +35,7 @@ dependencies { testImplementation "org.mockito:mockito-core" testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" + testImplementation 'org.skyscreamer:jsonassert' } integrationTest { diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java new file mode 100644 index 0000000000..bcf7dd4220 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; + +/** + * This is a Jackson mixin class helps in serialize/deserialize + * {@link org.springframework.security.ldap.userdetails.InetOrgPerson} class. To use this + * class you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new LdapJackson2Module());
+ * 
+ * + * Note: This class will save full class name into a property called @class + * + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class InetOrgPersonMixin { + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java new file mode 100644 index 0000000000..151500df82 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; + +/** + * This is a Jackson mixin class helps in serialize/deserialize + * {@link org.springframework.security.ldap.userdetails.LdapAuthority} class. To use this + * class you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new LdapJackson2Module());
+ * 
+ * + * Note: This class will save full class name into a property called @class + * + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class LdapAuthorityMixin { + + /** + * Constructor used by Jackson to create object of + * {@link org.springframework.security.ldap.userdetails.LdapAuthority}. + * @param role + * @param dn + * @param attributes + */ + @JsonCreator + LdapAuthorityMixin(@JsonProperty("role") String role, @JsonProperty("dn") String dn, + @JsonProperty("attributes") Map> attributes) { + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java new file mode 100644 index 0000000000..1362f76b00 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; +import org.springframework.security.ldap.userdetails.LdapAuthority; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; +import org.springframework.security.ldap.userdetails.Person; + +/** + * Jackson module for spring-security-ldap. This module registers + * {@link LdapAuthorityMixin}, {@link LdapUserDetailsImplMixin}, {@link PersonMixin}, + * {@link InetOrgPersonMixin}. If no default typing enabled by default then it'll enable + * it because typing info is needed to properly serialize/deserialize objects. In order to + * use this module just add this module into your ObjectMapper configuration. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new LdapJackson2Module());
+ * 
+ * + * Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list of all + * security modules. + * + * @see SecurityJackson2Modules + */ +public class LdapJackson2Module extends SimpleModule { + + public LdapJackson2Module() { + super(LdapJackson2Module.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void setupModule(SetupContext context) { + SecurityJackson2Modules.enableDefaultTyping(context.getOwner()); + context.setMixInAnnotations(LdapAuthority.class, LdapAuthorityMixin.class); + context.setMixInAnnotations(LdapUserDetailsImpl.class, LdapUserDetailsImplMixin.class); + context.setMixInAnnotations(Person.class, PersonMixin.class); + context.setMixInAnnotations(InetOrgPerson.class, InetOrgPersonMixin.class); + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java new file mode 100644 index 0000000000..ecf060ba49 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; + +/** + * This is a Jackson mixin class helps in serialize/deserialize + * {@link org.springframework.security.ldap.userdetails.LdapUserDetailsImpl} class. To use + * this class you need to register it with + * {@link com.fasterxml.jackson.databind.ObjectMapper}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new LdapJackson2Module());
+ * 
+ * + * Note: This class will save full class name into a property called @class + * + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class LdapUserDetailsImplMixin { + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java new file mode 100644 index 0000000000..c261c253a2 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson2; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson2.SecurityJackson2Modules; + +/** + * This is a Jackson mixin class helps in serialize/deserialize + * {@link org.springframework.security.ldap.userdetails.Person} class. To use this class + * you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new LdapJackson2Module());
+ * 
+ * + * Note: This class will save full class name into a property called @class + * + * @see LdapJackson2Module + * @see SecurityJackson2Modules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +abstract class PersonMixin { + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java new file mode 100644 index 0000000000..efd328d812 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.ldap.jackson2; + +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; +import org.springframework.security.ldap.userdetails.Person; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link InetOrgPersonMixin}. + */ +class InetOrgPersonMixinTests { + + private ObjectMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Disabled + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + InetOrgPerson.Essence essence = new InetOrgPerson.Essence(createUserContext()); + InetOrgPerson p = (InetOrgPerson) essence.createUserDetails(); + + String expectedJson = asJson(p); + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(expectedJson, json, true); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(new DistinguishedName("ignored=ignored")); + ctx.setAttributeValue("uid", "ghengis"); + ctx.setAttributeValue("userPassword", "pillage"); + ctx.setAttributeValue("carLicense", "HORS1"); + ctx.setAttributeValue("cn", "Ghengis Khan"); + ctx.setAttributeValue("description", "Scary"); + ctx.setAttributeValue("destinationIndicator", "West"); + ctx.setAttributeValue("displayName", "Ghengis McCann"); + ctx.setAttributeValue("givenName", "Ghengis"); + ctx.setAttributeValue("homePhone", "+467575436521"); + ctx.setAttributeValue("initials", "G"); + ctx.setAttributeValue("employeeNumber", "00001"); + ctx.setAttributeValue("homePostalAddress", "Steppes"); + ctx.setAttributeValue("mail", "ghengis@mongolia"); + ctx.setAttributeValue("mobile", "always"); + ctx.setAttributeValue("o", "Hordes"); + ctx.setAttributeValue("ou", "Horde1"); + ctx.setAttributeValue("postalAddress", "On the Move"); + ctx.setAttributeValue("postalCode", "Changes Frequently"); + ctx.setAttributeValue("roomNumber", "Yurt 1"); + ctx.setAttributeValue("roomNumber", "Yurt 1"); + ctx.setAttributeValue("sn", "Khan"); + ctx.setAttributeValue("street", "Westward Avenue"); + ctx.setAttributeValue("telephoneNumber", "+442075436521"); + return ctx; + } + + private String asJson(Person person) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.ldap.userdetails.InetOrgPerson\"\n" + + "}"; + // @formatter:on + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java new file mode 100644 index 0000000000..b2e1255cde --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.ldap.jackson2; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link LdapAuthorityMixin}. + */ +class LdapAuthorityMixinTests { + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java new file mode 100644 index 0000000000..70c8b81d02 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.ldap.jackson2; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link LdapUserDetailsImplMixin}. + */ +class LdapUserDetailsImplMixinTests { + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java new file mode 100644 index 0000000000..7040c73174 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.ldap.jackson2; + +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.Person; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link PersonMixin}. + */ +class PersonMixinTests { + + private ObjectMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Disabled + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + Person person = null; + String expectedJson = asJson(person); + String json = this.mapper.writeValueAsString(person); + JSONAssert.assertEquals(expectedJson, json, true); + } + + private String asJson(Person person) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.ldap.userdetails.Person\"\n" + + "}"; + // @formatter:on + } +} From bbeca7cd653e585cc06c1326a3409f4d85f05fc0 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Mon, 29 Nov 2021 17:48:04 +0100 Subject: [PATCH 053/589] Polish LDAP serialization Closes gh-9263 --- .../jackson2/SecurityJackson2Modules.java | 3 +- .../ldap/jackson2/InetOrgPersonMixin.java | 15 +- .../ldap/jackson2/LdapAuthorityMixin.java | 22 +-- .../ldap/jackson2/LdapJackson2Module.java | 13 +- .../jackson2/LdapUserDetailsImplMixin.java | 16 +- .../security/ldap/jackson2/PersonMixin.java | 15 +- .../jackson2/InetOrgPersonMixinTests.java | 151 +++++++++++++++--- .../jackson2/LdapAuthorityMixinTests.java | 25 --- .../LdapUserDetailsImplMixinTests.java | 105 +++++++++++- .../ldap/jackson2/PersonMixinTests.java | 115 ++++++++++--- 10 files changed, 352 insertions(+), 128 deletions(-) delete mode 100644 ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java diff --git a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java index ceaed240ab..7bd06f379d 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -209,6 +209,7 @@ public final class SecurityJackson2Modules { names.add("java.util.HashMap"); names.add("java.util.LinkedHashMap"); names.add("org.springframework.security.core.context.SecurityContextImpl"); + names.add("java.util.Arrays$ArrayList"); ALLOWLIST_CLASS_NAMES = Collections.unmodifiableSet(names); } diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java index bcf7dd4220..fca449114e 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixin.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,19 +21,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; /** - * This is a Jackson mixin class helps in serialize/deserialize - * {@link org.springframework.security.ldap.userdetails.InetOrgPerson} class. To use this - * class you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}. - * - *
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * 
- * - * Note: This class will save full class name into a property called @class + * This Jackson mixin is used to serialize/deserialize {@link InetOrgPerson}. * + * @since 5.7 * @see LdapJackson2Module * @see SecurityJackson2Modules */ diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java index 151500df82..85fe16f5fd 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixin.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,19 +26,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.LdapAuthority; /** - * This is a Jackson mixin class helps in serialize/deserialize - * {@link org.springframework.security.ldap.userdetails.LdapAuthority} class. To use this - * class you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}. - * - *
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * 
- * - * Note: This class will save full class name into a property called @class + * This Jackson mixin is used to serialize/deserialize {@link LdapAuthority}. * + * @since 5.7 * @see LdapJackson2Module * @see SecurityJackson2Modules */ @@ -47,13 +40,6 @@ import org.springframework.security.jackson2.SecurityJackson2Modules; @JsonIgnoreProperties(ignoreUnknown = true) abstract class LdapAuthorityMixin { - /** - * Constructor used by Jackson to create object of - * {@link org.springframework.security.ldap.userdetails.LdapAuthority}. - * @param role - * @param dn - * @param attributes - */ @JsonCreator LdapAuthorityMixin(@JsonProperty("role") String role, @JsonProperty("dn") String dn, @JsonProperty("attributes") Map> attributes) { diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java index 1362f76b00..62cb17a11a 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapJackson2Module.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,11 +26,13 @@ import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; import org.springframework.security.ldap.userdetails.Person; /** - * Jackson module for spring-security-ldap. This module registers + * Jackson module for {@code spring-security-ldap}. This module registers * {@link LdapAuthorityMixin}, {@link LdapUserDetailsImplMixin}, {@link PersonMixin}, - * {@link InetOrgPersonMixin}. If no default typing enabled by default then it'll enable - * it because typing info is needed to properly serialize/deserialize objects. In order to - * use this module just add this module into your ObjectMapper configuration. + * {@link InetOrgPersonMixin}. + * + * If not already enabled, default typing will be automatically enabled as type info is + * required to properly serialize/deserialize objects. In order to use this module just + * add it to your {@code ObjectMapper} configuration. * *
  *     ObjectMapper mapper = new ObjectMapper();
@@ -40,6 +42,7 @@ import org.springframework.security.ldap.userdetails.Person;
  * Note: use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get list of all
  * security modules.
  *
+ * @since 5.7
  * @see SecurityJackson2Modules
  */
 public class LdapJackson2Module extends SimpleModule {
diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java
index ecf060ba49..a441102e6b 100644
--- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java
+++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixin.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2020 the original author or authors.
+ * Copyright 2015-2021 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,20 +21,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
 import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl;
 
 /**
- * This is a Jackson mixin class helps in serialize/deserialize
- * {@link org.springframework.security.ldap.userdetails.LdapUserDetailsImpl} class. To use
- * this class you need to register it with
- * {@link com.fasterxml.jackson.databind.ObjectMapper}.
- *
- * 
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * 
- * - * Note: This class will save full class name into a property called @class + * This Jackson mixin is used to serialize/deserialize {@link LdapUserDetailsImpl}. * + * @since 5.7 * @see LdapJackson2Module * @see SecurityJackson2Modules */ diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java index c261c253a2..a3a0ddebc5 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/PersonMixin.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,19 +21,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.Person; /** - * This is a Jackson mixin class helps in serialize/deserialize - * {@link org.springframework.security.ldap.userdetails.Person} class. To use this class - * you need to register it with {@link com.fasterxml.jackson.databind.ObjectMapper}. - * - *
- *     ObjectMapper mapper = new ObjectMapper();
- *     mapper.registerModule(new LdapJackson2Module());
- * 
- * - * Note: This class will save full class name into a property called @class + * This Jackson mixin is used to serialize/deserialize {@link Person}. * + * @since 5.7 * @see LdapJackson2Module * @see SecurityJackson2Modules */ diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java index efd328d812..d9a05e6531 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/InetOrgPersonMixinTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,27 +13,74 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.ldap.jackson2; -import org.springframework.ldap.core.DirContextAdapter; -import org.springframework.ldap.core.DistinguishedName; -import org.springframework.security.jackson2.SecurityJackson2Modules; -import org.springframework.security.ldap.userdetails.InetOrgPerson; -import org.springframework.security.ldap.userdetails.Person; - +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; +import org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper; + import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link InetOrgPersonMixin}. */ -class InetOrgPersonMixinTests { +public class InetOrgPersonMixinTests { + + private static final String USER_PASSWORD = "Password1234"; + + private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + + // @formatter:off + private static final String INET_ORG_PERSON_JSON = "{\n" + + "\"@class\": \"org.springframework.security.ldap.userdetails.InetOrgPerson\"," + + "\"dn\": \"ignored=ignored\"," + + "\"uid\": \"ghengis\"," + + "\"username\": \"ghengis\"," + + "\"password\": \"" + USER_PASSWORD + "\"," + + "\"carLicense\": \"HORS1\"," + + "\"givenName\": \"Ghengis\"," + + "\"destinationIndicator\": \"West\"," + + "\"displayName\": \"Ghengis McCann\"," + + "\"givenName\": \"Ghengis\"," + + "\"homePhone\": \"+467575436521\"," + + "\"initials\": \"G\"," + + "\"employeeNumber\": \"00001\"," + + "\"homePostalAddress\": \"Steppes\"," + + "\"mail\": \"ghengis@mongolia\"," + + "\"mobile\": \"always\"," + + "\"o\": \"Hordes\"," + + "\"ou\": \"Horde1\"," + + "\"postalAddress\": \"On the Move\"," + + "\"postalCode\": \"Changes Frequently\"," + + "\"roomNumber\": \"Yurt 1\"," + + "\"sn\": \"Khan\"," + + "\"street\": \"Westward Avenue\"," + + "\"telephoneNumber\": \"+442075436521\"," + + "\"departmentNumber\": \"5679\"," + + "\"title\": \"T\"," + + "\"cn\": [\"java.util.Arrays$ArrayList\",[\"Ghengis Khan\"]]," + + "\"description\": \"Scary\"," + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + + "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + "," + + "\"timeBeforeExpiration\": " + Integer.MAX_VALUE + + "}"; + // @formatter:on private ObjectMapper mapper; @@ -44,22 +91,83 @@ class InetOrgPersonMixinTests { this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); } - @Disabled @Test public void serializeWhenMixinRegisteredThenSerializes() throws Exception { - InetOrgPerson.Essence essence = new InetOrgPerson.Essence(createUserContext()); - InetOrgPerson p = (InetOrgPerson) essence.createUserDetails(); + InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper(); + InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); - String expectedJson = asJson(p); String json = this.mapper.writeValueAsString(p); - JSONAssert.assertEquals(expectedJson, json, true); + JSONAssert.assertEquals(INET_ORG_PERSON_JSON, json, true); + } + + @Test + public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull() + throws JsonProcessingException, JSONException { + InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper(); + InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + p.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(INET_ORG_PERSON_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + assertThatExceptionOfType(JsonProcessingException.class) + .isThrownBy(() -> new ObjectMapper().readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper(); + InetOrgPerson expectedAuthentication = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + InetOrgPerson authentication = this.mapper.readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getCarLicense()).isEqualTo(expectedAuthentication.getCarLicense()); + assertThat(authentication.getDepartmentNumber()).isEqualTo(expectedAuthentication.getDepartmentNumber()); + assertThat(authentication.getDestinationIndicator()) + .isEqualTo(expectedAuthentication.getDestinationIndicator()); + assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn()); + assertThat(authentication.getDescription()).isEqualTo(expectedAuthentication.getDescription()); + assertThat(authentication.getDisplayName()).isEqualTo(expectedAuthentication.getDisplayName()); + assertThat(authentication.getUid()).isEqualTo(expectedAuthentication.getUid()); + assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername()); + assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword()); + assertThat(authentication.getHomePhone()).isEqualTo(expectedAuthentication.getHomePhone()); + assertThat(authentication.getEmployeeNumber()).isEqualTo(expectedAuthentication.getEmployeeNumber()); + assertThat(authentication.getHomePostalAddress()).isEqualTo(expectedAuthentication.getHomePostalAddress()); + assertThat(authentication.getInitials()).isEqualTo(expectedAuthentication.getInitials()); + assertThat(authentication.getMail()).isEqualTo(expectedAuthentication.getMail()); + assertThat(authentication.getMobile()).isEqualTo(expectedAuthentication.getMobile()); + assertThat(authentication.getO()).isEqualTo(expectedAuthentication.getO()); + assertThat(authentication.getOu()).isEqualTo(expectedAuthentication.getOu()); + assertThat(authentication.getPostalAddress()).isEqualTo(expectedAuthentication.getPostalAddress()); + assertThat(authentication.getPostalCode()).isEqualTo(expectedAuthentication.getPostalCode()); + assertThat(authentication.getRoomNumber()).isEqualTo(expectedAuthentication.getRoomNumber()); + assertThat(authentication.getStreet()).isEqualTo(expectedAuthentication.getStreet()); + assertThat(authentication.getSn()).isEqualTo(expectedAuthentication.getSn()); + assertThat(authentication.getTitle()).isEqualTo(expectedAuthentication.getTitle()); + assertThat(authentication.getGivenName()).isEqualTo(expectedAuthentication.getGivenName()); + assertThat(authentication.getTelephoneNumber()).isEqualTo(expectedAuthentication.getTelephoneNumber()); + assertThat(authentication.getGraceLoginsRemaining()) + .isEqualTo(expectedAuthentication.getGraceLoginsRemaining()); + assertThat(authentication.getTimeBeforeExpiration()) + .isEqualTo(expectedAuthentication.getTimeBeforeExpiration()); + assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired()); + assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked()); + assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled()); + assertThat(authentication.isCredentialsNonExpired()) + .isEqualTo(expectedAuthentication.isCredentialsNonExpired()); } private DirContextAdapter createUserContext() { DirContextAdapter ctx = new DirContextAdapter(); ctx.setDn(new DistinguishedName("ignored=ignored")); ctx.setAttributeValue("uid", "ghengis"); - ctx.setAttributeValue("userPassword", "pillage"); + ctx.setAttributeValue("userPassword", USER_PASSWORD); ctx.setAttributeValue("carLicense", "HORS1"); ctx.setAttributeValue("cn", "Ghengis Khan"); ctx.setAttributeValue("description", "Scary"); @@ -77,19 +185,12 @@ class InetOrgPersonMixinTests { ctx.setAttributeValue("postalAddress", "On the Move"); ctx.setAttributeValue("postalCode", "Changes Frequently"); ctx.setAttributeValue("roomNumber", "Yurt 1"); - ctx.setAttributeValue("roomNumber", "Yurt 1"); ctx.setAttributeValue("sn", "Khan"); ctx.setAttributeValue("street", "Westward Avenue"); ctx.setAttributeValue("telephoneNumber", "+442075436521"); + ctx.setAttributeValue("departmentNumber", "5679"); + ctx.setAttributeValue("title", "T"); return ctx; } - private String asJson(Person person) { - // @formatter:off - return "{\n" + - " \"@class\": \"org.springframework.security.ldap.userdetails.InetOrgPerson\"\n" + - "}"; - // @formatter:on - } - } diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java deleted file mode 100644 index b2e1255cde..0000000000 --- a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapAuthorityMixinTests.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2002-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.ldap.jackson2; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Tests for {@link LdapAuthorityMixin}. - */ -class LdapAuthorityMixinTests { - -} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java index 70c8b81d02..755623ba8f 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/LdapUserDetailsImplMixinTests.java @@ -13,13 +13,114 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.ldap.jackson2; -import static org.junit.jupiter.api.Assertions.*; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; +import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link LdapUserDetailsImplMixin}. */ -class LdapUserDetailsImplMixinTests { +public class LdapUserDetailsImplMixinTests { + + private static final String USER_PASSWORD = "Password1234"; + + private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + + // @formatter:off + private static final String USER_JSON = "{" + + "\"@class\": \"org.springframework.security.ldap.userdetails.LdapUserDetailsImpl\", " + + "\"dn\": \"ignored=ignored\"," + + "\"username\": \"ghengis\"," + + "\"password\": \"" + USER_PASSWORD + "\"," + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + + "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + "," + + "\"timeBeforeExpiration\": " + Integer.MAX_VALUE + + "}"; + // @formatter:on + + private ObjectMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = new ObjectMapper(); + this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); + LdapUserDetailsImpl p = (LdapUserDetailsImpl) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(USER_JSON, json, true); + } + + @Test + public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull() + throws JsonProcessingException, JSONException { + LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); + LdapUserDetailsImpl p = (LdapUserDetailsImpl) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + p.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(USER_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + assertThatExceptionOfType(JsonProcessingException.class) + .isThrownBy(() -> new ObjectMapper().readValue(USER_JSON, LdapUserDetailsImpl.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); + LdapUserDetailsImpl expectedAuthentication = (LdapUserDetailsImpl) mapper + .mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES); + + LdapUserDetailsImpl authentication = this.mapper.readValue(USER_JSON, LdapUserDetailsImpl.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn()); + assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername()); + assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword()); + assertThat(authentication.getGraceLoginsRemaining()) + .isEqualTo(expectedAuthentication.getGraceLoginsRemaining()); + assertThat(authentication.getTimeBeforeExpiration()) + .isEqualTo(expectedAuthentication.getTimeBeforeExpiration()); + assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired()); + assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked()); + assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled()); + assertThat(authentication.isCredentialsNonExpired()) + .isEqualTo(expectedAuthentication.isCredentialsNonExpired()); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(new DistinguishedName("ignored=ignored")); + ctx.setAttributeValue("userPassword", USER_PASSWORD); + return ctx; + } } diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java index 7040c73174..018058888e 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson2/PersonMixinTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,23 +13,55 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.security.ldap.jackson2; -import org.springframework.security.jackson2.SecurityJackson2Modules; -import org.springframework.security.ldap.userdetails.Person; - +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; -import static org.junit.jupiter.api.Assertions.*; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DistinguishedName; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.ldap.userdetails.Person; +import org.springframework.security.ldap.userdetails.PersonContextMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link PersonMixin}. */ -class PersonMixinTests { +public class PersonMixinTests { + + private static final String USER_PASSWORD = "Password1234"; + + private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + + // @formatter:off + private static final String PERSON_JSON = "{" + + "\"@class\": \"org.springframework.security.ldap.userdetails.Person\", " + + "\"dn\": \"ignored=ignored\"," + + "\"username\": \"ghengis\"," + + "\"password\": \"" + USER_PASSWORD + "\"," + + "\"givenName\": \"Ghengis\"," + + "\"sn\": \"Khan\"," + + "\"cn\": [\"java.util.Arrays$ArrayList\",[\"Ghengis Khan\"]]," + + "\"description\": \"Scary\"," + + "\"telephoneNumber\": \"+442075436521\"," + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + + "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + "," + + "\"timeBeforeExpiration\": " + Integer.MAX_VALUE + + "}"; + // @formatter:on private ObjectMapper mapper; @@ -40,20 +72,67 @@ class PersonMixinTests { this.mapper.registerModules(SecurityJackson2Modules.getModules(loader)); } - @Disabled @Test public void serializeWhenMixinRegisteredThenSerializes() throws Exception { - Person person = null; - String expectedJson = asJson(person); - String json = this.mapper.writeValueAsString(person); - JSONAssert.assertEquals(expectedJson, json, true); + PersonContextMapper mapper = new PersonContextMapper(); + Person p = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES); + + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(PERSON_JSON, json, true); } - private String asJson(Person person) { - // @formatter:off - return "{\n" + - " \"@class\": \"org.springframework.security.ldap.userdetails.Person\"\n" + - "}"; - // @formatter:on + @Test + public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull() + throws JsonProcessingException, JSONException { + PersonContextMapper mapper = new PersonContextMapper(); + Person p = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES); + p.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(PERSON_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true); } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + assertThatExceptionOfType(JsonProcessingException.class) + .isThrownBy(() -> new ObjectMapper().readValue(PERSON_JSON, Person.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + PersonContextMapper mapper = new PersonContextMapper(); + Person expectedAuthentication = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + Person authentication = this.mapper.readValue(PERSON_JSON, Person.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn()); + assertThat(authentication.getDescription()).isEqualTo(expectedAuthentication.getDescription()); + assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername()); + assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword()); + assertThat(authentication.getSn()).isEqualTo(expectedAuthentication.getSn()); + assertThat(authentication.getGivenName()).isEqualTo(expectedAuthentication.getGivenName()); + assertThat(authentication.getTelephoneNumber()).isEqualTo(expectedAuthentication.getTelephoneNumber()); + assertThat(authentication.getGraceLoginsRemaining()) + .isEqualTo(expectedAuthentication.getGraceLoginsRemaining()); + assertThat(authentication.getTimeBeforeExpiration()) + .isEqualTo(expectedAuthentication.getTimeBeforeExpiration()); + assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired()); + assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked()); + assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled()); + assertThat(authentication.isCredentialsNonExpired()) + .isEqualTo(expectedAuthentication.isCredentialsNonExpired()); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(new DistinguishedName("ignored=ignored")); + ctx.setAttributeValue("userPassword", USER_PASSWORD); + ctx.setAttributeValue("cn", "Ghengis Khan"); + ctx.setAttributeValue("description", "Scary"); + ctx.setAttributeValue("givenName", "Ghengis"); + ctx.setAttributeValue("sn", "Khan"); + ctx.setAttributeValue("telephoneNumber", "+442075436521"); + return ctx; + } + } From dec858a5b76d214ad1d586a759192a09674bc5d8 Mon Sep 17 00:00:00 2001 From: Jonas Erbe Date: Mon, 22 Nov 2021 19:47:01 +0100 Subject: [PATCH 054/589] Fix JwtClaimValidator wrong error code Previously JwtClaimValidator returned the invalid_request error on claim validation failure. But validators have to return invalid_token errors on failure according to: https://datatracker.ietf.org/doc/html/rfc6750#section-3.1. Also see gh-10337 Closes gh-10337 --- .../security/oauth2/jwt/JwtClaimValidator.java | 4 ++-- .../security/oauth2/jwt/JwtClaimValidatorTests.java | 8 +++++++- .../security/oauth2/jwt/JwtTimestampValidatorTests.java | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java index 73c13c7dc2..0202815cc2 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtClaimValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ public final class JwtClaimValidator implements OAuth2TokenValidator { Assert.notNull(test, "test can not be null"); this.claim = claim; this.test = test; - this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "The " + this.claim + " claim is not valid", + this.error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "The " + this.claim + " claim is not valid", "https://tools.ietf.org/html/rfc6750#section-3.1"); } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java index 430f707892..a43989c868 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtClaimValidatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ package org.springframework.security.oauth2.jwt; +import java.util.Collection; +import java.util.Objects; import java.util.function.Predicate; import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import static org.assertj.core.api.Assertions.assertThat; @@ -45,7 +49,9 @@ public class JwtClaimValidatorTests { @Test public void validateWhenClaimFailsTheTestThenReturnsFailure() { Jwt jwt = TestJwts.jwt().claim(JwtClaimNames.ISS, "http://abc").build(); + Collection details = this.validator.validate(jwt).getErrors(); assertThat(this.validator.validate(jwt).getErrors().isEmpty()).isFalse(); + assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN)); } @Test diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java index 7f8a093ad3..72164cf21b 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.time.ZoneId; import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -64,6 +65,7 @@ public class JwtTimestampValidatorTests { .collect(Collectors.toList()); // @formatter:on assertThat(messages).contains("Jwt expired at " + oneHourAgo); + assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN)); } @Test @@ -78,6 +80,7 @@ public class JwtTimestampValidatorTests { .collect(Collectors.toList()); // @formatter:on assertThat(messages).contains("Jwt used before " + oneHourFromNow); + assertThat(details).allMatch((error) -> Objects.equals(error.getErrorCode(), OAuth2ErrorCodes.INVALID_TOKEN)); } @Test From 43317c5a616a5599775fc2bd63aea7af175d8b00 Mon Sep 17 00:00:00 2001 From: Guirong Hu Date: Mon, 28 Jun 2021 00:46:00 +0800 Subject: [PATCH 055/589] Support IP whitelist for Spring Security Webflux Closes gh-7765 --- .../config/web/server/ServerHttpSecurity.java | 12 ++ ...IpAddressReactiveAuthorizationManager.java | 59 ++++++++ .../IpAddressServerWebExchangeMatcher.java | 63 +++++++++ ...ressReactiveAuthorizationManagerTests.java | 75 +++++++++++ ...pAddressServerWebExchangeMatcherTests.java | 126 ++++++++++++++++++ 5 files changed, 335 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManager.java create mode 100644 web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java create mode 100644 web/src/test/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManagerTests.java create mode 100644 web/src/test/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcherTests.java diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 282896dd83..a5f6cc3548 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -133,6 +133,7 @@ import org.springframework.security.web.server.authorization.AuthorizationContex import org.springframework.security.web.server.authorization.AuthorizationWebFilter; import org.springframework.security.web.server.authorization.DelegatingReactiveAuthorizationManager; import org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter; +import org.springframework.security.web.server.authorization.IpAddressReactiveAuthorizationManager; import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; import org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler; import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; @@ -1682,6 +1683,17 @@ public class ServerHttpSecurity { return access(AuthenticatedReactiveAuthorizationManager.authenticated()); } + /** + * Require a specific IP address or range using an IP/Netmask (e.g. + * 192.168.1.0/24). + * @param ipAddress the address or range of addresses from which the request + * must come. + * @return the {@link AuthorizeExchangeSpec} to configure + */ + public AuthorizeExchangeSpec hasIpAddress(String ipAddress) { + return access(IpAddressReactiveAuthorizationManager.hasIpAddress(ipAddress)); + } + /** * Allows plugging in a custom authorization strategy * @param manager the authorization manager to use diff --git a/web/src/main/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManager.java new file mode 100644 index 0000000000..5b814a5276 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManager.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authorization; + +import reactor.core.publisher.Mono; + +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.ReactiveAuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.util.matcher.IpAddressServerWebExchangeMatcher; +import org.springframework.util.Assert; + +/** + * A {@link ReactiveAuthorizationManager}, that determines if the current request contains + * the specified address or range of addresses + * + * @author Guirong Hu + * @since 5.7 + */ +public final class IpAddressReactiveAuthorizationManager implements ReactiveAuthorizationManager { + + private final IpAddressServerWebExchangeMatcher ipAddressExchangeMatcher; + + IpAddressReactiveAuthorizationManager(String ipAddress) { + this.ipAddressExchangeMatcher = new IpAddressServerWebExchangeMatcher(ipAddress); + } + + @Override + public Mono check(Mono authentication, AuthorizationContext context) { + return Mono.just(context.getExchange()).flatMap(this.ipAddressExchangeMatcher::matches) + .map((matchResult) -> new AuthorizationDecision(matchResult.isMatch())); + } + + /** + * Creates an instance of {@link IpAddressReactiveAuthorizationManager} with the + * provided IP address. + * @param ipAddress the address or range of addresses from which the request must + * @return the new instance + */ + public static IpAddressReactiveAuthorizationManager hasIpAddress(String ipAddress) { + Assert.notNull(ipAddress, "This IP address is required; it must not be null"); + return new IpAddressReactiveAuthorizationManager(ipAddress); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java b/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java new file mode 100644 index 0000000000..29354ac3b6 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.util.matcher; + +import reactor.core.publisher.Mono; + +import org.springframework.security.web.util.matcher.IpAddressMatcher; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Matches a request based on IP Address or subnet mask matching against the remote + * address. + * + * @author Guirong Hu + * @since 5.7 + */ +public class IpAddressServerWebExchangeMatcher implements ServerWebExchangeMatcher { + + private final IpAddressMatcher ipAddressMatcher; + + /** + * Takes a specific IP address or a range specified using the IP/Netmask (e.g. + * 192.168.1.0/24 or 202.24.0.0/14). + * @param ipAddress the address or range of addresses from which the request must + * come. + */ + public IpAddressServerWebExchangeMatcher(String ipAddress) { + Assert.hasText(ipAddress, "IP address cannot be empty"); + this.ipAddressMatcher = new IpAddressMatcher(ipAddress); + } + + @Override + public Mono matches(ServerWebExchange exchange) { + // @formatter:off + return Mono.justOrEmpty(exchange.getRequest().getRemoteAddress()) + .map((remoteAddress) -> remoteAddress.getAddress().getHostAddress()) + .map(this.ipAddressMatcher::matches) + .flatMap((matches) -> matches ? MatchResult.match() : MatchResult.notMatch()) + .switchIfEmpty(MatchResult.notMatch()); + // @formatter:on + } + + @Override + public String toString() { + return "IpAddressServerWebExchangeMatcher{ipAddressMatcher=" + this.ipAddressMatcher + '}'; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManagerTests.java new file mode 100644 index 0000000000..5b42423e2c --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authorization/IpAddressReactiveAuthorizationManagerTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.authorization; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IpAddressReactiveAuthorizationManager} + * + * @author Guirong Hu + */ +public class IpAddressReactiveAuthorizationManagerTests { + + @Test + public void checkWhenHasIpv6AddressThenReturnTrue() throws UnknownHostException { + IpAddressReactiveAuthorizationManager v6manager = IpAddressReactiveAuthorizationManager + .hasIpAddress("fe80::21f:5bff:fe33:bd68"); + boolean granted = v6manager.check(null, context("fe80::21f:5bff:fe33:bd68")).block().isGranted(); + assertThat(granted).isTrue(); + } + + @Test + public void checkWhenHasIpv6AddressThenReturnFalse() throws UnknownHostException { + IpAddressReactiveAuthorizationManager v6manager = IpAddressReactiveAuthorizationManager + .hasIpAddress("fe80::21f:5bff:fe33:bd68"); + boolean granted = v6manager.check(null, context("fe80::1c9a:7cfd:29a8:a91e")).block().isGranted(); + assertThat(granted).isFalse(); + } + + @Test + public void checkWhenHasIpv4AddressThenReturnTrue() throws UnknownHostException { + IpAddressReactiveAuthorizationManager v4manager = IpAddressReactiveAuthorizationManager + .hasIpAddress("192.168.1.104"); + boolean granted = v4manager.check(null, context("192.168.1.104")).block().isGranted(); + assertThat(granted).isTrue(); + } + + @Test + public void checkWhenHasIpv4AddressThenReturnFalse() throws UnknownHostException { + IpAddressReactiveAuthorizationManager v4manager = IpAddressReactiveAuthorizationManager + .hasIpAddress("192.168.1.104"); + boolean granted = v4manager.check(null, context("192.168.100.15")).block().isGranted(); + assertThat(granted).isFalse(); + } + + private static AuthorizationContext context(String ipAddress) throws UnknownHostException { + MockServerWebExchange exchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/") + .remoteAddress(new InetSocketAddress(InetAddress.getByName(ipAddress), 8080))).build(); + return new AuthorizationContext(exchange); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcherTests.java b/web/src/test/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcherTests.java new file mode 100644 index 0000000000..3c26dfdfd9 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcherTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.util.matcher; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link IpAddressServerWebExchangeMatcher} + * + * @author Guirong Hu + */ +@ExtendWith(MockitoExtension.class) +public class IpAddressServerWebExchangeMatcherTests { + + @Test + public void matchesWhenIpv6RangeAndIpv6AddressThenTrue() throws UnknownHostException { + ServerWebExchange ipv6Exchange = exchange("fe80::21f:5bff:fe33:bd68"); + ServerWebExchangeMatcher.MatchResult matches = new IpAddressServerWebExchangeMatcher("fe80::21f:5bff:fe33:bd68") + .matches(ipv6Exchange).block(); + assertThat(matches.isMatch()).isTrue(); + } + + @Test + public void matchesWhenIpv6RangeAndIpv4AddressThenFalse() throws UnknownHostException { + ServerWebExchange ipv4Exchange = exchange("192.168.1.104"); + ServerWebExchangeMatcher.MatchResult matches = new IpAddressServerWebExchangeMatcher("fe80::21f:5bff:fe33:bd68") + .matches(ipv4Exchange).block(); + assertThat(matches.isMatch()).isFalse(); + } + + @Test + public void matchesWhenIpv4RangeAndIpv4AddressThenTrue() throws UnknownHostException { + ServerWebExchange ipv4Exchange = exchange("192.168.1.104"); + ServerWebExchangeMatcher.MatchResult matches = new IpAddressServerWebExchangeMatcher("192.168.1.104") + .matches(ipv4Exchange).block(); + assertThat(matches.isMatch()).isTrue(); + } + + @Test + public void matchesWhenIpv4SubnetAndIpv4AddressThenTrue() throws UnknownHostException { + ServerWebExchange ipv4Exchange = exchange("192.168.1.104"); + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("192.168.1.0/24"); + assertThat(matcher.matches(ipv4Exchange).block().isMatch()).isTrue(); + } + + @Test + public void matchesWhenIpv4SubnetAndIpv4AddressThenFalse() throws UnknownHostException { + ServerWebExchange ipv4Exchange = exchange("192.168.1.104"); + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("192.168.1.128/25"); + assertThat(matcher.matches(ipv4Exchange).block().isMatch()).isFalse(); + } + + @Test + public void matchesWhenIpv6SubnetAndIpv6AddressThenTrue() throws UnknownHostException { + ServerWebExchange ipv6Exchange = exchange("2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF"); + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("2001:DB8::/48"); + assertThat(matcher.matches(ipv6Exchange).block().isMatch()).isTrue(); + } + + @Test + public void matchesWhenIpv6SubnetAndIpv6AddressThenFalse() throws UnknownHostException { + ServerWebExchange ipv6Exchange = exchange("2001:DB8:1:0:0:0:0:0"); + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("2001:DB8::/48"); + assertThat(matcher.matches(ipv6Exchange).block().isMatch()).isFalse(); + } + + @Test + public void matchesWhenZeroMaskAndAnythingThenTrue() throws UnknownHostException { + IpAddressServerWebExchangeMatcher matcher = new IpAddressServerWebExchangeMatcher("0.0.0.0/0"); + assertThat(matcher.matches(exchange("123.4.5.6")).block().isMatch()).isTrue(); + assertThat(matcher.matches(exchange("192.168.0.159")).block().isMatch()).isTrue(); + matcher = new IpAddressServerWebExchangeMatcher("192.168.0.159/0"); + assertThat(matcher.matches(exchange("123.4.5.6")).block().isMatch()).isTrue(); + assertThat(matcher.matches(exchange("192.168.0.159")).block().isMatch()).isTrue(); + } + + @Test + public void constructorWhenIpv4AddressMaskTooLongThenIllegalArgumentException() { + String ipv4AddressWithTooLongMask = "192.168.1.104/33"; + assertThatIllegalArgumentException() + .isThrownBy(() -> new IpAddressServerWebExchangeMatcher(ipv4AddressWithTooLongMask)) + .withMessage(String.format("IP address %s is too short for bitmask of length %d", "192.168.1.104", 33)); + } + + @Test + public void constructorWhenIpv6AddressMaskTooLongThenIllegalArgumentException() { + String ipv6AddressWithTooLongMask = "fe80::21f:5bff:fe33:bd68/129"; + assertThatIllegalArgumentException() + .isThrownBy(() -> new IpAddressServerWebExchangeMatcher(ipv6AddressWithTooLongMask)) + .withMessage(String.format("IP address %s is too short for bitmask of length %d", + "fe80::21f:5bff:fe33:bd68", 129)); + } + + private static ServerWebExchange exchange(String ipAddress) throws UnknownHostException { + return MockServerWebExchange.builder(MockServerHttpRequest.get("/") + .remoteAddress(new InetSocketAddress(InetAddress.getByName(ipAddress), 8080))).build(); + } + +} From 204f0b45992a13b4b7f86cc7d9aa2b5ee127bdc6 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Tue, 30 Nov 2021 10:17:53 -0600 Subject: [PATCH 056/589] Polish gh-10007 --- .../server/util/matcher/IpAddressServerWebExchangeMatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java b/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java index 29354ac3b6..5bf439a5e0 100644 --- a/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java +++ b/web/src/main/java/org/springframework/security/web/server/util/matcher/IpAddressServerWebExchangeMatcher.java @@ -29,7 +29,7 @@ import org.springframework.web.server.ServerWebExchange; * @author Guirong Hu * @since 5.7 */ -public class IpAddressServerWebExchangeMatcher implements ServerWebExchangeMatcher { +public final class IpAddressServerWebExchangeMatcher implements ServerWebExchangeMatcher { private final IpAddressMatcher ipAddressMatcher; From 23e895f0b1b9d73cc725ea9eb60ac2b0a03abe5c Mon Sep 17 00:00:00 2001 From: Jonas Dittrich Date: Thu, 15 Jul 2021 14:03:05 +0200 Subject: [PATCH 057/589] Add ObjectIdentityGenerator customization to JdbcAclService Providing the possibility to change, how ObjectIdentitys are created inside the BasicLookupStrategy,JdbcAclService There was a problem with hard coded object identity creation inside the BasicLookupStrategy and the JdbcAclService. It was overkill to overwrite these classes only for changing this, so introducing an ObjectIdentityGenerator seems the be the better solution here. At default, the standard ObjectIdentityRetrievalStrategyImpl is used, but can be customized due to setters. Closes gh-10079 --- .../acls/jdbc/BasicLookupStrategy.java | 19 +- .../security/acls/jdbc/JdbcAclService.java | 204 +++++++++--------- 2 files changed, 121 insertions(+), 102 deletions(-) diff --git a/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java b/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java index 9d4d099b25..f9a0eb4ef6 100644 --- a/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java +++ b/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java @@ -35,6 +35,7 @@ import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.security.acls.domain.*; import org.springframework.security.acls.domain.AccessControlEntryImpl; import org.springframework.security.acls.domain.AclAuthorizationStrategy; import org.springframework.security.acls.domain.AclImpl; @@ -42,7 +43,6 @@ import org.springframework.security.acls.domain.AuditLogger; import org.springframework.security.acls.domain.DefaultPermissionFactory; import org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy; import org.springframework.security.acls.domain.GrantedAuthoritySid; -import org.springframework.security.acls.domain.ObjectIdentityImpl; import org.springframework.security.acls.domain.PermissionFactory; import org.springframework.security.acls.domain.PrincipalSid; import org.springframework.security.acls.model.AccessControlEntry; @@ -51,6 +51,7 @@ import org.springframework.security.acls.model.AclCache; import org.springframework.security.acls.model.MutableAcl; import org.springframework.security.acls.model.NotFoundException; import org.springframework.security.acls.model.ObjectIdentity; +import org.springframework.security.acls.model.ObjectIdentityGenerator; import org.springframework.security.acls.model.Permission; import org.springframework.security.acls.model.PermissionGrantingStrategy; import org.springframework.security.acls.model.Sid; @@ -109,6 +110,8 @@ public class BasicLookupStrategy implements LookupStrategy { private final AclAuthorizationStrategy aclAuthorizationStrategy; + private ObjectIdentityGenerator objectIdentityGenerator; + private PermissionFactory permissionFactory = new DefaultPermissionFactory(); private final AclCache aclCache; @@ -134,6 +137,7 @@ public class BasicLookupStrategy implements LookupStrategy { private AclClassIdUtils aclClassIdUtils; + /** * Constructor accepting mandatory arguments * @param dataSource to access the database @@ -152,8 +156,9 @@ public class BasicLookupStrategy implements LookupStrategy { * @param aclAuthorizationStrategy authorization strategy (required) * @param grantingStrategy the PermissionGrantingStrategy */ - public BasicLookupStrategy(DataSource dataSource, AclCache aclCache, - AclAuthorizationStrategy aclAuthorizationStrategy, PermissionGrantingStrategy grantingStrategy) { + public BasicLookupStrategy(DataSource dataSource, AclCache aclCache, + AclAuthorizationStrategy aclAuthorizationStrategy, PermissionGrantingStrategy grantingStrategy) { + Assert.notNull(dataSource, "DataSource required"); Assert.notNull(aclCache, "AclCache required"); Assert.notNull(aclAuthorizationStrategy, "AclAuthorizationStrategy required"); @@ -162,6 +167,7 @@ public class BasicLookupStrategy implements LookupStrategy { this.aclCache = aclCache; this.aclAuthorizationStrategy = aclAuthorizationStrategy; this.grantingStrategy = grantingStrategy; + this.objectIdentityGenerator = new ObjectIdentityRetrievalStrategyImpl(); this.aclClassIdUtils = new AclClassIdUtils(); this.fieldAces.setAccessible(true); this.fieldAcl.setAccessible(true); @@ -488,6 +494,11 @@ public class BasicLookupStrategy implements LookupStrategy { } } + public void setObjectIdentityGenerator(ObjectIdentityGenerator objectIdentityGenerator) { + Assert.notNull(objectIdentityGenerator,"The provided strategy has to be not null!"); + this.objectIdentityGenerator = objectIdentityGenerator; + } + public final void setConversionService(ConversionService conversionService) { this.aclClassIdUtils = new AclClassIdUtils(conversionService); } @@ -569,7 +580,7 @@ public class BasicLookupStrategy implements LookupStrategy { // target id type, e.g. UUID. Serializable identifier = (Serializable) rs.getObject("object_id_identity"); identifier = BasicLookupStrategy.this.aclClassIdUtils.identifierFrom(identifier, rs); - ObjectIdentity objectIdentity = new ObjectIdentityImpl(rs.getString("class"), identifier); + ObjectIdentity objectIdentity = objectIdentityGenerator.createObjectIdentity(identifier,rs.getString("class")); Acl parentAcl = null; long parentAclId = rs.getLong("parent_object"); diff --git a/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java b/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java index 935466f5d1..f1cbc51609 100644 --- a/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java +++ b/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java @@ -31,11 +31,12 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.convert.ConversionService; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.domain.ObjectIdentityRetrievalStrategyImpl; import org.springframework.security.acls.model.Acl; import org.springframework.security.acls.model.AclService; import org.springframework.security.acls.model.NotFoundException; import org.springframework.security.acls.model.ObjectIdentity; +import org.springframework.security.acls.model.ObjectIdentityGenerator; import org.springframework.security.acls.model.Sid; import org.springframework.util.Assert; @@ -50,123 +51,130 @@ import org.springframework.util.Assert; */ public class JdbcAclService implements AclService { - protected static final Log log = LogFactory.getLog(JdbcAclService.class); + protected static final Log log = LogFactory.getLog(JdbcAclService.class); - private static final String DEFAULT_SELECT_ACL_CLASS_COLUMNS = "class.class as class"; + private static final String DEFAULT_SELECT_ACL_CLASS_COLUMNS = "class.class as class"; - private static final String DEFAULT_SELECT_ACL_CLASS_COLUMNS_WITH_ID_TYPE = DEFAULT_SELECT_ACL_CLASS_COLUMNS - + ", class.class_id_type as class_id_type"; + private static final String DEFAULT_SELECT_ACL_CLASS_COLUMNS_WITH_ID_TYPE = DEFAULT_SELECT_ACL_CLASS_COLUMNS + + ", class.class_id_type as class_id_type"; - private static final String DEFAULT_SELECT_ACL_WITH_PARENT_SQL = "select obj.object_id_identity as obj_id, " - + DEFAULT_SELECT_ACL_CLASS_COLUMNS - + " from acl_object_identity obj, acl_object_identity parent, acl_class class " - + "where obj.parent_object = parent.id and obj.object_id_class = class.id " - + "and parent.object_id_identity = ? and parent.object_id_class = (" - + "select id FROM acl_class where acl_class.class = ?)"; + private static final String DEFAULT_SELECT_ACL_WITH_PARENT_SQL = "select obj.object_id_identity as obj_id, " + + DEFAULT_SELECT_ACL_CLASS_COLUMNS + + " from acl_object_identity obj, acl_object_identity parent, acl_class class " + + "where obj.parent_object = parent.id and obj.object_id_class = class.id " + + "and parent.object_id_identity = ? and parent.object_id_class = (" + + "select id FROM acl_class where acl_class.class = ?)"; - private static final String DEFAULT_SELECT_ACL_WITH_PARENT_SQL_WITH_CLASS_ID_TYPE = "select obj.object_id_identity as obj_id, " - + DEFAULT_SELECT_ACL_CLASS_COLUMNS_WITH_ID_TYPE - + " from acl_object_identity obj, acl_object_identity parent, acl_class class " - + "where obj.parent_object = parent.id and obj.object_id_class = class.id " - + "and parent.object_id_identity = ? and parent.object_id_class = (" - + "select id FROM acl_class where acl_class.class = ?)"; + private static final String DEFAULT_SELECT_ACL_WITH_PARENT_SQL_WITH_CLASS_ID_TYPE = "select obj.object_id_identity as obj_id, " + + DEFAULT_SELECT_ACL_CLASS_COLUMNS_WITH_ID_TYPE + + " from acl_object_identity obj, acl_object_identity parent, acl_class class " + + "where obj.parent_object = parent.id and obj.object_id_class = class.id " + + "and parent.object_id_identity = ? and parent.object_id_class = (" + + "select id FROM acl_class where acl_class.class = ?)"; - protected final JdbcOperations jdbcOperations; + protected final JdbcOperations jdbcOperations; - private final LookupStrategy lookupStrategy; + private final LookupStrategy lookupStrategy; - private boolean aclClassIdSupported; + private boolean aclClassIdSupported; - private String findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL; + private String findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL; - private AclClassIdUtils aclClassIdUtils; + private AclClassIdUtils aclClassIdUtils; + private ObjectIdentityGenerator objectIdentityGenerator; - public JdbcAclService(DataSource dataSource, LookupStrategy lookupStrategy) { - this(new JdbcTemplate(dataSource), lookupStrategy); - } + public JdbcAclService(DataSource dataSource, LookupStrategy lookupStrategy) { + this(new JdbcTemplate(dataSource), lookupStrategy); + } - public JdbcAclService(JdbcOperations jdbcOperations, LookupStrategy lookupStrategy) { - Assert.notNull(jdbcOperations, "JdbcOperations required"); - Assert.notNull(lookupStrategy, "LookupStrategy required"); - this.jdbcOperations = jdbcOperations; - this.lookupStrategy = lookupStrategy; - this.aclClassIdUtils = new AclClassIdUtils(); - } + public JdbcAclService(JdbcOperations jdbcOperations, LookupStrategy lookupStrategy) { + Assert.notNull(jdbcOperations, "JdbcOperations required"); + Assert.notNull(lookupStrategy, "LookupStrategy required"); + this.jdbcOperations = jdbcOperations; + this.lookupStrategy = lookupStrategy; + this.objectIdentityGenerator = new ObjectIdentityRetrievalStrategyImpl(); + this.aclClassIdUtils = new AclClassIdUtils(); + } - @Override - public List findChildren(ObjectIdentity parentIdentity) { - Object[] args = { parentIdentity.getIdentifier().toString(), parentIdentity.getType() }; - List objects = this.jdbcOperations.query(this.findChildrenSql, args, - (rs, rowNum) -> mapObjectIdentityRow(rs)); - return (!objects.isEmpty()) ? objects : null; - } + @Override + public List findChildren(ObjectIdentity parentIdentity) { + Object[] args = {parentIdentity.getIdentifier().toString(), parentIdentity.getType()}; + List objects = this.jdbcOperations.query(this.findChildrenSql, args, + (rs, rowNum) -> mapObjectIdentityRow(rs)); + return (!objects.isEmpty()) ? objects : null; + } - private ObjectIdentity mapObjectIdentityRow(ResultSet rs) throws SQLException { - String javaType = rs.getString("class"); - Serializable identifier = (Serializable) rs.getObject("obj_id"); - identifier = this.aclClassIdUtils.identifierFrom(identifier, rs); - return new ObjectIdentityImpl(javaType, identifier); - } + private ObjectIdentity mapObjectIdentityRow(ResultSet rs) throws SQLException { + String javaType = rs.getString("class"); + Serializable identifier = (Serializable) rs.getObject("obj_id"); + identifier = this.aclClassIdUtils.identifierFrom(identifier, rs); + return objectIdentityGenerator.createObjectIdentity(identifier, javaType); + } - @Override - public Acl readAclById(ObjectIdentity object, List sids) throws NotFoundException { - Map map = readAclsById(Collections.singletonList(object), sids); - Assert.isTrue(map.containsKey(object), - () -> "There should have been an Acl entry for ObjectIdentity " + object); - return map.get(object); - } + @Override + public Acl readAclById(ObjectIdentity object, List sids) throws NotFoundException { + Map map = readAclsById(Collections.singletonList(object), sids); + Assert.isTrue(map.containsKey(object), + () -> "There should have been an Acl entry for ObjectIdentity " + object); + return map.get(object); + } - @Override - public Acl readAclById(ObjectIdentity object) throws NotFoundException { - return readAclById(object, null); - } + @Override + public Acl readAclById(ObjectIdentity object) throws NotFoundException { + return readAclById(object, null); + } - @Override - public Map readAclsById(List objects) throws NotFoundException { - return readAclsById(objects, null); - } + @Override + public Map readAclsById(List objects) throws NotFoundException { + return readAclsById(objects, null); + } - @Override - public Map readAclsById(List objects, List sids) - throws NotFoundException { - Map result = this.lookupStrategy.readAclsById(objects, sids); - // Check every requested object identity was found (throw NotFoundException if - // needed) - for (ObjectIdentity oid : objects) { - if (!result.containsKey(oid)) { - throw new NotFoundException("Unable to find ACL information for object identity '" + oid + "'"); - } - } - return result; - } + @Override + public Map readAclsById(List objects, List sids) + throws NotFoundException { + Map result = this.lookupStrategy.readAclsById(objects, sids); + // Check every requested object identity was found (throw NotFoundException if + // needed) + for (ObjectIdentity oid : objects) { + if (!result.containsKey(oid)) { + throw new NotFoundException("Unable to find ACL information for object identity '" + oid + "'"); + } + } + return result; + } - /** - * Allows customization of the SQL query used to find child object identities. - * @param findChildrenSql - */ - public void setFindChildrenQuery(String findChildrenSql) { - this.findChildrenSql = findChildrenSql; - } + /** + * Allows customization of the SQL query used to find child object identities. + * + * @param findChildrenSql + */ + public void setFindChildrenQuery(String findChildrenSql) { + this.findChildrenSql = findChildrenSql; + } - public void setAclClassIdSupported(boolean aclClassIdSupported) { - this.aclClassIdSupported = aclClassIdSupported; - if (aclClassIdSupported) { - // Change the default children select if it hasn't been overridden - if (this.findChildrenSql.equals(DEFAULT_SELECT_ACL_WITH_PARENT_SQL)) { - this.findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL_WITH_CLASS_ID_TYPE; - } - else { - log.debug("Find children statement has already been overridden, so not overridding the default"); - } - } - } + public void setAclClassIdSupported(boolean aclClassIdSupported) { + this.aclClassIdSupported = aclClassIdSupported; + if (aclClassIdSupported) { + // Change the default children select if it hasn't been overridden + if (this.findChildrenSql.equals(DEFAULT_SELECT_ACL_WITH_PARENT_SQL)) { + this.findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL_WITH_CLASS_ID_TYPE; + } else { + log.debug("Find children statement has already been overridden, so not overridding the default"); + } + } + } - public void setConversionService(ConversionService conversionService) { - this.aclClassIdUtils = new AclClassIdUtils(conversionService); - } + public void setConversionService(ConversionService conversionService) { + this.aclClassIdUtils = new AclClassIdUtils(conversionService); + } - protected boolean isAclClassIdSupported() { - return this.aclClassIdSupported; - } + public void setObjectIdentityGenerator(ObjectIdentityGenerator objectIdentityGenerator) { + Assert.notNull(objectIdentityGenerator,"The provided strategy has to be not null!"); + this.objectIdentityGenerator = objectIdentityGenerator; + } + + protected boolean isAclClassIdSupported() { + return this.aclClassIdSupported; + } } From f838b7cb1d01b817f7d993a3aa7ad85eb246f186 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Tue, 23 Nov 2021 15:45:25 -0600 Subject: [PATCH 058/589] Polish gh-10081 --- .../acls/jdbc/BasicLookupStrategy.java | 15 +- .../security/acls/jdbc/JdbcAclService.java | 207 +++++++++--------- .../AbstractBasicLookupStrategyTests.java | 9 + .../acls/jdbc/JdbcAclServiceTests.java | 21 ++ 4 files changed, 141 insertions(+), 111 deletions(-) diff --git a/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java b/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java index f9a0eb4ef6..5f8db850e5 100644 --- a/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java +++ b/acl/src/main/java/org/springframework/security/acls/jdbc/BasicLookupStrategy.java @@ -35,7 +35,6 @@ import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; -import org.springframework.security.acls.domain.*; import org.springframework.security.acls.domain.AccessControlEntryImpl; import org.springframework.security.acls.domain.AclAuthorizationStrategy; import org.springframework.security.acls.domain.AclImpl; @@ -43,6 +42,7 @@ import org.springframework.security.acls.domain.AuditLogger; import org.springframework.security.acls.domain.DefaultPermissionFactory; import org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy; import org.springframework.security.acls.domain.GrantedAuthoritySid; +import org.springframework.security.acls.domain.ObjectIdentityRetrievalStrategyImpl; import org.springframework.security.acls.domain.PermissionFactory; import org.springframework.security.acls.domain.PrincipalSid; import org.springframework.security.acls.model.AccessControlEntry; @@ -137,7 +137,6 @@ public class BasicLookupStrategy implements LookupStrategy { private AclClassIdUtils aclClassIdUtils; - /** * Constructor accepting mandatory arguments * @param dataSource to access the database @@ -156,9 +155,8 @@ public class BasicLookupStrategy implements LookupStrategy { * @param aclAuthorizationStrategy authorization strategy (required) * @param grantingStrategy the PermissionGrantingStrategy */ - public BasicLookupStrategy(DataSource dataSource, AclCache aclCache, - AclAuthorizationStrategy aclAuthorizationStrategy, PermissionGrantingStrategy grantingStrategy) { - + public BasicLookupStrategy(DataSource dataSource, AclCache aclCache, + AclAuthorizationStrategy aclAuthorizationStrategy, PermissionGrantingStrategy grantingStrategy) { Assert.notNull(dataSource, "DataSource required"); Assert.notNull(aclCache, "AclCache required"); Assert.notNull(aclAuthorizationStrategy, "AclAuthorizationStrategy required"); @@ -494,8 +492,8 @@ public class BasicLookupStrategy implements LookupStrategy { } } - public void setObjectIdentityGenerator(ObjectIdentityGenerator objectIdentityGenerator) { - Assert.notNull(objectIdentityGenerator,"The provided strategy has to be not null!"); + public final void setObjectIdentityGenerator(ObjectIdentityGenerator objectIdentityGenerator) { + Assert.notNull(objectIdentityGenerator, "objectIdentityGenerator cannot be null"); this.objectIdentityGenerator = objectIdentityGenerator; } @@ -580,7 +578,8 @@ public class BasicLookupStrategy implements LookupStrategy { // target id type, e.g. UUID. Serializable identifier = (Serializable) rs.getObject("object_id_identity"); identifier = BasicLookupStrategy.this.aclClassIdUtils.identifierFrom(identifier, rs); - ObjectIdentity objectIdentity = objectIdentityGenerator.createObjectIdentity(identifier,rs.getString("class")); + ObjectIdentity objectIdentity = BasicLookupStrategy.this.objectIdentityGenerator + .createObjectIdentity(identifier, rs.getString("class")); Acl parentAcl = null; long parentAclId = rs.getLong("parent_object"); diff --git a/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java b/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java index f1cbc51609..e499577388 100644 --- a/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java +++ b/acl/src/main/java/org/springframework/security/acls/jdbc/JdbcAclService.java @@ -51,130 +51,131 @@ import org.springframework.util.Assert; */ public class JdbcAclService implements AclService { - protected static final Log log = LogFactory.getLog(JdbcAclService.class); + protected static final Log log = LogFactory.getLog(JdbcAclService.class); - private static final String DEFAULT_SELECT_ACL_CLASS_COLUMNS = "class.class as class"; + private static final String DEFAULT_SELECT_ACL_CLASS_COLUMNS = "class.class as class"; - private static final String DEFAULT_SELECT_ACL_CLASS_COLUMNS_WITH_ID_TYPE = DEFAULT_SELECT_ACL_CLASS_COLUMNS - + ", class.class_id_type as class_id_type"; + private static final String DEFAULT_SELECT_ACL_CLASS_COLUMNS_WITH_ID_TYPE = DEFAULT_SELECT_ACL_CLASS_COLUMNS + + ", class.class_id_type as class_id_type"; - private static final String DEFAULT_SELECT_ACL_WITH_PARENT_SQL = "select obj.object_id_identity as obj_id, " - + DEFAULT_SELECT_ACL_CLASS_COLUMNS - + " from acl_object_identity obj, acl_object_identity parent, acl_class class " - + "where obj.parent_object = parent.id and obj.object_id_class = class.id " - + "and parent.object_id_identity = ? and parent.object_id_class = (" - + "select id FROM acl_class where acl_class.class = ?)"; + private static final String DEFAULT_SELECT_ACL_WITH_PARENT_SQL = "select obj.object_id_identity as obj_id, " + + DEFAULT_SELECT_ACL_CLASS_COLUMNS + + " from acl_object_identity obj, acl_object_identity parent, acl_class class " + + "where obj.parent_object = parent.id and obj.object_id_class = class.id " + + "and parent.object_id_identity = ? and parent.object_id_class = (" + + "select id FROM acl_class where acl_class.class = ?)"; - private static final String DEFAULT_SELECT_ACL_WITH_PARENT_SQL_WITH_CLASS_ID_TYPE = "select obj.object_id_identity as obj_id, " - + DEFAULT_SELECT_ACL_CLASS_COLUMNS_WITH_ID_TYPE - + " from acl_object_identity obj, acl_object_identity parent, acl_class class " - + "where obj.parent_object = parent.id and obj.object_id_class = class.id " - + "and parent.object_id_identity = ? and parent.object_id_class = (" - + "select id FROM acl_class where acl_class.class = ?)"; + private static final String DEFAULT_SELECT_ACL_WITH_PARENT_SQL_WITH_CLASS_ID_TYPE = "select obj.object_id_identity as obj_id, " + + DEFAULT_SELECT_ACL_CLASS_COLUMNS_WITH_ID_TYPE + + " from acl_object_identity obj, acl_object_identity parent, acl_class class " + + "where obj.parent_object = parent.id and obj.object_id_class = class.id " + + "and parent.object_id_identity = ? and parent.object_id_class = (" + + "select id FROM acl_class where acl_class.class = ?)"; - protected final JdbcOperations jdbcOperations; + protected final JdbcOperations jdbcOperations; - private final LookupStrategy lookupStrategy; + private final LookupStrategy lookupStrategy; - private boolean aclClassIdSupported; + private boolean aclClassIdSupported; - private String findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL; + private String findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL; - private AclClassIdUtils aclClassIdUtils; - private ObjectIdentityGenerator objectIdentityGenerator; + private AclClassIdUtils aclClassIdUtils; - public JdbcAclService(DataSource dataSource, LookupStrategy lookupStrategy) { - this(new JdbcTemplate(dataSource), lookupStrategy); - } + private ObjectIdentityGenerator objectIdentityGenerator; - public JdbcAclService(JdbcOperations jdbcOperations, LookupStrategy lookupStrategy) { - Assert.notNull(jdbcOperations, "JdbcOperations required"); - Assert.notNull(lookupStrategy, "LookupStrategy required"); - this.jdbcOperations = jdbcOperations; - this.lookupStrategy = lookupStrategy; - this.objectIdentityGenerator = new ObjectIdentityRetrievalStrategyImpl(); - this.aclClassIdUtils = new AclClassIdUtils(); - } + public JdbcAclService(DataSource dataSource, LookupStrategy lookupStrategy) { + this(new JdbcTemplate(dataSource), lookupStrategy); + } - @Override - public List findChildren(ObjectIdentity parentIdentity) { - Object[] args = {parentIdentity.getIdentifier().toString(), parentIdentity.getType()}; - List objects = this.jdbcOperations.query(this.findChildrenSql, args, - (rs, rowNum) -> mapObjectIdentityRow(rs)); - return (!objects.isEmpty()) ? objects : null; - } + public JdbcAclService(JdbcOperations jdbcOperations, LookupStrategy lookupStrategy) { + Assert.notNull(jdbcOperations, "JdbcOperations required"); + Assert.notNull(lookupStrategy, "LookupStrategy required"); + this.jdbcOperations = jdbcOperations; + this.lookupStrategy = lookupStrategy; + this.aclClassIdUtils = new AclClassIdUtils(); + this.objectIdentityGenerator = new ObjectIdentityRetrievalStrategyImpl(); + } - private ObjectIdentity mapObjectIdentityRow(ResultSet rs) throws SQLException { - String javaType = rs.getString("class"); - Serializable identifier = (Serializable) rs.getObject("obj_id"); - identifier = this.aclClassIdUtils.identifierFrom(identifier, rs); - return objectIdentityGenerator.createObjectIdentity(identifier, javaType); - } + @Override + public List findChildren(ObjectIdentity parentIdentity) { + Object[] args = { parentIdentity.getIdentifier().toString(), parentIdentity.getType() }; + List objects = this.jdbcOperations.query(this.findChildrenSql, args, + (rs, rowNum) -> mapObjectIdentityRow(rs)); + return (!objects.isEmpty()) ? objects : null; + } - @Override - public Acl readAclById(ObjectIdentity object, List sids) throws NotFoundException { - Map map = readAclsById(Collections.singletonList(object), sids); - Assert.isTrue(map.containsKey(object), - () -> "There should have been an Acl entry for ObjectIdentity " + object); - return map.get(object); - } + private ObjectIdentity mapObjectIdentityRow(ResultSet rs) throws SQLException { + String javaType = rs.getString("class"); + Serializable identifier = (Serializable) rs.getObject("obj_id"); + identifier = this.aclClassIdUtils.identifierFrom(identifier, rs); + return this.objectIdentityGenerator.createObjectIdentity(identifier, javaType); + } - @Override - public Acl readAclById(ObjectIdentity object) throws NotFoundException { - return readAclById(object, null); - } + @Override + public Acl readAclById(ObjectIdentity object, List sids) throws NotFoundException { + Map map = readAclsById(Collections.singletonList(object), sids); + Assert.isTrue(map.containsKey(object), + () -> "There should have been an Acl entry for ObjectIdentity " + object); + return map.get(object); + } - @Override - public Map readAclsById(List objects) throws NotFoundException { - return readAclsById(objects, null); - } + @Override + public Acl readAclById(ObjectIdentity object) throws NotFoundException { + return readAclById(object, null); + } - @Override - public Map readAclsById(List objects, List sids) - throws NotFoundException { - Map result = this.lookupStrategy.readAclsById(objects, sids); - // Check every requested object identity was found (throw NotFoundException if - // needed) - for (ObjectIdentity oid : objects) { - if (!result.containsKey(oid)) { - throw new NotFoundException("Unable to find ACL information for object identity '" + oid + "'"); - } - } - return result; - } + @Override + public Map readAclsById(List objects) throws NotFoundException { + return readAclsById(objects, null); + } - /** - * Allows customization of the SQL query used to find child object identities. - * - * @param findChildrenSql - */ - public void setFindChildrenQuery(String findChildrenSql) { - this.findChildrenSql = findChildrenSql; - } + @Override + public Map readAclsById(List objects, List sids) + throws NotFoundException { + Map result = this.lookupStrategy.readAclsById(objects, sids); + // Check every requested object identity was found (throw NotFoundException if + // needed) + for (ObjectIdentity oid : objects) { + if (!result.containsKey(oid)) { + throw new NotFoundException("Unable to find ACL information for object identity '" + oid + "'"); + } + } + return result; + } - public void setAclClassIdSupported(boolean aclClassIdSupported) { - this.aclClassIdSupported = aclClassIdSupported; - if (aclClassIdSupported) { - // Change the default children select if it hasn't been overridden - if (this.findChildrenSql.equals(DEFAULT_SELECT_ACL_WITH_PARENT_SQL)) { - this.findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL_WITH_CLASS_ID_TYPE; - } else { - log.debug("Find children statement has already been overridden, so not overridding the default"); - } - } - } + /** + * Allows customization of the SQL query used to find child object identities. + * @param findChildrenSql + */ + public void setFindChildrenQuery(String findChildrenSql) { + this.findChildrenSql = findChildrenSql; + } - public void setConversionService(ConversionService conversionService) { - this.aclClassIdUtils = new AclClassIdUtils(conversionService); - } + public void setAclClassIdSupported(boolean aclClassIdSupported) { + this.aclClassIdSupported = aclClassIdSupported; + if (aclClassIdSupported) { + // Change the default children select if it hasn't been overridden + if (this.findChildrenSql.equals(DEFAULT_SELECT_ACL_WITH_PARENT_SQL)) { + this.findChildrenSql = DEFAULT_SELECT_ACL_WITH_PARENT_SQL_WITH_CLASS_ID_TYPE; + } + else { + log.debug("Find children statement has already been overridden, so not overridding the default"); + } + } + } - public void setObjectIdentityGenerator(ObjectIdentityGenerator objectIdentityGenerator) { - Assert.notNull(objectIdentityGenerator,"The provided strategy has to be not null!"); - this.objectIdentityGenerator = objectIdentityGenerator; - } + public void setConversionService(ConversionService conversionService) { + this.aclClassIdUtils = new AclClassIdUtils(conversionService); + } - protected boolean isAclClassIdSupported() { - return this.aclClassIdSupported; - } + public void setObjectIdentityGenerator(ObjectIdentityGenerator objectIdentityGenerator) { + Assert.notNull(objectIdentityGenerator, "objectIdentityGenerator cannot be null"); + this.objectIdentityGenerator = objectIdentityGenerator; + } + + protected boolean isAclClassIdSupported() { + return this.aclClassIdSupported; + } } diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/AbstractBasicLookupStrategyTests.java b/acl/src/test/java/org/springframework/security/acls/jdbc/AbstractBasicLookupStrategyTests.java index 4a6b1d695f..e1be44924d 100644 --- a/acl/src/test/java/org/springframework/security/acls/jdbc/AbstractBasicLookupStrategyTests.java +++ b/acl/src/test/java/org/springframework/security/acls/jdbc/AbstractBasicLookupStrategyTests.java @@ -318,4 +318,13 @@ public abstract class AbstractBasicLookupStrategyTests { assertThat(((GrantedAuthoritySid) result).getGrantedAuthority()).isEqualTo("sid"); } + @Test + public void setObjectIdentityGeneratorWhenNullThenThrowsIllegalArgumentException() { + // @formatter:off + assertThatIllegalArgumentException() + .isThrownBy(() -> this.strategy.setObjectIdentityGenerator(null)) + .withMessage("objectIdentityGenerator cannot be null"); + // @formatter:on + } + } diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java b/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java index 903fd3264c..cd91ae1745 100644 --- a/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java +++ b/acl/src/test/java/org/springframework/security/acls/jdbc/JdbcAclServiceTests.java @@ -45,6 +45,7 @@ import org.springframework.security.acls.model.Sid; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; @@ -170,6 +171,26 @@ public class JdbcAclServiceTests { .isEqualTo(UUID.fromString("25d93b3f-c3aa-4814-9d5e-c7c96ced7762")); } + @Test + public void setObjectIdentityGeneratorWhenNullThenThrowsIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.aclServiceIntegration.setObjectIdentityGenerator(null)) + .withMessage("objectIdentityGenerator cannot be null"); + } + + @Test + public void findChildrenWhenObjectIdentityGeneratorSetThenUsed() { + this.aclServiceIntegration + .setObjectIdentityGenerator((id, type) -> new ObjectIdentityImpl(type, "prefix:" + id)); + + ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US"); + this.aclServiceIntegration.setAclClassIdSupported(true); + List objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity); + assertThat(objectIdentities.size()).isEqualTo(1); + assertThat(objectIdentities.get(0).getType()).isEqualTo("location"); + assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("prefix:US-PAL"); + } + class MockLongIdDomainObject { private Object id; From a3a9de1b9bb3179ceaea8b767c5bb668184a9a75 Mon Sep 17 00:00:00 2001 From: Igor Pelesic Date: Thu, 18 Nov 2021 12:48:43 +0100 Subject: [PATCH 059/589] PermitAllSupport supports AuthorizeHttpRequestsConfigurer PermitAllSupport supports either an ExpressionUrlAuthorizationConfigurer or an AuthorizeHttpRequestsConfigurer. If none or both are configured an error message is thrown. Closes gh-10482 --- .../AuthorizeHttpRequestsConfigurer.java | 27 +++++++- .../web/configurers/PermitAllSupport.java | 19 ++++-- .../configurers/PermitAllSupportTests.java | 66 ++++++++++++++++++- ...MatcherDelegatingAuthorizationManager.java | 17 ++++- ...erDelegatingAuthorizationManagerTests.java | 38 +++++++++++ 5 files changed, 157 insertions(+), 10 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java index 44d2416cd5..ee7d7a4697 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.config.annotation.web.configurers; +import java.util.LinkedHashMap; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -46,6 +47,9 @@ import org.springframework.util.Assert; public final class AuthorizeHttpRequestsConfigurer> extends AbstractHttpConfigurer, H> { + static final AuthorizationManager permitAllAuthorizationManager = (a, + o) -> new AuthorizationDecision(true); + private final AuthorizationManagerRequestMatcherRegistry registry; /** @@ -81,6 +85,12 @@ public final class AuthorizeHttpRequestsConfigurer manager) { + this.registry.addFirst(matcher, manager); + return this.registry; + } + /** * Registry for mapping a {@link RequestMatcher} to an {@link AuthorizationManager}. * @@ -106,6 +116,19 @@ public final class AuthorizeHttpRequestsConfigurer manager) { + this.unmappedMatchers = null; + this.managerBuilder.mappings((m) -> { + LinkedHashMap> reorderedMap = new LinkedHashMap<>( + m.size() + 1); + reorderedMap.put(matcher, manager); + reorderedMap.putAll(m); + m.clear(); + m.putAll(reorderedMap); + }); + this.mappingCount++; + } + private AuthorizationManager createAuthorizationManager() { Assert.state(this.unmappedMatchers == null, () -> "An incomplete mapping was found for " + this.unmappedMatchers @@ -209,7 +232,7 @@ public final class AuthorizeHttpRequestsConfigurer new AuthorizationDecision(true)); + return access(permitAllAuthorizationManager); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java index 3af0eba172..ac96e48010 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,11 +48,22 @@ final class PermitAllSupport { RequestMatcher... requestMatchers) { ExpressionUrlAuthorizationConfigurer configurer = http .getConfigurer(ExpressionUrlAuthorizationConfigurer.class); - Assert.state(configurer != null, "permitAll only works with HttpSecurity.authorizeRequests()"); + AuthorizeHttpRequestsConfigurer httpConfigurer = http.getConfigurer(AuthorizeHttpRequestsConfigurer.class); + + boolean oneConfigurerPresent = configurer == null ^ httpConfigurer == null; + Assert.state(oneConfigurerPresent, + "permitAll only works with either HttpSecurity.authorizeRequests() or HttpSecurity.authorizeHttpRequests(). " + + "Please define one or the other but not both."); + for (RequestMatcher matcher : requestMatchers) { if (matcher != null) { - configurer.getRegistry().addMapping(0, new UrlMapping(matcher, - SecurityConfig.createList(ExpressionUrlAuthorizationConfigurer.permitAll))); + if (configurer != null) { + configurer.getRegistry().addMapping(0, new UrlMapping(matcher, + SecurityConfig.createList(ExpressionUrlAuthorizationConfigurer.permitAll))); + } + else { + httpConfigurer.addFirst(matcher, AuthorizeHttpRequestsConfigurer.permitAllAuthorizationManager); + } } } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.java index 70752fb1c5..9a5174f810 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PermitAllSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,11 +61,32 @@ public class PermitAllSupportTests { this.mvc.perform(getWithCsrf).andExpect(status().isFound()); } + @Test + public void performWhenUsingPermitAllExactUrlRequestMatcherThenMatchesExactUrlWithAuthorizeHttp() throws Exception { + this.spring.register(PermitAllConfigAuthorizeHttpRequests.class).autowire(); + MockHttpServletRequestBuilder request = get("/app/xyz").contextPath("/app"); + this.mvc.perform(request).andExpect(status().isNotFound()); + MockHttpServletRequestBuilder getWithQuery = get("/app/xyz?def").contextPath("/app"); + this.mvc.perform(getWithQuery).andExpect(status().isFound()); + MockHttpServletRequestBuilder postWithQueryAndCsrf = post("/app/abc?def").with(csrf()).contextPath("/app"); + this.mvc.perform(postWithQueryAndCsrf).andExpect(status().isNotFound()); + MockHttpServletRequestBuilder getWithCsrf = get("/app/abc").with(csrf()).contextPath("/app"); + this.mvc.perform(getWithCsrf).andExpect(status().isFound()); + } + @Test public void configureWhenNotAuthorizeRequestsThenException() { assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> this.spring.register(NoAuthorizedUrlsConfig.class).autowire()) - .withMessageContaining("permitAll only works with HttpSecurity.authorizeRequests"); + .isThrownBy(() -> this.spring.register(NoAuthorizedUrlsConfig.class).autowire()).withMessageContaining( + "permitAll only works with either HttpSecurity.authorizeRequests() or HttpSecurity.authorizeHttpRequests()"); + } + + @Test + public void configureWhenBothAuthorizeRequestsAndAuthorizeHttpRequestsThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.spring.register(PermitAllConfigWithBothConfigs.class).autowire()) + .withMessageContaining( + "permitAll only works with either HttpSecurity.authorizeRequests() or HttpSecurity.authorizeHttpRequests()"); } @EnableWebSecurity @@ -86,6 +107,45 @@ public class PermitAllSupportTests { } + @EnableWebSecurity + static class PermitAllConfigAuthorizeHttpRequests extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage("/xyz").permitAll() + .loginProcessingUrl("/abc?def").permitAll(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class PermitAllConfigWithBothConfigs extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .authorizeHttpRequests() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage("/xyz").permitAll() + .loginProcessingUrl("/abc?def").permitAll(); + // @formatter:on + } + + } + @EnableWebSecurity static class NoAuthorizedUrlsConfig extends WebSecurityConfigurerAdapter { diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java index b1c6378914..4d62085e8c 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.security.web.access.intercept; import java.util.LinkedHashMap; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; @@ -112,6 +113,20 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho return this; } + /** + * Allows to configure the {@link RequestMatcher} to {@link AuthorizationManager} + * mappings. + * @param mappingsConsumer used to configure the {@link RequestMatcher} to + * {@link AuthorizationManager} mappings. + * @return the {@link Builder} for further customizations + */ + public Builder mappings( + Consumer>> mappingsConsumer) { + Assert.notNull(mappingsConsumer, "mappingsConsumer cannot be null"); + mappingsConsumer.accept(this.mappings); + return this; + } + /** * Creates a {@link RequestMatcherDelegatingAuthorizationManager} instance. * @return the {@link RequestMatcherDelegatingAuthorizationManager} instance diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java index 829866340a..952132522b 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java @@ -22,9 +22,11 @@ import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.core.Authentication; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -83,4 +85,40 @@ public class RequestMatcherDelegatingAuthorizationManagerTests { assertThat(abstain).isNull(); } + @Test + public void checkWhenMultipleMappingsConfiguredWithConsumerThenDelegatesMatchingManager() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .mappings((m) -> { + m.put(new MvcRequestMatcher(null, "/grant"), (a, o) -> new AuthorizationDecision(true)); + m.put(AnyRequestMatcher.INSTANCE, AuthorityAuthorizationManager.hasRole("ADMIN")); + m.put(new MvcRequestMatcher(null, "/deny"), (a, o) -> new AuthorizationDecision(false)); + m.put(new MvcRequestMatcher(null, "/afterAny"), (a, o) -> new AuthorizationDecision(true)); + }).build(); + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", "ROLE_USER"); + + AuthorizationDecision grant = manager.check(authentication, new MockHttpServletRequest(null, "/grant")); + assertThat(grant).isNotNull(); + assertThat(grant.isGranted()).isTrue(); + + AuthorizationDecision deny = manager.check(authentication, new MockHttpServletRequest(null, "/deny")); + assertThat(deny).isNotNull(); + assertThat(deny.isGranted()).isFalse(); + + AuthorizationDecision afterAny = manager.check(authentication, new MockHttpServletRequest(null, "/afterAny")); + assertThat(afterAny).isNotNull(); + assertThat(afterAny.isGranted()).isFalse(); + + AuthorizationDecision unmapped = manager.check(authentication, new MockHttpServletRequest(null, "/unmapped")); + assertThat(unmapped).isNotNull(); + assertThat(unmapped.isGranted()).isFalse(); + } + + @Test + public void addWhenMappingsConsumerNullThenException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder().mappings(null).build()) + .withMessage("mappingsConsumer cannot be null"); + } + } From 1251cde04c848e9151750cd48886c7330cfee8b6 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 30 Nov 2021 15:01:36 -0700 Subject: [PATCH 060/589] Add Missing Since Issue gh-10482 --- .../intercept/RequestMatcherDelegatingAuthorizationManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java index 4d62085e8c..066bac5e69 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java @@ -119,6 +119,7 @@ public final class RequestMatcherDelegatingAuthorizationManager implements Autho * @param mappingsConsumer used to configure the {@link RequestMatcher} to * {@link AuthorizationManager} mappings. * @return the {@link Builder} for further customizations + * @since 5.7 */ public Builder mappings( Consumer>> mappingsConsumer) { From 2bc643d6c8d1e76e8109b116dff7fe50276eb7b0 Mon Sep 17 00:00:00 2001 From: Hiroshi Shirosaki Date: Wed, 16 Jun 2021 16:53:13 +0900 Subject: [PATCH 061/589] Address SecurityContextHolder memory leak To get current context without creating a new context. Creating a new context may cause ThreadLocal leak. Closes gh-9841 --- .../SecurityReactorContextConfiguration.java | 14 ++++++++++++-- .../GlobalSecurityContextHolderStrategy.java | 5 +++++ ...leThreadLocalSecurityContextHolderStrategy.java | 5 +++++ .../core/context/SecurityContextHolder.java | 8 ++++++++ .../context/SecurityContextHolderStrategy.java | 6 ++++++ .../ThreadLocalSecurityContextHolderStrategy.java | 5 +++++ 6 files changed, 41 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java index 2783cb358b..4210e90fe6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @@ -94,7 +95,12 @@ class SecurityReactorContextConfiguration { } private static boolean contextAttributesAvailable() { - return SecurityContextHolder.getContext().getAuthentication() != null + SecurityContext context = SecurityContextHolder.peekContext(); + Authentication authentication = null; + if (context != null) { + authentication = context.getAuthentication(); + } + return authentication != null || RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes; } @@ -107,7 +113,11 @@ class SecurityReactorContextConfiguration { servletRequest = servletRequestAttributes.getRequest(); servletResponse = servletRequestAttributes.getResponse(); // possible null } - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + SecurityContext context = SecurityContextHolder.peekContext(); + Authentication authentication = null; + if (context != null) { + authentication = context.getAuthentication(); + } if (authentication == null && servletRequest == null) { return Collections.emptyMap(); } diff --git a/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java index d8367c4ebd..330afdb743 100644 --- a/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java @@ -44,6 +44,11 @@ final class GlobalSecurityContextHolderStrategy implements SecurityContextHolder return contextHolder; } + @Override + public SecurityContext peekContext() { + return contextHolder; + } + @Override public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); diff --git a/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java index cb415500ca..d08b221c28 100644 --- a/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java @@ -44,6 +44,11 @@ final class InheritableThreadLocalSecurityContextHolderStrategy implements Secur return ctx; } + @Override + public SecurityContext peekContext() { + return contextHolder.get(); + } + @Override public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java index 337fde3a57..6671e6dd9d 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java @@ -123,6 +123,14 @@ public class SecurityContextHolder { return strategy.getContext(); } + /** + * Peeks the current SecurityContext. + * @return the security context (may be null) + */ + public static SecurityContext peekContext() { + return strategy.peekContext(); + } + /** * Primarily for troubleshooting purposes, this method shows how many times the class * has re-initialized its SecurityContextHolderStrategy. diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java index 4954db70aa..2a29566fae 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java @@ -38,6 +38,12 @@ public interface SecurityContextHolderStrategy { */ SecurityContext getContext(); + /** + * Peeks the current context without creating an empty context. + * @return a context (may be null) + */ + SecurityContext peekContext(); + /** * Sets the current context. * @param context to the new argument (should never be null, although diff --git a/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java index 801f5c8207..84f23bbe22 100644 --- a/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java @@ -45,6 +45,11 @@ final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextH return ctx; } + @Override + public SecurityContext peekContext() { + return contextHolder.get(); + } + @Override public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); From a68411566e0f96b875728a178ba3061837ee19ed Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 30 Nov 2021 12:32:36 -0700 Subject: [PATCH 062/589] Polish Memory Leak Mitigation Issue gh-9841 --- .../SecurityReactorContextConfiguration.java | 183 ++++++++++++++---- .../GlobalSecurityContextHolderStrategy.java | 5 - ...eadLocalSecurityContextHolderStrategy.java | 5 - .../core/context/SecurityContextHolder.java | 8 - .../SecurityContextHolderStrategy.java | 6 - ...eadLocalSecurityContextHolderStrategy.java | 5 - 6 files changed, 142 insertions(+), 70 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java index 4210e90fe6..41ab29c93a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/SecurityReactorContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ package org.springframework.security.config.annotation.web.configuration; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -36,7 +40,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @@ -68,17 +71,22 @@ class SecurityReactorContextConfiguration { private static final String SECURITY_REACTOR_CONTEXT_OPERATOR_KEY = "org.springframework.security.SECURITY_REACTOR_CONTEXT_OPERATOR"; + private static final Map> CONTEXT_ATTRIBUTE_VALUE_LOADERS = new HashMap<>(); + + static { + CONTEXT_ATTRIBUTE_VALUE_LOADERS.put(HttpServletRequest.class, + SecurityReactorContextSubscriberRegistrar::getRequest); + CONTEXT_ATTRIBUTE_VALUE_LOADERS.put(HttpServletResponse.class, + SecurityReactorContextSubscriberRegistrar::getResponse); + CONTEXT_ATTRIBUTE_VALUE_LOADERS.put(Authentication.class, + SecurityReactorContextSubscriberRegistrar::getAuthentication); + } + @Override public void afterPropertiesSet() throws Exception { Function, ? extends Publisher> lifter = Operators .liftPublisher((pub, sub) -> createSubscriberIfNecessary(sub)); - Hooks.onLastOperator(SECURITY_REACTOR_CONTEXT_OPERATOR_KEY, (pub) -> { - if (!contextAttributesAvailable()) { - // No need to decorate so return original Publisher - return pub; - } - return lifter.apply(pub); - }); + Hooks.onLastOperator(SECURITY_REACTOR_CONTEXT_OPERATOR_KEY, lifter::apply); } @Override @@ -94,45 +102,30 @@ class SecurityReactorContextConfiguration { return new SecurityReactorContextSubscriber<>(delegate, getContextAttributes()); } - private static boolean contextAttributesAvailable() { - SecurityContext context = SecurityContextHolder.peekContext(); - Authentication authentication = null; - if (context != null) { - authentication = context.getAuthentication(); - } - return authentication != null - || RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes; + private static Map getContextAttributes() { + return new LoadingMap<>(CONTEXT_ATTRIBUTE_VALUE_LOADERS); } - private static Map getContextAttributes() { - HttpServletRequest servletRequest = null; - HttpServletResponse servletResponse = null; + private static HttpServletRequest getRequest() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes instanceof ServletRequestAttributes) { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; - servletRequest = servletRequestAttributes.getRequest(); - servletResponse = servletRequestAttributes.getResponse(); // possible null - } - SecurityContext context = SecurityContextHolder.peekContext(); - Authentication authentication = null; - if (context != null) { - authentication = context.getAuthentication(); - } - if (authentication == null && servletRequest == null) { - return Collections.emptyMap(); - } - Map contextAttributes = new HashMap<>(); - if (servletRequest != null) { - contextAttributes.put(HttpServletRequest.class, servletRequest); - } - if (servletResponse != null) { - contextAttributes.put(HttpServletResponse.class, servletResponse); - } - if (authentication != null) { - contextAttributes.put(Authentication.class, authentication); + return servletRequestAttributes.getRequest(); } + return null; + } - return contextAttributes; + private static HttpServletResponse getResponse() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (requestAttributes instanceof ServletRequestAttributes) { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; + return servletRequestAttributes.getResponse(); // possible null + } + return null; + } + + private static Authentication getAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); } } @@ -185,4 +178,112 @@ class SecurityReactorContextConfiguration { } + /** + * A map that computes each value when {@link #get} is invoked + */ + static class LoadingMap implements Map { + + private final Map loaded = new ConcurrentHashMap<>(); + + private final Map> loaders; + + LoadingMap(Map> loaders) { + this.loaders = Collections.unmodifiableMap(new HashMap<>(loaders)); + } + + @Override + public int size() { + return this.loaders.size(); + } + + @Override + public boolean isEmpty() { + return this.loaders.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.loaders.containsKey(key); + } + + @Override + public Set keySet() { + return this.loaders.keySet(); + } + + @Override + public V get(Object key) { + if (!this.loaders.containsKey(key)) { + throw new IllegalArgumentException( + "This map only supports the following keys: " + this.loaders.keySet()); + } + return this.loaded.computeIfAbsent((K) key, (k) -> this.loaders.get(k).get()); + } + + @Override + public V put(K key, V value) { + if (!this.loaders.containsKey(key)) { + throw new IllegalArgumentException( + "This map only supports the following keys: " + this.loaders.keySet()); + } + return this.loaded.put(key, value); + } + + @Override + public V remove(Object key) { + if (!this.loaders.containsKey(key)) { + throw new IllegalArgumentException( + "This map only supports the following keys: " + this.loaders.keySet()); + } + return this.loaded.remove(key); + } + + @Override + public void putAll(Map m) { + for (Map.Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + this.loaded.clear(); + } + + @Override + public boolean containsValue(Object value) { + return this.loaded.containsValue(value); + } + + @Override + public Collection values() { + return this.loaded.values(); + } + + @Override + public Set> entrySet() { + return this.loaded.entrySet(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LoadingMap that = (LoadingMap) o; + + return this.loaded.equals(that.loaded); + } + + @Override + public int hashCode() { + return this.loaded.hashCode(); + } + + } + } diff --git a/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java index 330afdb743..d8367c4ebd 100644 --- a/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/GlobalSecurityContextHolderStrategy.java @@ -44,11 +44,6 @@ final class GlobalSecurityContextHolderStrategy implements SecurityContextHolder return contextHolder; } - @Override - public SecurityContext peekContext() { - return contextHolder; - } - @Override public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); diff --git a/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java index d08b221c28..cb415500ca 100644 --- a/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/InheritableThreadLocalSecurityContextHolderStrategy.java @@ -44,11 +44,6 @@ final class InheritableThreadLocalSecurityContextHolderStrategy implements Secur return ctx; } - @Override - public SecurityContext peekContext() { - return contextHolder.get(); - } - @Override public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java index 6671e6dd9d..337fde3a57 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolder.java @@ -123,14 +123,6 @@ public class SecurityContextHolder { return strategy.getContext(); } - /** - * Peeks the current SecurityContext. - * @return the security context (may be null) - */ - public static SecurityContext peekContext() { - return strategy.peekContext(); - } - /** * Primarily for troubleshooting purposes, this method shows how many times the class * has re-initialized its SecurityContextHolderStrategy. diff --git a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java index 2a29566fae..4954db70aa 100644 --- a/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/SecurityContextHolderStrategy.java @@ -38,12 +38,6 @@ public interface SecurityContextHolderStrategy { */ SecurityContext getContext(); - /** - * Peeks the current context without creating an empty context. - * @return a context (may be null) - */ - SecurityContext peekContext(); - /** * Sets the current context. * @param context to the new argument (should never be null, although diff --git a/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java b/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java index 84f23bbe22..801f5c8207 100644 --- a/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java +++ b/core/src/main/java/org/springframework/security/core/context/ThreadLocalSecurityContextHolderStrategy.java @@ -45,11 +45,6 @@ final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextH return ctx; } - @Override - public SecurityContext peekContext() { - return contextHolder.get(); - } - @Override public void setContext(SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); From dbe4d704f844d7d3a4b7048e004c07e1496033d2 Mon Sep 17 00:00:00 2001 From: Arnaud Mergey Date: Tue, 1 Dec 2020 16:54:13 +0100 Subject: [PATCH 063/589] Add SP NameIDFormat Support closes gh-9115 --- .../metadata/OpenSamlMetadataResolver.java | 12 +++++++- .../RelyingPartyRegistration.java | 30 +++++++++++++++++-- ...OpenSaml4AuthenticationRequestFactory.java | 20 ++++++++++++- ...aml4AuthenticationRequestFactoryTests.java | 12 ++++++++ .../OpenSamlMetadataResolverTests.java | 11 ++++++- .../RelyingPartyRegistrationTests.java | 2 ++ 6 files changed, 82 insertions(+), 5 deletions(-) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java index 1f0d5c19af..7b850a5483 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml.saml2.metadata.NameIDFormat; import org.opensaml.saml.saml2.metadata.SPSSODescriptor; import org.opensaml.saml.saml2.metadata.SingleLogoutService; import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; @@ -87,6 +88,9 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { .addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION)); spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration)); spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration)); + if (registration.getNameIdFormat() != null) { + spSsoDescriptor.getNameIDFormats().add(buildNameIDFormat(registration)); + } return spSsoDescriptor; } @@ -133,6 +137,12 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { return singleLogoutService; } + private NameIDFormat buildNameIDFormat(RelyingPartyRegistration registration) { + NameIDFormat nameIdFormat = build(NameIDFormat.DEFAULT_ELEMENT_NAME); + nameIdFormat.setFormat(registration.getNameIdFormat()); + return nameIdFormat; + } + @SuppressWarnings("unchecked") private T build(QName elementName) { XMLObjectBuilder builder = XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(elementName); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index d07a3664f8..43e61b11e1 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -87,6 +87,8 @@ public final class RelyingPartyRegistration { private final Saml2MessageBinding singleLogoutServiceBinding; + private final String nameIdFormat; + private final ProviderDetails providerDetails; private final List credentials; @@ -98,7 +100,7 @@ public final class RelyingPartyRegistration { private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation, Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation, String singleLogoutServiceResponseLocation, Saml2MessageBinding singleLogoutServiceBinding, - ProviderDetails providerDetails, + ProviderDetails providerDetails, String nameIdFormat, Collection credentials, Collection decryptionX509Credentials, Collection signingX509Credentials) { @@ -129,6 +131,7 @@ public final class RelyingPartyRegistration { this.singleLogoutServiceLocation = singleLogoutServiceLocation; this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation; this.singleLogoutServiceBinding = singleLogoutServiceBinding; + this.nameIdFormat = nameIdFormat; this.providerDetails = providerDetails; this.credentials = Collections.unmodifiableList(new LinkedList<>(credentials)); this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials)); @@ -234,6 +237,15 @@ public final class RelyingPartyRegistration { return this.singleLogoutServiceResponseLocation; } + /** + * Get the NameID format. + * @return the NameID format + * @since 5.7 + */ + public String getNameIdFormat() { + return this.nameIdFormat; + } + /** * Get the {@link Collection} of decryption {@link Saml2X509Credential}s associated * with this relying party @@ -424,6 +436,7 @@ public final class RelyingPartyRegistration { .singleLogoutServiceLocation(registration.getSingleLogoutServiceLocation()) .singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation()) .singleLogoutServiceBinding(registration.getSingleLogoutServiceBinding()) + .nameIdFormat(registration.getNameIdFormat()) .assertingPartyDetails((assertingParty) -> assertingParty .entityId(registration.getAssertingPartyDetails().getEntityId()) .wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) @@ -1018,6 +1031,8 @@ public final class RelyingPartyRegistration { private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.POST; + private String nameIdFormat = null; + private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder(); private Collection credentials = new HashSet<>(); @@ -1173,6 +1188,17 @@ public final class RelyingPartyRegistration { return this; } + /** + * Set the NameID format + * @param nameIdFormat + * @return the {@link Builder} for further configuration + * @since 5.7 + */ + public Builder nameIdFormat(String nameIdFormat) { + this.nameIdFormat = nameIdFormat; + return this; + } + /** * Apply this {@link Consumer} to further configure the Asserting Party details * @param assertingPartyDetails The {@link Consumer} to apply @@ -1321,7 +1347,7 @@ public final class RelyingPartyRegistration { return new RelyingPartyRegistration(this.registrationId, this.entityId, this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding, this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation, - this.singleLogoutServiceBinding, this.providerDetails.build(), this.credentials, + this.singleLogoutServiceBinding, this.providerDetails.build(), this.nameIdFormat, this.credentials, this.decryptionX509Credentials, this.signingX509Credentials); } diff --git a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java index dcfa1cfdbc..ec02ca2a06 100644 --- a/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java +++ b/saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,10 @@ import org.opensaml.core.xml.config.XMLObjectProviderRegistry; import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.NameIDPolicy; import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder; import org.opensaml.saml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml.saml2.core.impl.NameIDPolicyBuilder; import org.springframework.core.convert.converter.Converter; import org.springframework.security.saml2.core.OpenSamlInitializationService; @@ -56,6 +58,8 @@ public final class OpenSaml4AuthenticationRequestFactory implements Saml2Authent private final IssuerBuilder issuerBuilder; + private final NameIDPolicyBuilder nameIdPolicyBuilder; + private Clock clock = Clock.systemUTC(); private Converter authenticationRequestContextConverter; @@ -69,6 +73,8 @@ public final class OpenSaml4AuthenticationRequestFactory implements Saml2Authent this.authnRequestBuilder = (AuthnRequestBuilder) registry.getBuilderFactory() .getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); this.issuerBuilder = (IssuerBuilder) registry.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); + this.nameIdPolicyBuilder = (NameIDPolicyBuilder) registry.getBuilderFactory() + .getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME); } /** @@ -152,6 +158,9 @@ public final class OpenSaml4AuthenticationRequestFactory implements Saml2Authent auth.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); } auth.setProtocolBinding(protocolBinding); + if (auth.getNameIDPolicy() == null) { + setNameIdPolicy(auth, context.getRelyingPartyRegistration()); + } Issuer iss = this.issuerBuilder.buildObject(); iss.setValue(issuer); auth.setIssuer(iss); @@ -160,6 +169,15 @@ public final class OpenSaml4AuthenticationRequestFactory implements Saml2Authent return auth; } + private void setNameIdPolicy(AuthnRequest authnRequest, RelyingPartyRegistration registration) { + if (!StringUtils.hasText(registration.getNameIdFormat())) { + return; + } + NameIDPolicy nameIdPolicy = this.nameIdPolicyBuilder.buildObject(); + nameIdPolicy.setFormat(registration.getNameIdFormat()); + authnRequest.setNameIDPolicy(nameIdPolicy); + } + /** * Set the strategy for building an {@link AuthnRequest} from a given context * @param authenticationRequestContextConverter the conversion strategy to use diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java index 84c415ebe5..0aced67097 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationRequestFactoryTests.java @@ -242,6 +242,18 @@ public class OpenSaml4AuthenticationRequestFactoryTests { assertThat(result.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); } + @Test + public void createAuthenticationRequestWhenSetNameIDPolicyThenReturnsCorrectNameIDPolicy() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().nameIdFormat("format").build(); + this.context = this.contextBuilder.relayState("Relay State Value").relyingPartyRegistration(registration) + .build(); + AuthnRequest authn = getAuthNRequest(Saml2MessageBinding.POST); + assertThat(authn.getNameIDPolicy()).isNotNull(); + assertThat(authn.getNameIDPolicy().getAllowCreate()).isFalse(); + assertThat(authn.getNameIDPolicy().getFormat()).isEqualTo("format"); + assertThat(authn.getNameIDPolicy().getSPNameQualifier()).isNull(); + } + private AuthnRequest authnRequest() { AuthnRequest authnRequest = TestOpenSamlObjects.authnRequest(); authnRequest.setIssueInstant(Instant.now()); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java index d42fc875be..be2069ab94 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,4 +61,13 @@ public class OpenSamlMetadataResolverTests { .contains("ResponseLocation=\"https://rp.example.org/logout/saml2/response\""); } + @Test + public void resolveWhenRelyingPartyNameIDFormatThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full().nameIdFormat("format") + .build(); + OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); + String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).contains("format"); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java index d25d4b981c..63e9d58505 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java @@ -28,6 +28,7 @@ public class RelyingPartyRegistrationTests { @Test public void withRelyingPartyRegistrationWorks() { RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .nameIdFormat("format") .assertingPartyDetails((a) -> a.singleSignOnServiceBinding(Saml2MessageBinding.POST)) .assertingPartyDetails((a) -> a.wantAuthnRequestsSigned(false)) .assertingPartyDetails((a) -> a.signingAlgorithms((algs) -> algs.add("alg"))) @@ -74,6 +75,7 @@ public class RelyingPartyRegistrationTests { .isEqualTo(registration.getAssertingPartyDetails().getVerificationX509Credentials()); assertThat(copy.getAssertingPartyDetails().getSigningAlgorithms()) .isEqualTo(registration.getAssertingPartyDetails().getSigningAlgorithms()); + assertThat(copy.getNameIdFormat()).isEqualTo(registration.getNameIdFormat()); } @Test From f49c286050e3899eabedd9ecdea600213c919660 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 1 Dec 2021 12:36:22 -0600 Subject: [PATCH 064/589] Fix case sensitive headers comparison Closes gh-10557 --- .../header/StaticServerHttpHeadersWriter.java | 13 +++++++++--- .../StaticServerHttpHeadersWriterTests.java | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java index 1f636f5cd3..1e7d422a4b 100644 --- a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java @@ -17,7 +17,6 @@ package org.springframework.security.web.server.header; import java.util.Arrays; -import java.util.Collections; import reactor.core.publisher.Mono; @@ -41,8 +40,16 @@ public class StaticServerHttpHeadersWriter implements ServerHttpHeadersWriter { @Override public Mono writeHttpHeaders(ServerWebExchange exchange) { HttpHeaders headers = exchange.getResponse().getHeaders(); - boolean containsOneHeaderToAdd = Collections.disjoint(headers.keySet(), this.headersToAdd.keySet()); - if (containsOneHeaderToAdd) { + // Note: We need to ensure that the following algorithm compares headers + // case insensitively, which should be true of headers.containsKey(). + boolean containsNoHeadersToAdd = true; + for (String headerName : this.headersToAdd.keySet()) { + if (headers.containsKey(headerName)) { + containsNoHeadersToAdd = false; + break; + } + } + if (containsNoHeadersToAdd) { this.headersToAdd.forEach(headers::put); } return Mono.empty(); diff --git a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java index e411ee745b..c0ea8c34c3 100644 --- a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java @@ -16,11 +16,14 @@ package org.springframework.security.web.server.header; +import java.util.Locale; + import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; @@ -56,6 +59,24 @@ public class StaticServerHttpHeadersWriterTests { .containsOnly(headerValue); } + // gh-10557 + @Test + public void writeHeadersWhenHeaderWrittenWithDifferentCaseThenDoesNotWriteHeaders() { + String headerName = HttpHeaders.CACHE_CONTROL.toLowerCase(Locale.ROOT); + String headerValue = "max-age=120"; + this.headers.set(headerName, headerValue); + // Note: This test inverts which collection uses case sensitive headers, + // due to the fact that gh-10557 reports NettyHeadersAdapter as the + // response headers implementation, which is not accessible here. + HttpHeaders caseSensitiveHeaders = new HttpHeaders(new LinkedMultiValueMap<>()); + caseSensitiveHeaders.set(HttpHeaders.CACHE_CONTROL, CacheControlServerHttpHeadersWriter.CACHE_CONTRTOL_VALUE); + caseSensitiveHeaders.set(HttpHeaders.PRAGMA, CacheControlServerHttpHeadersWriter.PRAGMA_VALUE); + caseSensitiveHeaders.set(HttpHeaders.EXPIRES, CacheControlServerHttpHeadersWriter.EXPIRES_VALUE); + this.writer = new StaticServerHttpHeadersWriter(caseSensitiveHeaders); + this.writer.writeHttpHeaders(this.exchange); + assertThat(this.headers.get(headerName)).containsOnly(headerValue); + } + @Test public void writeHeadersWhenMultiHeaderThenWritesAllHeaders() { this.writer = StaticServerHttpHeadersWriter.builder() From bb2d80fea37a604b30b1d73faaabfbc9fe50dc66 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 1 Dec 2021 17:12:03 -0600 Subject: [PATCH 065/589] Update copyright year Issue gh-10557 --- .../web/server/header/StaticServerHttpHeadersWriter.java | 2 +- .../web/server/header/StaticServerHttpHeadersWriterTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java index 1e7d422a4b..fb3d3c4d77 100644 --- a/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java +++ b/web/src/main/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java index c0ea8c34c3..604d20d56d 100644 --- a/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/server/header/StaticServerHttpHeadersWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 3af619d56507068f39aaf059c6b0bbcc8925ed28 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 1 Dec 2021 13:32:44 -0600 Subject: [PATCH 066/589] Add hasIpAddress to Reactive Kotlin DSL Closes gh-10571 --- .../config/web/server/AuthorizeExchangeDsl.kt | 10 ++++- .../web/server/AuthorizeExchangeDslTests.kt | 42 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt index 8df31aaca5..bfd029ec98 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.springframework.security.authorization.AuthorizationDecision import org.springframework.security.authorization.ReactiveAuthorizationManager import org.springframework.security.core.Authentication import org.springframework.security.web.server.authorization.AuthorizationContext +import org.springframework.security.web.server.authorization.IpAddressReactiveAuthorizationManager import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers import org.springframework.security.web.util.matcher.RequestMatcher @@ -108,6 +109,13 @@ class AuthorizeExchangeDsl { fun hasAnyAuthority(vararg authorities: String): ReactiveAuthorizationManager = AuthorityReactiveAuthorizationManager.hasAnyAuthority(*authorities) + /** + * Require a specific IP or range of IP addresses. + * @since 5.7 + */ + fun hasIpAddress(ipAddress: String): ReactiveAuthorizationManager = + IpAddressReactiveAuthorizationManager.hasIpAddress(ipAddress) + /** * Require an authenticated user. */ diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt index fd840ebd93..870b9a9283 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt @@ -22,16 +22,16 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity -import org.springframework.security.core.userdetails.MapReactiveUserDetailsService -import org.springframework.security.core.userdetails.User import org.springframework.security.config.test.SpringTestContext import org.springframework.security.config.test.SpringTestContextExtension +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.reactive.config.EnableWebFlux -import java.util.* +import java.util.Base64 /** * Tests for [AuthorizeExchangeDsl] @@ -181,4 +181,40 @@ class AuthorizeExchangeDslTests { return MapReactiveUserDetailsService(user) } } + + @Test + fun `request when ip address does not match then responds with forbidden`() { + this.spring.register(HasIpAddressConfig::class.java).autowire() + + this.client + .get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + .expectStatus().isForbidden + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HasIpAddressConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, hasIpAddress("10.0.0.0/24")) + } + httpBasic { } + } + } + + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } } From 074e38d565990d1d040795957e4e1d531e9ae345 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 1 Dec 2021 10:50:42 -0600 Subject: [PATCH 067/589] Add missing since Issue gh-7765 --- .../security/config/web/server/ServerHttpSecurity.java | 1 + 1 file changed, 1 insertion(+) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index a5f6cc3548..8e3d7861a1 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -1689,6 +1689,7 @@ public class ServerHttpSecurity { * @param ipAddress the address or range of addresses from which the request * must come. * @return the {@link AuthorizeExchangeSpec} to configure + * @since 5.7 */ public AuthorizeExchangeSpec hasIpAddress(String ipAddress) { return access(IpAddressReactiveAuthorizationManager.hasIpAddress(ipAddress)); From 925d531cbe99357e3b1c9ab1dd8150218fce89e1 Mon Sep 17 00:00:00 2001 From: Karl Tinawi Date: Sun, 11 Apr 2021 15:13:13 +0100 Subject: [PATCH 068/589] Set details on authentication token created by HttpServlet3RequestFactory Currently the login mechanism when triggered by executing HttpServlet3RequestFactory#login does not set any details on the underlying authentication token that is authenticated. This change adds an AuthenticationDetailsSource on the HttpServlet3RequestFactory, which defaults to a WebAuthenticationDetailsSource. Closes gh-9579 --- .../web/configurers/ServletApiConfigurer.java | 6 +++++ .../ServletApiConfigurerTests.java | 26 +++++++++++++++++++ .../HttpServlet3RequestFactory.java | 22 +++++++++++++++- ...curityContextHolderAwareRequestFilter.java | 18 +++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java index 5959d9d08e..68ebc4350a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java @@ -21,6 +21,7 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -90,6 +91,11 @@ public final class ServletApiConfigurer> if (trustResolver != null) { this.securityContextRequestFilter.setTrustResolver(trustResolver); } + AuthenticationDetailsSource authenticationDetailsSource = http + .getSharedObject(AuthenticationDetailsSource.class); + if (authenticationDetailsSource != null) { + this.securityContextRequestFilter.setAuthenticationDetailsSource(authenticationDetailsSource); + } ApplicationContext context = http.getSharedObject(ApplicationContext.class); if (context != null) { String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java index f86916d26a..aedc12417c 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -149,6 +150,15 @@ public class ServletApiConfigurerTests { verify(SharedTrustResolverConfig.TR, atLeastOnce()).isAnonymous(any()); } + @Test + public void configureWhenSharedObjectAuthenticationDetailsSourceThenAuthenticationDetailsSourceUsed() { + this.spring.register(SharedAuthenticationDetailsSourceConfig.class).autowire(); + SecurityContextHolderAwareRequestFilter scaFilter = getFilter(SecurityContextHolderAwareRequestFilter.class); + AuthenticationDetailsSource authenticationDetailsSource = getFieldValue(scaFilter, + "authenticationDetailsSource"); + assertThat(authenticationDetailsSource).isEqualTo(SharedAuthenticationDetailsSourceConfig.ADS); + } + @Test public void requestWhenServletApiWithDefaultsInLambdaThenUsesDefaultRolePrefix() throws Exception { this.spring.register(ServletApiWithDefaultsInLambdaConfig.class, AdminController.class).autowire(); @@ -321,6 +331,22 @@ public class ServletApiConfigurerTests { } + @EnableWebSecurity + static class SharedAuthenticationDetailsSourceConfig extends WebSecurityConfigurerAdapter { + + @SuppressWarnings("unchecked") + static AuthenticationDetailsSource ADS = spy(AuthenticationDetailsSource.class); + + @Override + protected void configure(HttpSecurity http) { + // @formatter:off + http + .setSharedObject(AuthenticationDetailsSource.class, ADS); + // @formatter:on + } + + } + @EnableWebSecurity static class ServletApiWithDefaultsInLambdaConfig extends WebSecurityConfigurerAdapter { diff --git a/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java b/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java index 51113ac551..00c2e0dead 100644 --- a/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java +++ b/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java @@ -32,6 +32,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; @@ -42,6 +43,7 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -79,6 +81,8 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory { private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + private AuthenticationEntryPoint authenticationEntryPoint; private AuthenticationManager authenticationManager; @@ -158,6 +162,18 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory { this.trustResolver = trustResolver; } + /** + * Sets the {@link AuthenticationDetailsSource} to be used. The default is + * {@link WebAuthenticationDetailsSource}. + * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} to use. + * Cannot be null. + */ + void setAuthenticationDetailsSource( + AuthenticationDetailsSource authenticationDetailsSource) { + Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); + this.authenticationDetailsSource = authenticationDetailsSource; + } + @Override public HttpServletRequest create(HttpServletRequest request, HttpServletResponse response) { return new Servlet3SecurityContextHolderAwareRequestWrapper(request, this.rolePrefix, response); @@ -233,7 +249,11 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory { private Authentication getAuthentication(AuthenticationManager authManager, String username, String password) throws ServletException { try { - return authManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, + password); + Object details = HttpServlet3RequestFactory.this.authenticationDetailsSource.buildDetails(this); + authentication.setDetails(details); + return authManager.authenticate(authentication); } catch (AuthenticationException ex) { SecurityContextHolder.clearContext(); diff --git a/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.java b/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.java index 2b6238e139..17ae1ce221 100644 --- a/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.java +++ b/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.java @@ -27,12 +27,14 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; @@ -80,6 +82,8 @@ public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean { private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + public void setRolePrefix(String rolePrefix) { Assert.notNull(rolePrefix, "Role prefix must not be null"); this.rolePrefix = rolePrefix; @@ -172,9 +176,23 @@ public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean { updateFactory(); } + /** + * Sets the {@link AuthenticationDetailsSource} to be used. The default is + * {@link WebAuthenticationDetailsSource}. + * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} to use. + * Cannot be null. + */ + public void setAuthenticationDetailsSource( + AuthenticationDetailsSource authenticationDetailsSource) { + Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); + this.authenticationDetailsSource = authenticationDetailsSource; + updateFactory(); + } + private HttpServletRequestFactory createServlet3Factory(String rolePrefix) { HttpServlet3RequestFactory factory = new HttpServlet3RequestFactory(rolePrefix); factory.setTrustResolver(this.trustResolver); + factory.setAuthenticationDetailsSource(this.authenticationDetailsSource); factory.setAuthenticationEntryPoint(this.authenticationEntryPoint); factory.setAuthenticationManager(this.authenticationManager); factory.setLogoutHandlers(this.logoutHandlers); From df0f6f83af42354d250986e645a7b009efe2931c Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 2 Dec 2021 16:28:22 -0600 Subject: [PATCH 069/589] Polish gh-9597 --- .../web/configurers/ServletApiConfigurer.java | 6 ----- .../ServletApiConfigurerTests.java | 26 ------------------ .../HttpServlet3RequestFactory.java | 14 +--------- ...curityContextHolderAwareRequestFilter.java | 18 ------------- ...yContextHolderAwareRequestFilterTests.java | 27 ++++++++++++++++++- 5 files changed, 27 insertions(+), 64 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java index 68ebc4350a..5959d9d08e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java @@ -21,7 +21,6 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; import org.springframework.context.ApplicationContext; -import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -91,11 +90,6 @@ public final class ServletApiConfigurer> if (trustResolver != null) { this.securityContextRequestFilter.setTrustResolver(trustResolver); } - AuthenticationDetailsSource authenticationDetailsSource = http - .getSharedObject(AuthenticationDetailsSource.class); - if (authenticationDetailsSource != null) { - this.securityContextRequestFilter.setAuthenticationDetailsSource(authenticationDetailsSource); - } ApplicationContext context = http.getSharedObject(ApplicationContext.class); if (context != null) { String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java index aedc12417c..f86916d26a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java @@ -30,7 +30,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -150,15 +149,6 @@ public class ServletApiConfigurerTests { verify(SharedTrustResolverConfig.TR, atLeastOnce()).isAnonymous(any()); } - @Test - public void configureWhenSharedObjectAuthenticationDetailsSourceThenAuthenticationDetailsSourceUsed() { - this.spring.register(SharedAuthenticationDetailsSourceConfig.class).autowire(); - SecurityContextHolderAwareRequestFilter scaFilter = getFilter(SecurityContextHolderAwareRequestFilter.class); - AuthenticationDetailsSource authenticationDetailsSource = getFieldValue(scaFilter, - "authenticationDetailsSource"); - assertThat(authenticationDetailsSource).isEqualTo(SharedAuthenticationDetailsSourceConfig.ADS); - } - @Test public void requestWhenServletApiWithDefaultsInLambdaThenUsesDefaultRolePrefix() throws Exception { this.spring.register(ServletApiWithDefaultsInLambdaConfig.class, AdminController.class).autowire(); @@ -331,22 +321,6 @@ public class ServletApiConfigurerTests { } - @EnableWebSecurity - static class SharedAuthenticationDetailsSourceConfig extends WebSecurityConfigurerAdapter { - - @SuppressWarnings("unchecked") - static AuthenticationDetailsSource ADS = spy(AuthenticationDetailsSource.class); - - @Override - protected void configure(HttpSecurity http) { - // @formatter:off - http - .setSharedObject(AuthenticationDetailsSource.class, ADS); - // @formatter:on - } - - } - @EnableWebSecurity static class ServletApiWithDefaultsInLambdaConfig extends WebSecurityConfigurerAdapter { diff --git a/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java b/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java index 00c2e0dead..ed6208b938 100644 --- a/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java +++ b/web/src/main/java/org/springframework/security/web/servletapi/HttpServlet3RequestFactory.java @@ -81,7 +81,7 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory { private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); + private final AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); private AuthenticationEntryPoint authenticationEntryPoint; @@ -162,18 +162,6 @@ final class HttpServlet3RequestFactory implements HttpServletRequestFactory { this.trustResolver = trustResolver; } - /** - * Sets the {@link AuthenticationDetailsSource} to be used. The default is - * {@link WebAuthenticationDetailsSource}. - * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} to use. - * Cannot be null. - */ - void setAuthenticationDetailsSource( - AuthenticationDetailsSource authenticationDetailsSource) { - Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); - this.authenticationDetailsSource = authenticationDetailsSource; - } - @Override public HttpServletRequest create(HttpServletRequest request, HttpServletResponse response) { return new Servlet3SecurityContextHolderAwareRequestWrapper(request, this.rolePrefix, response); diff --git a/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.java b/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.java index 17ae1ce221..2b6238e139 100644 --- a/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.java +++ b/web/src/main/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilter.java @@ -27,14 +27,12 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; @@ -82,8 +80,6 @@ public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean { private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); - private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); - public void setRolePrefix(String rolePrefix) { Assert.notNull(rolePrefix, "Role prefix must not be null"); this.rolePrefix = rolePrefix; @@ -176,23 +172,9 @@ public class SecurityContextHolderAwareRequestFilter extends GenericFilterBean { updateFactory(); } - /** - * Sets the {@link AuthenticationDetailsSource} to be used. The default is - * {@link WebAuthenticationDetailsSource}. - * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} to use. - * Cannot be null. - */ - public void setAuthenticationDetailsSource( - AuthenticationDetailsSource authenticationDetailsSource) { - Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); - this.authenticationDetailsSource = authenticationDetailsSource; - updateFactory(); - } - private HttpServletRequestFactory createServlet3Factory(String rolePrefix) { HttpServlet3RequestFactory factory = new HttpServlet3RequestFactory(rolePrefix); factory.setTrustResolver(this.trustResolver); - factory.setAuthenticationDetailsSource(this.authenticationDetailsSource); factory.setAuthenticationEntryPoint(this.authenticationEntryPoint); factory.setAuthenticationManager(this.authenticationManager); factory.setLogoutHandlers(this.logoutHandlers); diff --git a/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java b/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java index 5e81db0a10..611f66f125 100644 --- a/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * Copyright 2004, 2005, 2006, 2021 Acegi Technology Pty Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -45,12 +46,14 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -59,6 +62,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; /** * Tests {@link SecurityContextHolderAwareRequestFilter}. @@ -217,6 +221,27 @@ public class SecurityContextHolderAwareRequestFilterTests { verifyZeroInteractions(this.authenticationEntryPoint, this.authenticationManager, this.logoutHandler); } + @Test + public void loginWhenHttpServletRequestHasAuthenticationDetailsThenAuthenticationRequestHasDetails() + throws Exception { + String ipAddress = "10.0.0.100"; + String sessionId = "session-id"; + when(this.request.getRemoteAddr()).thenReturn(ipAddress); + when(this.request.getSession(anyBoolean())).thenReturn(new MockHttpSession(null, sessionId)); + wrappedRequest().login("username", "password"); + + ArgumentCaptor authenticationCaptor = ArgumentCaptor + .forClass(UsernamePasswordAuthenticationToken.class); + verify(this.authenticationManager).authenticate(authenticationCaptor.capture()); + + UsernamePasswordAuthenticationToken authenticationRequest = authenticationCaptor.getValue(); + assertThat(authenticationRequest.getDetails()).isInstanceOf(WebAuthenticationDetails.class); + + WebAuthenticationDetails details = (WebAuthenticationDetails) authenticationRequest.getDetails(); + assertThat(details.getRemoteAddress()).isEqualTo(ipAddress); + assertThat(details.getSessionId()).isEqualTo(sessionId); + } + @Test public void logout() throws Exception { TestingAuthenticationToken expectedAuth = new TestingAuthenticationToken("user", "password", "ROLE_USER"); From 62e8799a8dff65582a1214406bc4592773ce98b9 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 2 Dec 2021 17:40:25 -0600 Subject: [PATCH 070/589] Use BDD in tests --- .../SecurityContextHolderAwareRequestFilterTests.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java b/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java index 611f66f125..78352903c1 100644 --- a/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/servletapi/SecurityContextHolderAwareRequestFilterTests.java @@ -62,7 +62,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; /** * Tests {@link SecurityContextHolderAwareRequestFilter}. @@ -226,8 +225,8 @@ public class SecurityContextHolderAwareRequestFilterTests { throws Exception { String ipAddress = "10.0.0.100"; String sessionId = "session-id"; - when(this.request.getRemoteAddr()).thenReturn(ipAddress); - when(this.request.getSession(anyBoolean())).thenReturn(new MockHttpSession(null, sessionId)); + given(this.request.getRemoteAddr()).willReturn(ipAddress); + given(this.request.getSession(anyBoolean())).willReturn(new MockHttpSession(null, sessionId)); wrappedRequest().login("username", "password"); ArgumentCaptor authenticationCaptor = ArgumentCaptor From ed3b0fbaad5099f6301e20aee700bdf2320ca9c9 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Thu, 2 Dec 2021 15:58:09 -0300 Subject: [PATCH 071/589] Prevent using both authorizeRequests and authorizeHttpRequests Closes gh-10573 --- .../annotation/web/builders/HttpSecurity.java | 7 +++ .../HttpSecurityConfigurationTests.java | 58 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 14d32ecd76..3ed743869b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -2889,8 +2889,15 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder expressionConfigurer = getConfigurer( + ExpressionUrlAuthorizationConfigurer.class); + AuthorizeHttpRequestsConfigurer httpConfigurer = getConfigurer(AuthorizeHttpRequestsConfigurer.class); + boolean oneConfigurerPresent = expressionConfigurer == null ^ httpConfigurer == null; + Assert.state((expressionConfigurer == null && httpConfigurer == null) || oneConfigurerPresent, + "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one."); this.filters.sort(OrderComparator.INSTANCE); List sortedFilters = new ArrayList<>(this.filters.size()); for (Filter filter : this.filters) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java index 813723e283..1a4f22e69f 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfigurationTests.java @@ -24,6 +24,7 @@ import com.google.common.net.HttpHeaders; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,6 +48,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -200,6 +202,24 @@ public class HttpSecurityConfigurationTests { this.mockMvc.perform(get("/login?logout")).andExpect(status().isOk()); } + @Test + public void configureWhenAuthorizeHttpRequestsBeforeAuthorizeRequestThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy( + () -> this.spring.register(AuthorizeHttpRequestsBeforeAuthorizeRequestsConfig.class).autowire()) + .withMessageContaining( + "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one."); + } + + @Test + public void configureWhenAuthorizeHttpRequestsAfterAuthorizeRequestThenException() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy( + () -> this.spring.register(AuthorizeHttpRequestsAfterAuthorizeRequestsConfig.class).autowire()) + .withMessageContaining( + "authorizeHttpRequests cannot be used in conjunction with authorizeRequests. Please select just one."); + } + @RestController static class NameController { @@ -270,6 +290,44 @@ public class HttpSecurityConfigurationTests { } + @EnableWebSecurity + static class AuthorizeHttpRequestsBeforeAuthorizeRequestsConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ) + .authorizeRequests((requests) -> requests + .anyRequest().authenticated() + ) + .build(); + // @formatter:on + } + + } + + @EnableWebSecurity + static class AuthorizeHttpRequestsAfterAuthorizeRequestsConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeRequests((requests) -> requests + .anyRequest().authenticated() + ) + .authorizeHttpRequests((requests) -> requests + .anyRequest().authenticated() + ) + .build(); + // @formatter:on + } + + } + @RestController static class BaseController { From 7ec3b55ab33c2d639557dfdb28d6912177f3fe27 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 3 Dec 2021 17:21:02 -0600 Subject: [PATCH 072/589] Fix Reactive OAuth2 Kotlin DSL examples Closes gh-10580 --- .../oauth2/client/authorization-grants.adoc | 12 ++----- .../pages/reactive/oauth2/client/index.adoc | 4 +-- .../pages/reactive/oauth2/login/advanced.adoc | 32 +++++-------------- .../pages/reactive/oauth2/login/core.adoc | 12 ++----- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc index 11fe4d541b..ab33687111 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc @@ -162,7 +162,7 @@ class SecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } @@ -170,8 +170,6 @@ class SecurityConfig { authorizationRequestResolver = authorizationRequestResolver(customClientRegistrationRepository) } } - - return http.build() } private fun authorizationRequestResolver( @@ -282,13 +280,11 @@ class OAuth2ClientSecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Client { authorizationRequestRepository = authorizationRequestRepository() } } - - return http.build() } } ---- @@ -363,13 +359,11 @@ class OAuth2ClientSecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Client { authenticationManager = authorizationCodeAuthenticationManager() } } - - return http.build() } private fun authorizationCodeAuthenticationManager(): ReactiveAuthenticationManager { diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc index b04019a5a4..614a3e45fa 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/index.adoc @@ -55,7 +55,7 @@ class OAuth2ClientSecurityConfig { @Bean fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Client { clientRegistrationRepository = clientRegistrationRepository() authorizedClientRepository = authorizedClientRepository() @@ -64,8 +64,6 @@ class OAuth2ClientSecurityConfig { authenticationManager = authenticationManager() } } - - return http.build() } } ---- diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc index a9b0a23e65..8dfb8c9e93 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/advanced.adoc @@ -60,7 +60,7 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { authenticationConverter = authenticationConverter() authenticationMatcher = authenticationMatcher() @@ -75,8 +75,6 @@ class OAuth2LoginSecurityConfig { securityContextRepository = securityContextRepository() } } - - return http.build() } } ---- @@ -158,7 +156,7 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { exceptionHandling { authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/login/oauth2") } @@ -166,8 +164,6 @@ class OAuth2LoginSecurityConfig { authorizationRequestResolver = authorizationRequestResolver() } } - - return http.build() } private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver { @@ -243,13 +239,11 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}") } } - - return http.build() } } ---- @@ -369,11 +363,9 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { } } - - return http.build() } @Bean @@ -458,11 +450,9 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { } } - - return http.build() } @Bean @@ -536,11 +526,9 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { } } - - return http.build() } @Bean @@ -594,11 +582,9 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { oauth2Login { } } - - return http.build() } @Bean @@ -730,7 +716,7 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } @@ -739,8 +725,6 @@ class OAuth2LoginSecurityConfig { logoutSuccessHandler = oidcLogoutSuccessHandler() } } - - return http.build() } private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler { diff --git a/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc index 9c6a47f752..037fcff5f1 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/login/core.adoc @@ -341,14 +341,12 @@ class OAuth2LoginSecurityConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } oauth2Login { } } - - return http.build() } } ---- @@ -411,14 +409,12 @@ class OAuth2LoginConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } oauth2Login { } } - - return http.build() } @Bean @@ -505,14 +501,12 @@ class OAuth2LoginConfig { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { - http { + return http { authorizeExchange { authorize(anyExchange, authenticated) } oauth2Login { } } - - return http.build() } @Bean From 65426a40ec932803b423587f2de5ecb94982f606 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 3 Dec 2021 16:47:21 -0300 Subject: [PATCH 073/589] Add Cross Origin Policies headers Add DSL support for Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy and Cross-Origin-Resource-Policy headers Closes gh-9385, gh-10118 --- .../web/configurers/HeadersConfigurer.java | 229 +++++++++++++++++- .../http/HeadersBeanDefinitionParser.java | 83 ++++++- .../config/web/server/ServerHttpSecurity.java | 186 +++++++++++++- .../ServerCrossOriginEmbedderPolicyDsl.kt | 42 ++++ .../ServerCrossOriginOpenerPolicyDsl.kt | 42 ++++ .../ServerCrossOriginResourcePolicyDsl.kt | 42 ++++ .../config/web/server/ServerHeadersDsl.kt | 52 +++- .../security/config/web/servlet/HeadersDsl.kt | 60 +++++ .../headers/CrossOriginEmbedderPolicyDsl.kt | 43 ++++ .../headers/CrossOriginOpenerPolicyDsl.kt | 43 ++++ .../headers/CrossOriginResourcePolicyDsl.kt | 43 ++++ .../security/config/spring-security-5.7.rnc | 23 +- .../security/config/spring-security-5.7.xsd | 74 ++++++ .../configurers/HeadersConfigurerTests.java | 78 +++++- .../config/http/HttpHeadersConfigTests.java | 50 +++- .../config/web/server/HeaderSpecTests.java | 53 +++- .../web/server/ServerHeadersDslTests.kt | 59 +++++ .../config/web/servlet/HeadersDslTests.kt | 2 +- ...sDisabledWithCrossOriginEmbedderPolicy.xml | 36 +++ ...ltsDisabledWithCrossOriginOpenerPolicy.xml | 36 +++ ...efaultsDisabledWithCrossOriginPolicies.xml | 38 +++ ...sDisabledWithCrossOriginResourcePolicy.xml | 36 +++ .../ROOT/pages/features/exploits/headers.adoc | 20 ++ .../ROOT/pages/reactive/exploits/headers.adoc | 62 +++++ .../servlet/appendix/namespace/http.adoc | 66 +++++ .../ROOT/pages/servlet/exploits/headers.adoc | 61 +++++ ...CrossOriginEmbedderPolicyHeaderWriter.java | 84 +++++++ .../CrossOriginOpenerPolicyHeaderWriter.java | 86 +++++++ ...CrossOriginResourcePolicyHeaderWriter.java | 86 +++++++ ...EmbedderPolicyServerHttpHeadersWriter.java | 78 ++++++ ...inOpenerPolicyServerHttpHeadersWriter.java | 80 ++++++ ...ResourcePolicyServerHttpHeadersWriter.java | 80 ++++++ ...OriginEmbedderPolicyHeaderWriterTests.java | 80 ++++++ ...ssOriginOpenerPolicyHeaderWriterTests.java | 80 ++++++ ...OriginResourcePolicyHeaderWriterTests.java | 80 ++++++ ...derPolicyServerHttpHeadersWriterTests.java | 76 ++++++ ...nerPolicyServerHttpHeadersWriterTests.java | 77 ++++++ ...rcePolicyServerHttpHeadersWriterTests.java | 76 ++++++ 38 files changed, 2513 insertions(+), 9 deletions(-) create mode 100644 config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt create mode 100644 config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt create mode 100644 config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt create mode 100644 config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginEmbedderPolicyDsl.kt create mode 100644 config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginOpenerPolicyDsl.kt create mode 100644 config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginResourcePolicyDsl.kt create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginEmbedderPolicy.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginOpenerPolicy.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginPolicies.xml create mode 100644 config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginResourcePolicy.xml create mode 100644 web/src/main/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriter.java create mode 100644 web/src/main/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriter.java create mode 100644 web/src/main/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriter.java create mode 100644 web/src/main/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriter.java create mode 100644 web/src/main/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriter.java create mode 100644 web/src/main/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriter.java create mode 100644 web/src/test/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriterTests.java create mode 100644 web/src/test/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriterTests.java create mode 100644 web/src/test/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriterTests.java create mode 100644 web/src/test/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriterTests.java create mode 100644 web/src/test/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriterTests.java create mode 100644 web/src/test/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriterTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index cd177581b9..bd20c50953 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,9 @@ import org.springframework.security.web.header.HeaderWriter; import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.header.writers.CacheControlHeadersWriter; import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter; import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter; import org.springframework.security.web.header.writers.HpkpHeaderWriter; import org.springframework.security.web.header.writers.HstsHeaderWriter; @@ -97,6 +100,12 @@ public class HeadersConfigurer> private final PermissionsPolicyConfig permissionsPolicy = new PermissionsPolicyConfig(); + private final CrossOriginOpenerPolicyConfig crossOriginOpenerPolicy = new CrossOriginOpenerPolicyConfig(); + + private final CrossOriginEmbedderPolicyConfig crossOriginEmbedderPolicy = new CrossOriginEmbedderPolicyConfig(); + + private final CrossOriginResourcePolicyConfig crossOriginResourcePolicy = new CrossOriginResourcePolicyConfig(); + /** * Creates a new instance * @@ -392,6 +401,9 @@ public class HeadersConfigurer> addIfNotNull(writers, this.referrerPolicy.writer); addIfNotNull(writers, this.featurePolicy.writer); addIfNotNull(writers, this.permissionsPolicy.writer); + addIfNotNull(writers, this.crossOriginOpenerPolicy.writer); + addIfNotNull(writers, this.crossOriginEmbedderPolicy.writer); + addIfNotNull(writers, this.crossOriginResourcePolicy.writer); writers.addAll(this.headerWriters); return writers; } @@ -544,6 +556,129 @@ public class HeadersConfigurer> return this.permissionsPolicy; } + /** + * Allows configuration for + * Cross-Origin-Opener-Policy header. + *

+ * Configuration is provided to the {@link CrossOriginOpenerPolicyHeaderWriter} which + * responsible for writing the header. + *

+ * @return the {@link CrossOriginOpenerPolicyConfig} for additional confniguration + * @since 5.7 + * @see CrossOriginOpenerPolicyHeaderWriter + */ + public CrossOriginOpenerPolicyConfig crossOriginOpenerPolicy() { + this.crossOriginOpenerPolicy.writer = new CrossOriginOpenerPolicyHeaderWriter(); + return this.crossOriginOpenerPolicy; + } + + /** + * Allows configuration for + * Cross-Origin-Opener-Policy header. + *

+ * Calling this method automatically enables (includes) the + * {@code Cross-Origin-Opener-Policy} header in the response using the supplied + * policy. + *

+ *

+ * Configuration is provided to the {@link CrossOriginOpenerPolicyHeaderWriter} which + * responsible for writing the header. + *

+ * @return the {@link HeadersConfigurer} for additional customizations + * @since 5.7 + * @see CrossOriginOpenerPolicyHeaderWriter + */ + public HeadersConfigurer crossOriginOpenerPolicy( + Customizer crossOriginOpenerPolicyCustomizer) { + this.crossOriginOpenerPolicy.writer = new CrossOriginOpenerPolicyHeaderWriter(); + crossOriginOpenerPolicyCustomizer.customize(this.crossOriginOpenerPolicy); + return HeadersConfigurer.this; + } + + /** + * Allows configuration for + * Cross-Origin-Embedder-Policy header. + *

+ * Configuration is provided to the {@link CrossOriginEmbedderPolicyHeaderWriter} + * which is responsible for writing the header. + *

+ * @return the {@link CrossOriginEmbedderPolicyConfig} for additional customizations + * @since 5.7 + * @see CrossOriginEmbedderPolicyHeaderWriter + */ + public CrossOriginEmbedderPolicyConfig crossOriginEmbedderPolicy() { + this.crossOriginEmbedderPolicy.writer = new CrossOriginEmbedderPolicyHeaderWriter(); + return this.crossOriginEmbedderPolicy; + } + + /** + * Allows configuration for + * Cross-Origin-Embedder-Policy header. + *

+ * Calling this method automatically enables (includes) the + * {@code Cross-Origin-Embedder-Policy} header in the response using the supplied + * policy. + *

+ *

+ * Configuration is provided to the {@link CrossOriginEmbedderPolicyHeaderWriter} + * which is responsible for writing the header. + *

+ * @return the {@link HeadersConfigurer} for additional customizations + * @since 5.7 + * @see CrossOriginEmbedderPolicyHeaderWriter + */ + public HeadersConfigurer crossOriginEmbedderPolicy( + Customizer crossOriginEmbedderPolicyCustomizer) { + this.crossOriginEmbedderPolicy.writer = new CrossOriginEmbedderPolicyHeaderWriter(); + crossOriginEmbedderPolicyCustomizer.customize(this.crossOriginEmbedderPolicy); + return HeadersConfigurer.this; + } + + /** + * Allows configuration for + * Cross-Origin-Resource-Policy header. + *

+ * Configuration is provided to the {@link CrossOriginResourcePolicyHeaderWriter} + * which is responsible for writing the header: + *

+ * @return the {@link HeadersConfigurer} for additional customizations + * @since 5.7 + * @see CrossOriginResourcePolicyHeaderWriter + */ + public CrossOriginResourcePolicyConfig crossOriginResourcePolicy() { + this.crossOriginResourcePolicy.writer = new CrossOriginResourcePolicyHeaderWriter(); + return this.crossOriginResourcePolicy; + } + + /** + * Allows configuration for + * Cross-Origin-Resource-Policy header. + *

+ * Calling this method automatically enables (includes) the + * {@code Cross-Origin-Resource-Policy} header in the response using the supplied + * policy. + *

+ *

+ * Configuration is provided to the {@link CrossOriginResourcePolicyHeaderWriter} + * which is responsible for writing the header: + *

+ * @return the {@link HeadersConfigurer} for additional customizations + * @since 5.7 + * @see CrossOriginResourcePolicyHeaderWriter + */ + public HeadersConfigurer crossOriginResourcePolicy( + Customizer crossOriginResourcePolicyCustomizer) { + this.crossOriginResourcePolicy.writer = new CrossOriginResourcePolicyHeaderWriter(); + crossOriginResourcePolicyCustomizer.customize(this.crossOriginResourcePolicy); + return HeadersConfigurer.this; + } + public final class ContentTypeOptionsConfig { private XContentTypeOptionsHeaderWriter writer; @@ -1142,4 +1277,96 @@ public class HeadersConfigurer> } + public final class CrossOriginOpenerPolicyConfig { + + private CrossOriginOpenerPolicyHeaderWriter writer; + + public CrossOriginOpenerPolicyConfig() { + } + + /** + * Sets the policy to be used in the {@code Cross-Origin-Opener-Policy} header + * @param openerPolicy a {@code Cross-Origin-Opener-Policy} + * @return the {@link CrossOriginOpenerPolicyConfig} for additional configuration + * @throws IllegalArgumentException if openerPolicy is null + */ + public CrossOriginOpenerPolicyConfig policy( + CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy openerPolicy) { + this.writer.setPolicy(openerPolicy); + return this; + } + + /** + * Allows completing configuration of Cross Origin Opener Policy and continuing + * configuration of headers. + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + + public final class CrossOriginEmbedderPolicyConfig { + + private CrossOriginEmbedderPolicyHeaderWriter writer; + + public CrossOriginEmbedderPolicyConfig() { + } + + /** + * Sets the policy to be used in the {@code Cross-Origin-Embedder-Policy} header + * @param embedderPolicy a {@code Cross-Origin-Embedder-Policy} + * @return the {@link CrossOriginEmbedderPolicyConfig} for additional + * configuration + * @throws IllegalArgumentException if embedderPolicy is null + */ + public CrossOriginEmbedderPolicyConfig policy( + CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy embedderPolicy) { + this.writer.setPolicy(embedderPolicy); + return this; + } + + /** + * Allows completing configuration of Cross-Origin-Embedder-Policy and continuing + * configuration of headers. + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + + public final class CrossOriginResourcePolicyConfig { + + private CrossOriginResourcePolicyHeaderWriter writer; + + public CrossOriginResourcePolicyConfig() { + } + + /** + * Sets the policy to be used in the {@code Cross-Origin-Resource-Policy} header + * @param resourcePolicy a {@code Cross-Origin-Resource-Policy} + * @return the {@link CrossOriginResourcePolicyConfig} for additional + * configuration + * @throws IllegalArgumentException if resourcePolicy is null + */ + public CrossOriginResourcePolicyConfig policy( + CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy resourcePolicy) { + this.writer.setPolicy(resourcePolicy); + return this; + } + + /** + * Allows completing configuration of Cross-Origin-Resource-Policy and continuing + * configuration of headers. + * @return the {@link HeadersConfigurer} for additional configuration + */ + public HeadersConfigurer and() { + return HeadersConfigurer.this; + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java index 7f42ff724e..b980f635a7 100644 --- a/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,9 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.header.writers.CacheControlHeadersWriter; import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter; import org.springframework.security.web.header.writers.FeaturePolicyHeaderWriter; import org.springframework.security.web.header.writers.HpkpHeaderWriter; import org.springframework.security.web.header.writers.HstsHeaderWriter; @@ -122,6 +125,12 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { private static final String PERMISSIONS_POLICY_ELEMENT = "permissions-policy"; + private static final String CROSS_ORIGIN_OPENER_POLICY_ELEMENT = "cross-origin-opener-policy"; + + private static final String CROSS_ORIGIN_EMBEDDER_POLICY_ELEMENT = "cross-origin-embedder-policy"; + + private static final String CROSS_ORIGIN_RESOURCE_POLICY_ELEMENT = "cross-origin-resource-policy"; + private static final String ALLOW_FROM = "ALLOW-FROM"; private ManagedList headerWriters; @@ -144,6 +153,9 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { parseReferrerPolicyElement(element, parserContext); parseFeaturePolicyElement(element, parserContext); parsePermissionsPolicyElement(element, parserContext); + parseCrossOriginOpenerPolicy(disabled, element); + parseCrossOriginEmbedderPolicy(disabled, element); + parseCrossOriginResourcePolicy(disabled, element); parseHeaderElements(element); boolean noWriters = this.headerWriters.isEmpty(); if (disabled && !noWriters) { @@ -376,6 +388,75 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser { this.headerWriters.add(headersWriter.getBeanDefinition()); } + private void parseCrossOriginOpenerPolicy(boolean elementDisabled, Element element) { + if (elementDisabled || element == null) { + return; + } + CrossOriginOpenerPolicyHeaderWriter writer = new CrossOriginOpenerPolicyHeaderWriter(); + Element crossOriginOpenerPolicyElement = DomUtils.getChildElementByTagName(element, + CROSS_ORIGIN_OPENER_POLICY_ELEMENT); + if (crossOriginOpenerPolicyElement != null) { + addCrossOriginOpenerPolicy(crossOriginOpenerPolicyElement, writer); + } + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(CrossOriginOpenerPolicyHeaderWriter.class, () -> writer); + this.headerWriters.add(builder.getBeanDefinition()); + } + + private void parseCrossOriginEmbedderPolicy(boolean elementDisabled, Element element) { + if (elementDisabled || element == null) { + return; + } + CrossOriginEmbedderPolicyHeaderWriter writer = new CrossOriginEmbedderPolicyHeaderWriter(); + Element crossOriginEmbedderPolicyElement = DomUtils.getChildElementByTagName(element, + CROSS_ORIGIN_EMBEDDER_POLICY_ELEMENT); + if (crossOriginEmbedderPolicyElement != null) { + addCrossOriginEmbedderPolicy(crossOriginEmbedderPolicyElement, writer); + } + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(CrossOriginEmbedderPolicyHeaderWriter.class, () -> writer); + this.headerWriters.add(builder.getBeanDefinition()); + } + + private void parseCrossOriginResourcePolicy(boolean elementDisabled, Element element) { + if (elementDisabled || element == null) { + return; + } + CrossOriginResourcePolicyHeaderWriter writer = new CrossOriginResourcePolicyHeaderWriter(); + Element crossOriginResourcePolicyElement = DomUtils.getChildElementByTagName(element, + CROSS_ORIGIN_RESOURCE_POLICY_ELEMENT); + if (crossOriginResourcePolicyElement != null) { + addCrossOriginResourcePolicy(crossOriginResourcePolicyElement, writer); + } + BeanDefinitionBuilder builder = BeanDefinitionBuilder + .genericBeanDefinition(CrossOriginResourcePolicyHeaderWriter.class, () -> writer); + this.headerWriters.add(builder.getBeanDefinition()); + } + + private void addCrossOriginResourcePolicy(Element crossOriginResourcePolicyElement, + CrossOriginResourcePolicyHeaderWriter writer) { + String policy = crossOriginResourcePolicyElement.getAttribute(ATT_POLICY); + if (StringUtils.hasText(policy)) { + writer.setPolicy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.from(policy)); + } + } + + private void addCrossOriginEmbedderPolicy(Element crossOriginEmbedderPolicyElement, + CrossOriginEmbedderPolicyHeaderWriter writer) { + String policy = crossOriginEmbedderPolicyElement.getAttribute(ATT_POLICY); + if (StringUtils.hasText(policy)) { + writer.setPolicy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.from(policy)); + } + } + + private void addCrossOriginOpenerPolicy(Element crossOriginOpenerPolicyElement, + CrossOriginOpenerPolicyHeaderWriter writer) { + String policy = crossOriginOpenerPolicyElement.getAttribute(ATT_POLICY); + if (StringUtils.hasText(policy)) { + writer.setPolicy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.from(policy)); + } + } + private void attrNotAllowed(ParserContext context, String attrName, String otherAttrName, Element element) { context.getReaderContext().error("Only one of '" + attrName + "' or '" + otherAttrName + "' can be set.", element); diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 8e3d7861a1..5c0094f079 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -149,6 +149,12 @@ import org.springframework.security.web.server.header.CacheControlServerHttpHead import org.springframework.security.web.server.header.CompositeServerHttpHeadersWriter; import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy; +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy; +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy; import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter; import org.springframework.security.web.server.header.PermissionsPolicyServerHttpHeadersWriter; @@ -2380,10 +2386,17 @@ public class ServerHttpSecurity { private ReferrerPolicyServerHttpHeadersWriter referrerPolicy = new ReferrerPolicyServerHttpHeadersWriter(); + private CrossOriginOpenerPolicyServerHttpHeadersWriter crossOriginOpenerPolicy = new CrossOriginOpenerPolicyServerHttpHeadersWriter(); + + private CrossOriginEmbedderPolicyServerHttpHeadersWriter crossOriginEmbedderPolicy = new CrossOriginEmbedderPolicyServerHttpHeadersWriter(); + + private CrossOriginResourcePolicyServerHttpHeadersWriter crossOriginResourcePolicy = new CrossOriginResourcePolicyServerHttpHeadersWriter(); + private HeaderSpec() { this.writers = new ArrayList<>(Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts, this.frameOptions, this.xss, this.featurePolicy, this.permissionsPolicy, this.contentSecurityPolicy, - this.referrerPolicy)); + this.referrerPolicy, this.crossOriginOpenerPolicy, this.crossOriginEmbedderPolicy, + this.crossOriginResourcePolicy)); } /** @@ -2595,6 +2608,84 @@ public class ServerHttpSecurity { return this; } + /** + * Configures the + * Cross-Origin-Opener-Policy header. + * @return the {@link CrossOriginOpenerPolicySpec} to configure + * @since 5.7 + * @see CrossOriginOpenerPolicyServerHttpHeadersWriter + */ + public CrossOriginOpenerPolicySpec crossOriginOpenerPolicy() { + return new CrossOriginOpenerPolicySpec(); + } + + /** + * Configures the + * Cross-Origin-Opener-Policy header. + * @return the {@link HeaderSpec} to customize + * @since 5.7 + * @see CrossOriginOpenerPolicyServerHttpHeadersWriter + */ + public HeaderSpec crossOriginOpenerPolicy( + Customizer crossOriginOpenerPolicyCustomizer) { + crossOriginOpenerPolicyCustomizer.customize(new CrossOriginOpenerPolicySpec()); + return this; + } + + /** + * Configures the + * Cross-Origin-Embedder-Policy header. + * @return the {@link CrossOriginEmbedderPolicySpec} to configure + * @since 5.7 + * @see CrossOriginEmbedderPolicyServerHttpHeadersWriter + */ + public CrossOriginEmbedderPolicySpec crossOriginEmbedderPolicy() { + return new CrossOriginEmbedderPolicySpec(); + } + + /** + * Configures the + * Cross-Origin-Embedder-Policy header. + * @return the {@link HeaderSpec} to customize + * @since 5.7 + * @see CrossOriginEmbedderPolicyServerHttpHeadersWriter + */ + public HeaderSpec crossOriginEmbedderPolicy( + Customizer crossOriginEmbedderPolicyCustomizer) { + crossOriginEmbedderPolicyCustomizer.customize(new CrossOriginEmbedderPolicySpec()); + return this; + } + + /** + * Configures the + * Cross-Origin-Resource-Policy header. + * @return the {@link CrossOriginResourcePolicySpec} to configure + * @since 5.7 + * @see CrossOriginResourcePolicyServerHttpHeadersWriter + */ + public CrossOriginResourcePolicySpec crossOriginResourcePolicy() { + return new CrossOriginResourcePolicySpec(); + } + + /** + * Configures the + * Cross-Origin-Resource-Policy header. + * @return the {@link HeaderSpec} to customize + * @since 5.7 + * @see CrossOriginResourcePolicyServerHttpHeadersWriter + */ + public HeaderSpec crossOriginResourcePolicy( + Customizer crossOriginResourcePolicyCustomizer) { + crossOriginResourcePolicyCustomizer.customize(new CrossOriginResourcePolicySpec()); + return this; + } + /** * Configures cache control headers * @@ -2910,6 +3001,99 @@ public class ServerHttpSecurity { } + /** + * Configures the Cross-Origin-Opener-Policy header + * + * @since 5.7 + */ + public final class CrossOriginOpenerPolicySpec { + + private CrossOriginOpenerPolicySpec() { + } + + /** + * Sets the value to be used in the `Cross-Origin-Opener-Policy` header + * @param openerPolicy a opener policy + * @return the {@link CrossOriginOpenerPolicySpec} to continue configuring + */ + public CrossOriginOpenerPolicySpec policy(CrossOriginOpenerPolicy openerPolicy) { + HeaderSpec.this.crossOriginOpenerPolicy.setPolicy(openerPolicy); + return this; + } + + /** + * Allows method chaining to continue configuring the + * {@link ServerHttpSecurity}. + * @return the {@link HeaderSpec} to continue configuring + */ + public HeaderSpec and() { + return HeaderSpec.this; + } + + } + + /** + * Configures the Cross-Origin-Embedder-Policy header + * + * @since 5.7 + */ + public final class CrossOriginEmbedderPolicySpec { + + private CrossOriginEmbedderPolicySpec() { + } + + /** + * Sets the value to be used in the `Cross-Origin-Embedder-Policy` header + * @param embedderPolicy a opener policy + * @return the {@link CrossOriginEmbedderPolicySpec} to continue configuring + */ + public CrossOriginEmbedderPolicySpec policy(CrossOriginEmbedderPolicy embedderPolicy) { + HeaderSpec.this.crossOriginEmbedderPolicy.setPolicy(embedderPolicy); + return this; + } + + /** + * Allows method chaining to continue configuring the + * {@link ServerHttpSecurity}. + * @return the {@link HeaderSpec} to continue configuring + */ + public HeaderSpec and() { + return HeaderSpec.this; + } + + } + + /** + * Configures the Cross-Origin-Resource-Policy header + * + * @since 5.7 + */ + public final class CrossOriginResourcePolicySpec { + + private CrossOriginResourcePolicySpec() { + } + + /** + * Sets the value to be used in the `Cross-Origin-Resource-Policy` header + * @param resourcePolicy a opener policy + * @return the {@link CrossOriginResourcePolicySpec} to continue configuring + */ + public CrossOriginResourcePolicySpec policy(CrossOriginResourcePolicy resourcePolicy) { + HeaderSpec.this.crossOriginResourcePolicy.setPolicy(resourcePolicy); + return this; + } + + /** + * Allows method chaining to continue configuring the + * {@link ServerHttpSecurity}. + * @return the {@link HeaderSpec} to continue configuring + */ + public HeaderSpec and() { + return HeaderSpec.this; + } + + } + } /** diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt new file mode 100644 index 0000000000..cf5ae7ec9d --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginEmbedderPolicyDsl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Embedder-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@ServerSecurityMarker +class ServerCrossOriginEmbedderPolicyDsl { + + var policy: CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.CrossOriginEmbedderPolicySpec) -> Unit { + return { crossOriginEmbedderPolicy -> + policy?.also { + crossOriginEmbedderPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt new file mode 100644 index 0000000000..70d6576c83 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginOpenerPolicyDsl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Opener-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@ServerSecurityMarker +class ServerCrossOriginOpenerPolicyDsl { + + var policy: CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.CrossOriginOpenerPolicySpec) -> Unit { + return { crossOriginOpenerPolicy -> + policy?.also { + crossOriginOpenerPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt new file mode 100644 index 0000000000..580ee355ee --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCrossOriginResourcePolicyDsl.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.server + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Resource-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@ServerSecurityMarker +class ServerCrossOriginResourcePolicyDsl { + + var policy: CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.CrossOriginResourcePolicySpec) -> Unit { + return { crossOriginResourcePolicy -> + policy?.also { + crossOriginResourcePolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt index f38b152721..37bd1f177a 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt @@ -16,7 +16,12 @@ package org.springframework.security.config.web.server -import org.springframework.security.web.server.header.* +import org.springframework.security.web.server.header.CacheControlServerHttpHeadersWriter +import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter +import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter /** * A Kotlin DSL to configure [ServerHttpSecurity] headers using idiomatic Kotlin code. @@ -35,6 +40,9 @@ class ServerHeadersDsl { private var referrerPolicy: ((ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit)? = null private var featurePolicyDirectives: String? = null private var permissionsPolicy: ((ServerHttpSecurity.HeaderSpec.PermissionsPolicySpec) -> Unit)? = null + private var crossOriginOpenerPolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginOpenerPolicySpec) -> Unit)? = null + private var crossOriginEmbedderPolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginEmbedderPolicySpec) -> Unit)? = null + private var crossOriginResourcePolicy: ((ServerHttpSecurity.HeaderSpec.CrossOriginResourcePolicySpec) -> Unit)? = null private var disabled = false @@ -157,6 +165,39 @@ class ServerHeadersDsl { this.permissionsPolicy = ServerPermissionsPolicyDsl().apply(permissionsPolicyConfig).get() } + /** + * Allows configuration for + * Cross-Origin-Opener-Policy header. + * + * @since 5.7 + * @param crossOriginOpenerPolicyConfig the customization to apply to the header + */ + fun crossOriginOpenerPolicy(crossOriginOpenerPolicyConfig: ServerCrossOriginOpenerPolicyDsl.() -> Unit) { + this.crossOriginOpenerPolicy = ServerCrossOriginOpenerPolicyDsl().apply(crossOriginOpenerPolicyConfig).get() + } + + /** + * Allows configuration for + * Cross-Origin-Embedder-Policy header. + * + * @since 5.7 + * @param crossOriginEmbedderPolicyConfig the customization to apply to the header + */ + fun crossOriginEmbedderPolicy(crossOriginEmbedderPolicyConfig: ServerCrossOriginEmbedderPolicyDsl.() -> Unit) { + this.crossOriginEmbedderPolicy = ServerCrossOriginEmbedderPolicyDsl().apply(crossOriginEmbedderPolicyConfig).get() + } + + /** + * Allows configuration for + * Cross-Origin-Resource-Policy header. + * + * @since 5.7 + * @param crossOriginResourcePolicyConfig the customization to apply to the header + */ + fun crossOriginResourcePolicy(crossOriginResourcePolicyConfig: ServerCrossOriginResourcePolicyDsl.() -> Unit) { + this.crossOriginResourcePolicy = ServerCrossOriginResourcePolicyDsl().apply(crossOriginResourcePolicyConfig).get() + } + /** * Disables HTTP response headers. */ @@ -194,6 +235,15 @@ class ServerHeadersDsl { referrerPolicy?.also { headers.referrerPolicy(referrerPolicy) } + crossOriginOpenerPolicy?.also { + headers.crossOriginOpenerPolicy(crossOriginOpenerPolicy) + } + crossOriginEmbedderPolicy?.also { + headers.crossOriginEmbedderPolicy(crossOriginEmbedderPolicy) + } + crossOriginResourcePolicy?.also { + headers.crossOriginResourcePolicy(crossOriginResourcePolicy) + } if (disabled) { headers.disable() } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt index 3079dd11ff..d7dd463929 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HeadersDsl.kt @@ -42,6 +42,9 @@ class HeadersDsl { private var referrerPolicy: ((HeadersConfigurer.ReferrerPolicyConfig) -> Unit)? = null private var featurePolicyDirectives: String? = null private var permissionsPolicy: ((HeadersConfigurer.PermissionsPolicyConfig) -> Unit)? = null + private var crossOriginOpenerPolicy: ((HeadersConfigurer.CrossOriginOpenerPolicyConfig) -> Unit)? = null + private var crossOriginEmbedderPolicy: ((HeadersConfigurer.CrossOriginEmbedderPolicyConfig) -> Unit)? = null + private var crossOriginResourcePolicy: ((HeadersConfigurer.CrossOriginResourcePolicyConfig) -> Unit)? = null private var disabled = false private var headerWriters = mutableListOf() @@ -181,6 +184,54 @@ class HeadersDsl { this.permissionsPolicy = PermissionsPolicyDsl().apply(permissionsPolicyConfig).get() } + /** + * Allows configuration for + * Cross-Origin-Opener-Policy header. + * + *

+ * Calling this method automatically enables (includes) the Cross-Origin-Opener-Policy + * header in the response using the supplied policy. + *

+ * + * @since 5.7 + * @param crossOriginOpenerPolicyConfig the customization to apply to the header + */ + fun crossOriginOpenerPolicy(crossOriginOpenerPolicyConfig: CrossOriginOpenerPolicyDsl.() -> Unit) { + this.crossOriginOpenerPolicy = CrossOriginOpenerPolicyDsl().apply(crossOriginOpenerPolicyConfig).get() + } + + /** + * Allows configuration for + * Cross-Origin-Embedder-Policy header. + * + *

+ * Calling this method automatically enables (includes) the Cross-Origin-Embedder-Policy + * header in the response using the supplied policy. + *

+ * + * @since 5.7 + * @param crossOriginEmbedderPolicyConfig the customization to apply to the header + */ + fun crossOriginEmbedderPolicy(crossOriginEmbedderPolicyConfig: CrossOriginEmbedderPolicyDsl.() -> Unit) { + this.crossOriginEmbedderPolicy = CrossOriginEmbedderPolicyDsl().apply(crossOriginEmbedderPolicyConfig).get() + } + + /** + * Configures the + * Cross-Origin-Resource-Policy header. + * + *

+ * Calling this method automatically enables (includes) the Cross-Origin-Resource-Policy + * header in the response using the supplied policy. + *

+ * + * @since 5.7 + * @param crossOriginResourcePolicyConfig the customization to apply to the header + */ + fun crossOriginResourcePolicy(crossOriginResourcePolicyConfig: CrossOriginResourcePolicyDsl.() -> Unit) { + this.crossOriginResourcePolicy = CrossOriginResourcePolicyDsl().apply(crossOriginResourcePolicyConfig).get() + } + /** * Adds a [HeaderWriter] instance. * @@ -238,6 +289,15 @@ class HeadersDsl { permissionsPolicy?.also { headers.permissionsPolicy(permissionsPolicy) } + crossOriginOpenerPolicy?.also { + headers.crossOriginOpenerPolicy(crossOriginOpenerPolicy) + } + crossOriginEmbedderPolicy?.also { + headers.crossOriginEmbedderPolicy(crossOriginEmbedderPolicy) + } + crossOriginResourcePolicy?.also { + headers.crossOriginResourcePolicy(crossOriginResourcePolicy) + } headerWriters.forEach { headerWriter -> headers.addHeaderWriter(headerWriter) } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginEmbedderPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginEmbedderPolicyDsl.kt new file mode 100644 index 0000000000..facd6e6d02 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginEmbedderPolicyDsl.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Embedder-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@HeadersSecurityMarker +class CrossOriginEmbedderPolicyDsl { + + var policy: CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy? = null + + internal fun get(): (HeadersConfigurer.CrossOriginEmbedderPolicyConfig) -> Unit { + return { crossOriginEmbedderPolicy -> + policy?.also { + crossOriginEmbedderPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginOpenerPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginOpenerPolicyDsl.kt new file mode 100644 index 0000000000..ea6c19da50 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginOpenerPolicyDsl.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Opener-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@HeadersSecurityMarker +class CrossOriginOpenerPolicyDsl { + + var policy: CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy? = null + + internal fun get(): (HeadersConfigurer.CrossOriginOpenerPolicyConfig) -> Unit { + return { crossOriginOpenerPolicy -> + policy?.also { + crossOriginOpenerPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginResourcePolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginResourcePolicyDsl.kt new file mode 100644 index 0000000000..fd58258205 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CrossOriginResourcePolicyDsl.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.web.servlet.headers + +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer +import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter + +/** + * A Kotlin DSL to configure the [HttpSecurity] Cross-Origin-Resource-Policy header using + * idiomatic Kotlin code. + * + * @author Marcus Da Coregio + * @since 5.7 + * @property policy the policy to be used in the response header. + */ +@HeadersSecurityMarker +class CrossOriginResourcePolicyDsl { + + var policy: CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy? = null + + internal fun get(): (HeadersConfigurer.CrossOriginResourcePolicyConfig) -> Unit { + return { crossOriginResourcePolicy -> + policy?.also { + crossOriginResourcePolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc index f8c3f8ab13..42b1349c0e 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.rnc @@ -943,7 +943,7 @@ csrf-options.attlist &= headers = ## Element for configuration of the HeaderWritersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers. -element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & header*)} +element headers { headers-options.attlist, (cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & hpkp? & content-security-policy? & referrer-policy? & feature-policy? & permissions-policy? & cross-origin-opener-policy? & cross-origin-embedder-policy? & cross-origin-resource-policy? & header*)} headers-options.attlist &= ## Specifies if the default headers should be disabled. Default false. attribute defaults-disabled {xsd:token}? @@ -1092,6 +1092,27 @@ content-type-options.attlist &= ## If disabled, the X-Content-Type-Options header will not be included. Default false. attribute disabled {xsd:boolean}? +cross-origin-opener-policy = + ## Adds support for Cross-Origin-Opener-Policy header + element cross-origin-opener-policy {cross-origin-opener-policy-options.attlist,empty} +cross-origin-opener-policy-options.attlist &= + ## The policies for the Cross-Origin-Opener-Policy header. + attribute policy {"unsafe-none","same-origin","same-origin-allow-popups"}? + +cross-origin-embedder-policy = + ## Adds support for Cross-Origin-Embedder-Policy header + element cross-origin-embedder-policy {cross-origin-embedder-policy-options.attlist,empty} +cross-origin-embedder-policy-options.attlist &= + ## The policies for the Cross-Origin-Embedder-Policy header. + attribute policy {"unsafe-none","require-corp"}? + +cross-origin-resource-policy = + ## Adds support for Cross-Origin-Resource-Policy header + element cross-origin-resource-policy {cross-origin-resource-policy-options.attlist,empty} +cross-origin-resource-policy-options.attlist &= + ## The policies for the Cross-Origin-Resource-Policy header. + attribute policy {"cross-origin","same-origin","same-site"}? + header= ## Add additional headers to the response. element header {header.attlist} diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd index 0297b1bafe..201953d2cf 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.7.xsd @@ -2768,6 +2768,9 @@ + + + @@ -3151,6 +3154,77 @@ + + + Adds support for Cross-Origin-Opener-Policy header + + + + + + + + + + The policies for the Cross-Origin-Opener-Policy header. + + + + + + + + + + + + + + Adds support for Cross-Origin-Embedder-Policy header + + + + + + + + + + The policies for the Cross-Origin-Embedder-Policy header. + + + + + + + + + + + + + Adds support for Cross-Origin-Resource-Policy header + + + + + + + + + + The policies for the Cross-Origin-Resource-Policy header. + + + + + + + + + + + Add additional headers to the response. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java index cc64f4b92a..2b2f2a0b55 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,11 +26,16 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.header.writers.CrossOriginEmbedderPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter; +import org.springframework.security.web.header.writers.CrossOriginResourcePolicyHeaderWriter; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode; import org.springframework.test.web.servlet.MockMvc; @@ -52,6 +57,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Eddú Meléndez * @author Vedran Pavic * @author Eleftheria Stein + * @author Marcus Da Coregio */ @ExtendWith(SpringTestContextExtension.class) public class HeadersConfigurerTests { @@ -514,6 +520,30 @@ public class HeadersConfigurerTests { assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.STRICT_TRANSPORT_SECURITY); } + @Test + public void getWhenCustomCrossOriginPoliciesInLambdaThenCrossOriginPolicyHeadersWithCustomValuesInResponse() + throws Exception { + this.spring.register(CrossOriginCustomPoliciesInLambdaConfig.class).autowire(); + MvcResult mvcResult = this.mvc.perform(get("/")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_OPENER_POLICY, "same-origin")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_EMBEDDER_POLICY, "require-corp")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY, "same-origin")).andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.CROSS_ORIGIN_OPENER_POLICY, + HttpHeaders.CROSS_ORIGIN_EMBEDDER_POLICY, HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY); + } + + @Test + public void getWhenCustomCrossOriginPoliciesThenCrossOriginPolicyHeadersWithCustomValuesInResponse() + throws Exception { + this.spring.register(CrossOriginCustomPoliciesConfig.class).autowire(); + MvcResult mvcResult = this.mvc.perform(get("/")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_OPENER_POLICY, "same-origin")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_EMBEDDER_POLICY, "require-corp")) + .andExpect(header().string(HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY, "same-origin")).andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.CROSS_ORIGIN_OPENER_POLICY, + HttpHeaders.CROSS_ORIGIN_EMBEDDER_POLICY, HttpHeaders.CROSS_ORIGIN_RESOURCE_POLICY); + } + @EnableWebSecurity static class HeadersConfig extends WebSecurityConfigurerAdapter { @@ -1146,4 +1176,50 @@ public class HeadersConfigurerTests { } + @EnableWebSecurity + static class CrossOriginCustomPoliciesInLambdaConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http.headers((headers) -> headers + .defaultsDisabled() + .crossOriginOpenerPolicy((policy) -> policy + .policy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN) + ) + .crossOriginEmbedderPolicy((policy) -> policy + .policy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP) + ) + .crossOriginResourcePolicy((policy) -> policy + .policy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_ORIGIN) + ) + ); + // @formatter:on + return http.build(); + } + + } + + @EnableWebSecurity + static class CrossOriginCustomPoliciesConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // @formatter:off + http.headers() + .defaultsDisabled() + .crossOriginOpenerPolicy() + .policy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN) + .and() + .crossOriginEmbedderPolicy() + .policy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP) + .and() + .crossOriginResourcePolicy() + .policy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_ORIGIN); + // @formatter:on + return http.build(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java index c399b1994a..088bd5334d 100644 --- a/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/HttpHeadersConfigTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,6 +48,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Tim Ysewyn * @author Josh Cummings * @author Rafiullah Hamedy + * @author Marcus Da Coregio */ @ExtendWith(SpringTestContextExtension.class) public class HttpHeadersConfigTests { @@ -733,6 +734,53 @@ public class HttpHeadersConfigTests { // @formatter:on } + @Test + public void requestWhenCrossOriginOpenerPolicyWithSameOriginAllowPopupsThenRespondsWithSameOriginAllowPopups() + throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithCrossOriginOpenerPolicy")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Cross-Origin-Opener-Policy", "same-origin-allow-popups")); + // @formatter:on + } + + @Test + public void requestWhenCrossOriginEmbedderPolicyWithRequireCorpThenRespondsWithRequireCorp() throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithCrossOriginEmbedderPolicy")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Cross-Origin-Embedder-Policy", "require-corp")); + // @formatter:on + } + + @Test + public void requestWhenCrossOriginResourcePolicyWithSameOriginThenRespondsWithSameOrigin() throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithCrossOriginResourcePolicy")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Cross-Origin-Resource-Policy", "same-origin")); + // @formatter:on + } + + @Test + public void requestWhenCrossOriginPoliciesRespondsCrossOriginPolicies() throws Exception { + this.spring.configLocations(this.xml("DefaultsDisabledWithCrossOriginPolicies")).autowire(); + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(excludesDefaults()) + .andExpect(header().string("Cross-Origin-Opener-Policy", "same-origin")) + .andExpect(header().string("Cross-Origin-Embedder-Policy", "require-corp")) + .andExpect(header().string("Cross-Origin-Resource-Policy", "same-origin")); + // @formatter:on + } + private static ResultMatcher includesDefaults() { return includes(defaultHeaders); } diff --git a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java index a3167f2d14..f4b85f45ba 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter; +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter; import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy; @@ -48,6 +51,7 @@ import static org.springframework.security.config.Customizer.withDefaults; * @author Rob Winch * @author Vedran Pavic * @author Ankur Pathak + * @author Marcus Da Coregio * @since 5.0 */ public class HeaderSpecTests { @@ -406,6 +410,53 @@ public class HeaderSpecTests { assertHeaders(); } + @Test + public void headersWhenCrossOriginPoliciesCustomEnabledThenCustomCrossOriginPoliciesWritten() { + this.expectedHeaders.add(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY, + CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS + .getPolicy()); + this.expectedHeaders.add(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY, + CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP.getPolicy()); + this.expectedHeaders.add(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY, + CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN.getPolicy()); + // @formatter:off + this.http.headers() + .crossOriginOpenerPolicy() + .policy(CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS) + .and() + .crossOriginEmbedderPolicy() + .policy(CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP) + .and() + .crossOriginResourcePolicy() + .policy(CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN); + // @formatter:on + assertHeaders(); + } + + @Test + public void headersWhenCrossOriginPoliciesCustomEnabledInLambdaThenCustomCrossOriginPoliciesWritten() { + this.expectedHeaders.add(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY, + CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS + .getPolicy()); + this.expectedHeaders.add(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY, + CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP.getPolicy()); + this.expectedHeaders.add(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY, + CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN.getPolicy()); + // @formatter:off + this.http.headers() + .crossOriginOpenerPolicy((policy) -> policy + .policy(CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS) + ) + .crossOriginEmbedderPolicy((policy) -> policy + .policy(CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP) + ) + .crossOriginResourcePolicy((policy) -> policy + .policy(CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN) + ); + // @formatter:on + assertHeaders(); + } + private void expectHeaderNamesNotPresent(String... headerNames) { for (String headerName : headerNames) { this.expectedHeaders.remove(headerName); diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt index 3c404e2807..c68de3b4e7 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt @@ -28,6 +28,9 @@ import org.springframework.security.config.test.SpringTestContextExtension import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.CrossOriginEmbedderPolicyServerHttpHeadersWriter +import org.springframework.security.web.server.header.CrossOriginOpenerPolicyServerHttpHeadersWriter +import org.springframework.security.web.server.header.CrossOriginResourcePolicyServerHttpHeadersWriter import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter @@ -133,4 +136,60 @@ class ServerHeadersDslTests { } } } + + @Test + fun `request when no cross-origin policies configured then does not write cross-origin policies headers in response`() { + this.spring.register(CrossOriginPoliciesConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist("Cross-Origin-Opener-Policy") + .expectHeader().doesNotExist("Cross-Origin-Embedder-Policy") + .expectHeader().doesNotExist("Cross-Origin-Resource-Policy") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CrossOriginPoliciesConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { } + } + } + } + + @Test + fun `request when cross-origin custom policies configured then cross-origin custom policies headers in response`() { + this.spring.register(CrossOriginPoliciesCustomConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("Cross-Origin-Opener-Policy", "same-origin") + .expectHeader().valueEquals("Cross-Origin-Embedder-Policy", "require-corp") + .expectHeader().valueEquals("Cross-Origin-Resource-Policy", "same-origin") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CrossOriginPoliciesCustomConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + crossOriginOpenerPolicy { + policy = CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN + } + crossOriginEmbedderPolicy { + policy = CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP + } + crossOriginResourcePolicy { + policy = CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN + } + } + } + } + } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt index c2cbfb371d..b4e9964610 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HeadersDslTests.kt @@ -19,13 +19,13 @@ package org.springframework.security.config.web.servlet import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean import org.springframework.http.HttpHeaders import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.config.test.SpringTestContext import org.springframework.security.config.test.SpringTestContextExtension -import org.springframework.security.config.web.servlet.headers.PermissionsPolicyDsl import org.springframework.security.web.header.writers.StaticHeadersWriter import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginEmbedderPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginEmbedderPolicy.xml new file mode 100644 index 0000000000..cfa473c0d5 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginEmbedderPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginOpenerPolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginOpenerPolicy.xml new file mode 100644 index 0000000000..1e688e556b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginOpenerPolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginPolicies.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginPolicies.xml new file mode 100644 index 0000000000..d667ebc5e9 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginPolicies.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginResourcePolicy.xml b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginResourcePolicy.xml new file mode 100644 index 0000000000..667933f8d6 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/HttpHeadersConfigTests-DefaultsDisabledWithCrossOriginResourcePolicy.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/docs/modules/ROOT/pages/features/exploits/headers.adoc b/docs/modules/ROOT/pages/features/exploits/headers.adoc index be19d3028c..e142718275 100644 --- a/docs/modules/ROOT/pages/features/exploits/headers.adoc +++ b/docs/modules/ROOT/pages/features/exploits/headers.adoc @@ -378,6 +378,26 @@ Clear-Site-Data: "cache", "cookies", "storage", "executionContexts" This is a nice clean-up action to perform on logout. +[[headers-cross-origin-policies]] +== Cross-Origin Policies + +[NOTE] +==== +Refer to the relevant sections to see how to configure for both <> and <> based applications. +==== + +Spring Security provides support for some important Cross-Origin Policies headers. +Those headers are: + +* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy[`Cross-Origin-Opener-Policy`] +* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy[`Cross-Origin-Embedder-Policy`] +* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy[`Cross-Origin-Resource-Policy`] + +`Cross-Origin-Opener-Policy` (COOP) allows a top-level document to break the association between its window and any others in the browsing context group (e.g., between a popup and its opener), preventing any direct DOM access between them. + +Enabling `Cross-Origin-Embedder-Policy` (COEP) prevents a document from loading any non-same-origin resources which don't explicitly grant the document permission to be loaded. + +The `Cross-Origin-Resource-Policy` (CORP) header allows you to control the set of origins that are empowered to include a resource. It is a robust defense against attacks like https://meltdownattack.com[Spectre], as it allows browsers to block a given response before it enters an attacker's process. [[headers-custom]] == Custom Headers diff --git a/docs/modules/ROOT/pages/reactive/exploits/headers.adoc b/docs/modules/ROOT/pages/reactive/exploits/headers.adoc index 9b61d12b3a..30b4779fd9 100644 --- a/docs/modules/ROOT/pages/reactive/exploits/headers.adoc +++ b/docs/modules/ROOT/pages/reactive/exploits/headers.adoc @@ -578,3 +578,65 @@ fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { } ---- ==== + +[[webflux-headers-cross-origin-policies]] +== Cross-Origin Policies + +Spring Security provides built-in support for adding some Cross-Origin policies headers, those headers are: + +[source] +---- +Cross-Origin-Opener-Policy +Cross-Origin-Embedder-Policy +Cross-Origin-Resource-Policy +---- + +Spring Security does not add <> headers by default. +The headers can be added with the following configuration: + +.Cross-Origin Policies +==== +.Java +[source,java,role="primary"] +---- +@EnableWebFluxSecurity +@EnableWebFlux +public class WebSecurityConfig { + + @Bean + SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { + http.headers((headers) -> headers + .crossOriginOpenerPolicy(CrossOriginOpenerPolicy.SAME_ORIGIN) + .crossOriginEmbedderPolicy(CrossOriginEmbedderPolicy.REQUIRE_CORP) + .crossOriginResourcePolicy(CrossOriginResourcePolicy.SAME_ORIGIN)); + return http.build(); + } +} +---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebFluxSecurity +@EnableWebFlux +open class CrossOriginPoliciesCustomConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + crossOriginOpenerPolicy(CrossOriginOpenerPolicy.SAME_ORIGIN) + crossOriginEmbedderPolicy(CrossOriginEmbedderPolicy.REQUIRE_CORP) + crossOriginResourcePolicy(CrossOriginResourcePolicy.SAME_ORIGIN) + } + } + } +} +---- +==== + +This configuration will write the headers with the values provided: +[source] +---- +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: same-origin +---- diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index 0c3b995553..2191efcc06 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -238,6 +238,9 @@ This allows HTTPS websites to resist impersonation by attackers using mis-issued https://www.w3.org/TR/CSP2/[Content Security Policy (CSP)] is a mechanism that web applications can leverage to mitigate content injection vulnerabilities, such as cross-site scripting (XSS). ** `Referrer-Policy` - Can be set using the <> element, https://www.w3.org/TR/referrer-policy/[Referrer-Policy] is a mechanism that web applications can leverage to manage the referrer field, which contains the last page the user was on. ** `Feature-Policy` - Can be set using the <> element, https://wicg.github.io/feature-policy/[Feature-Policy] is a mechanism that allows web developers to selectively enable, disable, and modify the behavior of certain APIs and web features in the browser. +** `Cross-Origin-Opener-Policy` - Can be set using the <> element, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy[Cross-Origin-Opener-Policy] is a mechanism that allows you to ensure a top-level document does not share a browsing context group with cross-origin documents. +** `Cross-Origin-Embedder-Policy` - Can be set using the <> element, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy[Cross-Origin-Embedder-Policy] is a mechanism that prevents a document from loading any cross-origin resources that don't explicitly grant the document permission. +** `Cross-Origin-Resource-Policy` - Can be set using the <> element, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy[Cross-Origin-Resource-Policy] is a mechanism that conveys a desire that the browser blocks no-cors cross-origin/cross-site requests to the given resource. [[nsa-headers-attributes]] === Attributes @@ -269,6 +272,9 @@ The default is false (the headers are enabled). * <> * <> * <> +* <> +* <> +* <> * <> * <> * <> @@ -584,6 +590,66 @@ Default false. +[[nsa-cross-origin-embedder-policy]] +==== +When enabled adds the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy[Cross-Origin-Embedder-Policy] header to the response. + + +[[nsa-cross-origin-embedder-policy-attributes]] +===== Attributes + +[[nsa-cross-origin-embedder-policy-policy]] +* **policy** +The policy for the `Cross-Origin-Embedder-Policy` header. + +[[nsa-cross-origin-embedder-policy-parents]] +===== Parent Elements of + + +* <> + + + +[[nsa-cross-origin-opener-policy]] +==== +When enabled adds the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy[Cross-Origin-Opener-Policy] header to the response. + + +[[nsa-cross-origin-opener-policy-attributes]] +===== Attributes + +[[nsa-cross-origin-opener-policy-policy]] +* **policy** +The policy for the `Cross-Origin-Opener-Policy` header. + +[[nsa-cross-origin-opener-policy-parents]] +===== Parent Elements of + + +* <> + + + +[[nsa-cross-origin-resource-policy]] +==== +When enabled adds the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy[Cross-Origin-Resource-Policy] header to the response. + + +[[nsa-cross-origin-resource-policy-attributes]] +===== Attributes + +[[nsa-cross-origin-resource-policy-policy]] +* **policy** +The policy for the `Cross-Origin-Resource-Policy` header. + +[[nsa-cross-origin-resource-policy-parents]] +===== Parent Elements of + + +* <> + + + [[nsa-header]] ==

Add additional headers to the response, both the name and value need to be specified. diff --git a/docs/modules/ROOT/pages/servlet/exploits/headers.adoc b/docs/modules/ROOT/pages/servlet/exploits/headers.adoc index 535f3e976b..de8171d258 100644 --- a/docs/modules/ROOT/pages/servlet/exploits/headers.adoc +++ b/docs/modules/ROOT/pages/servlet/exploits/headers.adoc @@ -938,6 +938,67 @@ class SecurityConfig : WebSecurityConfigurerAdapter() { ---- ==== +[[servlet-headers-cross-origin-policies]] +== Cross-Origin Policies + +Spring Security provides built-in support for adding some Cross-Origin policies headers, those headers are: + +[source] +---- +Cross-Origin-Opener-Policy +Cross-Origin-Embedder-Policy +Cross-Origin-Resource-Policy +---- + +Spring Security does not add <> headers by default. +The headers can be added with the following configuration: + +.Cross-Origin Policies +==== +.Java +[source,java,role="primary"] +---- +@EnableWebSecurity +public class WebSecurityConfig { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) { + http.headers((headers) -> headers + .crossOriginOpenerPolicy(CrossOriginOpenerPolicy.SAME_ORIGIN) + .crossOriginEmbedderPolicy(CrossOriginEmbedderPolicy.REQUIRE_CORP) + .crossOriginResourcePolicy(CrossOriginResourcePolicy.SAME_ORIGIN))); + return http.build(); + } +} +---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +open class CrossOriginPoliciesConfig { + @Bean + open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + headers { + crossOriginOpenerPolicy(CrossOriginOpenerPolicy.SAME_ORIGIN) + crossOriginEmbedderPolicy(CrossOriginEmbedderPolicy.REQUIRE_CORP) + crossOriginResourcePolicy(CrossOriginResourcePolicy.SAME_ORIGIN) + } + } + return http.build() + } +} +---- +==== + +This configuration will write the headers with the values provided: +[source] +---- +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: same-origin +---- + [[servlet-headers-custom]] == Custom Headers Spring Security has mechanisms to make it convenient to add the more common security headers to your application. diff --git a/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriter.java new file mode 100644 index 0000000000..5ec60c051e --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +/** + * Inserts Cross-Origin-Embedder-Policy header. + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Embedder-Policy + */ +public final class CrossOriginEmbedderPolicyHeaderWriter implements HeaderWriter { + + private static final String EMBEDDER_POLICY = "Cross-Origin-Embedder-Policy"; + + private CrossOriginEmbedderPolicy policy; + + /** + * Sets the {@link CrossOriginEmbedderPolicy} value to be used in the + * {@code Cross-Origin-Embedder-Policy} header + * @param embedderPolicy the {@link CrossOriginEmbedderPolicy} to use + */ + public void setPolicy(CrossOriginEmbedderPolicy embedderPolicy) { + Assert.notNull(embedderPolicy, "embedderPolicy cannot be null"); + this.policy = embedderPolicy; + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + if (this.policy != null && !response.containsHeader(EMBEDDER_POLICY)) { + response.addHeader(EMBEDDER_POLICY, this.policy.getPolicy()); + } + } + + public enum CrossOriginEmbedderPolicy { + + UNSAFE_NONE("unsafe-none"), + + REQUIRE_CORP("require-corp"); + + private final String policy; + + CrossOriginEmbedderPolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + public static CrossOriginEmbedderPolicy from(String embedderPolicy) { + for (CrossOriginEmbedderPolicy policy : values()) { + if (policy.getPolicy().equals(embedderPolicy)) { + return policy; + } + } + return null; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriter.java new file mode 100644 index 0000000000..182a62c414 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +/** + * Inserts the Cross-Origin-Opener-Policy header + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Opener-Policy + */ +public final class CrossOriginOpenerPolicyHeaderWriter implements HeaderWriter { + + private static final String OPENER_POLICY = "Cross-Origin-Opener-Policy"; + + private CrossOriginOpenerPolicy policy; + + /** + * Sets the {@link CrossOriginOpenerPolicy} value to be used in the + * {@code Cross-Origin-Opener-Policy} header + * @param openerPolicy the {@link CrossOriginOpenerPolicy} to use + */ + public void setPolicy(CrossOriginOpenerPolicy openerPolicy) { + Assert.notNull(openerPolicy, "openerPolicy cannot be null"); + this.policy = openerPolicy; + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + if (this.policy != null && !response.containsHeader(OPENER_POLICY)) { + response.addHeader(OPENER_POLICY, this.policy.getPolicy()); + } + } + + public enum CrossOriginOpenerPolicy { + + UNSAFE_NONE("unsafe-none"), + + SAME_ORIGIN_ALLOW_POPUPS("same-origin-allow-popups"), + + SAME_ORIGIN("same-origin"); + + private final String policy; + + CrossOriginOpenerPolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + public static CrossOriginOpenerPolicy from(String openerPolicy) { + for (CrossOriginOpenerPolicy policy : values()) { + if (policy.getPolicy().equals(openerPolicy)) { + return policy; + } + } + return null; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriter.java new file mode 100644 index 0000000000..d454ce780a --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.header.HeaderWriter; +import org.springframework.util.Assert; + +/** + * Inserts Cross-Origin-Resource-Policy header + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Resource-Policy + */ +public final class CrossOriginResourcePolicyHeaderWriter implements HeaderWriter { + + private static final String RESOURCE_POLICY = "Cross-Origin-Resource-Policy"; + + private CrossOriginResourcePolicy policy; + + /** + * Sets the {@link CrossOriginResourcePolicy} value to be used in the + * {@code Cross-Origin-Resource-Policy} header + * @param resourcePolicy the {@link CrossOriginResourcePolicy} to use + */ + public void setPolicy(CrossOriginResourcePolicy resourcePolicy) { + Assert.notNull(resourcePolicy, "resourcePolicy cannot be null"); + this.policy = resourcePolicy; + } + + @Override + public void writeHeaders(HttpServletRequest request, HttpServletResponse response) { + if (this.policy != null && !response.containsHeader(RESOURCE_POLICY)) { + response.addHeader(RESOURCE_POLICY, this.policy.getPolicy()); + } + } + + public enum CrossOriginResourcePolicy { + + SAME_SITE("same-site"), + + SAME_ORIGIN("same-origin"), + + CROSS_ORIGIN("cross-origin"); + + private final String policy; + + CrossOriginResourcePolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + public static CrossOriginResourcePolicy from(String resourcePolicy) { + for (CrossOriginResourcePolicy policy : values()) { + if (policy.getPolicy().equals(resourcePolicy)) { + return policy; + } + } + return null; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriter.java new file mode 100644 index 0000000000..17446845dd --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Inserts Cross-Origin-Embedder-Policy headers. + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Embedder-Policy + */ +public final class CrossOriginEmbedderPolicyServerHttpHeadersWriter implements ServerHttpHeadersWriter { + + public static final String EMBEDDER_POLICY = "Cross-Origin-Embedder-Policy"; + + private ServerHttpHeadersWriter delegate; + + /** + * Sets the {@link CrossOriginEmbedderPolicy} value to be used in the + * {@code Cross-Origin-Embedder-Policy} header + * @param embedderPolicy the {@link CrossOriginEmbedderPolicy} to use + */ + public void setPolicy(CrossOriginEmbedderPolicy embedderPolicy) { + Assert.notNull(embedderPolicy, "embedderPolicy cannot be null"); + this.delegate = createDelegate(embedderPolicy); + } + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange) : Mono.empty(); + } + + private static ServerHttpHeadersWriter createDelegate(CrossOriginEmbedderPolicy embedderPolicy) { + StaticServerHttpHeadersWriter.Builder builder = StaticServerHttpHeadersWriter.builder(); + builder.header(EMBEDDER_POLICY, embedderPolicy.getPolicy()); + return builder.build(); + } + + public enum CrossOriginEmbedderPolicy { + + UNSAFE_NONE("unsafe-none"), + + REQUIRE_CORP("require-corp"); + + private final String policy; + + CrossOriginEmbedderPolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriter.java new file mode 100644 index 0000000000..d02add2320 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriter.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Inserts Cross-Origin-Opener-Policy header. + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Opener-Policy + */ +public final class CrossOriginOpenerPolicyServerHttpHeadersWriter implements ServerHttpHeadersWriter { + + public static final String OPENER_POLICY = "Cross-Origin-Opener-Policy"; + + private ServerHttpHeadersWriter delegate; + + /** + * Sets the {@link CrossOriginOpenerPolicy} value to be used in the + * {@code Cross-Origin-Opener-Policy} header + * @param openerPolicy the {@link CrossOriginOpenerPolicy} to use + */ + public void setPolicy(CrossOriginOpenerPolicy openerPolicy) { + Assert.notNull(openerPolicy, "openerPolicy cannot be null"); + this.delegate = createDelegate(openerPolicy); + } + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange) : Mono.empty(); + } + + private static ServerHttpHeadersWriter createDelegate(CrossOriginOpenerPolicy openerPolicy) { + StaticServerHttpHeadersWriter.Builder builder = StaticServerHttpHeadersWriter.builder(); + builder.header(OPENER_POLICY, openerPolicy.getPolicy()); + return builder.build(); + } + + public enum CrossOriginOpenerPolicy { + + UNSAFE_NONE("unsafe-none"), + + SAME_ORIGIN_ALLOW_POPUPS("same-origin-allow-popups"), + + SAME_ORIGIN("same-origin"); + + private final String policy; + + CrossOriginOpenerPolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriter.java b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriter.java new file mode 100644 index 0000000000..dff2574944 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriter.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import reactor.core.publisher.Mono; + +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Inserts Cross-Origin-Resource-Policy headers. + * + * @author Marcus Da Coregio + * @since 5.7 + * @see + * Cross-Origin-Resource-Policy + */ +public final class CrossOriginResourcePolicyServerHttpHeadersWriter implements ServerHttpHeadersWriter { + + public static final String RESOURCE_POLICY = "Cross-Origin-Resource-Policy"; + + private ServerHttpHeadersWriter delegate; + + /** + * Sets the {@link CrossOriginResourcePolicy} value to be used in the + * {@code Cross-Origin-Embedder-Policy} header + * @param resourcePolicy the {@link CrossOriginResourcePolicy} to use + */ + public void setPolicy(CrossOriginResourcePolicy resourcePolicy) { + Assert.notNull(resourcePolicy, "resourcePolicy cannot be null"); + this.delegate = createDelegate(resourcePolicy); + } + + @Override + public Mono writeHttpHeaders(ServerWebExchange exchange) { + return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange) : Mono.empty(); + } + + private static ServerHttpHeadersWriter createDelegate(CrossOriginResourcePolicy resourcePolicy) { + StaticServerHttpHeadersWriter.Builder builder = StaticServerHttpHeadersWriter.builder(); + builder.header(RESOURCE_POLICY, resourcePolicy.getPolicy()); + return builder.build(); + } + + public enum CrossOriginResourcePolicy { + + SAME_SITE("same-site"), + + SAME_ORIGIN("same-origin"), + + CROSS_ORIGIN("cross-origin"); + + private final String policy; + + CrossOriginResourcePolicy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return this.policy; + } + + } + +} diff --git a/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriterTests.java new file mode 100644 index 0000000000..0b90c57dea --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginEmbedderPolicyHeaderWriterTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginEmbedderPolicyHeaderWriterTests { + + private static final String EMBEDDER_HEADER_NAME = "Cross-Origin-Embedder-Policy"; + + private CrossOriginEmbedderPolicyHeaderWriter writer; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @BeforeEach + void setup() { + this.writer = new CrossOriginEmbedderPolicyHeaderWriter(); + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + void setEmbedderPolicyWhenNullEmbedderPolicyThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("embedderPolicy cannot be null"); + } + + @Test + void writeHeadersWhenDefaultValuesThenDontWriteHeaders() { + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(0); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.response.addHeader(EMBEDDER_HEADER_NAME, "require-corp"); + this.writer.setPolicy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.UNSAFE_NONE); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(EMBEDDER_HEADER_NAME)).isEqualTo("require-corp"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(EMBEDDER_HEADER_NAME)).isEqualTo("require-corp"); + } + + @Test + void writeHeadersWhenSetEmbedderPolicyThenWritesEmbedderPolicy() { + this.writer.setPolicy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.UNSAFE_NONE); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader(EMBEDDER_HEADER_NAME)).isEqualTo("unsafe-none"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriterTests.java new file mode 100644 index 0000000000..863351bb8b --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginOpenerPolicyHeaderWriterTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginOpenerPolicyHeaderWriterTests { + + private static final String OPENER_HEADER_NAME = "Cross-Origin-Opener-Policy"; + + private CrossOriginOpenerPolicyHeaderWriter writer; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @BeforeEach + void setup() { + this.writer = new CrossOriginOpenerPolicyHeaderWriter(); + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + void setOpenerPolicyWhenNullOpenerPolicyThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("openerPolicy cannot be null"); + } + + @Test + void writeHeadersWhenDefaultValuesThenDontWriteHeaders() { + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(0); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.response.addHeader(OPENER_HEADER_NAME, "same-origin"); + this.writer.setPolicy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(OPENER_HEADER_NAME)).isEqualTo("same-origin"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(OPENER_HEADER_NAME)).isEqualTo("same-origin-allow-popups"); + } + + @Test + void writeHeadersWhenSetOpenerPolicyThenWritesOpenerPolicy() { + this.writer.setPolicy(CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader(OPENER_HEADER_NAME)).isEqualTo("same-origin-allow-popups"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriterTests.java new file mode 100644 index 0000000000..14b8f04a03 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/header/writers/CrossOriginResourcePolicyHeaderWriterTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.header.writers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginResourcePolicyHeaderWriterTests { + + private static final String RESOURCE_HEADER_NAME = "Cross-Origin-Resource-Policy"; + + private CrossOriginResourcePolicyHeaderWriter writer; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + @BeforeEach + void setup() { + this.writer = new CrossOriginResourcePolicyHeaderWriter(); + this.request = new MockHttpServletRequest(); + this.response = new MockHttpServletResponse(); + } + + @Test + void setResourcePolicyWhenNullThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("resourcePolicy cannot be null"); + } + + @Test + void writeHeadersWhenDefaultValuesThenDontWriteHeaders() { + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(0); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.response.addHeader(RESOURCE_HEADER_NAME, "same-site"); + this.writer.setPolicy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.CROSS_ORIGIN); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(RESOURCE_HEADER_NAME)).isEqualTo("same-site"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_ORIGIN); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeader(RESOURCE_HEADER_NAME)).isEqualTo("same-origin"); + } + + @Test + void writeHeadersWhenSetResourcePolicyThenWritesResourcePolicy() { + this.writer.setPolicy(CrossOriginResourcePolicyHeaderWriter.CrossOriginResourcePolicy.SAME_SITE); + this.writer.writeHeaders(this.request, this.response); + assertThat(this.response.getHeaderNames()).hasSize(1); + assertThat(this.response.getHeader(RESOURCE_HEADER_NAME)).isEqualTo("same-site"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriterTests.java new file mode 100644 index 0000000000..b4e99336fc --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginEmbedderPolicyServerHttpHeadersWriterTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginEmbedderPolicyServerHttpHeadersWriterTests { + + private ServerWebExchange exchange; + + private CrossOriginEmbedderPolicyServerHttpHeadersWriter writer; + + @BeforeEach + void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.writer = new CrossOriginEmbedderPolicyServerHttpHeadersWriter(); + } + + @Test + void setEmbedderPolicyWhenNullEmbedderPolicyThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("embedderPolicy cannot be null"); + } + + @Test + void writeHeadersWhenNoValuesThenDoesNotWriteHeaders() { + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).isEmpty(); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.exchange.getResponse().getHeaders().add(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY, + "require-corp"); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY)) + .containsOnly("require-corp"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginEmbedderPolicyServerHttpHeadersWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginEmbedderPolicyServerHttpHeadersWriter.EMBEDDER_POLICY)) + .containsOnly("require-corp"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriterTests.java new file mode 100644 index 0000000000..0159665b4e --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginOpenerPolicyServerHttpHeadersWriterTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginOpenerPolicyServerHttpHeadersWriterTests { + + private ServerWebExchange exchange; + + private CrossOriginOpenerPolicyServerHttpHeadersWriter writer; + + @BeforeEach + void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.writer = new CrossOriginOpenerPolicyServerHttpHeadersWriter(); + } + + @Test + void setOpenerPolicyWhenNullOpenerPolicyThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("openerPolicy cannot be null"); + } + + @Test + void writeHeadersWhenNoValuesThenDoesNotWriteHeaders() { + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).isEmpty(); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.exchange.getResponse().getHeaders().add(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY, + "same-origin"); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY)) + .containsOnly("same-origin"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy( + CrossOriginOpenerPolicyServerHttpHeadersWriter.CrossOriginOpenerPolicy.SAME_ORIGIN_ALLOW_POPUPS); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginOpenerPolicyServerHttpHeadersWriter.OPENER_POLICY)) + .containsOnly("same-origin-allow-popups"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriterTests.java b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriterTests.java new file mode 100644 index 0000000000..a3ba9a2ec9 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/header/CrossOriginResourcePolicyServerHttpHeadersWriterTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.header; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class CrossOriginResourcePolicyServerHttpHeadersWriterTests { + + private ServerWebExchange exchange; + + private CrossOriginResourcePolicyServerHttpHeadersWriter writer; + + @BeforeEach + void setup() { + this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + this.writer = new CrossOriginResourcePolicyServerHttpHeadersWriter(); + } + + @Test + void setResourcePolicyWhenNullThenThrowsIllegalArgument() { + assertThatIllegalArgumentException().isThrownBy(() -> this.writer.setPolicy(null)) + .withMessage("resourcePolicy cannot be null"); + } + + @Test + void writeHeadersWhenNoValuesThenDoesNotWriteHeaders() { + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).isEmpty(); + } + + @Test + void writeHeadersWhenResponseHeaderExistsThenDontOverride() { + this.exchange.getResponse().getHeaders().add(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY, + "same-origin"); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY)) + .containsOnly("same-origin"); + } + + @Test + void writeHeadersWhenSetHeaderValuesThenWrites() { + this.writer.setPolicy(CrossOriginResourcePolicyServerHttpHeadersWriter.CrossOriginResourcePolicy.SAME_ORIGIN); + this.writer.writeHttpHeaders(this.exchange); + HttpHeaders headers = this.exchange.getResponse().getHeaders(); + assertThat(headers).hasSize(1); + assertThat(headers.get(CrossOriginResourcePolicyServerHttpHeadersWriter.RESOURCE_POLICY)) + .containsOnly("same-origin"); + } + +} From 22379e79e70970765f94c6fdc5a7a64535ce4fb2 Mon Sep 17 00:00:00 2001 From: Guirong Hu Date: Wed, 8 Dec 2021 10:39:13 +0800 Subject: [PATCH 074/589] Fix the bug that the custom GrantedAuthority comparison fails Closes gh-10566 --- .../AuthorityAuthorizationManager.java | 6 +++-- ...AuthorityReactiveAuthorizationManager.java | 5 ++-- .../AuthorityAuthorizationManagerTests.java | 26 +++++++++++++++++++ ...rityReactiveAuthorizationManagerTests.java | 19 ++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java index 8cfc0dcf0a..1959c8c416 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java @@ -133,8 +133,10 @@ public final class AuthorityAuthorizationManager implements AuthorizationMana private boolean isAuthorized(Authentication authentication) { for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { - if (this.authorities.contains(grantedAuthority)) { - return true; + for (GrantedAuthority authority : this.authorities) { + if (authority.getAuthority().equals(grantedAuthority.getAuthority())) { + return true; + } } } return false; diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java index 5c98cf3061..6a91cfb893 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManager.java @@ -45,9 +45,10 @@ public class AuthorityReactiveAuthorizationManager implements ReactiveAuthori @Override public Mono check(Mono authentication, T object) { // @formatter:off - return authentication.filter((a) -> a.isAuthenticated()) + return authentication.filter(Authentication::isAuthenticated) .flatMapIterable(Authentication::getAuthorities) - .any(this.authorities::contains) + .map(GrantedAuthority::getAuthority) + .any((grantedAuthority) -> this.authorities.stream().anyMatch((authority) -> authority.getAuthority().equals(grantedAuthority))) .map((granted) -> ((AuthorizationDecision) new AuthorityAuthorizationDecision(granted, this.authorities))) .defaultIfEmpty(new AuthorityAuthorizationDecision(false, this.authorities)); // @formatter:on diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java index 43d8d0631c..ce5d40604b 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorityAuthorizationManagerTests.java @@ -16,12 +16,14 @@ package org.springframework.security.authorization; +import java.util.Collections; import java.util.function.Supplier; import org.junit.jupiter.api.Test; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -133,6 +135,30 @@ public class AuthorityAuthorizationManagerTests { assertThat(manager.check(authentication, object).isGranted()).isFalse(); } + @Test + public void hasAuthorityWhenUserHasCustomAuthorityThenGrantedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAuthority("ADMIN"); + GrantedAuthority customGrantedAuthority = () -> "ADMIN"; + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + Collections.singletonList(customGrantedAuthority)); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isTrue(); + } + + @Test + public void hasAuthorityWhenUserHasNotCustomAuthorityThenDeniedDecision() { + AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAuthority("ADMIN"); + GrantedAuthority customGrantedAuthority = () -> "USER"; + + Supplier authentication = () -> new TestingAuthenticationToken("user", "password", + Collections.singletonList(customGrantedAuthority)); + Object object = new Object(); + + assertThat(manager.check(authentication, object).isGranted()).isFalse(); + } + @Test public void hasAnyRoleWhenUserHasAnyRoleThenGrantedDecision() { AuthorityAuthorizationManager manager = AuthorityAuthorizationManager.hasAnyRole("ADMIN", "USER"); diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java index 2fd6ac42e4..ac937cfbf6 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorityReactiveAuthorizationManagerTests.java @@ -27,6 +27,7 @@ import reactor.test.StepVerifier; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -88,6 +89,24 @@ public class AuthorityReactiveAuthorizationManagerTests { assertThat(granted).isTrue(); } + @Test + public void checkWhenHasCustomAuthorityAndAuthorizedThenReturnTrue() { + GrantedAuthority customGrantedAuthority = () -> "ADMIN"; + this.authentication = new TestingAuthenticationToken("rob", "secret", + Collections.singletonList(customGrantedAuthority)); + boolean granted = this.manager.check(Mono.just(this.authentication), null).block().isGranted(); + assertThat(granted).isTrue(); + } + + @Test + public void checkWhenHasCustomAuthorityAndAuthenticatedAndWrongAuthoritiesThenReturnFalse() { + GrantedAuthority customGrantedAuthority = () -> "USER"; + this.authentication = new TestingAuthenticationToken("rob", "secret", + Collections.singletonList(customGrantedAuthority)); + boolean granted = this.manager.check(Mono.just(this.authentication), null).block().isGranted(); + assertThat(granted).isFalse(); + } + @Test public void checkWhenHasRoleAndAuthorizedThenReturnTrue() { this.manager = AuthorityReactiveAuthorizationManager.hasRole("ADMIN"); From 5598688fa6217a6ffda9c843fe679a16d261fa11 Mon Sep 17 00:00:00 2001 From: James Howe <675056+OrangeDog@users.noreply.github.com> Date: Wed, 13 Nov 2019 10:47:18 +0000 Subject: [PATCH 075/589] Clarify behaviour of enableSessionUrlRewriting See #3087 --- .../web/configurers/SessionManagementConfigurer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 86b9cc0275..3027c85dd4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -199,8 +199,9 @@ public final class SessionManagementConfigurer> /** * If set to true, allows HTTP sessions to be rewritten in the URLs when using * {@link HttpServletResponse#encodeRedirectURL(String)} or - * {@link HttpServletResponse#encodeURL(String)}, otherwise disallows HTTP sessions to - * be included in the URL. This prevents leaking information to external domains. + * {@link HttpServletResponse#encodeURL(String)}, otherwise disallows all URL + * rewriting, including resource chain functionality. + * This prevents leaking information to external domains. * @param enableSessionUrlRewriting true if should allow the JSESSIONID to be * rewritten into the URLs, else false (default) * @return the {@link SessionManagementConfigurer} for further customization From cd8983d4e57aa2c1e61567d4f92cccd416064c85 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 9 Dec 2021 12:03:32 -0700 Subject: [PATCH 076/589] Polish enableSessionUrlRewriting Clarification Closes gh-7644 --- .../web/configurers/SessionManagementConfigurer.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index 3027c85dd4..94d8bcf231 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -199,9 +199,14 @@ public final class SessionManagementConfigurer> /** * If set to true, allows HTTP sessions to be rewritten in the URLs when using * {@link HttpServletResponse#encodeRedirectURL(String)} or - * {@link HttpServletResponse#encodeURL(String)}, otherwise disallows all URL - * rewriting, including resource chain functionality. - * This prevents leaking information to external domains. + * {@link HttpServletResponse#encodeURL(String)}, otherwise disallows HTTP sessions to + * be included in the URL. This prevents leaking information to external domains. + *

+ * This is achieved by guarding {@link HttpServletResponse#encodeURL} and + * {@link HttpServletResponse#encodeRedirectURL} invocations. Any code that also + * overrides either of these two methods, like + * {@link org.springframework.web.servlet.resource.ResourceUrlEncodingFilter}, needs + * to come after the security filter chain or risk being skipped. * @param enableSessionUrlRewriting true if should allow the JSESSIONID to be * rewritten into the URLs, else false (default) * @return the {@link SessionManagementConfigurer} for further customization From 53b8cff26f993a56db1b031a1f6aa991e440153c Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 6 Dec 2021 15:12:37 -0300 Subject: [PATCH 077/589] Introduce AuthorizationManagerWebInvocationPrivilegeEvaluator Closes gh-10590 --- ...anagerWebInvocationPrivilegeEvaluator.java | 57 ++++++++++++++++ ...rWebInvocationPrivilegeEvaluatorTests.java | 68 +++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java create mode 100644 web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java diff --git a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java new file mode 100644 index 0000000000..eb479a65e1 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.FilterInvocation; +import org.springframework.util.Assert; + +/** + * An implementation of {@link WebInvocationPrivilegeEvaluator} which delegates the checks + * to an instance of {@link AuthorizationManager} + * + * @author Marcus Da Coregio + * @since 5.7 + */ +public final class AuthorizationManagerWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + private final AuthorizationManager authorizationManager; + + public AuthorizationManagerWebInvocationPrivilegeEvaluator( + AuthorizationManager authorizationManager) { + Assert.notNull(authorizationManager, "authorizationManager cannot be null"); + this.authorizationManager = authorizationManager; + } + + @Override + public boolean isAllowed(String uri, Authentication authentication) { + return isAllowed(null, uri, null, authentication); + } + + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method); + AuthorizationDecision decision = this.authorizationManager.check(() -> authentication, + filterInvocation.getHttpRequest()); + return decision != null && decision.isGranted(); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java new file mode 100644 index 0000000000..2253e93fae --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluatorTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.security.authentication.TestAuthentication; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class AuthorizationManagerWebInvocationPrivilegeEvaluatorTests { + + @InjectMocks + private AuthorizationManagerWebInvocationPrivilegeEvaluator privilegeEvaluator; + + @Mock + private AuthorizationManager authorizationManager; + + @Test + void constructorWhenAuthorizationManagerNullThenIllegalArgument() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new AuthorizationManagerWebInvocationPrivilegeEvaluator(null)) + .withMessage("authorizationManager cannot be null"); + } + + @Test + void isAllowedWhenAuthorizationManagerAllowsThenAllowedTrue() { + given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true)); + boolean allowed = this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + assertThat(allowed).isTrue(); + verify(this.authorizationManager).check(any(), any()); + } + + @Test + void isAllowedWhenAuthorizationManagerDeniesAllowedFalse() { + given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false)); + boolean allowed = this.privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser()); + assertThat(allowed).isFalse(); + } + +} From 7e17a00197cd0c72766f6684caa96b26ce2aa8f0 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Fri, 10 Dec 2021 08:14:12 -0300 Subject: [PATCH 078/589] Add RequestMatcherEntry --- .../web/util/matcher/RequestMatcherEntry.java | 44 +++++++++++++++++++ .../matcher/RequestMatcherEntryTests.java | 41 +++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java create mode 100644 web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java new file mode 100644 index 0000000000..ea83cc7ac1 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.util.matcher; + +/** + * A rich object for associating a {@link RequestMatcher} to another object. + * + * @author Marcus Da Coregio + * @since 5.7 + */ +public class RequestMatcherEntry { + + private final RequestMatcher requestMatcher; + + private final T entry; + + public RequestMatcherEntry(RequestMatcher requestMatcher, T entry) { + this.requestMatcher = requestMatcher; + this.entry = entry; + } + + public RequestMatcher getRequestMatcher() { + return this.requestMatcher; + } + + public T getEntry() { + return this.entry; + } + +} diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java new file mode 100644 index 0000000000..b293b68caa --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/util/matcher/RequestMatcherEntryTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.util.matcher; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class RequestMatcherEntryTests { + + @Test + void constructWhenGetRequestMatcherAndEntryThenSameRequestMatcherAndEntry() { + RequestMatcher requestMatcher = mock(RequestMatcher.class); + RequestMatcherEntry entry = new RequestMatcherEntry<>(requestMatcher, "entry"); + assertThat(entry.getRequestMatcher()).isSameAs(requestMatcher); + assertThat(entry.getEntry()).isEqualTo("entry"); + } + + @Test + void constructWhenNullValuesThenNullValues() { + RequestMatcherEntry entry = new RequestMatcherEntry<>(null, null); + assertThat(entry.getRequestMatcher()).isNull(); + assertThat(entry.getEntry()).isNull(); + } + +} From 18427b64113276c7bcaeda2e2e98cd7473c493aa Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 6 Dec 2021 15:13:05 -0300 Subject: [PATCH 079/589] Configure WebInvocationPrivilegeEvaluator bean for multiple filter chains Closes gh-10554 --- .../annotation/web/builders/WebSecurity.java | 55 ++++- .../WebSecurityConfiguration.java | 4 +- .../WebSecurityConfigurationTests.java | 196 +++++++++++++++++- ...gatingWebInvocationPrivilegeEvaluator.java | 122 +++++++++++ .../access/intercept/AuthorizationFilter.java | 10 +- .../security/web/debug/DebugFilter.java | 6 +- ...gWebInvocationPrivilegeEvaluatorTests.java | 179 ++++++++++++++++ .../TestWebInvocationPrivilegeEvaluator.java | 66 ++++++ .../intercept/AuthorizationFilterTests.java | 9 +- 9 files changed, 633 insertions(+), 14 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java create mode 100644 web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java create mode 100644 web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index f0395b840e..c4934cb862 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; import javax.servlet.Filter; +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -33,6 +34,7 @@ import org.springframework.http.HttpMethod; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.SecurityExpressionHandler; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; @@ -47,9 +49,12 @@ import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator; +import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator; import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; +import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.debug.DebugFilter; import org.springframework.security.web.firewall.HttpFirewall; @@ -57,7 +62,9 @@ import org.springframework.security.web.firewall.RequestRejectedHandler; import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.util.Assert; +import org.springframework.web.context.ServletContextAware; import org.springframework.web.filter.DelegatingFilterProxy; /** @@ -81,7 +88,7 @@ import org.springframework.web.filter.DelegatingFilterProxy; * @see WebSecurityConfiguration */ public final class WebSecurity extends AbstractConfiguredSecurityBuilder - implements SecurityBuilder, ApplicationContextAware { + implements SecurityBuilder, ApplicationContextAware, ServletContextAware { private final Log logger = LogFactory.getLog(getClass()); @@ -108,6 +115,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder { }; + private ServletContext servletContext; + /** * Creates a new instance * @param objectPostProcessor the {@link ObjectPostProcessor} to use @@ -252,6 +261,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder securityFilterChains = new ArrayList<>(chainSize); + List>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>(); for (RequestMatcher ignoredRequest : this.ignoredRequests) { - securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); + SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest); + securityFilterChains.add(securityFilterChain); + requestMatcherPrivilegeEvaluatorsEntries + .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain)); } for (SecurityBuilder securityFilterChainBuilder : this.securityFilterChainBuilders) { - securityFilterChains.add(securityFilterChainBuilder.build()); + SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build(); + securityFilterChains.add(securityFilterChain); + requestMatcherPrivilegeEvaluatorsEntries + .add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain)); + } + if (this.privilegeEvaluator == null) { + this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + requestMatcherPrivilegeEvaluatorsEntries); } FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); if (this.httpFirewall != null) { @@ -306,6 +328,26 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder> getRequestMatcherPrivilegeEvaluatorsEntry( + SecurityFilterChain securityFilterChain) { + List privilegeEvaluators = new ArrayList<>(); + for (Filter filter : securityFilterChain.getFilters()) { + if (filter instanceof FilterSecurityInterceptor) { + DefaultWebInvocationPrivilegeEvaluator defaultWebInvocationPrivilegeEvaluator = new DefaultWebInvocationPrivilegeEvaluator( + (FilterSecurityInterceptor) filter); + defaultWebInvocationPrivilegeEvaluator.setServletContext(this.servletContext); + privilegeEvaluators.add(defaultWebInvocationPrivilegeEvaluator); + continue; + } + if (filter instanceof AuthorizationFilter) { + AuthorizationManager authorizationManager = ((AuthorizationFilter) filter) + .getAuthorizationManager(); + privilegeEvaluators.add(new AuthorizationManagerWebInvocationPrivilegeEvaluator(authorizationManager)); + } + } + return new RequestMatcherEntry<>(securityFilterChain::matches, privilegeEvaluators); + } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.defaultWebSecurityExpressionHandler.setApplicationContext(applicationContext); @@ -333,6 +375,11 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder requests.antMatchers("/path1/**")) + .authorizeRequests((requests) -> requests.anyRequest().authenticated()); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + + } + + @EnableWebSecurity(debug = true) + static class TwoSecurityFilterChainDebugConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain path1(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/path1/**")) + .authorizeRequests((requests) -> requests.anyRequest().authenticated()); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + + } + + @EnableWebSecurity + @Import(AuthenticationTestConfiguration.class) + static class MultipleSecurityFilterChainConfig { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain notAuthorized(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/user")) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("USER")); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 1) + public SecurityFilterChain path1(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/admin")) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("ADMIN")); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + + } + + @EnableWebSecurity + @Import(AuthenticationTestConfiguration.class) + static class MultipleSecurityFilterChainIgnoringConfig { + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().antMatchers("/ignoring1/**"); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain notAuthorized(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/user")) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("USER")); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 1) + public SecurityFilterChain admin(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers((requests) -> requests.antMatchers("/admin")) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("ADMIN")); + // @formatter:on + return http.build(); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public SecurityFilterChain permitAll(HttpSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + + } + } diff --git a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java new file mode 100644 index 0000000000..b4f5c1dc60 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; +import org.springframework.util.Assert; + +/** + * A {@link WebInvocationPrivilegeEvaluator} which delegates to a list of + * {@link WebInvocationPrivilegeEvaluator} based on a + * {@link org.springframework.security.web.util.matcher.RequestMatcher} evaluation + * + * @author Marcus Da Coregio + * @since 5.7 + */ +public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + private final List>> delegates; + + public RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + List>> requestMatcherPrivilegeEvaluatorsEntries) { + Assert.notNull(requestMatcherPrivilegeEvaluatorsEntries, "requestMatcherPrivilegeEvaluators cannot be null"); + for (RequestMatcherEntry> entry : requestMatcherPrivilegeEvaluatorsEntries) { + Assert.notNull(entry.getRequestMatcher(), "requestMatcher cannot be null"); + Assert.notNull(entry.getEntry(), "webInvocationPrivilegeEvaluators cannot be null"); + } + this.delegates = requestMatcherPrivilegeEvaluatorsEntries; + } + + /** + * Determines whether the user represented by the supplied Authentication + * object is allowed to invoke the supplied URI. + *

+ * Uses the provided URI in the + * {@link org.springframework.security.web.util.matcher.RequestMatcher#matches(HttpServletRequest)} + * for every {@code RequestMatcher} configured. If no {@code RequestMatcher} is + * matched, or if there is not an available {@code WebInvocationPrivilegeEvaluator}, + * returns {@code true}. + * @param uri the URI excluding the context path (a default context path setting will + * be used) + * @return true if access is allowed, false if denied + */ + @Override + public boolean isAllowed(String uri, Authentication authentication) { + List privilegeEvaluators = getDelegate(null, uri, null); + if (privilegeEvaluators.isEmpty()) { + return true; + } + for (WebInvocationPrivilegeEvaluator evaluator : privilegeEvaluators) { + boolean isAllowed = evaluator.isAllowed(uri, authentication); + if (!isAllowed) { + return false; + } + } + return true; + } + + /** + * Determines whether the user represented by the supplied Authentication + * object is allowed to invoke the supplied URI. + *

+ * Uses the provided URI in the + * {@link org.springframework.security.web.util.matcher.RequestMatcher#matches(HttpServletRequest)} + * for every {@code RequestMatcher} configured. If no {@code RequestMatcher} is + * matched, or if there is not an available {@code WebInvocationPrivilegeEvaluator}, + * returns {@code true}. + * @param uri the URI excluding the context path (a default context path setting will + * be used) + * @param contextPath the context path (may be null, in which case a default value + * will be used). + * @param method the HTTP method (or null, for any method) + * @param authentication the Authentication instance whose authorities should + * be used in evaluation whether access should be granted. + * @return true if access is allowed, false if denied + */ + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + List privilegeEvaluators = getDelegate(contextPath, uri, method); + if (privilegeEvaluators.isEmpty()) { + return true; + } + for (WebInvocationPrivilegeEvaluator evaluator : privilegeEvaluators) { + boolean isAllowed = evaluator.isAllowed(contextPath, uri, method, authentication); + if (!isAllowed) { + return false; + } + } + return true; + } + + private List getDelegate(String contextPath, String uri, String method) { + FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method); + for (RequestMatcherEntry> delegate : this.delegates) { + if (delegate.getRequestMatcher().matches(filterInvocation.getHttpRequest())) { + return delegate.getEntry(); + } + } + return Collections.emptyList(); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java index 37b65ab0fb..19bcbfdc11 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/AuthorizationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,4 +67,12 @@ public class AuthorizationFilter extends OncePerRequestFilter { return authentication; } + /** + * Gets the {@link AuthorizationManager} used by this filter + * @return the {@link AuthorizationManager} + */ + public AuthorizationManager getAuthorizationManager() { + return this.authorizationManager; + } + } diff --git a/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java b/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java index a25b7927ac..8c4a3aff48 100644 --- a/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java +++ b/web/src/main/java/org/springframework/security/web/debug/DebugFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -151,6 +151,10 @@ public final class DebugFilter implements Filter { public void destroy() { } + public FilterChainProxy getFilterChainProxy() { + return this.filterChainProxy; + } + static class DebugRequestWrapper extends HttpServletRequestWrapper { private static final Logger logger = new Logger(); diff --git a/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java new file mode 100644 index 0000000000..95feee1b56 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Tests for {@link RequestMatcherDelegatingWebInvocationPrivilegeEvaluator} + * + * @author Marcus Da Coregio + */ +class RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests { + + private final RequestMatcher alwaysMatch = mock(RequestMatcher.class); + + private final RequestMatcher alwaysDeny = mock(RequestMatcher.class); + + private final String uri = "/test"; + + private final Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + + @BeforeEach + void setup() { + given(this.alwaysMatch.matches(any())).willReturn(true); + given(this.alwaysDeny.matches(any())).willReturn(false); + } + + @Test + void isAllowedWhenDelegatesEmptyThenAllowed() { + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.emptyList()); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + } + + @Test + void isAllowedWhenNotMatchThenAllowed() { + RequestMatcherEntry> notMatch = new RequestMatcherEntry<>(this.alwaysDeny, + Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(notMatch)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + verify(notMatch.getRequestMatcher()).matches(any()); + } + + @Test + void isAllowedWhenPrivilegeEvaluatorAllowThenAllowedTrue() { + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + } + + @Test + void isAllowedWhenPrivilegeEvaluatorDenyThenAllowedFalse() { + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysDeny())); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse(); + } + + @Test + void isAllowedWhenNotMatchThenMatchThenOnlySecondDelegateInvoked() { + RequestMatcherEntry> notMatchDelegate = new RequestMatcherEntry<>( + this.alwaysDeny, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherEntry> matchDelegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(TestWebInvocationPrivilegeEvaluator.alwaysAllow())); + RequestMatcherEntry> spyNotMatchDelegate = spy(notMatchDelegate); + RequestMatcherEntry> spyMatchDelegate = spy(matchDelegate); + + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Arrays.asList(notMatchDelegate, spyMatchDelegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + verify(spyNotMatchDelegate.getRequestMatcher()).matches(any()); + verify(spyNotMatchDelegate, never()).getEntry(); + verify(spyMatchDelegate.getRequestMatcher()).matches(any()); + verify(spyMatchDelegate, times(2)).getEntry(); // 2 times, one for constructor and + // other one in isAllowed + } + + @Test + void isAllowedWhenDelegatePrivilegeEvaluatorsEmptyThenAllowedTrue() { + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.emptyList()); + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + assertThat(delegating.isAllowed(this.uri, this.authentication)).isTrue(); + } + + @Test + void isAllowedWhenFirstDelegateDenyThenDoNotInvokeOthers() { + WebInvocationPrivilegeEvaluator deny = TestWebInvocationPrivilegeEvaluator.alwaysDeny(); + WebInvocationPrivilegeEvaluator allow = TestWebInvocationPrivilegeEvaluator.alwaysAllow(); + WebInvocationPrivilegeEvaluator spyDeny = spy(deny); + WebInvocationPrivilegeEvaluator spyAllow = spy(allow); + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Arrays.asList(spyDeny, spyAllow)); + + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + + assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse(); + verify(spyDeny).isAllowed(any(), any()); + verifyNoInteractions(spyAllow); + } + + @Test + void isAllowedWhenDifferentArgumentsThenCallSpecificIsAllowedInDelegate() { + WebInvocationPrivilegeEvaluator deny = TestWebInvocationPrivilegeEvaluator.alwaysDeny(); + WebInvocationPrivilegeEvaluator spyDeny = spy(deny); + RequestMatcherEntry> delegate = new RequestMatcherEntry<>( + this.alwaysMatch, Collections.singletonList(spyDeny)); + + RequestMatcherDelegatingWebInvocationPrivilegeEvaluator delegating = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator( + Collections.singletonList(delegate)); + + assertThat(delegating.isAllowed(this.uri, this.authentication)).isFalse(); + assertThat(delegating.isAllowed("/cp", this.uri, "GET", this.authentication)).isFalse(); + verify(spyDeny).isAllowed(any(), any()); + verify(spyDeny).isAllowed(any(), any(), any(), any()); + verifyNoMoreInteractions(spyDeny); + } + + @Test + void constructorWhenPrivilegeEvaluatorsNullThenException() { + RequestMatcherEntry> entry = new RequestMatcherEntry<>(this.alwaysMatch, + null); + assertThatIllegalArgumentException().isThrownBy( + () -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry))) + .withMessageContaining("webInvocationPrivilegeEvaluators cannot be null"); + } + + @Test + void constructorWhenRequestMatcherNullThenException() { + RequestMatcherEntry> entry = new RequestMatcherEntry<>(null, + Collections.singletonList(mock(WebInvocationPrivilegeEvaluator.class))); + assertThatIllegalArgumentException().isThrownBy( + () -> new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(Collections.singletonList(entry))) + .withMessageContaining("requestMatcher cannot be null"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java b/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java new file mode 100644 index 0000000000..54ab666cd5 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/access/TestWebInvocationPrivilegeEvaluator.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.access; + +import org.springframework.security.core.Authentication; + +public final class TestWebInvocationPrivilegeEvaluator { + + private static final AlwaysAllowWebInvocationPrivilegeEvaluator ALWAYS_ALLOW = new AlwaysAllowWebInvocationPrivilegeEvaluator(); + + private static final AlwaysDenyWebInvocationPrivilegeEvaluator ALWAYS_DENY = new AlwaysDenyWebInvocationPrivilegeEvaluator(); + + private TestWebInvocationPrivilegeEvaluator() { + } + + public static WebInvocationPrivilegeEvaluator alwaysAllow() { + return ALWAYS_ALLOW; + } + + public static WebInvocationPrivilegeEvaluator alwaysDeny() { + return ALWAYS_DENY; + } + + private static class AlwaysAllowWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + @Override + public boolean isAllowed(String uri, Authentication authentication) { + return true; + } + + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + return true; + } + + } + + private static class AlwaysDenyWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { + + @Override + public boolean isAllowed(String uri, Authentication authentication) { + return false; + } + + @Override + public boolean isAllowed(String contextPath, String uri, String method, Authentication authentication) { + return false; + } + + } + +} diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java index b2b22c5666..0923605216 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/AuthorizationFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -125,4 +125,11 @@ public class AuthorizationFilterTests { verifyNoInteractions(mockFilterChain); } + @Test + public void getAuthorizationManager() { + AuthorizationManager authorizationManager = mock(AuthorizationManager.class); + AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager); + assertThat(authorizationFilter.getAuthorizationManager()).isSameAs(authorizationManager); + } + } From b9453da34385c5bf71de244035cdb795cfc25dce Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 17 Dec 2021 15:18:41 -0700 Subject: [PATCH 080/589] Support No SingleLogoutServiceLocation Closes gh-10674 --- .../metadata/OpenSamlMetadataResolver.java | 4 +++- .../RelyingPartyRegistration.java | 2 ++ .../logout/OpenSamlLogoutRequestResolver.java | 3 +++ .../OpenSamlLogoutResponseResolver.java | 3 +++ .../logout/Saml2LogoutRequestFilter.java | 6 ++++++ .../logout/Saml2LogoutResponseFilter.java | 6 ++++++ .../OpenSaml3LogoutRequestResolverTests.java | 4 +++- .../OpenSaml3LogoutResponseResolverTests.java | 5 ++++- .../OpenSaml4LogoutRequestResolverTests.java | 4 +++- .../OpenSaml4LogoutResponseResolverTests.java | 5 ++++- .../OpenSamlMetadataResolverTests.java | 9 +++++++++ .../logout/Saml2LogoutRequestFilterTests.java | 16 +++++++++++++++ .../Saml2LogoutResponseFilterTests.java | 20 +++++++++++++++++++ 13 files changed, 82 insertions(+), 5 deletions(-) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java index 7b850a5483..731b9c8cf8 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolver.java @@ -87,7 +87,9 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver { spSsoDescriptor.getKeyDescriptors() .addAll(buildKeys(registration.getDecryptionX509Credentials(), UsageType.ENCRYPTION)); spSsoDescriptor.getAssertionConsumerServices().add(buildAssertionConsumerService(registration)); - spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration)); + if (registration.getSingleLogoutServiceLocation() != null) { + spSsoDescriptor.getSingleLogoutServices().add(buildSingleLogoutService(registration)); + } if (registration.getNameIdFormat() != null) { spSsoDescriptor.getNameIDFormats().add(buildNameIDFormat(registration)); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index 43e61b11e1..fa0a4a59d2 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -108,6 +108,8 @@ public final class RelyingPartyRegistration { Assert.hasText(entityId, "entityId cannot be empty"); Assert.hasText(assertionConsumerServiceLocation, "assertionConsumerServiceLocation cannot be empty"); Assert.notNull(assertionConsumerServiceBinding, "assertionConsumerServiceBinding cannot be null"); + Assert.isTrue(singleLogoutServiceLocation == null || singleLogoutServiceBinding != null, + "singleLogoutServiceBinding cannot be null when singleLogoutServiceLocation is set"); Assert.notNull(providerDetails, "providerDetails cannot be null"); Assert.notEmpty(credentials, "credentials cannot be empty"); for (org.springframework.security.saml2.credentials.Saml2X509Credential c : credentials) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java index 5a5e64c6e3..5fd296b5a8 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java @@ -111,6 +111,9 @@ final class OpenSamlLogoutRequestResolver { if (registration == null) { return null; } + if (registration.getAssertingPartyDetails().getSingleLogoutServiceLocation() == null) { + return null; + } LogoutRequest logoutRequest = this.logoutRequestBuilder.buildObject(); logoutRequest.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation()); Issuer issuer = this.issuerBuilder.buildObject(); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java index 935fb1febf..864eed0943 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java @@ -132,6 +132,9 @@ final class OpenSamlLogoutResponseResolver { if (registration == null) { return null; } + if (registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation() == null) { + return null; + } String serialized = request.getParameter(Saml2ParameterNames.SAML_REQUEST); byte[] b = Saml2Utils.samlDecode(serialized); LogoutRequest logoutRequest = parse(inflateIfRequired(registration, b)); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java index 99e88c9ae8..1aa5e69b37 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.java @@ -120,6 +120,12 @@ public final class Saml2LogoutRequestFilter extends OncePerRequestFilter { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } + if (registration.getSingleLogoutServiceLocation() == null) { + this.logger.trace( + "Did not process logout request since RelyingPartyRegistration has not been configured with a logout request endpoint"); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return; + } if (!isCorrectBinding(request, registration)) { this.logger.trace("Did not process logout request since used incorrect binding"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java index 83b4c8eccd..239249719a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.java @@ -120,6 +120,12 @@ public final class Saml2LogoutResponseFilter extends OncePerRequestFilter { response.sendError(HttpServletResponse.SC_BAD_REQUEST, error.toString()); return; } + if (registration.getSingleLogoutServiceResponseLocation() == null) { + this.logger.trace( + "Did not process logout response since RelyingPartyRegistration has not been configured with a logout response endpoint"); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return; + } if (!isCorrectBinding(request, registration)) { this.logger.trace("Did not process logout request since used incorrect binding"); response.sendError(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java index 99e5d225b1..57a7e7247b 100644 --- a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutRequestResolverTests.java @@ -47,7 +47,9 @@ public class OpenSaml3LogoutRequestResolverTests { this.relyingPartyRegistrationResolver); logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid")); HttpServletRequest request = new MockHttpServletRequest(); - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .assertingPartyDetails((party) -> party.singleLogoutServiceLocation("https://ap.example.com/logout")) + .build(); Authentication authentication = new TestingAuthenticationToken("user", "password"); given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication); diff --git a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java index 2e5a4a0a43..628d2401e4 100644 --- a/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml3Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml3LogoutResponseResolverTests.java @@ -53,7 +53,10 @@ public class OpenSaml3LogoutResponseResolverTests { Consumer parametersConsumer = mock(Consumer.class); logoutResponseResolver.setParametersConsumer(parametersConsumer); MockHttpServletRequest request = new MockHttpServletRequest(); - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .assertingPartyDetails( + (party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout")) + .build(); Authentication authentication = new TestingAuthenticationToken("user", "password"); LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); request.setParameter(Saml2ParameterNames.SAML_REQUEST, diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java index 6ea35b4716..4d7c5c266e 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutRequestResolverTests.java @@ -47,7 +47,9 @@ public class OpenSaml4LogoutRequestResolverTests { this.relyingPartyRegistrationResolver); logoutRequestResolver.setParametersConsumer((parameters) -> parameters.getLogoutRequest().setID("myid")); HttpServletRequest request = new MockHttpServletRequest(); - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .assertingPartyDetails((party) -> party.singleLogoutServiceLocation("https://ap.example.com/logout")) + .build(); Authentication authentication = new TestingAuthenticationToken("user", "password"); given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); Saml2LogoutRequest logoutRequest = logoutRequestResolver.resolve(request, authentication); diff --git a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java index 7353318fb9..20d1801857 100644 --- a/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java +++ b/saml2/saml2-service-provider/src/opensaml4Test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSaml4LogoutResponseResolverTests.java @@ -53,7 +53,10 @@ public class OpenSaml4LogoutResponseResolverTests { Consumer parametersConsumer = mock(Consumer.class); logoutResponseResolver.setParametersConsumer(parametersConsumer); MockHttpServletRequest request = new MockHttpServletRequest(); - RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration().build(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .assertingPartyDetails( + (party) -> party.singleLogoutServiceResponseLocation("https://ap.example.com/logout")) + .build(); Authentication authentication = new TestingAuthenticationToken("user", "password"); LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); request.setParameter(Saml2ParameterNames.SAML_REQUEST, diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java index be2069ab94..f5e6e44560 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlMetadataResolverTests.java @@ -70,4 +70,13 @@ public class OpenSamlMetadataResolverTests { assertThat(metadata).contains("format"); } + @Test + public void resolveWhenRelyingPartyNoLogoutThenMetadataMatches() { + RelyingPartyRegistration relyingPartyRegistration = TestRelyingPartyRegistrations.full() + .singleLogoutServiceLocation(null).nameIdFormat("format").build(); + OpenSamlMetadataResolver openSamlMetadataResolver = new OpenSamlMetadataResolver(); + String metadata = openSamlMetadataResolver.resolve(relyingPartyRegistration); + assertThat(metadata).doesNotContain("ResponseLocation"); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java index 2f08d6c122..bb604c4775 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilterTests.java @@ -153,4 +153,20 @@ public class Saml2LogoutRequestFilterTests { verifyNoInteractions(this.logoutHandler); } + @Test + public void doFilterWhenNoRelyingPartyLogoutThen401() throws Exception { + Authentication authentication = new TestingAuthenticationToken("user", "password"); + SecurityContextHolder.getContext().setAuthentication(authentication); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo"); + request.setServletPath("/logout/saml2/slo"); + request.setParameter(Saml2ParameterNames.SAML_REQUEST, "request"); + MockHttpServletResponse response = new MockHttpServletResponse(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().singleLogoutServiceLocation(null) + .build(); + given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); + this.logoutRequestProcessingFilter.doFilterInternal(request, response, new MockFilterChain()); + assertThat(response.getStatus()).isEqualTo(401); + verifyNoInteractions(this.logoutHandler); + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java index 2a86a06a26..b201e52a05 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilterTests.java @@ -37,6 +37,7 @@ import org.springframework.security.saml2.provider.service.registration.TestRely import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.mock; @@ -151,4 +152,23 @@ public class Saml2LogoutResponseFilterTests { verifyNoInteractions(this.logoutSuccessHandler); } + @Test + public void doFilterWhenNoRelyingPartyLogoutThen401() throws Exception { + Authentication authentication = new TestingAuthenticationToken("user", "password"); + SecurityContextHolder.getContext().setAuthentication(authentication); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/saml2/slo"); + request.setServletPath("/logout/saml2/slo"); + request.setParameter(Saml2ParameterNames.SAML_RESPONSE, "response"); + MockHttpServletResponse response = new MockHttpServletResponse(); + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().singleLogoutServiceLocation(null) + .singleLogoutServiceResponseLocation(null).build(); + given(this.relyingPartyRegistrationResolver.resolve(any(), any())).willReturn(registration); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest("request").build(); + given(this.logoutRequestRepository.removeLogoutRequest(request, response)).willReturn(logoutRequest); + this.logoutResponseProcessingFilter.doFilterInternal(request, response, new MockFilterChain()); + assertThat(response.getStatus()).isEqualTo(401); + verifyNoInteractions(this.logoutSuccessHandler); + } + } From 6b54afe9a3d01cbf19a9880443455ece067ce0e6 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 17 Dec 2021 16:05:39 -0700 Subject: [PATCH 081/589] Remove SAML 2.0 Logout Default Closes gh-10607 --- docs/modules/ROOT/pages/servlet/saml2/logout.adoc | 5 +++++ .../service/registration/RelyingPartyRegistration.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index 04c6ed0f94..9dba271b78 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -35,6 +35,7 @@ RelyingPartyRegistrationRepository registrations() { RelyingPartyRegistration registration = RelyingPartyRegistrations .fromMetadataLocation("https://ap.example.org/metadata") .registrationId("id") + .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo") .signingX509Credentials((signing) -> signing.add(credential)) <1> .build(); return new InMemoryRelyingPartyRegistrationRepository(registration); @@ -73,6 +74,10 @@ Also, your application can participate in an AP-initiated logout when the assert 3. Create, sign, and serialize a `` based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the just logged-out user 4. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] +NOTE: Adding `saml2Logout` adds the capability for logout to the service provider. +Because it is an optional capability, you need to enable it for each individual `RelyingPartyRegistration`. +You can do this by setting the `RelyingPartyRegistration.Builder#singleLogoutServiceLocation` property. + == Configuring Logout Endpoints There are three behaviors that can be triggered by different endpoints: diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index fa0a4a59d2..ab1ce03f6b 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -1027,7 +1027,7 @@ public final class RelyingPartyRegistration { private Saml2MessageBinding assertionConsumerServiceBinding = Saml2MessageBinding.POST; - private String singleLogoutServiceLocation = "{baseUrl}/logout/saml2/slo"; + private String singleLogoutServiceLocation; private String singleLogoutServiceResponseLocation; From 6884a167265ac94bcef9affd70869841e8b46db1 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 3 Jan 2022 13:31:03 -0600 Subject: [PATCH 082/589] Add CheckAntoraVersionPlugin --- buildSrc/build.gradle | 11 +- .../antora/CheckAntoraVersionPlugin.java | 66 +++++ .../gradle/antora/CheckAntoraVersionTask.java | 72 +++++ .../antora/CheckAntoraVersionPluginTests.java | 265 ++++++++++++++++++ docs/spring-security-docs.gradle | 1 + 5 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java create mode 100644 buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 3b1e9cf196..0890614beb 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -27,6 +27,10 @@ sourceSets { gradlePlugin { plugins { + checkAntoraVersion { + id = "org.springframework.antora.check-version" + implementationClass = "org.springframework.gradle.antora.CheckAntoraVersionPlugin" + } trang { id = "trang" implementationClass = "trang.TrangPlugin" @@ -72,6 +76,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.6' implementation 'com.thaiopensource:trang:20091111' implementation 'net.sourceforge.saxon:saxon:9.1.0.8' + implementation 'org.yaml:snakeyaml:1.30' implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' @@ -99,7 +104,11 @@ dependencies { } -test { +tasks.named('test', Test).configure { onlyIf { !project.hasProperty("buildSrc.skipTests") } useJUnitPlatform() + jvmArgs( + '--add-opens', 'java.base/java.lang=ALL-UNNAMED', + '--add-opens', 'java.base/java.util=ALL-UNNAMED' + ) } diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java new file mode 100644 index 0000000000..50bc8ea59d --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java @@ -0,0 +1,66 @@ +package org.springframework.gradle.antora; + +import org.gradle.api.Action; +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.language.base.plugins.LifecycleBasePlugin; + +public class CheckAntoraVersionPlugin implements Plugin { + public static final String ANTORA_CHECK_VERSION_TASK_NAME = "antoraCheckVersion"; + + @Override + public void apply(Project project) { + TaskProvider antoraCheckVersion = project.getTasks().register(ANTORA_CHECK_VERSION_TASK_NAME, CheckAntoraVersionTask.class, new Action() { + @Override + public void execute(CheckAntoraVersionTask antoraCheckVersion) { + antoraCheckVersion.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); + antoraCheckVersion.setDescription("Checks the antora.yml version properties match the Gradle version"); + antoraCheckVersion.getAntoraVersion().convention(project.provider(() -> getDefaultAntoraVersion(project))); + antoraCheckVersion.getAntoraPrerelease().convention(project.provider(() -> getDefaultAntoraPrerelease(project))); + antoraCheckVersion.getAntoraYmlFile().fileProvider(project.provider(() -> project.file("antora.yml"))); + } + }); + project.getPlugins().withType(LifecycleBasePlugin.class, new Action() { + @Override + public void execute(LifecycleBasePlugin lifecycleBasePlugin) { + project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME).configure(new Action() { + @Override + public void execute(Task check) { + check.dependsOn(antoraCheckVersion); + } + }); + } + }); + } + + private static String getDefaultAntoraVersion(Project project) { + String projectVersion = getProjectVersion(project); + int preReleaseIndex = getPreReleaseIndex(projectVersion); + return isPreRelease(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; + } + + private static String getDefaultAntoraPrerelease(Project project) { + String projectVersion = getProjectVersion(project); + int preReleaseIndex = getPreReleaseIndex(projectVersion); + return isPreRelease(projectVersion) ? projectVersion.substring(preReleaseIndex) : null; + } + + private static String getProjectVersion(Project project) { + Object projectVersion = project.getVersion(); + if (projectVersion == null) { + throw new GradleException("Please define antoraVersion and antoraPrerelease on " + ANTORA_CHECK_VERSION_TASK_NAME + " or provide a Project version so they can be defaulted"); + } + return String.valueOf(projectVersion); + } + + private static int getPreReleaseIndex(String projectVersion) { + return projectVersion.lastIndexOf("-"); + } + + private static boolean isPreRelease(String projectVersion) { + return getPreReleaseIndex(projectVersion) >= 0; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java new file mode 100644 index 0000000000..01225d79dc --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java @@ -0,0 +1,72 @@ +package org.springframework.gradle.antora; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.TaskAction; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +public abstract class CheckAntoraVersionTask extends DefaultTask { + + @TaskAction + public void check() throws FileNotFoundException { + File antoraYmlFile = getAntoraYmlFile().getAsFile().get(); + String expectedAntoraVersion = getAntoraVersion().get(); + String expectedAntoraPrerelease = getAntoraPrerelease().getOrElse(null); + + Representer representer = new Representer(); + representer.getPropertyUtils().setSkipMissingProperties(true); + + Yaml yaml = new Yaml(new Constructor(AntoraYml.class), representer); + AntoraYml antoraYml = yaml.load(new FileInputStream(antoraYmlFile)); + + String actualAntoraPrerelease = antoraYml.getPrerelease(); + boolean preReleaseMatches = antoraYml.getPrerelease() == null && expectedAntoraPrerelease == null || + (actualAntoraPrerelease != null && actualAntoraPrerelease.equals(expectedAntoraPrerelease)); + String actualAntoraVersion = antoraYml.getVersion(); + if (!preReleaseMatches || + !expectedAntoraVersion.equals(actualAntoraVersion)) { + throw new GradleException("The Gradle version of '" + getProject().getVersion() + "' should have version: '" + expectedAntoraVersion + "' and prerelease: '" + expectedAntoraPrerelease + "' defined in " + antoraYmlFile + " but got version: '" + actualAntoraVersion+"' and prerelease: '" + actualAntoraPrerelease + "'"); + } + } + + @InputFile + public abstract RegularFileProperty getAntoraYmlFile(); + + @Input + public abstract Property getAntoraVersion(); + + @Input + public abstract Property getAntoraPrerelease(); + + public static class AntoraYml { + private String version; + + private String prerelease; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getPrerelease() { + return prerelease; + } + + public void setPrerelease(String prerelease) { + this.prerelease = prerelease; + } + } +} diff --git a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java new file mode 100644 index 0000000000..719ab68ac2 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java @@ -0,0 +1,265 @@ +package org.springframework.gradle.antora; + +import org.apache.commons.io.IOUtils; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.testfixtures.ProjectBuilder; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; + +class CheckAntoraVersionPluginTests { + + @Test + void defaultsPropertiesWhenSnapshot() { + String expectedVersion = "1.0.0-SNAPSHOT"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void defaultsPropertiesWhenMilestone() { + String expectedVersion = "1.0.0-M1"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-M1"); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void defaultsPropertiesWhenRc() { + String expectedVersion = "1.0.0-RC1"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-RC1"); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void defaultsPropertiesWhenRelease() { + String expectedVersion = "1.0.0"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().isPresent()).isFalse(); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void explicitProperties() { + Project project = ProjectBuilder.builder().build(); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.getAntoraVersion().set("1.0.0"); + checkAntoraVersionTask.getAntoraPrerelease().set("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); + } + + @Test + void versionNotDefined() throws Exception { + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThatExceptionOfType(GradleException.class).isThrownBy(() -> checkAntoraVersionTask.check()); + } + + @Test + void antoraFileNotFound() throws Exception { + String expectedVersion = "1.0.0-SNAPSHOT"; + Project project = ProjectBuilder.builder().build(); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThatIOException().isThrownBy(() -> checkAntoraVersionTask.check()); + } + + @Test + void actualAntoraPrereleaseNull() throws Exception { + String expectedVersion = "1.0.0-SNAPSHOT"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + assertThatExceptionOfType(GradleException.class).isThrownBy(() -> checkAntoraVersionTask.check()); + + } + + @Test + void matchesWhenSnapshot() throws Exception { + String expectedVersion = "1.0.0-SNAPSHOT"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenMilestone() throws Exception { + String expectedVersion = "1.0.0-M1"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'\nprerelease: '-M1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenRc() throws Exception { + String expectedVersion = "1.0.0-RC1"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'\nprerelease: '-RC1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenReleaseAndPrereleaseUndefined() throws Exception { + String expectedVersion = "1.0.0"; + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.setVersion(expectedVersion); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenExplicitRelease() throws Exception { + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + ((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0"); + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenExplicitPrerelease() throws Exception { + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + ((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0"); + ((CheckAntoraVersionTask) task).getAntoraPrerelease().set("-SNAPSHOT"); + checkAntoraVersionTask.check(); + } + + @Test + void matchesWhenMissingPropertyDefined() throws Exception { + Project project = ProjectBuilder.builder().build(); + File rootDir = project.getRootDir(); + IOUtils.write("name: 'ROOT'\nversion: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + + Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + + assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); + CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; + ((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0"); + checkAntoraVersionTask.check(); + } + +} diff --git a/docs/spring-security-docs.gradle b/docs/spring-security-docs.gradle index a28a20c5b3..304c3ab40e 100644 --- a/docs/spring-security-docs.gradle +++ b/docs/spring-security-docs.gradle @@ -1,5 +1,6 @@ plugins { id "io.github.rwinch.antora" version "0.0.2" + id "org.springframework.antora.check-version" } apply plugin: 'io.spring.convention.docs' From 89366d08749658ee5cf3f9eb8f82025a33c0958b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 4 Jan 2022 15:50:42 -0600 Subject: [PATCH 083/589] Update RELEASE.adoc for antora.yml --- RELEASE.adoc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/RELEASE.adoc b/RELEASE.adoc index 964c7d48e7..b86aa832ce 100644 --- a/RELEASE.adoc +++ b/RELEASE.adoc @@ -78,6 +78,16 @@ Alternatively, you can manually check using https://github.com/spring-projects/s Update the version number in `gradle.properties` for the release, for example `5.5.0-M1`, `5.5.0-RC1`, `5.5.0` += Update Antora Version + +You will need to update the antora.yml version. +If you are unsure of what the values should be, the following task will instruct you what the expected values are: + +[source,bash] +---- +./gradlew :spring-security-docs:antoraCheckVersion +---- + = Build Locally Run the build using @@ -119,7 +129,7 @@ git push origin 5.4.0-RC1 == 7. Update to Next Development Version -* Update `gradle.properties` version to next `+SNAPSHOT+` version and then push +* Update `gradle.properties` version to next `+SNAPSHOT+` version, update antora.yml, and then push == 8. Update version on project page From 3bb82c4449b817c2ece43a75b259cb550f6e6772 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 5 Jan 2022 09:59:18 -0600 Subject: [PATCH 084/589] Antora prerelease: true for milestone and rc --- .../antora/CheckAntoraVersionPlugin.java | 24 +++++++++++++------ .../antora/CheckAntoraVersionPluginTests.java | 12 +++++----- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java index 50bc8ea59d..a0dcb966cc 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java @@ -38,14 +38,20 @@ public class CheckAntoraVersionPlugin implements Plugin { private static String getDefaultAntoraVersion(Project project) { String projectVersion = getProjectVersion(project); - int preReleaseIndex = getPreReleaseIndex(projectVersion); - return isPreRelease(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; + int preReleaseIndex = getSnapshotIndex(projectVersion); + return isSnapshot(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; } private static String getDefaultAntoraPrerelease(Project project) { String projectVersion = getProjectVersion(project); - int preReleaseIndex = getPreReleaseIndex(projectVersion); - return isPreRelease(projectVersion) ? projectVersion.substring(preReleaseIndex) : null; + if (isSnapshot(projectVersion)) { + int preReleaseIndex = getSnapshotIndex(projectVersion); + return projectVersion.substring(preReleaseIndex); + } + if (isPreRelease(projectVersion)) { + return Boolean.TRUE.toString(); + } + return null; } private static String getProjectVersion(Project project) { @@ -56,11 +62,15 @@ public class CheckAntoraVersionPlugin implements Plugin { return String.valueOf(projectVersion); } - private static int getPreReleaseIndex(String projectVersion) { - return projectVersion.lastIndexOf("-"); + private static boolean isSnapshot(String projectVersion) { + return getSnapshotIndex(projectVersion) >= 0; + } + + private static int getSnapshotIndex(String projectVersion) { + return projectVersion.lastIndexOf("-SNAPSHOT"); } private static boolean isPreRelease(String projectVersion) { - return getPreReleaseIndex(projectVersion) >= 0; + return projectVersion.lastIndexOf("-") >= 0; } } diff --git a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java index 719ab68ac2..81f5502572 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java @@ -46,8 +46,8 @@ class CheckAntoraVersionPluginTests { assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; - assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); - assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-M1"); + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-M1"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true"); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -63,8 +63,8 @@ class CheckAntoraVersionPluginTests { assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; - assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); - assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-RC1"); + assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-RC1"); + assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true"); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -170,7 +170,7 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-M1"; Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); - IOUtils.write("version: '1.0.0'\nprerelease: '-M1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + IOUtils.write("version: '1.0.0-M1'\nprerelease: 'true'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); project.getPluginManager().apply(CheckAntoraVersionPlugin.class); @@ -187,7 +187,7 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-RC1"; Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); - IOUtils.write("version: '1.0.0'\nprerelease: '-RC1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + IOUtils.write("version: '1.0.0-RC1'\nprerelease: 'true'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); project.getPluginManager().apply(CheckAntoraVersionPlugin.class); From f04cd641b035fc1a3bbed671cc84a3c7c19c759a Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Thu, 6 Jan 2022 13:16:10 -0300 Subject: [PATCH 085/589] Fix @since tag Issue gh-10590, gh-10554 --- .../AuthorizationManagerWebInvocationPrivilegeEvaluator.java | 2 +- ...RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java | 2 +- .../security/web/util/matcher/RequestMatcherEntry.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java index eb479a65e1..20776d8614 100644 --- a/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java +++ b/web/src/main/java/org/springframework/security/web/access/AuthorizationManagerWebInvocationPrivilegeEvaluator.java @@ -29,7 +29,7 @@ import org.springframework.util.Assert; * to an instance of {@link AuthorizationManager} * * @author Marcus Da Coregio - * @since 5.7 + * @since 5.5.5 */ public final class AuthorizationManagerWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { diff --git a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java index b4f5c1dc60..f2614962bb 100644 --- a/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java +++ b/web/src/main/java/org/springframework/security/web/access/RequestMatcherDelegatingWebInvocationPrivilegeEvaluator.java @@ -32,7 +32,7 @@ import org.springframework.util.Assert; * {@link org.springframework.security.web.util.matcher.RequestMatcher} evaluation * * @author Marcus Da Coregio - * @since 5.7 + * @since 5.5.5 */ public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator implements WebInvocationPrivilegeEvaluator { diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java index ea83cc7ac1..0cad6e3f08 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RequestMatcherEntry.java @@ -20,7 +20,7 @@ package org.springframework.security.web.util.matcher; * A rich object for associating a {@link RequestMatcher} to another object. * * @author Marcus Da Coregio - * @since 5.7 + * @since 5.5.5 */ public class RequestMatcherEntry { From 1ab0705b47620df48f9a6c400836e084056c2123 Mon Sep 17 00:00:00 2001 From: heowc Date: Sat, 8 Jan 2022 16:46:19 +0900 Subject: [PATCH 086/589] Fix typo --- .../web/configurers/ExpressionUrlAuthorizationConfigurer.java | 4 +--- .../security/web/util/matcher/ELRequestMatcher.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java index 1c0499693f..0a990fa876 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java @@ -369,9 +369,7 @@ public final class ExpressionUrlAuthorizationConfigurersubnet. + * Specify that URLs requires a specific IP Address or subnet. * @param ipaddressExpression the ipaddress (i.e. 192.168.1.79) or local subnet * (i.e. 192.168.0/24) * @return the {@link ExpressionUrlAuthorizationConfigurer} for further diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java index f3ce9d70a6..492ea1eda2 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/ELRequestMatcher.java @@ -29,7 +29,7 @@ import org.springframework.security.web.authentication.DelegatingAuthenticationE * *

* With the default EvaluationContext ({@link ELRequestMatcherContext}) you can use - * hasIpAdress() and hasHeader() + * hasIpAddress() and hasHeader() *

* *

From 214cfe807eb5726e46fc1eb13da76743f7f6bb42 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 7 Jan 2022 13:23:02 -0500 Subject: [PATCH 087/589] Allow Jwt assertion to be resolved Closes gh-9812 --- .../oauth2/client/authorization-grants.adoc | 6 ++++ .../oauth2/client/authorization-grants.adoc | 6 ++++ ...tBearerOAuth2AuthorizedClientProvider.java | 27 ++++++++++++-- ...eactiveOAuth2AuthorizedClientProvider.java | 36 +++++++++++++++---- ...erOAuth2AuthorizedClientProviderTests.java | 36 +++++++++++++++++-- ...veOAuth2AuthorizedClientProviderTests.java | 35 ++++++++++++++++-- 6 files changed, 131 insertions(+), 15 deletions(-) diff --git a/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc index ab33687111..a2a7a77cd4 100644 --- a/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc +++ b/docs/modules/ROOT/pages/reactive/oauth2/client/authorization-grants.adoc @@ -1092,3 +1092,9 @@ class OAuth2ResourceServerController { } ---- ==== + +[NOTE] +`JwtBearerReactiveOAuth2AuthorizedClientProvider` resolves the `Jwt` assertion via `OAuth2AuthorizationContext.getPrincipal().getPrincipal()` by default, hence the use of `JwtAuthenticationToken` in the preceding example. + +[TIP] +If you need to resolve the `Jwt` assertion from a different source, you can provide `JwtBearerReactiveOAuth2AuthorizedClientProvider.setJwtAssertionResolver()` with a custom `Function>`. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc b/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc index 09a98abbd3..192c9ebde5 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/client/authorization-grants.adoc @@ -1270,3 +1270,9 @@ class OAuth2ResourceServerController { } ---- ==== + +[NOTE] +`JwtBearerOAuth2AuthorizedClientProvider` resolves the `Jwt` assertion via `OAuth2AuthorizationContext.getPrincipal().getPrincipal()` by default, hence the use of `JwtAuthenticationToken` in the preceding example. + +[TIP] +If you need to resolve the `Jwt` assertion from a different source, you can provide `JwtBearerOAuth2AuthorizedClientProvider.setJwtAssertionResolver()` with a custom `Function`. diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java index 87accc63a3..857f38af0b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.security.oauth2.client; import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.function.Function; import org.springframework.lang.Nullable; import org.springframework.security.oauth2.client.endpoint.DefaultJwtBearerTokenResponseClient; @@ -45,6 +46,8 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth private OAuth2AccessTokenResponseClient accessTokenResponseClient = new DefaultJwtBearerTokenResponseClient(); + private Function jwtAssertionResolver = this::resolveJwtAssertion; + private Duration clockSkew = Duration.ofSeconds(60); private Clock clock = Clock.systemUTC(); @@ -75,10 +78,10 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth // need for re-authorization return null; } - if (!(context.getPrincipal().getPrincipal() instanceof Jwt)) { + Jwt jwt = this.jwtAssertionResolver.apply(context); + if (jwt == null) { return null; } - Jwt jwt = (Jwt) context.getPrincipal().getPrincipal(); // As per spec, in section 4.1 Using Assertions as Authorization Grants // https://tools.ietf.org/html/rfc7521#section-4.1 // @@ -97,6 +100,13 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth tokenResponse.getAccessToken()); } + private Jwt resolveJwtAssertion(OAuth2AuthorizationContext context) { + if (!(context.getPrincipal().getPrincipal() instanceof Jwt)) { + return null; + } + return (Jwt) context.getPrincipal().getPrincipal(); + } + private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration, JwtBearerGrantRequest jwtBearerGrantRequest) { try { @@ -123,6 +133,17 @@ public final class JwtBearerOAuth2AuthorizedClientProvider implements OAuth2Auth this.accessTokenResponseClient = accessTokenResponseClient; } + /** + * Sets the resolver used for resolving the {@link Jwt} assertion. + * @param jwtAssertionResolver the resolver used for resolving the {@link Jwt} + * assertion + * @since 5.7 + */ + public void setJwtAssertionResolver(Function jwtAssertionResolver) { + Assert.notNull(jwtAssertionResolver, "jwtAssertionResolver cannot be null"); + this.jwtAssertionResolver = jwtAssertionResolver; + } + /** * Sets the maximum acceptable clock skew, which is used when checking the * {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java index eb60c3c4bb..a15da34b3c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.security.oauth2.client; import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.function.Function; import reactor.core.publisher.Mono; @@ -45,6 +46,8 @@ public final class JwtBearerReactiveOAuth2AuthorizedClientProvider implements Re private ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient = new WebClientReactiveJwtBearerTokenResponseClient(); + private Function> jwtAssertionResolver = this::resolveJwtAssertion; + private Duration clockSkew = Duration.ofSeconds(60); private Clock clock = Clock.systemUTC(); @@ -74,10 +77,7 @@ public final class JwtBearerReactiveOAuth2AuthorizedClientProvider implements Re // need for re-authorization return Mono.empty(); } - if (!(context.getPrincipal().getPrincipal() instanceof Jwt)) { - return Mono.empty(); - } - Jwt jwt = (Jwt) context.getPrincipal().getPrincipal(); + // As per spec, in section 4.1 Using Assertions as Authorization Grants // https://tools.ietf.org/html/rfc7521#section-4.1 // @@ -90,13 +90,26 @@ public final class JwtBearerReactiveOAuth2AuthorizedClientProvider implements Re // issued with a reasonably short lifetime. Clients can refresh an // expired access token by requesting a new one using the same // assertion, if it is still valid, or with a new assertion. - return Mono.just(new JwtBearerGrantRequest(clientRegistration, jwt)) + + // @formatter:off + return this.jwtAssertionResolver.apply(context) + .map((jwt) -> new JwtBearerGrantRequest(clientRegistration, jwt)) .flatMap(this.accessTokenResponseClient::getTokenResponse) .onErrorMap(OAuth2AuthorizationException.class, (ex) -> new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex)) .map((tokenResponse) -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(), tokenResponse.getAccessToken())); + // @formatter:on + } + + private Mono resolveJwtAssertion(OAuth2AuthorizationContext context) { + // @formatter:off + return Mono.just(context) + .map((ctx) -> ctx.getPrincipal().getPrincipal()) + .filter((principal) -> principal instanceof Jwt) + .cast(Jwt.class); + // @formatter:on } private boolean hasTokenExpired(OAuth2Token token) { @@ -115,6 +128,17 @@ public final class JwtBearerReactiveOAuth2AuthorizedClientProvider implements Re this.accessTokenResponseClient = accessTokenResponseClient; } + /** + * Sets the resolver used for resolving the {@link Jwt} assertion. + * @param jwtAssertionResolver the resolver used for resolving the {@link Jwt} + * assertion + * @since 5.7 + */ + public void setJwtAssertionResolver(Function> jwtAssertionResolver) { + Assert.notNull(jwtAssertionResolver, "jwtAssertionResolver cannot be null"); + this.jwtAssertionResolver = jwtAssertionResolver; + } + /** * Sets the maximum acceptable clock skew, which is used when checking the * {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java index 0ea5e25552..49d8dc416e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerOAuth2AuthorizedClientProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client; import java.time.Duration; import java.time.Instant; +import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link JwtBearerOAuth2AuthorizedClientProvider}. @@ -87,6 +89,13 @@ public class JwtBearerOAuth2AuthorizedClientProviderTests { .withMessage("accessTokenResponseClient cannot be null"); } + @Test + public void setJwtAssertionResolverWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.authorizedClientProvider.setJwtAssertionResolver(null)) + .withMessage("jwtAssertionResolver cannot be null"); + } + @Test public void setClockSkewWhenNullThenThrowIllegalArgumentException() { // @formatter:off @@ -198,7 +207,7 @@ public class JwtBearerOAuth2AuthorizedClientProviderTests { } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalNotJwtThenUnableToAuthorize() { + public void authorizeWhenJwtBearerAndNotAuthorizedAndJwtDoesNotResolveThenUnableToAuthorize() { // @formatter:off OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext .withClientRegistration(this.clientRegistration) @@ -209,7 +218,7 @@ public class JwtBearerOAuth2AuthorizedClientProviderTests { } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalJwtThenAuthorize() { + public void authorizeWhenJwtBearerAndNotAuthorizedAndJwtResolvesThenAuthorize() { OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse); // @formatter:off @@ -224,4 +233,25 @@ public class JwtBearerOAuth2AuthorizedClientProviderTests { assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); } + @Test + public void authorizeWhenCustomJwtAssertionResolverSetThenUsed() { + Function jwtAssertionResolver = mock(Function.class); + given(jwtAssertionResolver.apply(any())).willReturn(this.jwtAssertion); + this.authorizedClientProvider.setJwtAssertionResolver(jwtAssertionResolver); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse); + // @formatter:off + TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password"); + OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext + .withClientRegistration(this.clientRegistration) + .principal(principal) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = this.authorizedClientProvider.authorize(authorizationContext); + verify(jwtAssertionResolver).apply(any()); + assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(principal.getName()); + assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + } + } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProviderTests.java index 33279c6f94..2ec6e2f4a0 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JwtBearerReactiveOAuth2AuthorizedClientProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.security.oauth2.client; import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -93,6 +94,13 @@ public class JwtBearerReactiveOAuth2AuthorizedClientProviderTests { .withMessage("accessTokenResponseClient cannot be null"); } + @Test + public void setJwtAssertionResolverWhenNullThenThrowIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.authorizedClientProvider.setJwtAssertionResolver(null)) + .withMessage("jwtAssertionResolver cannot be null"); + } + @Test public void setClockSkewWhenNullThenThrowIllegalArgumentException() { // @formatter:off @@ -222,7 +230,7 @@ public class JwtBearerReactiveOAuth2AuthorizedClientProviderTests { } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalNotJwtThenUnableToAuthorize() { + public void authorizeWhenJwtBearerAndNotAuthorizedAndJwtDoesNotResolveThenUnableToAuthorize() { // @formatter:off OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext .withClientRegistration(this.clientRegistration) @@ -251,7 +259,7 @@ public class JwtBearerReactiveOAuth2AuthorizedClientProviderTests { } @Test - public void authorizeWhenJwtBearerAndNotAuthorizedAndPrincipalJwtThenAuthorize() { + public void authorizeWhenJwtBearerAndNotAuthorizedAndJwtResolvesThenAuthorize() { OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(Mono.just(accessTokenResponse)); // @formatter:off @@ -266,4 +274,25 @@ public class JwtBearerReactiveOAuth2AuthorizedClientProviderTests { assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); } + @Test + public void authorizeWhenCustomJwtAssertionResolverSetThenUsed() { + Function> jwtAssertionResolver = mock(Function.class); + given(jwtAssertionResolver.apply(any())).willReturn(Mono.just(this.jwtAssertion)); + this.authorizedClientProvider.setJwtAssertionResolver(jwtAssertionResolver); + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse().build(); + given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(Mono.just(accessTokenResponse)); + // @formatter:off + TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password"); + OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext + .withClientRegistration(this.clientRegistration) + .principal(principal) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = this.authorizedClientProvider.authorize(authorizationContext).block(); + verify(jwtAssertionResolver).apply(any()); + assertThat(authorizedClient.getClientRegistration()).isSameAs(this.clientRegistration); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(principal.getName()); + assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + } + } From 60ed3602f6281d1a34c643484dfcb3440e2243d5 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Tue, 11 Jan 2022 08:24:49 -0300 Subject: [PATCH 088/589] Make source code compatible with JDK 8 Closes gh-10695 --- build.gradle | 1 + .../OAuth2ResourceServerConfigurer.java | 11 +- .../saml2/Saml2LoginConfigurer.java | 3 +- .../saml2/Saml2LogoutConfigurer.java | 3 +- .../configurers/NamespaceHttpX509Tests.java | 12 +- .../core/SpringSecurityCoreVersionTests.java | 18 ++- .../core/StaticFinalReflectionUtils.java | 115 ------------------ 7 files changed, 31 insertions(+), 132 deletions(-) delete mode 100644 core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java diff --git a/build.gradle b/build.gradle index 86306c7518..5e92ef6d74 100644 --- a/build.gradle +++ b/build.gradle @@ -105,6 +105,7 @@ subprojects { tasks.withType(JavaCompile) { options.encoding = "UTF-8" options.compilerArgs.add("-parameters") + options.release = 8 } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index ca7ef5720d..0f26791597 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.se import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Supplier; @@ -27,6 +28,7 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.context.ApplicationContext; import org.springframework.core.convert.converter.Converter; import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; @@ -159,13 +161,18 @@ public final class OAuth2ResourceServerConfigurer(Map.of(CsrfException.class, new AccessDeniedHandlerImpl())), - new BearerTokenAccessDeniedHandler()); + new LinkedHashMap<>(createAccessDeniedHandlers()), new BearerTokenAccessDeniedHandler()); private AuthenticationEntryPoint authenticationEntryPoint = new BearerTokenAuthenticationEntryPoint(); private BearerTokenRequestMatcher requestMatcher = new BearerTokenRequestMatcher(); + private static Map, AccessDeniedHandler> createAccessDeniedHandlers() { + Map, AccessDeniedHandler> handlers = new HashMap<>(); + handlers.put(CsrfException.class, new AccessDeniedHandlerImpl()); + return handlers; + } + public OAuth2ResourceServerConfigurer(ApplicationContext context) { Assert.notNull(context, "context cannot be null"); this.context = context; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index aa1ddb29af..ed594983c4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -297,8 +297,7 @@ public final class Saml2LoginConfigurer> if (version != null) { return version; } - return Version.class.getModule().getDescriptor().version().map(Object::toString) - .orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version")); + return Version.getVersion(); } private void registerDefaultAuthenticationProvider(B http) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java index 45bd549c01..9e4c4eac0f 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -319,8 +319,7 @@ public final class Saml2LogoutConfigurer> if (version != null) { return version; } - return Version.class.getModule().getDescriptor().version().map(Object::toString) - .orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version")); + return Version.getVersion(); } /** diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.java index d54a0a286e..7868021f15 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpX509Tests.java @@ -21,11 +21,13 @@ import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import javax.security.auth.x500.X500Principal; import javax.servlet.http.HttpServletRequest; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import sun.security.x509.X500Name; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -240,12 +242,8 @@ public class NamespaceHttpX509Tests { } private String extractCommonName(X509Certificate certificate) { - try { - return ((X500Name) certificate.getSubjectDN()).getCommonName(); - } - catch (Exception ex) { - throw new IllegalArgumentException(ex); - } + X500Principal principal = certificate.getSubjectX500Principal(); + return new X500Name(principal.getName()).getRDNs(BCStyle.CN)[0].getFirst().getValue().toString(); } } diff --git a/core/src/test/java/org/springframework/security/core/SpringSecurityCoreVersionTests.java b/core/src/test/java/org/springframework/security/core/SpringSecurityCoreVersionTests.java index fe2bfbadb1..d2d9b0e665 100644 --- a/core/src/test/java/org/springframework/security/core/SpringSecurityCoreVersionTests.java +++ b/core/src/test/java/org/springframework/security/core/SpringSecurityCoreVersionTests.java @@ -18,6 +18,7 @@ package org.springframework.security.core; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -60,15 +61,24 @@ public class SpringSecurityCoreVersionTests { @BeforeEach public void setup() throws Exception { - Field logger = ReflectionUtils.findField(SpringSecurityCoreVersion.class, "logger"); - StaticFinalReflectionUtils.setField(logger, this.logger); + setFinalStaticField(SpringSecurityCoreVersion.class, "logger", this.logger); } @AfterEach public void cleanup() throws Exception { System.clearProperty(getDisableChecksProperty()); - Field logger = ReflectionUtils.findField(SpringSecurityCoreVersion.class, "logger"); - StaticFinalReflectionUtils.setField(logger, LogFactory.getLog(SpringSecurityCoreVersion.class)); + setFinalStaticField(SpringSecurityCoreVersion.class, "logger", + LogFactory.getLog(SpringSecurityCoreVersion.class)); + } + + private static void setFinalStaticField(Class clazz, String fieldName, Object value) + throws ReflectiveOperationException { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + Field modifiers = Field.class.getDeclaredField("modifiers"); + modifiers.setAccessible(true); + modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, value); } @Test diff --git a/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java b/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java deleted file mode 100644 index 1cff222680..0000000000 --- a/core/src/test/java/org/springframework/security/core/StaticFinalReflectionUtils.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2008 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.core; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.security.AccessController; -import java.security.PrivilegedAction; - -import sun.misc.Unsafe; - -import org.springframework.objenesis.instantiator.util.UnsafeUtils; - -/** - * Used for setting static variables even if they are private static final. - * - * The code in this class has been adopted from Powermock's WhiteboxImpl. - * - * @author Rob Winch - */ -final class StaticFinalReflectionUtils { - - /** - * Used to support setting static fields that are final using Java's Unsafe. If the - * field is not static final, use - * {@link org.springframework.test.util.ReflectionTestUtils}. - * @param field the field to set - * @param newValue the new value - */ - static void setField(final Field field, final Object newValue) { - try { - field.setAccessible(true); - int fieldModifiersMask = field.getModifiers(); - boolean isFinalModifierPresent = (fieldModifiersMask & Modifier.FINAL) == Modifier.FINAL; - if (isFinalModifierPresent) { - AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Object run() { - try { - Unsafe unsafe = UnsafeUtils.getUnsafe(); - long offset = unsafe.staticFieldOffset(field); - Object base = unsafe.staticFieldBase(field); - setFieldUsingUnsafe(base, field.getType(), offset, newValue, unsafe); - return null; - } - catch (Throwable thrown) { - throw new RuntimeException(thrown); - } - } - }); - } - else { - field.set(null, newValue); - } - } - catch (SecurityException ex) { - throw new RuntimeException(ex); - } - catch (IllegalAccessException ex) { - throw new RuntimeException(ex); - } - catch (IllegalArgumentException ex) { - throw new RuntimeException(ex); - } - } - - private static void setFieldUsingUnsafe(Object base, Class type, long offset, Object newValue, Unsafe unsafe) { - if (type == Integer.TYPE) { - unsafe.putInt(base, offset, ((Integer) newValue)); - } - else if (type == Short.TYPE) { - unsafe.putShort(base, offset, ((Short) newValue)); - } - else if (type == Long.TYPE) { - unsafe.putLong(base, offset, ((Long) newValue)); - } - else if (type == Byte.TYPE) { - unsafe.putByte(base, offset, ((Byte) newValue)); - } - else if (type == Boolean.TYPE) { - unsafe.putBoolean(base, offset, ((Boolean) newValue)); - } - else if (type == Float.TYPE) { - unsafe.putFloat(base, offset, ((Float) newValue)); - } - else if (type == Double.TYPE) { - unsafe.putDouble(base, offset, ((Double) newValue)); - } - else if (type == Character.TYPE) { - unsafe.putChar(base, offset, ((Character) newValue)); - } - else { - unsafe.putObject(base, offset, newValue); - } - } - - private StaticFinalReflectionUtils() { - } - -} From 194eaf8491cc419c6e7d4da890554c2c0b4e84ee Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 11 Jan 2022 09:58:35 -0700 Subject: [PATCH 089/589] Pull most recent Structure101 version Closes gh-10696 --- buildSrc/src/main/java/s101/S101Configurer.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/buildSrc/src/main/java/s101/S101Configurer.java b/buildSrc/src/main/java/s101/S101Configurer.java index 414e488535..1575f47fbd 100644 --- a/buildSrc/src/main/java/s101/S101Configurer.java +++ b/buildSrc/src/main/java/s101/S101Configurer.java @@ -164,14 +164,18 @@ public class S101Configurer { String source = "https://structure101.com/binaries/v6"; try (final WebClient webClient = new WebClient()) { HtmlPage page = webClient.getPage(source); + Matcher matcher = null; for (HtmlAnchor anchor : page.getAnchors()) { - Matcher matcher = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute()); - if (matcher.find()) { - copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2)); - return matcher.group(2); + Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute()); + if (candidate.find()) { + matcher = candidate; } } - return null; + if (matcher == null) { + return null; + } + copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2)); + return matcher.group(2); } catch (Exception ex) { throw new RuntimeException(ex); } From 8abd4e999f094f5cf2b08da8bba2bfcbacd20a4d Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 22 Dec 2021 10:05:59 -0600 Subject: [PATCH 090/589] Add GitHubReleasePlugin with createGitHubRelease task Closes gh-10456 Closes gh-10457 --- build.gradle | 8 + buildSrc/build.gradle | 4 + .../{milestones => }/RepositoryRef.java | 6 +- .../changelog/GitHubChangelogPlugin.java | 9 +- .../github/milestones/GitHubMilestoneApi.java | 6 +- .../GitHubMilestoneHasNoOpenIssuesTask.java | 2 + .../release/CreateGitHubReleaseTask.java | 130 +++++++++++++++ .../github/release/GitHubReleaseApi.java | 91 ++++++++++ .../github/release/GitHubReleasePlugin.java | 49 ++++++ .../gradle/github/release/Release.java | 156 ++++++++++++++++++ .../milestones/GitHubMilestoneApiTests.java | 7 +- .../milestones/GitHubMilestoneApiTests.java | 4 +- .../github/release/GitHubReleaseApiTests.java | 151 +++++++++++++++++ 13 files changed, 610 insertions(+), 13 deletions(-) rename buildSrc/src/main/java/org/springframework/gradle/github/{milestones => }/RepositoryRef.java (93%) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java create mode 100644 buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java diff --git a/build.gradle b/build.gradle index 5e92ef6d74..f1b7ecbfc9 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ apply plugin: 'org.springframework.security.update-dependencies' apply plugin: 'org.springframework.security.sagan' apply plugin: 'org.springframework.github.milestone' apply plugin: 'org.springframework.github.changelog' +apply plugin: 'org.springframework.github.release' group = 'org.springframework.security' description = 'Spring Security' @@ -46,6 +47,13 @@ tasks.named("gitHubCheckMilestoneHasNoOpenIssues") { } } +tasks.named("createGitHubRelease") { + repository { + owner = "spring-projects" + name = "spring-security" + } +} + tasks.named("updateDependencies") { // we aren't Gradle 7 compatible yet checkForGradleUpdate = false diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 0890614beb..380d1248ac 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -59,6 +59,10 @@ gradlePlugin { id = "org.springframework.github.changelog" implementationClass = "org.springframework.gradle.github.changelog.GitHubChangelogPlugin" } + githubRelease { + id = "org.springframework.github.release" + implementationClass = "org.springframework.gradle.github.release.GitHubReleasePlugin" + } s101 { id = "s101" implementationClass = "s101.S101Plugin" diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/RepositoryRef.java b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java similarity index 93% rename from buildSrc/src/main/java/org/springframework/gradle/github/milestones/RepositoryRef.java rename to buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java index 70eec3b150..e570a47e90 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/RepositoryRef.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java @@ -1,10 +1,11 @@ -package org.springframework.gradle.github.milestones; +package org.springframework.gradle.github; + public class RepositoryRef { private String owner; private String name; - RepositoryRef() { + public RepositoryRef() { } public RepositoryRef(String owner, String name) { @@ -62,4 +63,3 @@ public class RepositoryRef { } } } - diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java index 2000e1a378..0eab3d8006 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/changelog/GitHubChangelogPlugin.java @@ -16,6 +16,9 @@ package org.springframework.gradle.github.changelog; +import java.io.File; +import java.nio.file.Paths; + import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -28,12 +31,10 @@ import org.gradle.api.artifacts.repositories.IvyArtifactRepository; import org.gradle.api.artifacts.repositories.IvyPatternRepositoryLayout; import org.gradle.api.tasks.JavaExec; -import java.io.File; -import java.nio.file.Paths; - public class GitHubChangelogPlugin implements Plugin { public static final String CHANGELOG_GENERATOR_CONFIGURATION_NAME = "changelogGenerator"; + public static final String RELEASE_NOTES_PATH = "changelog/release-notes.md"; @Override public void apply(Project project) { @@ -42,7 +43,7 @@ public class GitHubChangelogPlugin implements Plugin { project.getTasks().register("generateChangelog", JavaExec.class, new Action() { @Override public void execute(JavaExec generateChangelog) { - File outputFile = project.file(Paths.get(project.getBuildDir().getPath(), "changelog/release-notes.md")); + File outputFile = project.file(Paths.get(project.getBuildDir().getPath(), RELEASE_NOTES_PATH)); outputFile.getParentFile().mkdirs(); generateChangelog.setGroup("Release"); generateChangelog.setDescription("Generates the changelog"); diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java index 31f1274adb..fd3c0d817b 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java @@ -16,6 +16,9 @@ package org.springframework.gradle.github.milestones; +import java.io.IOException; +import java.util.List; + import com.google.common.reflect.TypeToken; import com.google.gson.Gson; import okhttp3.Interceptor; @@ -23,8 +26,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import java.io.IOException; -import java.util.List; +import org.springframework.gradle.github.RepositoryRef; public class GitHubMilestoneApi { private String baseUrl = "https://api.github.com"; diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java index de846378f7..40b026c804 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java @@ -21,6 +21,8 @@ import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; +import org.springframework.gradle.github.RepositoryRef; + public class GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask { @Input private RepositoryRef repository = new RepositoryRef(); diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java new file mode 100644 index 0000000000..65c8b687be --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/CreateGitHubReleaseTask.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; + +import org.springframework.gradle.github.RepositoryRef; +import org.springframework.gradle.github.changelog.GitHubChangelogPlugin; + +/** + * @author Steve Riesenberg + */ +public class CreateGitHubReleaseTask extends DefaultTask { + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input @Optional + private String gitHubAccessToken; + + @Input + private String version; + + @Input @Optional + private String branch = "main"; + + @Input + private boolean createRelease = false; + + @TaskAction + public void createGitHubRelease() { + String body = readReleaseNotes(); + Release release = Release.tag(this.version) + .commit(this.branch) + .name(this.version) + .body(body) + .preRelease(this.version.contains("-")) + .build(); + + System.out.printf("%sCreating GitHub release for %s/%s@%s\n", + this.createRelease ? "" : "[DRY RUN] ", + this.repository.getOwner(), + this.repository.getName(), + this.version + ); + System.out.printf(" Release Notes:\n\n----\n%s\n----\n\n", body.trim()); + + if (this.createRelease) { + GitHubReleaseApi github = new GitHubReleaseApi(this.gitHubAccessToken); + github.publishRelease(this.repository, release); + } + } + + private String readReleaseNotes() { + Project project = getProject(); + File inputFile = project.file(Paths.get(project.getBuildDir().getPath(), GitHubChangelogPlugin.RELEASE_NOTES_PATH)); + try { + return Files.readString(inputFile.toPath()); + } catch (IOException ex) { + throw new RuntimeException("Unable to read release notes from " + inputFile, ex); + } + } + + public RepositoryRef getRepository() { + return repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public boolean isCreateRelease() { + return createRelease; + } + + public void setCreateRelease(boolean createRelease) { + this.createRelease = createRelease; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java new file mode 100644 index 0000000000..65238d0b82 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleaseApi.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import java.io.IOException; + +import com.google.gson.Gson; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * Manage GitHub releases. + * + * @author Steve Riesenberg + */ +public class GitHubReleaseApi { + private String baseUrl = "https://api.github.com"; + + private final OkHttpClient httpClient; + private Gson gson = new Gson(); + + public GitHubReleaseApi(String gitHubAccessToken) { + this.httpClient = new OkHttpClient.Builder() + .addInterceptor(new AuthorizationInterceptor(gitHubAccessToken)) + .build(); + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + /** + * Publish a release with no binary attachments. + * + * @param repository The repository owner/name + * @param release The contents of the release + */ + public void publishRelease(RepositoryRef repository, Release release) { + String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/releases"; + String json = this.gson.toJson(release); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), json); + Request request = new Request.Builder().url(url).post(body).build(); + try { + Response response = this.httpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException(String.format("Could not create release %s for repository %s/%s. Got response %s", + release.getName(), repository.getOwner(), repository.getName(), response)); + } + } catch (IOException ex) { + throw new RuntimeException(String.format("Could not create release %s for repository %s/%s", + release.getName(), repository.getOwner(), repository.getName()), ex); + } + } + + private static class AuthorizationInterceptor implements Interceptor { + private final String token; + + public AuthorizationInterceptor(String token) { + this.token = token; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request().newBuilder() + .addHeader("Authorization", "Bearer " + this.token) + .build(); + + return chain.proceed(request); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java new file mode 100644 index 0000000000..ae2c44a769 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import groovy.lang.MissingPropertyException; +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +/** + * @author Steve Riesenberg + */ +public class GitHubReleasePlugin implements Plugin { + @Override + public void apply(Project project) { + project.getTasks().register("createGitHubRelease", CreateGitHubReleaseTask.class, new Action() { + @Override + public void execute(CreateGitHubReleaseTask createGitHubRelease) { + createGitHubRelease.setGroup("Release"); + createGitHubRelease.setDescription("Create a github release"); + createGitHubRelease.dependsOn("generateChangelog"); + + createGitHubRelease.setCreateRelease("true".equals(project.findProperty("createRelease"))); + createGitHubRelease.setVersion((String) project.findProperty("nextVersion")); + if (project.hasProperty("branch")) { + createGitHubRelease.setBranch((String) project.findProperty("branch")); + } + createGitHubRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + if (createGitHubRelease.isCreateRelease() && createGitHubRelease.getGitHubAccessToken() == null) { + throw new MissingPropertyException("Please provide an access token with -PgitHubAccessToken=..."); + } + } + }); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java new file mode 100644 index 0000000000..6dec2ceb79 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/Release.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Steve Riesenberg + */ +public class Release { + @SerializedName("tag_name") + private final String tag; + + @SerializedName("target_commitish") + private final String commit; + + @SerializedName("name") + private final String name; + + @SerializedName("body") + private final String body; + + @SerializedName("draft") + private final boolean draft; + + @SerializedName("prerelease") + private final boolean preRelease; + + @SerializedName("generate_release_notes") + private final boolean generateReleaseNotes; + + private Release(String tag, String commit, String name, String body, boolean draft, boolean preRelease, boolean generateReleaseNotes) { + this.tag = tag; + this.commit = commit; + this.name = name; + this.body = body; + this.draft = draft; + this.preRelease = preRelease; + this.generateReleaseNotes = generateReleaseNotes; + } + + public String getTag() { + return tag; + } + + public String getCommit() { + return commit; + } + + public String getName() { + return name; + } + + public String getBody() { + return body; + } + + public boolean isDraft() { + return draft; + } + + public boolean isPreRelease() { + return preRelease; + } + + public boolean isGenerateReleaseNotes() { + return generateReleaseNotes; + } + + @Override + public String toString() { + return "Release{" + + "tag='" + tag + '\'' + + ", commit='" + commit + '\'' + + ", name='" + name + '\'' + + ", body='" + body + '\'' + + ", draft=" + draft + + ", preRelease=" + preRelease + + ", generateReleaseNotes=" + generateReleaseNotes + + '}'; + } + + public static Builder tag(String tag) { + return new Builder().tag(tag); + } + + public static Builder commit(String commit) { + return new Builder().commit(commit); + } + + public static final class Builder { + private String tag; + private String commit; + private String name; + private String body; + private boolean draft; + private boolean preRelease; + private boolean generateReleaseNotes; + + private Builder() { + } + + public Builder tag(String tag) { + this.tag = tag; + return this; + } + + public Builder commit(String commit) { + this.commit = commit; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder body(String body) { + this.body = body; + return this; + } + + public Builder draft(boolean draft) { + this.draft = draft; + return this; + } + + public Builder preRelease(boolean preRelease) { + this.preRelease = preRelease; + return this; + } + + public Builder generateReleaseNotes(boolean generateReleaseNotes) { + this.generateReleaseNotes = generateReleaseNotes; + return this; + } + + public Release build() { + return new Release(tag, commit, name, body, draft, preRelease, generateReleaseNotes); + } + } +} diff --git a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java index 183cf09d5a..b9b0764ee5 100644 --- a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -1,15 +1,16 @@ package io.spring.gradle.github.milestones; +import java.util.concurrent.TimeUnit; + import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.gradle.github.milestones.GitHubMilestoneApi; -import org.springframework.gradle.github.milestones.RepositoryRef; -import java.util.concurrent.TimeUnit; +import org.springframework.gradle.github.RepositoryRef; +import org.springframework.gradle.github.milestones.GitHubMilestoneApi; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java index b4072c079e..0a1a293ab0 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -1,5 +1,7 @@ package org.springframework.gradle.github.milestones; +import java.util.concurrent.TimeUnit; + import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -7,7 +9,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.concurrent.TimeUnit; +import org.springframework.gradle.github.RepositoryRef; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java new file mode 100644 index 0000000000..6ac7955722 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import java.util.concurrent.TimeUnit; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import org.springframework.gradle.github.RepositoryRef; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Steve Riesenberg + */ +public class GitHubReleaseApiTests { + private GitHubReleaseApi github; + + private RepositoryRef repository = new RepositoryRef("spring-projects", "spring-security"); + + private MockWebServer server; + + private String baseUrl; + + @BeforeEach + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.github = new GitHubReleaseApi("mock-oauth-token"); + this.baseUrl = this.server.url("/api").toString(); + this.github.setBaseUrl(this.baseUrl); + } + + @AfterEach + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void publishReleaseWhenValidParametersThenSuccess() throws Exception { + String responseJson = "{\n" + + " \"url\": \"https://api.github.com/spring-projects/spring-security/releases/1\",\n" + + " \"html_url\": \"https://github.com/spring-projects/spring-security/releases/tags/v1.0.0\",\n" + + " \"assets_url\": \"https://api.github.com/spring-projects/spring-security/releases/1/assets\",\n" + + " \"upload_url\": \"https://uploads.github.com/spring-projects/spring-security/releases/1/assets{?name,label}\",\n" + + " \"tarball_url\": \"https://api.github.com/spring-projects/spring-security/tarball/v1.0.0\",\n" + + " \"zipball_url\": \"https://api.github.com/spring-projects/spring-security/zipball/v1.0.0\",\n" + + " \"discussion_url\": \"https://github.com/spring-projects/spring-security/discussions/90\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDc6UmVsZWFzZTE=\",\n" + + " \"tag_name\": \"v1.0.0\",\n" + + " \"target_commitish\": \"main\",\n" + + " \"name\": \"v1.0.0\",\n" + + " \"body\": \"Description of the release\",\n" + + " \"draft\": false,\n" + + " \"prerelease\": false,\n" + + " \"created_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"published_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"author\": {\n" + + " \"login\": \"sjohnr\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDQ6VXNlcjE=\",\n" + + " \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" + + " \"gravatar_id\": \"\",\n" + + " \"url\": \"https://api.github.com/users/sjohnr\",\n" + + " \"html_url\": \"https://github.com/sjohnr\",\n" + + " \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" + + " \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" + + " \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" + + " \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" + + " \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" + + " \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" + + " \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" + + " \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" + + " \"type\": \"User\",\n" + + " \"site_admin\": false\n" + + " },\n" + + " \"assets\": [\n" + + " {\n" + + " \"url\": \"https://api.github.com/spring-projects/spring-security/releases/assets/1\",\n" + + " \"browser_download_url\": \"https://github.com/spring-projects/spring-security/releases/download/v1.0.0/example.zip\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDEyOlJlbGVhc2VBc3NldDE=\",\n" + + " \"name\": \"example.zip\",\n" + + " \"label\": \"short description\",\n" + + " \"state\": \"uploaded\",\n" + + " \"content_type\": \"application/zip\",\n" + + " \"size\": 1024,\n" + + " \"download_count\": 42,\n" + + " \"created_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"updated_at\": \"2013-02-27T19:35:32Z\",\n" + + " \"uploader\": {\n" + + " \"login\": \"sjohnr\",\n" + + " \"id\": 1,\n" + + " \"node_id\": \"MDQ6VXNlcjE=\",\n" + + " \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" + + " \"gravatar_id\": \"\",\n" + + " \"url\": \"https://api.github.com/users/sjohnr\",\n" + + " \"html_url\": \"https://github.com/sjohnr\",\n" + + " \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" + + " \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" + + " \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" + + " \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" + + " \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" + + " \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" + + " \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" + + " \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" + + " \"type\": \"User\",\n" + + " \"site_admin\": false\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + this.github.publishRelease(this.repository, Release.tag("1.0.0").build()); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/releases"); + assertThat(recordedRequest.getBody().toString()).isEqualTo("{\"tag_name\":\"1.0.0\"}"); + } + + @Test + public void publishReleaseWhenErrorResponseThenException() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(400)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.publishRelease(this.repository, Release.tag("1.0.0").build())); + } +} From 493933150105202a80fde38d024f96f48e5e2ae7 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Wed, 22 Dec 2021 10:06:43 -0600 Subject: [PATCH 091/589] Fix inconsistency in hasProperty check --- .../gradle/github/milestones/GitHubMilestonePlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java index 527b767613..81663f2561 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java @@ -29,7 +29,7 @@ public class GitHubMilestonePlugin implements Plugin { githubCheckMilestoneHasNoOpenIssues.setGroup("Release"); githubCheckMilestoneHasNoOpenIssues.setDescription("Checks if there are any open issues for the specified repository and milestone"); githubCheckMilestoneHasNoOpenIssues.setMilestoneTitle((String) project.findProperty("nextVersion")); - if (project.hasProperty("githubAccessToken")) { + if (project.hasProperty("gitHubAccessToken")) { githubCheckMilestoneHasNoOpenIssues.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); } } From 801dcfdcb432d624638ccee852f4fb68eeda8e0d Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 13 Jan 2022 21:49:45 -0600 Subject: [PATCH 092/589] Allow milestones and release candidates in version upgrades --- build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index f1b7ecbfc9..2b49ab86eb 100644 --- a/build.gradle +++ b/build.gradle @@ -72,8 +72,6 @@ updateDependenciesSettings { dependencyExcludes { majorVersionBump() alphaBetaVersions() - releaseCandidatesVersions() - milestoneVersions() snapshotVersions() addRule { components -> components.withModule("commons-codec:commons-codec") { selection -> From 08139cf9f4822ac5be03ab170a5537c648256739 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:52:39 -0600 Subject: [PATCH 093/589] Update logback-classic to 1.2.10 Closes gh-10709 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 6529e68893..30f0c867c3 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -16,7 +16,7 @@ dependencies { api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") api platform("com.fasterxml.jackson:jackson-bom:2.13.0") constraints { - api "ch.qos.logback:logback-classic:1.2.7" + api "ch.qos.logback:logback-classic:1.2.10" api "com.google.inject:guice:3.0" api "com.nimbusds:nimbus-jose-jwt:9.14" api "com.nimbusds:oauth2-oidc-sdk:9.19" From 9b12616913fc65057893511cbd96aa955ad9c21b Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:52:41 -0600 Subject: [PATCH 094/589] Update jackson-bom to 2.13.1 Closes gh-10710 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 30f0c867c3..0734440dce 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -14,7 +14,7 @@ dependencies { api platform("org.springframework.data:spring-data-bom:2021.1.0") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") - api platform("com.fasterxml.jackson:jackson-bom:2.13.0") + api platform("com.fasterxml.jackson:jackson-bom:2.13.1") constraints { api "ch.qos.logback:logback-classic:1.2.10" api "com.google.inject:guice:3.0" From b91d38752ac95f350194ff183e77e51575eee259 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:52:48 -0600 Subject: [PATCH 095/589] Update com.nimbusds to 9.22 Closes gh-10713 --- dependencies/spring-security-dependencies.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 0734440dce..0fbc10062f 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -18,8 +18,8 @@ dependencies { constraints { api "ch.qos.logback:logback-classic:1.2.10" api "com.google.inject:guice:3.0" - api "com.nimbusds:nimbus-jose-jwt:9.14" - api "com.nimbusds:oauth2-oidc-sdk:9.19" + api "com.nimbusds:nimbus-jose-jwt:9.15.2" + api "com.nimbusds:oauth2-oidc-sdk:9.22" api "com.squareup.okhttp3:mockwebserver:3.14.9" api "com.squareup.okhttp3:okhttp:3.14.9" api "com.unboundid:unboundid-ldapsdk:4.0.14" From 7d0185f051e19be0e6f65ff05deeb7182126c0ce Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:52:50 -0600 Subject: [PATCH 096/589] Update mockk to 1.12.2 Closes gh-10714 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 0fbc10062f..af4365d3b0 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -26,7 +26,7 @@ dependencies { api "commons-codec:commons-codec:1.15" api "commons-collections:commons-collections:3.2.2" api "commons-logging:commons-logging:1.2" - api "io.mockk:mockk:1.12.0" + api "io.mockk:mockk:1.12.2" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" From 5cd7c719230dd501c1ba2183083941442eca00e1 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:52:53 -0600 Subject: [PATCH 097/589] Update io.projectreactor to 2020.0.15 Closes gh-10715 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 380d1248ac..2231abe18f 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -84,7 +84,7 @@ dependencies { implementation localGroovy() implementation 'io.github.gradle-nexus:publish-plugin:1.1.0' - implementation 'io.projectreactor:reactor-core:3.4.12' + implementation 'io.projectreactor:reactor-core:3.4.14' implementation 'gradle.plugin.org.gretty:gretty:3.0.1' implementation 'com.apollographql.apollo:apollo-runtime:2.4.5' implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index af4365d3b0..23c2f88c76 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -8,7 +8,7 @@ javaPlatform { dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") - api platform("io.projectreactor:reactor-bom:2020.0.13") + api platform("io.projectreactor:reactor-bom:2020.0.15") api platform("io.rsocket:rsocket-bom:1.1.1") api platform("org.junit:junit-bom:5.8.1") api platform("org.springframework.data:spring-data-bom:2021.1.0") From 7c54f98944932c8f244a65993c5468895eab3364 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:52:57 -0600 Subject: [PATCH 098/589] Update io.r2dbc to 0.9.0.RELEASE Closes gh-10717 --- oauth2/oauth2-client/spring-security-oauth2-client.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index 3a6b1c1394..99ba72bc7f 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -23,8 +23,8 @@ dependencies { testImplementation 'io.projectreactor:reactor-test' testImplementation 'io.projectreactor.tools:blockhound' testImplementation 'org.skyscreamer:jsonassert' - testImplementation 'io.r2dbc:r2dbc-h2:0.8.4.RELEASE' - testImplementation 'io.r2dbc:r2dbc-spi-test:0.8.6.RELEASE' + testImplementation 'io.r2dbc:r2dbc-h2:0.9.0.RELEASE' + testImplementation 'io.r2dbc:r2dbc-spi-test:0.9.0.RELEASE' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" From 60653ddf196fdfeb12340b934152a9e5885c8086 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:52:59 -0600 Subject: [PATCH 099/589] Update htmlunit to 2.56.0 Closes gh-10718 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 23c2f88c76..2334dc55da 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -35,7 +35,7 @@ dependencies { api "javax.xml.bind:jaxb-api:2.3.1" api "ldapsdk:ldapsdk:4.1" api "net.sf.ehcache:ehcache:2.10.9.2" - api "net.sourceforge.htmlunit:htmlunit:2.54.0" + api "net.sourceforge.htmlunit:htmlunit:2.56.0" api "net.sourceforge.nekohtml:nekohtml:1.9.22" api "org.apache.directory.server:apacheds-core-entry:1.5.5" api "org.apache.directory.server:apacheds-core:1.5.5" From bb92dd3cc5a9c62d8ac43d5198a1effffaa1fe53 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:53:01 -0600 Subject: [PATCH 100/589] Update org.aspectj to 1.9.8.RC3 Closes gh-10719 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 09d81058b8..6c22e52fcf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -aspectjVersion=1.9.7 +aspectjVersion=1.9.8.RC3 springJavaformatVersion=0.0.29 springBootVersion=2.4.2 springFrameworkVersion=5.3.13 From f8c8d049c3e718ecb527e1d2c9577e8a0fa5d5b3 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:53:04 -0600 Subject: [PATCH 101/589] Update assertj-core to 3.22.0 Closes gh-10720 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 2334dc55da..ec6c4957ac 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -46,7 +46,7 @@ dependencies { api "org.apache.httpcomponents:httpclient:4.5.13" api "org.aspectj:aspectjrt:$aspectjVersion" api "org.aspectj:aspectjweaver:$aspectjVersion" - api "org.assertj:assertj-core:3.21.0" + api "org.assertj:assertj-core:3.22.0" api "org.bouncycastle:bcpkix-jdk15on:1.69" api "org.bouncycastle:bcprov-jdk15on:1.69" api "org.eclipse.jetty:jetty-server:9.4.44.v20210927" From c6c27d795bd2931ed5993e27210d8dfcc87ae441 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:53:06 -0600 Subject: [PATCH 102/589] Update org.bouncycastle to 1.70 Closes gh-10721 --- dependencies/spring-security-dependencies.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index ec6c4957ac..95f04f9414 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -47,8 +47,8 @@ dependencies { api "org.aspectj:aspectjrt:$aspectjVersion" api "org.aspectj:aspectjweaver:$aspectjVersion" api "org.assertj:assertj-core:3.22.0" - api "org.bouncycastle:bcpkix-jdk15on:1.69" - api "org.bouncycastle:bcprov-jdk15on:1.69" + api "org.bouncycastle:bcpkix-jdk15on:1.70" + api "org.bouncycastle:bcprov-jdk15on:1.70" api "org.eclipse.jetty:jetty-server:9.4.44.v20210927" api "org.eclipse.jetty:jetty-servlet:9.4.44.v20210927" api "org.eclipse.persistence:javax.persistence:2.2.1" From 5187bea5eadd6b6e9b8e7127a5b297ddbf21fbf2 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:53:09 -0600 Subject: [PATCH 103/589] Update hibernate-entitymanager to 5.6.3.Final Closes gh-10722 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 95f04f9414..505b29f5f8 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -53,7 +53,7 @@ dependencies { api "org.eclipse.jetty:jetty-servlet:9.4.44.v20210927" api "org.eclipse.persistence:javax.persistence:2.2.1" api "org.hamcrest:hamcrest:2.2" - api "org.hibernate:hibernate-entitymanager:5.6.1.Final" + api "org.hibernate:hibernate-entitymanager:5.6.3.Final" api "org.hsqldb:hsqldb:2.6.1" api "org.jasig.cas.client:cas-client-core:3.6.2" api "org.mockito:mockito-core:3.12.4" From 815891d6f0f0f5eaf96f063639ea884d0f6a0699 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:53:11 -0600 Subject: [PATCH 104/589] Update cas-client-core to 3.6.4 Closes gh-10723 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 505b29f5f8..0e0ce309f0 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -55,7 +55,7 @@ dependencies { api "org.hamcrest:hamcrest:2.2" api "org.hibernate:hibernate-entitymanager:5.6.3.Final" api "org.hsqldb:hsqldb:2.6.1" - api "org.jasig.cas.client:cas-client-core:3.6.2" + api "org.jasig.cas.client:cas-client-core:3.6.4" api "org.mockito:mockito-core:3.12.4" api "org.mockito:mockito-inline:3.12.4" api "org.mockito:mockito-junit-jupiter:3.12.4" From bb9d9d7f9d3f7eea62a5fec14b669a5b5084e834 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:53:13 -0600 Subject: [PATCH 105/589] Update org.jetbrains.kotlin to 1.6.10 Closes gh-10724 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6c22e52fcf..37ea3bba10 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ springBootVersion=2.4.2 springFrameworkVersion=5.3.13 openSamlVersion=3.4.6 version=5.7.0-SNAPSHOT -kotlinVersion=1.5.31 +kotlinVersion=1.6.10 samplesBranch=main org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true From 2ed93ec0cf01445e08ee5e691dc8a57bc790fa01 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 12 Jan 2022 14:35:10 -0300 Subject: [PATCH 106/589] Rename integrationTestCompile/Runtime configurations The kotlin-gradle-plugin is changing some configuration's properties from configurations that has the same prefix as the sourceSet. It is enforcing the canBeResolved property to false. See https://youtrack.jetbrains.com/issue/KT-50748. This commits changes the suffix to compile -> compileClasspath, runtime -> runtimeClasspath to workaround this issue. Issue gh-10350 --- .../convention/IntegrationTestPlugin.groovy | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy index 9858458b8f..059fdfbc8b 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/IntegrationTestPlugin.groovy @@ -59,14 +59,22 @@ public class IntegrationTestPlugin implements Plugin { integrationTestRuntime { extendsFrom integrationTestCompile, testRuntime, testRuntimeOnly } + integrationTestCompileClasspath { + extendsFrom integrationTestCompile + canBeResolved = true + } + integrationTestRuntimeClasspath { + extendsFrom integrationTestRuntime + canBeResolved = true + } } project.sourceSets { integrationTest { java.srcDir project.file('src/integration-test/java') resources.srcDir project.file('src/integration-test/resources') - compileClasspath = project.sourceSets.main.output + project.sourceSets.test.output + project.configurations.integrationTestCompile - runtimeClasspath = output + compileClasspath + project.configurations.integrationTestRuntime + compileClasspath = project.sourceSets.main.output + project.sourceSets.test.output + project.configurations.integrationTestCompileClasspath + runtimeClasspath = output + compileClasspath + project.configurations.integrationTestRuntimeClasspath } } @@ -85,7 +93,7 @@ public class IntegrationTestPlugin implements Plugin { project.idea { module { testSourceDirs += project.file('src/integration-test/java') - scopes.TEST.plus += [ project.configurations.integrationTestCompile ] + scopes.TEST.plus += [ project.configurations.integrationTestCompileClasspath ] } } } @@ -115,7 +123,7 @@ public class IntegrationTestPlugin implements Plugin { project.plugins.withType(EclipsePlugin) { project.eclipse.classpath { - plusConfigurations += [ project.configurations.integrationTestCompile ] + plusConfigurations += [ project.configurations.integrationTestCompileClasspath ] } } } From e5212926a123e6d46946b1d71a6e64bba616a85d Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:53:15 -0600 Subject: [PATCH 107/589] Update org.jetbrains.kotlinx to 1.6.0 Closes gh-10725 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 0e0ce309f0..7841736fb5 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -13,7 +13,7 @@ dependencies { api platform("org.junit:junit-bom:5.8.1") api platform("org.springframework.data:spring-data-bom:2021.1.0") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") - api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2") + api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.0") api platform("com.fasterxml.jackson:jackson-bom:2.13.1") constraints { api "ch.qos.logback:logback-classic:1.2.10" From 08bc7a53d6b664b0fce011d9bdcc6d851e81f20b Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:53:18 -0600 Subject: [PATCH 108/589] Update junit-bom to 5.8.2 Closes gh-10726 --- buildSrc/build.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 2231abe18f..034706721b 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -96,7 +96,7 @@ dependencies { implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.24.20' implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1' - testImplementation platform('org.junit:junit-bom:5.8.1') + testImplementation platform('org.junit:junit-bom:5.8.2') testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" testImplementation "org.junit.jupiter:junit-jupiter-engine" diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 7841736fb5..5c63b55eda 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -10,7 +10,7 @@ dependencies { api platform("org.springframework:spring-framework-bom:$springFrameworkVersion") api platform("io.projectreactor:reactor-bom:2020.0.15") api platform("io.rsocket:rsocket-bom:1.1.1") - api platform("org.junit:junit-bom:5.8.1") + api platform("org.junit:junit-bom:5.8.2") api platform("org.springframework.data:spring-data-bom:2021.1.0") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.0") From 29bc6745e814f52f5e3c057378948afff9317547 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:53:23 -0600 Subject: [PATCH 109/589] Update htmlunit-driver to 2.56.0 Closes gh-10728 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 5c63b55eda..689fc6dc09 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -64,7 +64,7 @@ dependencies { api "org.opensaml:opensaml-saml-api:$openSamlVersion" api "org.opensaml:opensaml-saml-impl:$openSamlVersion" api "org.python:jython:2.5.3" - api "org.seleniumhq.selenium:htmlunit-driver:2.54.0" + api "org.seleniumhq.selenium:htmlunit-driver:2.56.0" api "org.seleniumhq.selenium:selenium-java:3.141.59" api "org.seleniumhq.selenium:selenium-support:3.141.59" api "org.skyscreamer:jsonassert:1.5.0" From 8aa7b5897ac0d79e076ff6b057b99b344d50c9bf Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:55:25 -0600 Subject: [PATCH 110/589] Update org.slf4j to 1.7.33 Closes gh-10729 --- dependencies/spring-security-dependencies.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 689fc6dc09..f92e975524 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -68,9 +68,9 @@ dependencies { api "org.seleniumhq.selenium:selenium-java:3.141.59" api "org.seleniumhq.selenium:selenium-support:3.141.59" api "org.skyscreamer:jsonassert:1.5.0" - api "org.slf4j:jcl-over-slf4j:1.7.32" - api "org.slf4j:log4j-over-slf4j:1.7.32" - api "org.slf4j:slf4j-api:1.7.32" + api "org.slf4j:jcl-over-slf4j:1.7.33" + api "org.slf4j:log4j-over-slf4j:1.7.33" + api "org.slf4j:slf4j-api:1.7.33" api "org.springframework.ldap:spring-ldap-core:2.3.4.RELEASE" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } From 28ea347b5ac602bf596470c9512b31d3188cf9a2 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:55:28 -0600 Subject: [PATCH 111/589] Update org.springframework to 5.3.15 Closes gh-10730 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 37ea3bba10..67cb9c6f54 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ aspectjVersion=1.9.8.RC3 springJavaformatVersion=0.0.29 springBootVersion=2.4.2 -springFrameworkVersion=5.3.13 +springFrameworkVersion=5.3.15 openSamlVersion=3.4.6 version=5.7.0-SNAPSHOT kotlinVersion=1.6.10 From ce35bee1bd684bab16931fba7133552be61d9867 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:55:30 -0600 Subject: [PATCH 112/589] Update org.springframework.data to 2021.2.0-M1 Closes gh-10731 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index f92e975524..00f90d743d 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -11,7 +11,7 @@ dependencies { api platform("io.projectreactor:reactor-bom:2020.0.15") api platform("io.rsocket:rsocket-bom:1.1.1") api platform("org.junit:junit-bom:5.8.2") - api platform("org.springframework.data:spring-data-bom:2021.1.0") + api platform("org.springframework.data:spring-data-bom:2021.2.0-M1") api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion") api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.0") api platform("com.fasterxml.jackson:jackson-bom:2.13.1") From 4fd5c7ffa325890d9ea96346412a1b4a04750d5f Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 11:55:32 -0600 Subject: [PATCH 113/589] Update spring-ldap-core to 2.4.0-M1 Closes gh-10732 --- dependencies/spring-security-dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 00f90d743d..7f5b1cf4cc 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -71,7 +71,7 @@ dependencies { api "org.slf4j:jcl-over-slf4j:1.7.33" api "org.slf4j:log4j-over-slf4j:1.7.33" api "org.slf4j:slf4j-api:1.7.33" - api "org.springframework.ldap:spring-ldap-core:2.3.4.RELEASE" + api "org.springframework.ldap:spring-ldap-core:2.4.0-M1" api "org.synchronoss.cloud:nio-multipart-parser:1.1.0" } } From a8457b518fa7185985504991340dc64837731bd1 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 12:16:08 -0600 Subject: [PATCH 114/589] Release 5.7.0-M1 --- docs/antora.yml | 4 ++-- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index c306b92d0e..2f314f81b1 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,3 @@ name: ROOT -version: '5.7.0' -prerelease: '-SNAPSHOT' +version: '5.7.0-M1' +prerelease: 'true' diff --git a/gradle.properties b/gradle.properties index 67cb9c6f54..e9633bb634 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.29 springBootVersion=2.4.2 springFrameworkVersion=5.3.15 openSamlVersion=3.4.6 -version=5.7.0-SNAPSHOT +version=5.7.0-M1 kotlinVersion=1.6.10 samplesBranch=main org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 7efbc9d5f7d45877a4eccacf4c159d29b8ca417b Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 14 Jan 2022 12:50:05 -0600 Subject: [PATCH 115/589] Next Development Version --- docs/antora.yml | 4 ++-- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 2f314f81b1..c306b92d0e 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,3 +1,3 @@ name: ROOT -version: '5.7.0-M1' -prerelease: 'true' +version: '5.7.0' +prerelease: '-SNAPSHOT' diff --git a/gradle.properties b/gradle.properties index e9633bb634..67cb9c6f54 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ springJavaformatVersion=0.0.29 springBootVersion=2.4.2 springFrameworkVersion=5.3.15 openSamlVersion=3.4.6 -version=5.7.0-M1 +version=5.7.0-SNAPSHOT kotlinVersion=1.6.10 samplesBranch=main org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError From 700cae8d3b2590397dc38ac1eb67d1b19203ec0d Mon Sep 17 00:00:00 2001 From: Robert Stoiber Date: Sat, 8 Jan 2022 10:00:28 +0100 Subject: [PATCH 116/589] Enabled SAML LogoutRequests with EncryptedID The OpenSamlLogoutRequestValidator validates the subject to be logged out. Formerly this was done only using the NameID from the OpenSamlLogoutRequest. Now the EncryptedID is also supported, Since the SAML2 Standard also allows the EncryptedID as subject identifiers, - added EncryptedID as valid subject in OpenSamlLogoutRequestValidator - added test Closes gh-10663 --- .../logout/LogoutRequestEncryptedIDUtils.java | 81 +++++++++++++++++++ .../OpenSamlLogoutRequestValidator.java | 37 +++++++-- .../authentication/TestOpenSamlObjects.java | 18 +++++ .../OpenSamlLogoutRequestValidatorTests.java | 21 +++++ 4 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIDUtils.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIDUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIDUtils.java new file mode 100644 index 0000000000..a7fd9a6f5f --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIDUtils.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication.logout; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.saml.saml2.core.EncryptedID; +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; +import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; +import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +/** + * Utility methods for decrypting EncryptedID from SAML logout request with OpenSAML + * + * For internal use only. + * + * this is mainly a adapted copy of OpenSamlDecryptionUtils + * + * @author Robert Stoiber + */ +final class LogoutRequestEncryptedIDUtils { + + private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( + Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), + new SimpleRetrievalMethodEncryptedKeyResolver())); + + static SAMLObject decryptEncryptedID(EncryptedID encryptedID, RelyingPartyRegistration registration) { + Decrypter decrypter = decrypter(registration); + try { + return decrypter.decrypt(encryptedID); + + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + + private static Decrypter decrypter(RelyingPartyRegistration registration) { + Collection credentials = new ArrayList<>(); + for (Saml2X509Credential key : registration.getDecryptionX509Credentials()) { + Credential cred = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); + credentials.add(cred); + } + KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials); + Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver); + decrypter.setRootInNewDocument(true); + return decrypter; + } + + private LogoutRequestEncryptedIDUtils() { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java index 69df68246a..e20082a760 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java @@ -25,6 +25,8 @@ import net.shibboleth.utilities.java.support.xml.ParserPool; import org.opensaml.core.config.ConfigurationService; import org.opensaml.core.xml.config.XMLObjectProviderRegistry; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.common.SAMLObject; +import org.opensaml.saml.saml2.core.EncryptedID; import org.opensaml.saml.saml2.core.LogoutRequest; import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.impl.LogoutRequestUnmarshaller; @@ -118,7 +120,7 @@ public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestV return (errors) -> { validateIssuer(request, registration).accept(errors); validateDestination(request, registration).accept(errors); - validateName(request, authentication).accept(errors); + validateSubject(request, registration, authentication).accept(errors); }; } @@ -153,23 +155,44 @@ public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestV }; } - private Consumer> validateName(LogoutRequest request, Authentication authentication) { + private Consumer> validateSubject(LogoutRequest request, + RelyingPartyRegistration registration, Authentication authentication) { return (errors) -> { if (authentication == null) { return; } NameID nameId = request.getNameID(); - if (nameId == null) { + EncryptedID encryptedID = request.getEncryptedID(); + if (nameId == null && encryptedID == null) { errors.add( new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest")); return; } - String name = nameId.getValue(); - if (!name.equals(authentication.getName())) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST, - "Failed to match subject in LogoutRequest with currently logged in user")); + + if (nameId != null) { + validateNameID(nameId, authentication, errors); + } + else { + final NameID nameIDFromEncryptedID = decryptNameID(encryptedID, registration); + validateNameID(nameIDFromEncryptedID, authentication, errors); } }; } + private void validateNameID(NameID nameId, Authentication authentication, Collection errors) { + String name = nameId.getValue(); + if (!name.equals(authentication.getName())) { + errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST, + "Failed to match subject in LogoutRequest with currently logged in user")); + } + } + + private NameID decryptNameID(EncryptedID encryptedID, RelyingPartyRegistration registration) { + final SAMLObject decryptedId = LogoutRequestEncryptedIDUtils.decryptEncryptedID(encryptedID, registration); + if (decryptedId instanceof NameID) { + return ((NameID) decryptedId); + } + return null; + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 0f38823e22..49eae5891d 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -365,6 +365,24 @@ public final class TestOpenSamlObjects { return logoutRequest; } + public static LogoutRequest assertingPartyLogoutRequestNameIdInEncryptedId(RelyingPartyRegistration registration) { + LogoutRequestBuilder logoutRequestBuilder = new LogoutRequestBuilder(); + LogoutRequest logoutRequest = logoutRequestBuilder.buildObject(); + logoutRequest.setID("id"); + NameIDBuilder nameIdBuilder = new NameIDBuilder(); + NameID nameId = nameIdBuilder.buildObject(); + nameId.setValue("user"); + logoutRequest.setNameID(null); + logoutRequest.setEncryptedID(encrypted(nameId, + registration.getAssertingPartyDetails().getEncryptionX509Credentials().stream().findFirst().get())); + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + issuer.setValue(registration.getAssertingPartyDetails().getEntityId()); + logoutRequest.setIssuer(issuer); + logoutRequest.setDestination(registration.getSingleLogoutServiceLocation()); + return logoutRequest; + } + public static LogoutResponse assertingPartyLogoutResponse(RelyingPartyRegistration registration) { LogoutResponseBuilder logoutResponseBuilder = new LogoutResponseBuilder(); LogoutResponse logoutResponse = logoutResponseBuilder.buildObject(); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java index e5c826fe37..8bd38f988e 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java @@ -60,6 +60,21 @@ public class OpenSamlLogoutRequestValidatorTests { assertThat(result.hasErrors()).isFalse(); } + @Test + public void handleWhenNameIdInEncryptedIdPostThenValidates() { + + RelyingPartyRegistration registration = registrationWithEncryption() + .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build(); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequestNameIdInEncryptedId(registration); + sign(logoutRequest, registration); + Saml2LogoutRequest request = post(logoutRequest, registration); + Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, + registration, authentication(registration)); + Saml2LogoutValidatorResult result = this.manager.validate(parameters); + assertThat(result.hasErrors()).withFailMessage(() -> result.getErrors().toString()).isFalse().isFalse(); + + } + @Test public void handleWhenRedirectBindingThenValidatesSignatureParameter() { RelyingPartyRegistration registration = registration() @@ -134,6 +149,12 @@ public class OpenSamlLogoutRequestValidatorTests { .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)); } + private RelyingPartyRegistration.Builder registrationWithEncryption() { + return signing(verifying(TestRelyingPartyRegistrations.full())) + .assertingPartyDetails((party) -> party.encryptionX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.assertingPartyEncryptingCredential()))); + } + private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) { return builder.assertingPartyDetails((party) -> party .verificationX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyVerifyingCredential()))); From 3c45d46bd74db9975568dddc3358a739131d062f Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 13 Jan 2022 16:22:26 -0700 Subject: [PATCH 117/589] Polish LogoutRequest#EncryptedID Support Issue gh-10663 --- ...ava => LogoutRequestEncryptedIdUtils.java} | 10 +++--- .../OpenSamlLogoutRequestValidator.java | 33 +++++++++++-------- .../authentication/TestOpenSamlObjects.java | 6 ++-- .../OpenSamlLogoutRequestValidatorTests.java | 19 ++++++----- 4 files changed, 39 insertions(+), 29 deletions(-) rename saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/{LogoutRequestEncryptedIDUtils.java => LogoutRequestEncryptedIdUtils.java} (92%) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIDUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIdUtils.java similarity index 92% rename from saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIDUtils.java rename to saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIdUtils.java index a7fd9a6f5f..5ff94a701b 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIDUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/LogoutRequestEncryptedIdUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,16 +46,16 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP * * @author Robert Stoiber */ -final class LogoutRequestEncryptedIDUtils { +final class LogoutRequestEncryptedIdUtils { private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), new SimpleRetrievalMethodEncryptedKeyResolver())); - static SAMLObject decryptEncryptedID(EncryptedID encryptedID, RelyingPartyRegistration registration) { + static SAMLObject decryptEncryptedId(EncryptedID encryptedId, RelyingPartyRegistration registration) { Decrypter decrypter = decrypter(registration); try { - return decrypter.decrypt(encryptedID); + return decrypter.decrypt(encryptedId); } catch (Exception ex) { @@ -75,7 +75,7 @@ final class LogoutRequestEncryptedIDUtils { return decrypter; } - private LogoutRequestEncryptedIDUtils() { + private LogoutRequestEncryptedIdUtils() { } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java index e20082a760..5345aa8875 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -161,25 +161,30 @@ public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestV if (authentication == null) { return; } - NameID nameId = request.getNameID(); - EncryptedID encryptedID = request.getEncryptedID(); - if (nameId == null && encryptedID == null) { + NameID nameId = getNameId(request, registration); + if (nameId == null) { errors.add( new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND, "Failed to find subject in LogoutRequest")); return; } - if (nameId != null) { - validateNameID(nameId, authentication, errors); - } - else { - final NameID nameIDFromEncryptedID = decryptNameID(encryptedID, registration); - validateNameID(nameIDFromEncryptedID, authentication, errors); - } + validateNameId(nameId, authentication, errors); }; } - private void validateNameID(NameID nameId, Authentication authentication, Collection errors) { + private NameID getNameId(LogoutRequest request, RelyingPartyRegistration registration) { + NameID nameId = request.getNameID(); + if (nameId != null) { + return nameId; + } + EncryptedID encryptedId = request.getEncryptedID(); + if (encryptedId == null) { + return null; + } + return decryptNameId(encryptedId, registration); + } + + private void validateNameId(NameID nameId, Authentication authentication, Collection errors) { String name = nameId.getValue(); if (!name.equals(authentication.getName())) { errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_REQUEST, @@ -187,8 +192,8 @@ public final class OpenSamlLogoutRequestValidator implements Saml2LogoutRequestV } } - private NameID decryptNameID(EncryptedID encryptedID, RelyingPartyRegistration registration) { - final SAMLObject decryptedId = LogoutRequestEncryptedIDUtils.decryptEncryptedID(encryptedID, registration); + private NameID decryptNameId(EncryptedID encryptedId, RelyingPartyRegistration registration) { + final SAMLObject decryptedId = LogoutRequestEncryptedIdUtils.decryptEncryptedId(encryptedId, registration); if (decryptedId instanceof NameID) { return ((NameID) decryptedId); } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 49eae5891d..643df7e685 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -373,8 +373,10 @@ public final class TestOpenSamlObjects { NameID nameId = nameIdBuilder.buildObject(); nameId.setValue("user"); logoutRequest.setNameID(null); - logoutRequest.setEncryptedID(encrypted(nameId, - registration.getAssertingPartyDetails().getEncryptionX509Credentials().stream().findFirst().get())); + Saml2X509Credential credential = registration.getAssertingPartyDetails().getEncryptionX509Credentials() + .iterator().next(); + EncryptedID encrypted = encrypted(nameId, credential); + logoutRequest.setEncryptedID(encrypted); IssuerBuilder issuerBuilder = new IssuerBuilder(); Issuer issuer = issuerBuilder.buildObject(); issuer.setValue(registration.getAssertingPartyDetails().getEntityId()); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java index 8bd38f988e..8def02122c 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidatorTests.java @@ -61,17 +61,16 @@ public class OpenSamlLogoutRequestValidatorTests { } @Test - public void handleWhenNameIdInEncryptedIdPostThenValidates() { + public void handleWhenNameIdIsEncryptedIdPostThenValidates() { - RelyingPartyRegistration registration = registrationWithEncryption() - .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)).build(); + RelyingPartyRegistration registration = decrypting(encrypting(registration())).build(); LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequestNameIdInEncryptedId(registration); sign(logoutRequest, registration); Saml2LogoutRequest request = post(logoutRequest, registration); Saml2LogoutRequestValidatorParameters parameters = new Saml2LogoutRequestValidatorParameters(request, registration, authentication(registration)); Saml2LogoutValidatorResult result = this.manager.validate(parameters); - assertThat(result.hasErrors()).withFailMessage(() -> result.getErrors().toString()).isFalse().isFalse(); + assertThat(result.hasErrors()).withFailMessage(() -> result.getErrors().toString()).isFalse(); } @@ -149,10 +148,14 @@ public class OpenSamlLogoutRequestValidatorTests { .assertingPartyDetails((party) -> party.singleLogoutServiceBinding(Saml2MessageBinding.POST)); } - private RelyingPartyRegistration.Builder registrationWithEncryption() { - return signing(verifying(TestRelyingPartyRegistrations.full())) - .assertingPartyDetails((party) -> party.encryptionX509Credentials( - (c) -> c.add(TestSaml2X509Credentials.assertingPartyEncryptingCredential()))); + private RelyingPartyRegistration.Builder decrypting(RelyingPartyRegistration.Builder builder) { + return builder + .decryptionX509Credentials((c) -> c.add(TestSaml2X509Credentials.relyingPartyDecryptingCredential())); + } + + private RelyingPartyRegistration.Builder encrypting(RelyingPartyRegistration.Builder builder) { + return builder.assertingPartyDetails((party) -> party.encryptionX509Credentials( + (c) -> c.add(TestSaml2X509Credentials.assertingPartyEncryptingCredential()))); } private RelyingPartyRegistration.Builder verifying(RelyingPartyRegistration.Builder builder) { From aaaf7d35235bf19d8207e890830c1557dbc76a32 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 14 Jan 2022 14:32:12 -0700 Subject: [PATCH 118/589] Use noNullElements Collection#contains(null) does not work for all collection types Closes gh-10703 --- .../MediaTypeServerWebExchangeMatcher.java | 4 ++-- .../web/util/matcher/AndRequestMatcher.java | 4 ++-- .../web/util/matcher/OrRequestMatcher.java | 4 ++-- ...ediaTypeServerWebExchangeMatcherTests.java | 19 +++++++++++++++++- .../util/matcher/AndRequestMatcherTests.java | 19 +++++++++++++++++- .../util/matcher/OrRequestMatcherTests.java | 20 ++++++++++++++++++- 6 files changed, 61 insertions(+), 9 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java b/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java index 3ddc6b09df..654940be5d 100644 --- a/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java +++ b/web/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,7 +66,7 @@ public class MediaTypeServerWebExchangeMatcher implements ServerWebExchangeMatch */ public MediaTypeServerWebExchangeMatcher(Collection matchingMediaTypes) { Assert.notEmpty(matchingMediaTypes, "matchingMediaTypes cannot be null"); - Assert.isTrue(!matchingMediaTypes.contains(null), + Assert.noNullElements(matchingMediaTypes, () -> "matchingMediaTypes cannot contain null. Got " + matchingMediaTypes); this.matchingMediaTypes = matchingMediaTypes; } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java index 07682a183f..765c540493 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/AndRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ public final class AndRequestMatcher implements RequestMatcher { */ public AndRequestMatcher(List requestMatchers) { Assert.notEmpty(requestMatchers, "requestMatchers must contain a value"); - Assert.isTrue(!requestMatchers.contains(null), "requestMatchers cannot contain null values"); + Assert.noNullElements(requestMatchers, "requestMatchers cannot contain null values"); this.requestMatchers = requestMatchers; } diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java index 92a2e2eb78..ae7dbaaaa5 100644 --- a/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matcher/OrRequestMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ public final class OrRequestMatcher implements RequestMatcher { */ public OrRequestMatcher(List requestMatchers) { Assert.notEmpty(requestMatchers, "requestMatchers must contain a value"); - Assert.isTrue(!requestMatchers.contains(null), "requestMatchers cannot contain null values"); + Assert.noNullElements(requestMatchers, "requestMatchers cannot contain null values"); this.requestMatchers = requestMatchers; } diff --git a/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java b/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java index d7c2d9d77d..bfb0d96511 100644 --- a/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.security.web.server.util.matcher; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -43,6 +45,21 @@ public class MediaTypeServerWebExchangeMatcherTests { assertThatIllegalArgumentException().isThrownBy(() -> new MediaTypeServerWebExchangeMatcher(types)); } + // gh-10703 + @Test + public void constructorListOfDoesNotThrowNullPointerException() { + List mediaTypes = new ArrayList(Arrays.asList(MediaType.ALL)) { + @Override + public boolean contains(Object o) { + if (o == null) { + throw new NullPointerException(); + } + return super.contains(o); + } + }; + new MediaTypeServerWebExchangeMatcher(mediaTypes); + } + @Test public void constructorMediaTypeArrayWhenContainsNullThenThrowsIllegalArgumentException() { MediaType[] types = { null }; diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java index a53e569c18..99b641580b 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/AndRequestMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.web.util.matcher; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -55,6 +56,22 @@ public class AndRequestMatcherTests { assertThatNullPointerException().isThrownBy(() -> new AndRequestMatcher((RequestMatcher[]) null)); } + // gh-10703 + @Test + public void constructorListOfDoesNotThrowNullPointer() { + List requestMatchers = new ArrayList( + Arrays.asList(AnyRequestMatcher.INSTANCE)) { + @Override + public boolean contains(Object o) { + if (o == null) { + throw new NullPointerException(); + } + return super.contains(o); + } + }; + new AndRequestMatcher(requestMatchers); + } + @Test public void constructorArrayContainsNull() { assertThatIllegalArgumentException().isThrownBy(() -> new AndRequestMatcher((RequestMatcher) null)); diff --git a/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java index 37314e174c..02f41fbc2f 100644 --- a/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/util/matcher/OrRequestMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.security.web.util.matcher; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -55,6 +56,23 @@ public class OrRequestMatcherTests { assertThatNullPointerException().isThrownBy(() -> new OrRequestMatcher((RequestMatcher[]) null)); } + // gh-10703 + @Test + public void constructorListOfDoesNotThrowNullPointer() { + // emulate List.of for pre-JDK 9 builds + List requestMatchers = new ArrayList( + Arrays.asList(AnyRequestMatcher.INSTANCE)) { + @Override + public boolean contains(Object o) { + if (o == null) { + throw new NullPointerException(); + } + return super.contains(o); + } + }; + new OrRequestMatcher(requestMatchers); + } + @Test public void constructorArrayContainsNull() { assertThatIllegalArgumentException().isThrownBy(() -> new OrRequestMatcher((RequestMatcher) null)); From 4ea57f3e3fe88000d2312e485b9425d868c481f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ostro=C5=BEl=C3=ADk?= Date: Sat, 11 Dec 2021 15:27:45 +0100 Subject: [PATCH 119/589] Support multiple RequestRejectedHandler beans Closes gh-10603 --- .../annotation/web/builders/WebSecurity.java | 13 +++++ .../web/builders/WebSecurityTests.java | 24 ++++++++ .../CompositeRequestRejectedHandler.java | 57 +++++++++++++++++++ .../CompositeRequestRejectedHandlerTests.java | 43 ++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandler.java create mode 100644 web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index c4934cb862..a67cdd53d4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -279,6 +279,19 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder requestRejectedhandlers; + + /** + * Creates a new instance. + * @param requestRejectedhandlers the {@link RequestRejectedHandler} instances to + * handle {@link org.springframework.security.web.firewall.RequestRejectedException} + */ + public CompositeRequestRejectedHandler(RequestRejectedHandler... requestRejectedhandlers) { + Assert.notEmpty(requestRejectedhandlers, "requestRejectedhandlers cannot be empty"); + this.requestRejectedhandlers = Arrays.asList(requestRejectedhandlers); + } + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + RequestRejectedException requestRejectedException) throws IOException, ServletException { + for (RequestRejectedHandler requestRejectedhandler : requestRejectedhandlers) { + requestRejectedhandler.handle(request, response, requestRejectedException); + } + } + +} diff --git a/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java b/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java new file mode 100644 index 0000000000..9838c136c3 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.firewall; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +public class CompositeRequestRejectedHandlerTests { + + @Test + void compositeRequestRejectedHandlerRethrowsTheException() { + RequestRejectedException requestRejectedException = new RequestRejectedException("rejected"); + DefaultRequestRejectedHandler sut = new DefaultRequestRejectedHandler(); + CompositeRequestRejectedHandler crrh = new CompositeRequestRejectedHandler(sut); + assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> crrh + .handle(mock(HttpServletRequest.class), mock(HttpServletResponse.class), requestRejectedException)) + .withMessage("rejected"); + } + + @Test + void compositeRequestRejectedHandlerForbidsEmptyHandlers() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(CompositeRequestRejectedHandler::new); + } + +} From 75f25bff828011ad633bef728c590a0002e5bd2c Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 14 Jan 2022 16:45:58 -0700 Subject: [PATCH 120/589] Polish multiple RequestRejectedHandlers support Issue gh-10603 --- .../config/annotation/web/builders/WebSecurity.java | 2 +- .../annotation/web/builders/WebSecurityTests.java | 6 +++--- .../firewall/CompositeRequestRejectedHandler.java | 13 +++++++------ .../CompositeRequestRejectedHandlerTests.java | 9 +++++---- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index a67cdd53d4..35678c6060 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java index 4f680b6118..9eb0abc7a6 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java @@ -16,8 +16,10 @@ package org.springframework.security.config.annotation.web.builders; -import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,8 +44,6 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import java.io.IOException; - import static org.assertj.core.api.Assertions.assertThat; /** diff --git a/web/src/main/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandler.java b/web/src/main/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandler.java index ea258b38ae..81013b6bcd 100644 --- a/web/src/main/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandler.java +++ b/web/src/main/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandler.java @@ -16,15 +16,16 @@ package org.springframework.security.web.firewall; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.springframework.util.Assert; - import java.io.IOException; import java.util.Arrays; import java.util.List; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.Assert; + /** * A {@link RequestRejectedHandler} that delegates to several other * {@link RequestRejectedHandler}s. @@ -49,7 +50,7 @@ public final class CompositeRequestRejectedHandler implements RequestRejectedHan @Override public void handle(HttpServletRequest request, HttpServletResponse response, RequestRejectedException requestRejectedException) throws IOException, ServletException { - for (RequestRejectedHandler requestRejectedhandler : requestRejectedhandlers) { + for (RequestRejectedHandler requestRejectedhandler : this.requestRejectedhandlers) { requestRejectedhandler.handle(request, response, requestRejectedException); } } diff --git a/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java b/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java index 9838c136c3..ba98649c43 100644 --- a/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java +++ b/web/src/test/java/org/springframework/security/web/firewall/CompositeRequestRejectedHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.security.web.firewall; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -28,9 +29,9 @@ public class CompositeRequestRejectedHandlerTests { @Test void compositeRequestRejectedHandlerRethrowsTheException() { RequestRejectedException requestRejectedException = new RequestRejectedException("rejected"); - DefaultRequestRejectedHandler sut = new DefaultRequestRejectedHandler(); - CompositeRequestRejectedHandler crrh = new CompositeRequestRejectedHandler(sut); - assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> crrh + CompositeRequestRejectedHandler handler = new CompositeRequestRejectedHandler( + new DefaultRequestRejectedHandler()); + assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> handler .handle(mock(HttpServletRequest.class), mock(HttpServletResponse.class), requestRejectedException)) .withMessage("rejected"); } From 7435da6bbf630fee8d106fcbd3b7c7b87064b415 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Tue, 4 Jan 2022 14:52:14 -0500 Subject: [PATCH 121/589] Add serialVersionUID to DefaultSavedRequest and SavedCookie Closes gh-10594 --- .../security/web/savedrequest/DefaultSavedRequest.java | 5 ++++- .../security/web/savedrequest/SavedCookie.java | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java b/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java index ea2358293f..1655190d01 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.web.PortResolver; import org.springframework.security.web.util.UrlUtils; import org.springframework.util.Assert; @@ -61,6 +62,8 @@ import org.springframework.util.ObjectUtils; */ public class DefaultSavedRequest implements SavedRequest { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + protected static final Log logger = LogFactory.getLog(DefaultSavedRequest.class); private static final String HEADER_IF_NONE_MATCH = "If-None-Match"; diff --git a/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java b/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java index 9357e98fbe..471863bf2c 100644 --- a/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java +++ b/web/src/main/java/org/springframework/security/web/savedrequest/SavedCookie.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.io.Serializable; import javax.servlet.http.Cookie; +import org.springframework.security.core.SpringSecurityCoreVersion; + /** * Stores off the values of a cookie in a serializable holder * @@ -27,6 +29,8 @@ import javax.servlet.http.Cookie; */ public class SavedCookie implements Serializable { + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + private final java.lang.String name; private final java.lang.String value; From d34132808577fb4cc7b266dde580e28151b6d4da Mon Sep 17 00:00:00 2001 From: Jerome Prinet Date: Mon, 17 Jan 2022 15:09:29 +0100 Subject: [PATCH 122/589] Bump up Gradle plugin dependencies --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 63a863b982..e8e4fa7674 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id "com.gradle.enterprise" version "3.6.1" + id "com.gradle.enterprise" version "3.8.1" id "io.spring.ge.conventions" version "0.0.7" } From 096a3403cb21f2b7bddcaddd5d1b9afa62f81ced Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Mon, 4 Oct 2021 12:31:00 +0200 Subject: [PATCH 123/589] Add embedded LDAP container interface Issue gh-10138 --- .../ldap/server/ApacheDSContainer.java | 7 +++- .../server/EmbeddedLdapServerContainer.java | 40 +++++++++++++++++++ .../ldap/server/UnboundIdContainer.java | 7 +++- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java index eb1eb79093..379faed965 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java +++ b/ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,7 +77,8 @@ import org.springframework.util.Assert; * supported with no GA version to replace it. */ @Deprecated -public class ApacheDSContainer implements InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { +public class ApacheDSContainer + implements EmbeddedLdapServerContainer, InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { private final Log logger = LogFactory.getLog(getClass()); @@ -177,10 +178,12 @@ public class ApacheDSContainer implements InitializingBean, DisposableBean, Life this.service.setWorkingDirectory(workingDir); } + @Override public void setPort(int port) { this.port = port; } + @Override public int getPort() { return this.port; } diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java new file mode 100644 index 0000000000..2ca55f44ed --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/server/EmbeddedLdapServerContainer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.server; + +/** + * Provides lifecycle services for an embedded LDAP server. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public interface EmbeddedLdapServerContainer { + + /** + * Returns the embedded LDAP server port. + * @return the embedded LDAP server port + */ + int getPort(); + + /** + * The embedded LDAP server port to connect to. Supplying 0 as the port indicates that + * a random available port should be selected. + * @param port the port to connect to + */ + void setPort(int port); + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java b/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java index 269b8adae1..f8c1d0d84a 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java +++ b/ldap/src/main/java/org/springframework/security/ldap/server/UnboundIdContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,8 @@ import org.springframework.util.StringUtils; /** * @author Eddú Meléndez */ -public class UnboundIdContainer implements InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { +public class UnboundIdContainer + implements EmbeddedLdapServerContainer, InitializingBean, DisposableBean, Lifecycle, ApplicationContextAware { private InMemoryDirectoryServer directoryServer; @@ -57,10 +58,12 @@ public class UnboundIdContainer implements InitializingBean, DisposableBean, Lif this.ldif = ldif; } + @Override public int getPort() { return this.port; } + @Override public void setPort(int port) { this.port = port; } From a537b636c196d02b2262f4b4bdcbb38580cfb0e0 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Mon, 4 Oct 2021 15:21:34 +0200 Subject: [PATCH 124/589] Add LDAP factory beans Issue gh-10138 --- config/spring-security-config.gradle | 1 + ...pServerContextSourceFactoryBeanITests.java | 185 ++++++++++++++ ...indAuthenticationManagerFactoryITests.java | 241 ++++++++++++++++++ ...sonAuthenticationManagerFactoryITests.java | 114 +++++++++ ...tractLdapAuthenticationManagerFactory.java | 183 +++++++++++++ ...dedLdapServerContextSourceFactoryBean.java | 185 ++++++++++++++ .../LdapBindAuthenticationManagerFactory.java | 41 +++ ...omparisonAuthenticationManagerFactory.java | 75 ++++++ .../src/integration-test/resources/users.ldif | 10 + 9 files changed, 1035 insertions(+) create mode 100644 config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java create mode 100644 config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java create mode 100644 config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java create mode 100644 config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java create mode 100644 config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java create mode 100644 config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java create mode 100644 config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index a54edbd15c..9274704bb4 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -74,6 +74,7 @@ dependencies { testImplementation "org.apache.directory.server:apacheds-protocol-ldap" testImplementation "org.apache.directory.server:apacheds-server-jndi" testImplementation 'org.apache.directory.shared:shared-ldap' + testImplementation "com.unboundid:unboundid-ldapsdk" testImplementation 'org.eclipse.persistence:javax.persistence' testImplementation 'org.hibernate:hibernate-entitymanager' testImplementation 'org.hsqldb:hsqldb' diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java new file mode 100644 index 0000000000..d20b3e302c --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBeanITests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.test.web.servlet.MockMvc; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +@ExtendWith(SpringTestContextExtension.class) +public class EmbeddedLdapServerContextSourceFactoryBeanITests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + public void contextSourceFactoryBeanWhenEmbeddedServerThenAuthenticates() throws Exception { + this.spring.register(FromEmbeddedLdapServerConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void contextSourceFactoryBeanWhenPortZeroThenAuthenticates() throws Exception { + this.spring.register(PortZeroConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void contextSourceFactoryBeanWhenCustomLdifAndRootThenAuthenticates() throws Exception { + this.spring.register(CustomLdifAndRootConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("pg").password("password")).andExpect(authenticated().withUsername("pg")); + } + + @Test + public void contextSourceFactoryBeanWhenCustomManagerDnThenAuthenticates() throws Exception { + this.spring.register(CustomManagerDnConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void contextSourceFactoryBeanWhenManagerDnAndNoPasswordThenException() { + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> this.spring.register(CustomManagerDnNoPasswordConfig.class).autowire()) + .withRootCauseInstanceOf(IllegalStateException.class) + .withMessageContaining("managerPassword is required if managerDn is supplied"); + } + + @EnableWebSecurity + static class FromEmbeddedLdapServerConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + return EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer(); + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class PortZeroConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setPort(0); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomLdifAndRootConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setLdif("classpath*:test-server2.xldif"); + factoryBean.setRoot("dc=monkeymachine,dc=co,dc=uk"); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=gorillas"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomManagerDnConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setManagerDn("uid=admin,ou=system"); + factoryBean.setManagerPassword("secret"); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomManagerDnNoPasswordConfig { + + @Bean + EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factoryBean = EmbeddedLdapServerContextSourceFactoryBean + .fromEmbeddedLdapServer(); + factoryBean.setManagerDn("uid=admin,ou=system"); + return factoryBean; + } + + @Bean + AuthenticationManager authenticationManager(LdapContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java new file mode 100644 index 0000000000..2b333441c0 --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactoryITests.java @@ -0,0 +1,241 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.ApacheDSContainer; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.Mockito.mock; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +@ExtendWith(SpringTestContextExtension.class) +public class LdapBindAuthenticationManagerFactoryITests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + public void authenticationManagerFactoryWhenFromContextSourceThenAuthenticates() throws Exception { + this.spring.register(FromContextSourceConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void ldapAuthenticationProviderCustomLdapAuthoritiesPopulator() throws Exception { + CustomAuthoritiesPopulatorConfig.LAP = new DefaultLdapAuthoritiesPopulator(mock(LdapContextSource.class), + null) { + @Override + protected Set getAdditionalRoles(DirContextOperations user, String username) { + return new HashSet<>(AuthorityUtils.createAuthorityList("ROLE_EXTRA")); + } + }; + + this.spring.register(CustomAuthoritiesPopulatorConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")).andExpect( + authenticated().withAuthorities(Collections.singleton(new SimpleGrantedAuthority("ROLE_EXTRA")))); + } + + @Test + public void authenticationManagerFactoryWhenCustomAuthoritiesMapperThenUsed() throws Exception { + CustomAuthoritiesMapperConfig.AUTHORITIES_MAPPER = ((authorities) -> AuthorityUtils + .createAuthorityList("ROLE_CUSTOM")); + + this.spring.register(CustomAuthoritiesMapperConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")).andExpect( + authenticated().withAuthorities(Collections.singleton(new SimpleGrantedAuthority("ROLE_CUSTOM")))); + } + + @Test + public void authenticationManagerFactoryWhenCustomUserDetailsContextMapperThenUsed() throws Exception { + CustomUserDetailsContextMapperConfig.CONTEXT_MAPPER = new UserDetailsContextMapper() { + @Override + public UserDetails mapUserFromContext(DirContextOperations ctx, String username, + Collection authorities) { + return User.withUsername("other").password("password").roles("USER").build(); + } + + @Override + public void mapUserToContext(UserDetails user, DirContextAdapter ctx) { + } + }; + + this.spring.register(CustomUserDetailsContextMapperConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("other")); + } + + @Test + public void authenticationManagerFactoryWhenCustomUserDnPatternsThenUsed() throws Exception { + this.spring.register(CustomUserDnPatternsConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void authenticationManagerFactoryWhenCustomUserSearchThenUsed() throws Exception { + this.spring.register(CustomUserSearchConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @EnableWebSecurity + static class FromContextSourceConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomAuthoritiesMapperConfig extends BaseLdapServerConfig { + + static GrantedAuthoritiesMapper AUTHORITIES_MAPPER; + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setAuthoritiesMapper(AUTHORITIES_MAPPER); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomAuthoritiesPopulatorConfig extends BaseLdapServerConfig { + + static LdapAuthoritiesPopulator LAP; + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setLdapAuthoritiesPopulator(LAP); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomUserDetailsContextMapperConfig extends BaseLdapServerConfig { + + static UserDetailsContextMapper CONTEXT_MAPPER; + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setUserDetailsContextMapper(CONTEXT_MAPPER); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomUserDnPatternsConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomUserSearchConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserSearchFilter("uid={0}"); + factory.setUserSearchBase("ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + abstract static class BaseLdapServerConfig implements DisposableBean { + + private ApacheDSContainer container; + + @Bean + ApacheDSContainer ldapServer() throws Exception { + this.container = new ApacheDSContainer("dc=springframework,dc=org", "classpath:/test-server.ldif"); + this.container.setPort(0); + return this.container; + } + + @Bean + BaseLdapPathContextSource contextSource(ApacheDSContainer container) { + int port = container.getLocalPort(); + return new DefaultSpringSecurityContextSource("ldap://localhost:" + port + "/dc=springframework,dc=org"); + } + + @Override + public void destroy() { + this.container.stop(); + } + + } + +} diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java new file mode 100644 index 0000000000..350cf8405c --- /dev/null +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactoryITests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.ApacheDSContainer; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; + +@ExtendWith(SpringTestContextExtension.class) +public class LdapPasswordComparisonAuthenticationManagerFactoryITests { + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired + private MockMvc mockMvc; + + @Test + public void authenticationManagerFactoryWhenCustomPasswordEncoderThenUsed() throws Exception { + this.spring.register(CustomPasswordEncoderConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bcrypt").password("password")) + .andExpect(authenticated().withUsername("bcrypt")); + } + + @Test + public void authenticationManagerFactoryWhenCustomPasswordAttributeThenUsed() throws Exception { + this.spring.register(CustomPasswordAttributeConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bob")).andExpect(authenticated().withUsername("bob")); + } + + @EnableWebSecurity + static class CustomPasswordEncoderConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, new BCryptPasswordEncoder()); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + static class CustomPasswordAttributeConfig extends BaseLdapServerConfig { + + @Bean + AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( + contextSource, NoOpPasswordEncoder.getInstance()); + factory.setPasswordAttribute("uid"); + factory.setUserDnPatterns("uid={0},ou=people"); + return factory.createAuthenticationManager(); + } + + } + + @EnableWebSecurity + abstract static class BaseLdapServerConfig implements DisposableBean { + + private ApacheDSContainer container; + + @Bean + ApacheDSContainer ldapServer() throws Exception { + this.container = new ApacheDSContainer("dc=springframework,dc=org", "classpath:/test-server.ldif"); + this.container.setPort(0); + return this.container; + } + + @Bean + BaseLdapPathContextSource contextSource(ApacheDSContainer container) { + int port = container.getLocalPort(); + return new DefaultSpringSecurityContextSource("ldap://localhost:" + port + "/dc=springframework,dc=org"); + } + + @Override + public void destroy() { + this.container.stop(); + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java new file mode 100644 index 0000000000..16069f09a4 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/AbstractLdapAuthenticationManagerFactory.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; +import org.springframework.security.ldap.userdetails.UserDetailsContextMapper; + +/** + * Creates an {@link AuthenticationManager} that can perform LDAP authentication. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public abstract class AbstractLdapAuthenticationManagerFactory { + + AbstractLdapAuthenticationManagerFactory(BaseLdapPathContextSource contextSource) { + this.contextSource = contextSource; + } + + private BaseLdapPathContextSource contextSource; + + private String[] userDnPatterns; + + private LdapAuthoritiesPopulator ldapAuthoritiesPopulator; + + private GrantedAuthoritiesMapper authoritiesMapper; + + private UserDetailsContextMapper userDetailsContextMapper; + + private String userSearchFilter; + + private String userSearchBase = ""; + + /** + * Sets the {@link BaseLdapPathContextSource} used to perform LDAP authentication. + * @param contextSource the {@link BaseLdapPathContextSource} used to perform LDAP + * authentication + */ + public void setContextSource(BaseLdapPathContextSource contextSource) { + this.contextSource = contextSource; + } + + /** + * Gets the {@link BaseLdapPathContextSource} used to perform LDAP authentication. + * @return the {@link BaseLdapPathContextSource} used to perform LDAP authentication + */ + protected final BaseLdapPathContextSource getContextSource() { + return this.contextSource; + } + + /** + * Sets the {@link LdapAuthoritiesPopulator} used to obtain a list of granted + * authorities for an LDAP user. + * @param ldapAuthoritiesPopulator the {@link LdapAuthoritiesPopulator} to use + */ + public void setLdapAuthoritiesPopulator(LdapAuthoritiesPopulator ldapAuthoritiesPopulator) { + this.ldapAuthoritiesPopulator = ldapAuthoritiesPopulator; + } + + /** + * Sets the {@link GrantedAuthoritiesMapper} used for converting the authorities + * loaded from storage to a new set of authorities which will be associated to the + * {@link UsernamePasswordAuthenticationToken}. + * @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the + * user's authorities + */ + public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { + this.authoritiesMapper = authoritiesMapper; + } + + /** + * Sets a custom strategy to be used for creating the {@link UserDetails} which will + * be stored as the principal in the {@link Authentication}. + * @param userDetailsContextMapper the strategy instance + */ + public void setUserDetailsContextMapper(UserDetailsContextMapper userDetailsContextMapper) { + this.userDetailsContextMapper = userDetailsContextMapper; + } + + /** + * If your users are at a fixed location in the directory (i.e. you can work out the + * DN directly from the username without doing a directory search), you can use this + * attribute to map directly to the DN. It maps directly to the userDnPatterns + * property of AbstractLdapAuthenticator. The value is a specific pattern used to + * build the user's DN, for example "uid={0},ou=people". The key "{0}" must be present + * and will be substituted with the username. + * @param userDnPatterns the LDAP patterns for finding the usernames + */ + public void setUserDnPatterns(String... userDnPatterns) { + this.userDnPatterns = userDnPatterns; + } + + /** + * The LDAP filter used to search for users (optional). For example "(uid={0})". The + * substituted parameter is the user's login name. + * @param userSearchFilter the LDAP filter used to search for users + */ + public void setUserSearchFilter(String userSearchFilter) { + this.userSearchFilter = userSearchFilter; + } + + /** + * Search base for user searches. Defaults to "". Only used with + * {@link #setUserSearchFilter(String)}. + * @param userSearchBase search base for user searches + */ + public void setUserSearchBase(String userSearchBase) { + this.userSearchBase = userSearchBase; + } + + /** + * Returns the configured {@link AuthenticationManager} that can be used to perform + * LDAP authentication. + * @return the configured {@link AuthenticationManager} + */ + public final AuthenticationManager createAuthenticationManager() { + LdapAuthenticationProvider ldapAuthenticationProvider = getProvider(); + return new ProviderManager(ldapAuthenticationProvider); + } + + private LdapAuthenticationProvider getProvider() { + AbstractLdapAuthenticator authenticator = getAuthenticator(); + LdapAuthenticationProvider provider; + if (this.ldapAuthoritiesPopulator != null) { + provider = new LdapAuthenticationProvider(authenticator, this.ldapAuthoritiesPopulator); + } + else { + provider = new LdapAuthenticationProvider(authenticator); + } + if (this.authoritiesMapper != null) { + provider.setAuthoritiesMapper(this.authoritiesMapper); + } + if (this.userDetailsContextMapper != null) { + provider.setUserDetailsContextMapper(this.userDetailsContextMapper); + } + return provider; + } + + private AbstractLdapAuthenticator getAuthenticator() { + AbstractLdapAuthenticator authenticator = createDefaultLdapAuthenticator(); + if (this.userSearchFilter != null) { + authenticator.setUserSearch( + new FilterBasedLdapUserSearch(this.userSearchBase, this.userSearchFilter, this.contextSource)); + } + if (this.userDnPatterns != null && this.userDnPatterns.length > 0) { + authenticator.setUserDnPatterns(this.userDnPatterns); + } + authenticator.afterPropertiesSet(); + return authenticator; + } + + /** + * Allows subclasses to supply the default {@link AbstractLdapAuthenticator}. + * @return the {@link AbstractLdapAuthenticator} that will be configured for LDAP + * authentication + */ + protected abstract T createDefaultLdapAuthenticator(); + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java b/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java new file mode 100644 index 0000000000..4a8c2d56d4 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/EmbeddedLdapServerContextSourceFactoryBean.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import java.io.IOException; +import java.net.ServerSocket; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.Lifecycle; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.server.EmbeddedLdapServerContainer; +import org.springframework.security.ldap.server.UnboundIdContainer; +import org.springframework.util.ClassUtils; + +/** + * Creates a {@link DefaultSpringSecurityContextSource} used to perform LDAP + * authentication and starts and in-memory LDAP server. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public class EmbeddedLdapServerContextSourceFactoryBean + implements FactoryBean, DisposableBean, ApplicationContextAware { + + private static final String UNBOUNDID_CLASSNAME = "com.unboundid.ldap.listener.InMemoryDirectoryServer"; + + private static final int DEFAULT_PORT = 33389; + + private static final int RANDOM_PORT = 0; + + private Integer port; + + private String ldif = "classpath*:*.ldif"; + + private String root = "dc=springframework,dc=org"; + + private ApplicationContext context; + + private String managerDn; + + private String managerPassword; + + private EmbeddedLdapServerContainer container; + + /** + * Create an EmbeddedLdapServerContextSourceFactoryBean that will use an embedded LDAP + * server to perform LDAP authentication. This requires a dependency on + * `com.unboundid:unboundid-ldapsdk`. + * @return the EmbeddedLdapServerContextSourceFactoryBean + */ + public static EmbeddedLdapServerContextSourceFactoryBean fromEmbeddedLdapServer() { + return new EmbeddedLdapServerContextSourceFactoryBean(); + } + + /** + * Specifies an LDIF to load at startup for an embedded LDAP server. The default is + * "classpath*:*.ldif". + * @param ldif the ldif to load at startup for an embedded LDAP server. + */ + public void setLdif(String ldif) { + this.ldif = ldif; + } + + /** + * The port to connect to LDAP to (the default is 33389 or random available port if + * unavailable). Supplying 0 as the port indicates that a random available port should + * be selected. + * @param port the port to connect to + */ + public void setPort(int port) { + this.port = port; + } + + /** + * Optional root suffix for the embedded LDAP server. Default is + * "dc=springframework,dc=org". + * @param root root suffix for the embedded LDAP server + */ + public void setRoot(String root) { + this.root = root; + } + + /** + * Username (DN) of the "manager" user identity (i.e. "uid=admin,ou=system") which + * will be used to authenticate to an LDAP server. If omitted, anonymous access will + * be used. + * @param managerDn the username (DN) of the "manager" user identity used to + * authenticate to a LDAP server. + */ + public void setManagerDn(String managerDn) { + this.managerDn = managerDn; + } + + /** + * The password for the manager DN. This is required if the + * {@link #setManagerDn(String)} is specified. + * @param managerPassword password for the manager DN + */ + public void setManagerPassword(String managerPassword) { + this.managerPassword = managerPassword; + } + + @Override + public DefaultSpringSecurityContextSource getObject() throws Exception { + if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) { + throw new IllegalStateException("Embedded LDAP server is not provided"); + } + this.container = getContainer(); + this.port = this.container.getPort(); + DefaultSpringSecurityContextSource contextSourceFromProviderUrl = new DefaultSpringSecurityContextSource( + "ldap://127.0.0.1:" + this.port + "/" + this.root); + if (this.managerDn != null) { + contextSourceFromProviderUrl.setUserDn(this.managerDn); + if (this.managerPassword == null) { + throw new IllegalStateException("managerPassword is required if managerDn is supplied"); + } + contextSourceFromProviderUrl.setPassword(this.managerPassword); + } + contextSourceFromProviderUrl.afterPropertiesSet(); + return contextSourceFromProviderUrl; + } + + @Override + public Class getObjectType() { + return DefaultSpringSecurityContextSource.class; + } + + @Override + public void destroy() { + if (this.container instanceof Lifecycle) { + ((Lifecycle) this.container).stop(); + } + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.context = applicationContext; + } + + private EmbeddedLdapServerContainer getContainer() { + if (!ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) { + throw new IllegalStateException("Embedded LDAP server is not provided"); + } + UnboundIdContainer unboundIdContainer = new UnboundIdContainer(this.root, this.ldif); + unboundIdContainer.setApplicationContext(this.context); + unboundIdContainer.setPort(getEmbeddedServerPort()); + unboundIdContainer.afterPropertiesSet(); + return unboundIdContainer; + } + + private int getEmbeddedServerPort() { + if (this.port == null) { + this.port = getDefaultEmbeddedServerPort(); + } + return this.port; + } + + private int getDefaultEmbeddedServerPort() { + try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { + return serverSocket.getLocalPort(); + } + catch (IOException ex) { + return RANDOM_PORT; + } + } + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java new file mode 100644 index 0000000000..a62fbfab44 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/LdapBindAuthenticationManagerFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.ldap.authentication.BindAuthenticator; + +/** + * Creates an {@link AuthenticationManager} that can perform LDAP authentication using + * bind authentication. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public class LdapBindAuthenticationManagerFactory extends AbstractLdapAuthenticationManagerFactory { + + public LdapBindAuthenticationManagerFactory(BaseLdapPathContextSource contextSource) { + super(contextSource); + } + + @Override + protected BindAuthenticator createDefaultLdapAuthenticator() { + return new BindAuthenticator(getContextSource()); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java b/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java new file mode 100644 index 0000000000..19c14f998d --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/ldap/LdapPasswordComparisonAuthenticationManagerFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.ldap; + +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator; +import org.springframework.util.Assert; + +/** + * Creates an {@link AuthenticationManager} that can perform LDAP authentication using + * password comparison. + * + * @author Eleftheria Stein + * @since 5.7 + */ +public class LdapPasswordComparisonAuthenticationManagerFactory + extends AbstractLdapAuthenticationManagerFactory { + + private PasswordEncoder passwordEncoder; + + private String passwordAttribute; + + public LdapPasswordComparisonAuthenticationManagerFactory(BaseLdapPathContextSource contextSource, + PasswordEncoder passwordEncoder) { + super(contextSource); + setPasswordEncoder(passwordEncoder); + } + + /** + * Specifies the {@link PasswordEncoder} to be used when authenticating with password + * comparison. + * @param passwordEncoder the {@link PasswordEncoder} to use + */ + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + Assert.notNull(passwordEncoder, "passwordEncoder must not be null."); + this.passwordEncoder = passwordEncoder; + } + + /** + * The attribute in the directory which contains the user password. Only used when + * authenticating with password comparison. Defaults to "userPassword". + * @param passwordAttribute the attribute in the directory which contains the user + * password + */ + public void setPasswordAttribute(String passwordAttribute) { + this.passwordAttribute = passwordAttribute; + } + + @Override + protected PasswordComparisonAuthenticator createDefaultLdapAuthenticator() { + PasswordComparisonAuthenticator ldapAuthenticator = new PasswordComparisonAuthenticator(getContextSource()); + if (this.passwordAttribute != null) { + ldapAuthenticator.setPasswordAttributeName(this.passwordAttribute); + } + ldapAuthenticator.setPasswordEncoder(this.passwordEncoder); + return ldapAuthenticator; + } + +} diff --git a/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif b/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif index 222e03793c..ca639f1096 100644 --- a/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif +++ b/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/users.ldif @@ -38,6 +38,16 @@ sn: Wombat uid: scott userPassword: wombat +dn: uid=bcrypt,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: BCrypt user +sn: BCrypt +uid: bcrypt +userPassword: $2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u + dn: cn=user,ou=groups,dc=springframework,dc=org objectclass: top objectclass: groupOfNames From 73dda2e19274c659e56257ab92364df4df7a22b3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 12:38:03 -0600 Subject: [PATCH 125/589] Fix Antora for Milestone & RC - Verify Antora display_version - Run workflow for tags - Allow run workflow manually Issue gh-10765 --- .github/workflows/antora-generate.yml | 2 ++ .../antora/CheckAntoraVersionPlugin.java | 9 ++++++++ .../gradle/antora/CheckAntoraVersionTask.java | 23 ++++++++++++++++++- .../antora/CheckAntoraVersionPluginTests.java | 9 ++++++-- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/antora-generate.yml b/.github/workflows/antora-generate.yml index f5cd25cfbf..089f0ac041 100644 --- a/.github/workflows/antora-generate.yml +++ b/.github/workflows/antora-generate.yml @@ -1,9 +1,11 @@ name: Generate Antora Files and Request Build on: + workflow_dispatch: push: branches-ignore: - 'gh-pages' + tags: '**' env: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java index a0dcb966cc..464b7ce677 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java @@ -20,6 +20,7 @@ public class CheckAntoraVersionPlugin implements Plugin { antoraCheckVersion.setDescription("Checks the antora.yml version properties match the Gradle version"); antoraCheckVersion.getAntoraVersion().convention(project.provider(() -> getDefaultAntoraVersion(project))); antoraCheckVersion.getAntoraPrerelease().convention(project.provider(() -> getDefaultAntoraPrerelease(project))); + antoraCheckVersion.getAntoraDisplayVersion().convention(project.provider(() -> getDefaultAntoraDisplayVersion(project))); antoraCheckVersion.getAntoraYmlFile().fileProvider(project.provider(() -> project.file("antora.yml"))); } }); @@ -54,6 +55,14 @@ public class CheckAntoraVersionPlugin implements Plugin { return null; } + private static String getDefaultAntoraDisplayVersion(Project project) { + String projectVersion = getProjectVersion(project); + if (!isSnapshot(projectVersion) && isPreRelease(projectVersion)) { + return getDefaultAntoraVersion(project); + } + return null; + } + private static String getProjectVersion(Project project) { Object projectVersion = project.getVersion(); if (projectVersion == null) { diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java index 01225d79dc..3a5b43e800 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java @@ -22,6 +22,7 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { File antoraYmlFile = getAntoraYmlFile().getAsFile().get(); String expectedAntoraVersion = getAntoraVersion().get(); String expectedAntoraPrerelease = getAntoraPrerelease().getOrElse(null); + String expectedAntoraDisplayVersion = getAntoraDisplayVersion().getOrElse(null); Representer representer = new Representer(); representer.getPropertyUtils().setSkipMissingProperties(true); @@ -32,10 +33,17 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { String actualAntoraPrerelease = antoraYml.getPrerelease(); boolean preReleaseMatches = antoraYml.getPrerelease() == null && expectedAntoraPrerelease == null || (actualAntoraPrerelease != null && actualAntoraPrerelease.equals(expectedAntoraPrerelease)); + String actualAntoraDisplayVersion = antoraYml.getDisplay_version(); + boolean displayVersionMatches = antoraYml.getDisplay_version() == null && expectedAntoraDisplayVersion == null || + (actualAntoraDisplayVersion != null && actualAntoraDisplayVersion.equals(expectedAntoraDisplayVersion)); String actualAntoraVersion = antoraYml.getVersion(); if (!preReleaseMatches || + !displayVersionMatches || !expectedAntoraVersion.equals(actualAntoraVersion)) { - throw new GradleException("The Gradle version of '" + getProject().getVersion() + "' should have version: '" + expectedAntoraVersion + "' and prerelease: '" + expectedAntoraPrerelease + "' defined in " + antoraYmlFile + " but got version: '" + actualAntoraVersion+"' and prerelease: '" + actualAntoraPrerelease + "'"); + throw new GradleException("The Gradle version of '" + getProject().getVersion() + "' should have version: '" + + expectedAntoraVersion + "' prerelease: '" + expectedAntoraPrerelease + "' display_version: '" + + expectedAntoraDisplayVersion + "' defined in " + antoraYmlFile + " but got version: '" + + actualAntoraVersion + "' prerelease: '" + actualAntoraPrerelease + "' display_version: '" + actualAntoraDisplayVersion + "'"); } } @@ -48,11 +56,16 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { @Input public abstract Property getAntoraPrerelease(); + @Input + public abstract Property getAntoraDisplayVersion(); + public static class AntoraYml { private String version; private String prerelease; + private String display_version; + public String getVersion() { return version; } @@ -68,5 +81,13 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { public void setPrerelease(String prerelease) { this.prerelease = prerelease; } + + public String getDisplay_version() { + return display_version; + } + + public void setDisplay_version(String display_version) { + this.display_version = display_version; + } } } diff --git a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java index 81f5502572..98eedad65e 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java @@ -31,6 +31,7 @@ class CheckAntoraVersionPluginTests { CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse(); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -48,6 +49,7 @@ class CheckAntoraVersionPluginTests { CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-M1"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true"); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().get()).isEqualTo(checkAntoraVersionTask.getAntoraVersion().get()); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -65,6 +67,7 @@ class CheckAntoraVersionPluginTests { CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-RC1"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true"); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().get()).isEqualTo(checkAntoraVersionTask.getAntoraVersion().get()); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -82,6 +85,7 @@ class CheckAntoraVersionPluginTests { CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().isPresent()).isFalse(); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse(); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -97,6 +101,7 @@ class CheckAntoraVersionPluginTests { checkAntoraVersionTask.getAntoraPrerelease().set("-SNAPSHOT"); assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0"); assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT"); + assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse(); assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml")); } @@ -170,7 +175,7 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-M1"; Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); - IOUtils.write("version: '1.0.0-M1'\nprerelease: 'true'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + IOUtils.write("version: '1.0.0-M1'\nprerelease: 'true'\ndisplay_version: '1.0.0-M1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); project.getPluginManager().apply(CheckAntoraVersionPlugin.class); @@ -187,7 +192,7 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-RC1"; Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); - IOUtils.write("version: '1.0.0-RC1'\nprerelease: 'true'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); + IOUtils.write("version: '1.0.0-RC1'\nprerelease: 'true'\ndisplay_version: '1.0.0-RC1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); project.getPluginManager().apply(CheckAntoraVersionPlugin.class); From c8713b1d9166526c4bdaf27590c13c12339640a3 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 14:29:14 -0600 Subject: [PATCH 126/589] CheckAntoraVersionTask has optional properties --- .../springframework/gradle/antora/CheckAntoraVersionTask.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java index 3a5b43e800..ae26a92f0f 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionTask.java @@ -6,6 +6,7 @@ import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.TaskAction; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.Constructor; @@ -54,9 +55,11 @@ public abstract class CheckAntoraVersionTask extends DefaultTask { public abstract Property getAntoraVersion(); @Input + @Optional public abstract Property getAntoraPrerelease(); @Input + @Optional public abstract Property getAntoraDisplayVersion(); public static class AntoraYml { From d146bcb7fc5eb9182ea4f0d982ced1b259488a14 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 14 Jan 2022 14:45:41 -0600 Subject: [PATCH 127/589] Add CheckClasspathForProhibitedDependencies Issues gh-10499 gh-10501 --- build.gradle | 5 + .../convention/SpringModulePlugin.groovy | 2 + ...eckClasspathForProhibitedDependencies.java | 99 +++++++++++++++++++ ...sspathForProhibitedDependenciesPlugin.java | 75 ++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java diff --git a/build.gradle b/build.gradle index 2b49ab86eb..477b967681 100644 --- a/build.gradle +++ b/build.gradle @@ -166,3 +166,8 @@ tasks.register('checkSamples') { s101 { configurationDirectory = project.file("etc/s101") } + +tasks.register('checkForProhibitedDependencies', check -> { + check.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP) + check.setDescription("Checks for prohibited dependencies") +}) diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy index 36a7013f55..0c1027f4f4 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/SpringModulePlugin.groovy @@ -20,6 +20,7 @@ import org.gradle.api.Project import org.gradle.api.plugins.JavaLibraryPlugin; import org.gradle.api.plugins.MavenPlugin; import org.gradle.api.plugins.PluginManager +import org.springframework.gradle.classpath.CheckClasspathForProhibitedDependenciesPlugin; import org.springframework.gradle.maven.SpringMavenPlugin; /** @@ -32,6 +33,7 @@ class SpringModulePlugin extends AbstractSpringJavaPlugin { PluginManager pluginManager = project.getPluginManager(); pluginManager.apply(JavaLibraryPlugin.class) pluginManager.apply(SpringMavenPlugin.class); + pluginManager.apply(CheckClasspathForProhibitedDependenciesPlugin.class); pluginManager.apply("io.spring.convention.jacoco"); def deployArtifacts = project.task("deployArtifacts") diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java new file mode 100644 index 0000000000..4738135e90 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependencies.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.classpath; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.ResolvedConfiguration; +import org.gradle.api.file.FileCollection; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.TaskAction; + +import java.io.IOException; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * A {@link Task} for checking the classpath for prohibited dependencies. + * + * @author Andy Wilkinson + */ +public class CheckClasspathForProhibitedDependencies extends DefaultTask { + + private Configuration classpath; + + public CheckClasspathForProhibitedDependencies() { + getOutputs().upToDateWhen((task) -> true); + } + + public void setClasspath(Configuration classpath) { + this.classpath = classpath; + } + + @Classpath + public FileCollection getClasspath() { + return this.classpath; + } + + @TaskAction + public void checkForProhibitedDependencies() throws IOException { + ResolvedConfiguration resolvedConfiguration = this.classpath.getResolvedConfiguration(); + TreeSet prohibited = resolvedConfiguration.getResolvedArtifacts().stream() + .map((artifact) -> artifact.getModuleVersion().getId()).filter(this::prohibited) + .map((id) -> id.getGroup() + ":" + id.getName()).collect(Collectors.toCollection(TreeSet::new)); + if (!prohibited.isEmpty()) { + StringBuilder message = new StringBuilder(String.format("Found prohibited dependencies in '%s':%n", this.classpath.getName())); + for (String dependency : prohibited) { + message.append(String.format(" %s%n", dependency)); + } + throw new GradleException(message.toString()); + } + } + + private boolean prohibited(ModuleVersionIdentifier id) { + String group = id.getGroup(); + if (group.equals("javax.batch")) { + return false; + } + if (group.equals("javax.cache")) { + return false; + } + if (group.equals("javax.money")) { + return false; + } + if (group.startsWith("javax")) { + return true; + } + if (group.equals("commons-logging")) { + return true; + } + if (group.equals("org.slf4j") && id.getName().equals("jcl-over-slf4j")) { + return true; + } + if (group.startsWith("org.jboss.spec")) { + return true; + } + if (group.equals("org.apache.geronimo.specs")) { + return true; + } + return false; + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java new file mode 100644 index 0000000000..c9847cdf69 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.classpath; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.language.base.plugins.LifecycleBasePlugin; +import org.springframework.util.StringUtils; + +/** + * @author Andy Wilkinson + * @author Rob Winch + */ +public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin { + public static final String CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME = "checkForProhibitedDependencies"; + + @Override + public void apply(Project project) { + project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> { + configureProhibitedDependencyChecks(project); + }); + } + + private void configureProhibitedDependencyChecks(Project project) { + TaskProvider checkProhibitedDependencies = project.getTasks().register(CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, task -> { + task.setGroup(JavaBasePlugin.VERIFICATION_GROUP); + task.setDescription("Checks both the compile/runtime classpath of every SourceSet for prohibited dependencies"); + }); + project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> { + checkTask.dependsOn(checkProhibitedDependencies); + }); + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + sourceSets.all((sourceSet) -> createProhibitedDependenciesChecks(project, + sourceSet.getCompileClasspathConfigurationName(), sourceSet.getRuntimeClasspathConfigurationName())); + } + + private void createProhibitedDependenciesChecks(Project project, String... configurationNames) { + ConfigurationContainer configurations = project.getConfigurations(); + for (String configurationName : configurationNames) { + Configuration configuration = configurations.getByName(configurationName); + createProhibitedDependenciesCheck(configuration, project); + } + } + + private void createProhibitedDependenciesCheck(Configuration classpath, Project project) { + String taskName = "check" + StringUtils.capitalize(classpath.getName() + "ForProhibitedDependencies"); + TaskProvider checkClasspathTask = project.getTasks().register(taskName, + CheckClasspathForProhibitedDependencies.class, checkClasspath -> { + checkClasspath.setGroup(LifecycleBasePlugin.CHECK_TASK_NAME); + checkClasspath.setDescription("Checks " + classpath.getName() + " for prohibited dependencies"); + checkClasspath.setClasspath(classpath); + }); + project.getTasks().named(CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, checkProhibitedTask -> checkProhibitedTask.dependsOn(checkClasspathTask)); + } +} From 72dd4e738bbd60dc0c012906dc37acc30a337c48 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 16:32:40 -0600 Subject: [PATCH 128/589] Add CheckProhibitedDependenciesLifecyclePlugin Issue gh-10501 --- build.gradle | 5 --- .../convention/RootProjectPlugin.groovy | 2 + ...sspathForProhibitedDependenciesPlugin.java | 12 +----- ...ProhibitedDependenciesLifecyclePlugin.java | 41 +++++++++++++++++++ 4 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java diff --git a/build.gradle b/build.gradle index 477b967681..2b49ab86eb 100644 --- a/build.gradle +++ b/build.gradle @@ -166,8 +166,3 @@ tasks.register('checkSamples') { s101 { configurationDirectory = project.file("etc/s101") } - -tasks.register('checkForProhibitedDependencies', check -> { - check.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP) - check.setDescription("Checks for prohibited dependencies") -}) diff --git a/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy b/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy index f06b15f508..506c5e077b 100644 --- a/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy +++ b/buildSrc/src/main/groovy/io/spring/gradle/convention/RootProjectPlugin.groovy @@ -21,6 +21,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.BasePlugin import org.gradle.api.plugins.PluginManager +import org.springframework.gradle.classpath.CheckProhibitedDependenciesLifecyclePlugin import org.springframework.gradle.maven.SpringNexusPublishPlugin class RootProjectPlugin implements Plugin { @@ -32,6 +33,7 @@ class RootProjectPlugin implements Plugin { pluginManager.apply(SchemaPlugin) pluginManager.apply(NoHttpPlugin) pluginManager.apply(SpringNexusPublishPlugin) + pluginManager.apply(CheckProhibitedDependenciesLifecyclePlugin) pluginManager.apply("org.sonarqube") project.repositories.mavenCentral() diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java index c9847cdf69..0791a193e8 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckClasspathForProhibitedDependenciesPlugin.java @@ -18,7 +18,6 @@ package org.springframework.gradle.classpath; import org.gradle.api.Plugin; import org.gradle.api.Project; -import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.plugins.JavaBasePlugin; @@ -32,23 +31,16 @@ import org.springframework.util.StringUtils; * @author Rob Winch */ public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin { - public static final String CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME = "checkForProhibitedDependencies"; @Override public void apply(Project project) { + project.getPlugins().apply(CheckProhibitedDependenciesLifecyclePlugin.class); project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> { configureProhibitedDependencyChecks(project); }); } private void configureProhibitedDependencyChecks(Project project) { - TaskProvider checkProhibitedDependencies = project.getTasks().register(CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, task -> { - task.setGroup(JavaBasePlugin.VERIFICATION_GROUP); - task.setDescription("Checks both the compile/runtime classpath of every SourceSet for prohibited dependencies"); - }); - project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> { - checkTask.dependsOn(checkProhibitedDependencies); - }); SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); sourceSets.all((sourceSet) -> createProhibitedDependenciesChecks(project, sourceSet.getCompileClasspathConfigurationName(), sourceSet.getRuntimeClasspathConfigurationName())); @@ -70,6 +62,6 @@ public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin checkProhibitedTask.dependsOn(checkClasspathTask)); + project.getTasks().named(CheckProhibitedDependenciesLifecyclePlugin.CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, checkProhibitedTask -> checkProhibitedTask.dependsOn(checkClasspathTask)); } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java new file mode 100644 index 0000000000..77fd369528 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/classpath/CheckProhibitedDependenciesLifecyclePlugin.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.classpath; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.tasks.TaskProvider; + +/** + * @author Rob Winch + */ +public class CheckProhibitedDependenciesLifecyclePlugin implements Plugin { + public static final String CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME = "checkForProhibitedDependencies"; + + @Override + public void apply(Project project) { + TaskProvider checkProhibitedDependencies = project.getTasks().register(CheckProhibitedDependenciesLifecyclePlugin.CHECK_PROHIBITED_DEPENDENCIES_TASK_NAME, task -> { + task.setGroup(JavaBasePlugin.VERIFICATION_GROUP); + task.setDescription("Checks both the compile/runtime classpath of every SourceSet for prohibited dependencies"); + }); + project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> { + checkTask.dependsOn(checkProhibitedDependencies); + }); + } +} From 3c641dee754bd1cb6a2c8ad6358a3e6089911de4 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 14 Jan 2022 16:35:12 -0600 Subject: [PATCH 129/589] Remove commons-logging Closes gh-10499 --- config/spring-security-config.gradle | 1 + dependencies/spring-security-dependencies.gradle | 1 - openid/spring-security-openid.gradle | 5 ++++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 9274704bb4..556a64dbbe 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -82,6 +82,7 @@ dependencies { testImplementation "org.mockito:mockito-inline" testImplementation ('org.openid4java:openid4java-nodeps') { exclude group: 'com.google.code.guice', module: 'guice' + exclude group: 'commons-logging', module: 'commons-logging' } testImplementation('org.seleniumhq.selenium:htmlunit-driver') { exclude group: 'commons-logging', module: 'commons-logging' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 7f5b1cf4cc..d619f5f6a7 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -25,7 +25,6 @@ dependencies { api "com.unboundid:unboundid-ldapsdk:4.0.14" api "commons-codec:commons-codec:1.15" api "commons-collections:commons-collections:3.2.2" - api "commons-logging:commons-logging:1.2" api "io.mockk:mockk:1.12.2" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" diff --git a/openid/spring-security-openid.gradle b/openid/spring-security-openid.gradle index 0725b0d0a8..7120a61371 100644 --- a/openid/spring-security-openid.gradle +++ b/openid/spring-security-openid.gradle @@ -16,6 +16,7 @@ dependencies { // We use the maven central version here instead. api('org.openid4java:openid4java-nodeps') { exclude group: 'com.google.code.guice', module: 'guice' + exclude group: 'commons-logging', module: 'commons-logging' } api 'org.springframework:spring-aop' api 'org.springframework:spring-beans' @@ -26,7 +27,9 @@ dependencies { provided 'javax.servlet:javax.servlet-api' runtimeOnly 'net.sourceforge.nekohtml:nekohtml' - runtimeOnly 'org.apache.httpcomponents:httpclient' + runtimeOnly('org.apache.httpcomponents:httpclient') { + exclude group: 'commons-logging', module: 'commons-logging' + } testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" From f8e14683f60a767deef7f3e50054b17b643b8e8b Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 14 Jan 2022 16:35:34 -0600 Subject: [PATCH 130/589] Remove jcl-over-slf4j Issue gh-10499 --- config/spring-security-config.gradle | 1 - core/spring-security-core.gradle | 1 - dependencies/spring-security-dependencies.gradle | 1 - ldap/spring-security-ldap.gradle | 1 - messaging/spring-security-messaging.gradle | 1 - 5 files changed, 5 deletions(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 556a64dbbe..55e8dfa2b4 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -91,7 +91,6 @@ dependencies { exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'io.netty', module: 'netty' } - testImplementation 'org.slf4j:jcl-over-slf4j' testImplementation 'org.springframework.ldap:spring-ldap-core' testImplementation 'org.springframework:spring-expression' testImplementation 'org.springframework:spring-jdbc' diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index 213f24f92c..06b9700e14 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -31,7 +31,6 @@ dependencies { testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" testImplementation 'org.skyscreamer:jsonassert' - testImplementation 'org.slf4j:jcl-over-slf4j' testImplementation 'org.springframework:spring-test' testRuntimeOnly 'org.hsqldb:hsqldb' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index d619f5f6a7..c07d76b90f 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -67,7 +67,6 @@ dependencies { api "org.seleniumhq.selenium:selenium-java:3.141.59" api "org.seleniumhq.selenium:selenium-support:3.141.59" api "org.skyscreamer:jsonassert:1.5.0" - api "org.slf4j:jcl-over-slf4j:1.7.33" api "org.slf4j:log4j-over-slf4j:1.7.33" api "org.slf4j:slf4j-api:1.7.33" api "org.springframework.ldap:spring-ldap-core:2.4.0-M1" diff --git a/ldap/spring-security-ldap.gradle b/ldap/spring-security-ldap.gradle index f1c8074af4..c4f6c082ed 100644 --- a/ldap/spring-security-ldap.gradle +++ b/ldap/spring-security-ldap.gradle @@ -26,7 +26,6 @@ dependencies { } testImplementation project(':spring-security-test') - testImplementation 'org.slf4j:jcl-over-slf4j' testImplementation 'org.slf4j:slf4j-api' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" diff --git a/messaging/spring-security-messaging.gradle b/messaging/spring-security-messaging.gradle index 6556c0e6b0..f1c7c16e7d 100644 --- a/messaging/spring-security-messaging.gradle +++ b/messaging/spring-security-messaging.gradle @@ -24,7 +24,6 @@ dependencies { testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" testImplementation "org.slf4j:slf4j-api" - testImplementation "org.slf4j:jcl-over-slf4j" testImplementation "org.slf4j:log4j-over-slf4j" testImplementation "ch.qos.logback:logback-classic" From d8b406ab7165937377e203b7e8b564b4dcb603f5 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Fri, 14 Jan 2022 16:35:45 -0600 Subject: [PATCH 131/589] Remove javax.inject Issue gh-10501 --- dependencies/spring-security-dependencies.gradle | 1 + openid/spring-security-openid.gradle | 2 ++ 2 files changed, 3 insertions(+) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index c07d76b90f..0e31eb93fa 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -28,6 +28,7 @@ dependencies { api "io.mockk:mockk:1.12.2" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" + api "jakarta.inject:jakarta.inject-api:1.0.5" api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" api "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3" api "javax.servlet:javax.servlet-api:4.0.1" diff --git a/openid/spring-security-openid.gradle b/openid/spring-security-openid.gradle index 7120a61371..bd506ae64a 100644 --- a/openid/spring-security-openid.gradle +++ b/openid/spring-security-openid.gradle @@ -10,6 +10,7 @@ dependencies { api project(':spring-security-web') api('com.google.inject:guice') { exclude group: 'aopalliance', module: 'aopalliance' + exclude group: 'javax.inject', module: 'javax.inject' } // openid4java has a compile time dep on guice with a group // name which is different from the maven central one. @@ -31,6 +32,7 @@ dependencies { exclude group: 'commons-logging', module: 'commons-logging' } + testImplementation "jakarta.inject:jakarta.inject-api" testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" From 8f64bb6c8cb0b66b88012f712639494ca7c68a60 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 17:01:23 -0600 Subject: [PATCH 132/589] javax.servlet:javax.servlet-api -> jakarta.servlet:jakarta.servlet-api Issue gh-10501 --- .../samples/integrationtest/withpropdeps/build.gradle | 2 +- cas/spring-security-cas.gradle | 2 +- config/spring-security-config.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- itest/context/spring-security-itest-context.gradle | 2 +- itest/web/spring-security-itest-web.gradle | 4 ++-- messaging/spring-security-messaging.gradle | 2 +- oauth2/oauth2-client/spring-security-oauth2-client.gradle | 2 +- .../spring-security-oauth2-resource-server.gradle | 2 +- openid/spring-security-openid.gradle | 2 +- .../spring-security-saml2-service-provider.gradle | 2 +- taglibs/spring-security-taglibs.gradle | 2 +- test/spring-security-test.gradle | 2 +- web/spring-security-web.gradle | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle b/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle index 732278d03b..a0526f319e 100644 --- a/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle +++ b/buildSrc/src/test/resources/samples/integrationtest/withpropdeps/build.gradle @@ -9,6 +9,6 @@ repositories { } dependencies { - optional 'javax.servlet:javax.servlet-api:3.1.0' + optional 'jakarta.servlet:jakarta.servlet-api:3.1.0' testCompile 'junit:junit:4.12' } \ No newline at end of file diff --git a/cas/spring-security-cas.gradle b/cas/spring-security-cas.gradle index 8b3d4630f7..4df4d66a6f 100644 --- a/cas/spring-security-cas.gradle +++ b/cas/spring-security-cas.gradle @@ -13,7 +13,7 @@ dependencies { optional 'com.fasterxml.jackson.core:jackson-databind' optional 'net.sf.ehcache:ehcache' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 55e8dfa2b4..66d167e49f 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -39,7 +39,7 @@ dependencies { optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' optional 'javax.annotation:jsr250-api' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(':spring-security-aspects') testImplementation project(':spring-security-cas') diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 0e31eb93fa..13eebd6f2e 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -31,7 +31,7 @@ dependencies { api "jakarta.inject:jakarta.inject-api:1.0.5" api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" api "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3" - api "javax.servlet:javax.servlet-api:4.0.1" + api "jakarta.servlet:jakarta.servlet-api:4.0.4" api "javax.xml.bind:jaxb-api:2.3.1" api "ldapsdk:ldapsdk:4.1" api "net.sf.ehcache:ehcache:2.10.9.2" diff --git a/itest/context/spring-security-itest-context.gradle b/itest/context/spring-security-itest-context.gradle index 9e3334454a..15d323cc9f 100644 --- a/itest/context/spring-security-itest-context.gradle +++ b/itest/context/spring-security-itest-context.gradle @@ -10,7 +10,7 @@ dependencies { implementation 'org.springframework:spring-tx' testImplementation project(':spring-security-web') - testImplementation 'javax.servlet:javax.servlet-api' + testImplementation 'jakarta.servlet:jakarta.servlet-api' testImplementation 'org.springframework:spring-web' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" diff --git a/itest/web/spring-security-itest-web.gradle b/itest/web/spring-security-itest-web.gradle index 26feb48b14..4a82c48b07 100644 --- a/itest/web/spring-security-itest-web.gradle +++ b/itest/web/spring-security-itest-web.gradle @@ -5,7 +5,7 @@ dependencies { implementation 'org.springframework:spring-context' implementation 'org.springframework:spring-web' - compileOnly 'javax.servlet:javax.servlet-api' + compileOnly 'jakarta.servlet:jakarta.servlet-api' testImplementation project(':spring-security-core') testImplementation project(':spring-security-test') @@ -21,7 +21,7 @@ dependencies { testImplementation "org.mockito:mockito-core" testImplementation "org.mockito:mockito-junit-jupiter" testImplementation "org.springframework:spring-test" - testImplementation 'javax.servlet:javax.servlet-api' + testImplementation 'jakarta.servlet:jakarta.servlet-api' testRuntimeOnly project(':spring-security-config') testRuntimeOnly project(':spring-security-ldap') diff --git a/messaging/spring-security-messaging.gradle b/messaging/spring-security-messaging.gradle index f1c7c16e7d..3ae0b4f23a 100644 --- a/messaging/spring-security-messaging.gradle +++ b/messaging/spring-security-messaging.gradle @@ -12,7 +12,7 @@ dependencies { optional project(':spring-security-web') optional 'org.springframework:spring-websocket' optional 'io.projectreactor:reactor-core' - optional 'javax.servlet:javax.servlet-api' + optional 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path: ':spring-security-core', configuration: 'tests') testImplementation 'commons-codec:commons-codec' diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index 99ba72bc7f..21b6ada9e6 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -35,5 +35,5 @@ dependencies { testRuntimeOnly 'org.hsqldb:hsqldb' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' } diff --git a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle index 438bbc8b5d..69e705766b 100644 --- a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle +++ b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle @@ -12,7 +12,7 @@ dependencies { optional 'io.projectreactor:reactor-core' optional 'org.springframework:spring-webflux' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path: ':spring-security-oauth2-jose', configuration: 'tests') testImplementation 'com.squareup.okhttp3:mockwebserver' diff --git a/openid/spring-security-openid.gradle b/openid/spring-security-openid.gradle index bd506ae64a..5bbd99d35b 100644 --- a/openid/spring-security-openid.gradle +++ b/openid/spring-security-openid.gradle @@ -25,7 +25,7 @@ dependencies { api 'org.springframework:spring-core' api 'org.springframework:spring-web' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' runtimeOnly 'net.sourceforge.nekohtml:nekohtml' runtimeOnly('org.apache.httpcomponents:httpclient') { diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle index 5bcc457a61..c00bf3d72c 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -50,7 +50,7 @@ dependencies { opensaml4MainImplementation "org.opensaml:opensaml-saml-api:4.1.0" opensaml4MainImplementation "org.opensaml:opensaml-saml-impl:4.1.0" - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation 'com.squareup.okhttp3:mockwebserver' testImplementation "org.assertj:assertj-core" diff --git a/taglibs/spring-security-taglibs.gradle b/taglibs/spring-security-taglibs.gradle index a45ad37f12..addda2dc48 100644 --- a/taglibs/spring-security-taglibs.gradle +++ b/taglibs/spring-security-taglibs.gradle @@ -13,7 +13,7 @@ dependencies { api 'org.springframework:spring-web' provided 'javax.servlet.jsp:javax.servlet.jsp-api' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testRuntimeOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api' diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index e5b977dda5..b40f9aa4e9 100644 --- a/test/spring-security-test.gradle +++ b/test/spring-security-test.gradle @@ -15,7 +15,7 @@ dependencies { optional 'org.springframework:spring-webmvc' optional 'org.springframework:spring-webflux' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path : ':spring-security-config', configuration : 'tests') testImplementation 'com.fasterxml.jackson.core:jackson-databind' diff --git a/web/spring-security-web.gradle b/web/spring-security-web.gradle index ad2279b46e..47eebfbc0f 100644 --- a/web/spring-security-web.gradle +++ b/web/spring-security-web.gradle @@ -17,7 +17,7 @@ dependencies { optional 'org.springframework:spring-webflux' optional 'org.springframework:spring-webmvc' - provided 'javax.servlet:javax.servlet-api' + provided 'jakarta.servlet:jakarta.servlet-api' testImplementation project(path: ':spring-security-core', configuration: 'tests') testImplementation 'commons-codec:commons-codec' From 0e8c03401bcf64060d33c1a9dde4a2a307877809 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 17:16:29 -0600 Subject: [PATCH 133/589] javax.xml.bind:jaxb-api -> jakarta.xml.bind:jakarta.xml.bind-api Issue gh-10501 --- config/spring-security-config.gradle | 2 +- data/spring-security-data.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- test/spring-security-test.gradle | 2 +- web/spring-security-web.gradle | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 66d167e49f..f2ffe2404b 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -63,7 +63,7 @@ dependencies { testImplementation 'io.projectreactor.netty:reactor-netty' testImplementation 'io.rsocket:rsocket-transport-netty' testImplementation 'javax.annotation:jsr250-api:1.0' - testImplementation 'javax.xml.bind:jaxb-api' + testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'ldapsdk:ldapsdk:4.1' testImplementation('net.sourceforge.htmlunit:htmlunit') { exclude group: 'commons-logging', module: 'commons-logging' diff --git a/data/spring-security-data.gradle b/data/spring-security-data.gradle index e0c9f14dab..3e915ef871 100644 --- a/data/spring-security-data.gradle +++ b/data/spring-security-data.gradle @@ -3,7 +3,7 @@ apply plugin: 'io.spring.convention.spring-module' dependencies { management platform(project(":spring-security-dependencies")) api project(':spring-security-core') - api 'javax.xml.bind:jaxb-api' + api 'jakarta.xml.bind:jakarta.xml.bind-api' api 'org.springframework.data:spring-data-commons' api 'org.springframework:spring-core' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 13eebd6f2e..7ee2da620b 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -32,7 +32,7 @@ dependencies { api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" api "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3" api "jakarta.servlet:jakarta.servlet-api:4.0.4" - api "javax.xml.bind:jaxb-api:2.3.1" + api "jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" api "ldapsdk:ldapsdk:4.1" api "net.sf.ehcache:ehcache:2.10.9.2" api "net.sourceforge.htmlunit:htmlunit:2.56.0" diff --git a/test/spring-security-test.gradle b/test/spring-security-test.gradle index b40f9aa4e9..92b3868438 100644 --- a/test/spring-security-test.gradle +++ b/test/spring-security-test.gradle @@ -21,7 +21,7 @@ dependencies { testImplementation 'com.fasterxml.jackson.core:jackson-databind' testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' testImplementation 'io.projectreactor:reactor-test' - testImplementation 'javax.xml.bind:jaxb-api' + testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" diff --git a/web/spring-security-web.gradle b/web/spring-security-web.gradle index 47eebfbc0f..81a4e8116f 100644 --- a/web/spring-security-web.gradle +++ b/web/spring-security-web.gradle @@ -22,7 +22,7 @@ dependencies { testImplementation project(path: ':spring-security-core', configuration: 'tests') testImplementation 'commons-codec:commons-codec' testImplementation 'io.projectreactor:reactor-test' - testImplementation 'javax.xml.bind:jaxb-api' + testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'org.hamcrest:hamcrest' testImplementation 'org.mockito:mockito-core' testImplementation 'org.mockito:mockito-inline' From a64b72b8371d9fad49b44830f8c9f67d58fec1b1 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 18 Jan 2022 17:29:45 -0600 Subject: [PATCH 134/589] Exclude javax from cas-client-core Issue gh-10501 --- cas/spring-security-cas.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cas/spring-security-cas.gradle b/cas/spring-security-cas.gradle index 4df4d66a6f..ed4331c3a4 100644 --- a/cas/spring-security-cas.gradle +++ b/cas/spring-security-cas.gradle @@ -4,7 +4,10 @@ dependencies { management platform(project(":spring-security-dependencies")) api project(':spring-security-core') api project(':spring-security-web') - api 'org.jasig.cas.client:cas-client-core' + api('org.jasig.cas.client:cas-client-core') { + exclude group: 'org.glassfish.jaxb', module: 'jaxb-core' + exclude group: 'javax.xml.bind', module: 'jaxb-api' + } api 'org.springframework:spring-beans' api 'org.springframework:spring-context' api 'org.springframework:spring-core' From 3d8041972782a9d1003739a406ccfc033b8b179e Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 13:54:42 -0600 Subject: [PATCH 135/589] javax.servlet.jsp.jstl-api -> jakarta.servlet.jsp.jstl-api Issue gh-10501 --- dependencies/spring-security-dependencies.gradle | 2 -- taglibs/spring-security-taglibs.gradle | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 7ee2da620b..b9228a3b63 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -29,8 +29,6 @@ dependencies { api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" api "jakarta.inject:jakarta.inject-api:1.0.5" - api "javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:1.2.2" - api "javax.servlet.jsp:javax.servlet.jsp-api:2.3.3" api "jakarta.servlet:jakarta.servlet-api:4.0.4" api "jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" api "ldapsdk:ldapsdk:4.1" diff --git a/taglibs/spring-security-taglibs.gradle b/taglibs/spring-security-taglibs.gradle index addda2dc48..4a843a8a1c 100644 --- a/taglibs/spring-security-taglibs.gradle +++ b/taglibs/spring-security-taglibs.gradle @@ -15,7 +15,7 @@ dependencies { provided 'javax.servlet.jsp:javax.servlet.jsp-api' provided 'jakarta.servlet:jakarta.servlet-api' - testRuntimeOnly 'javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api' + testRuntimeOnly 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" From 6e145b459f3bf0053423c6467426da8b39c3a943 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 13:56:03 -0600 Subject: [PATCH 136/589] javax.servlet.jsp-api -> jakarta.servlet.jsp-api Issue gh-10501 --- dependencies/spring-security-dependencies.gradle | 2 ++ taglibs/spring-security-taglibs.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index b9228a3b63..38aad5b573 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -29,6 +29,8 @@ dependencies { api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" api "javax.annotation:jsr250-api:1.0" api "jakarta.inject:jakarta.inject-api:1.0.5" + api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7" + api "jakarta.servlet.jsp:jakarta.servlet.jsp-api:2.3.6" api "jakarta.servlet:jakarta.servlet-api:4.0.4" api "jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" api "ldapsdk:ldapsdk:4.1" diff --git a/taglibs/spring-security-taglibs.gradle b/taglibs/spring-security-taglibs.gradle index 4a843a8a1c..587e83ffa7 100644 --- a/taglibs/spring-security-taglibs.gradle +++ b/taglibs/spring-security-taglibs.gradle @@ -12,7 +12,7 @@ dependencies { api 'org.springframework:spring-expression' api 'org.springframework:spring-web' - provided 'javax.servlet.jsp:javax.servlet.jsp-api' + provided 'jakarta.servlet.jsp:jakarta.servlet.jsp-api' provided 'jakarta.servlet:jakarta.servlet-api' testRuntimeOnly 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' From 678c386834b11cea36dfde493173570077bdac2e Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 13:57:06 -0600 Subject: [PATCH 137/589] jsr250-api -> jakarta.annotation-api Issue gh-10501 --- config/spring-security-config.gradle | 4 ++-- core/spring-security-core.gradle | 2 +- dependencies/spring-security-dependencies.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index f2ffe2404b..ed12d0218a 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -37,7 +37,7 @@ dependencies { optional'org.springframework:spring-websocket' optional 'org.jetbrains.kotlin:kotlin-reflect' optional 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - optional 'javax.annotation:jsr250-api' + optional 'jakarta.annotation:jakarta.annotation-api' provided 'jakarta.servlet:jakarta.servlet-api' @@ -62,7 +62,7 @@ dependencies { testImplementation 'ch.qos.logback:logback-classic' testImplementation 'io.projectreactor.netty:reactor-netty' testImplementation 'io.rsocket:rsocket-transport-netty' - testImplementation 'javax.annotation:jsr250-api:1.0' + testImplementation 'jakarta.annotation:jakarta.annotation-api:1.0' testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'ldapsdk:ldapsdk:4.1' testImplementation('net.sourceforge.htmlunit:htmlunit') { diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index 06b9700e14..3f135f759f 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -13,7 +13,7 @@ dependencies { optional 'com.fasterxml.jackson.core:jackson-databind' optional 'io.projectreactor:reactor-core' - optional 'javax.annotation:jsr250-api' + optional 'jakarta.annotation:jakarta.annotation-api' optional 'net.sf.ehcache:ehcache' optional 'org.aspectj:aspectjrt' optional 'org.springframework:spring-jdbc' diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index 38aad5b573..e76fb18e40 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -27,8 +27,8 @@ dependencies { api "commons-collections:commons-collections:3.2.2" api "io.mockk:mockk:1.12.2" api "io.projectreactor.tools:blockhound:1.0.6.RELEASE" - api "javax.annotation:jsr250-api:1.0" api "jakarta.inject:jakarta.inject-api:1.0.5" + api "jakarta.annotation:jakarta.annotation-api:1.3.5" api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7" api "jakarta.servlet.jsp:jakarta.servlet.jsp-api:2.3.6" api "jakarta.servlet:jakarta.servlet-api:4.0.4" From 29e62418696c53b4629a2f724333a323cf76371f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 10:39:31 -0600 Subject: [PATCH 138/589] dependencies jakarta.transaction-api Issue gh-10501 --- dependencies/spring-security-dependencies.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/dependencies/spring-security-dependencies.gradle b/dependencies/spring-security-dependencies.gradle index e76fb18e40..63584874f5 100644 --- a/dependencies/spring-security-dependencies.gradle +++ b/dependencies/spring-security-dependencies.gradle @@ -32,6 +32,7 @@ dependencies { api "jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:1.2.7" api "jakarta.servlet.jsp:jakarta.servlet.jsp-api:2.3.6" api "jakarta.servlet:jakarta.servlet-api:4.0.4" + api "jakarta.transaction:jakarta.transaction-api:1.3.3" api "jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" api "ldapsdk:ldapsdk:4.1" api "net.sf.ehcache:ehcache:2.10.9.2" From 9d4ecc9c370677bcd379ef4fd42b5cf91fb2222f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 10:42:51 -0600 Subject: [PATCH 139/589] Additional removal of javax.inject Issue gh-10501 --- config/spring-security-config.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index ed12d0218a..fae5c0d9b5 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -63,6 +63,7 @@ dependencies { testImplementation 'io.projectreactor.netty:reactor-netty' testImplementation 'io.rsocket:rsocket-transport-netty' testImplementation 'jakarta.annotation:jakarta.annotation-api:1.0' + testImplementation "jakarta.inject:jakarta.inject-api" testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'ldapsdk:ldapsdk:4.1' testImplementation('net.sourceforge.htmlunit:htmlunit') { From 27e1a2ca69956094ae7244d7f6c7bf405e5b77b5 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 10:43:30 -0600 Subject: [PATCH 140/589] Remove javax.transaction Issue gh-10501 --- config/spring-security-config.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index fae5c0d9b5..38e682fd6c 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -64,6 +64,7 @@ dependencies { testImplementation 'io.rsocket:rsocket-transport-netty' testImplementation 'jakarta.annotation:jakarta.annotation-api:1.0' testImplementation "jakarta.inject:jakarta.inject-api" + testImplementation "jakarta.transaction:jakarta.transaction-api" testImplementation 'jakarta.xml.bind:jakarta.xml.bind-api' testImplementation 'ldapsdk:ldapsdk:4.1' testImplementation('net.sourceforge.htmlunit:htmlunit') { @@ -77,7 +78,9 @@ dependencies { testImplementation 'org.apache.directory.shared:shared-ldap' testImplementation "com.unboundid:unboundid-ldapsdk" testImplementation 'org.eclipse.persistence:javax.persistence' - testImplementation 'org.hibernate:hibernate-entitymanager' + testImplementation('org.hibernate:hibernate-entitymanager') { + exclude group: 'org.jboss.spec.javax.transaction', module: 'jboss-transaction-api_1.2_spec' + } testImplementation 'org.hsqldb:hsqldb' testImplementation 'org.mockito:mockito-core' testImplementation "org.mockito:mockito-inline" From ba922dcdf0d58f9c2ac599af7937c9123168f3d2 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 19 Jan 2022 10:43:59 -0600 Subject: [PATCH 141/589] Exclude javax from hibernate dependency Issue gh-10501 --- config/spring-security-config.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index 38e682fd6c..2ce079d305 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -79,6 +79,9 @@ dependencies { testImplementation "com.unboundid:unboundid-ldapsdk" testImplementation 'org.eclipse.persistence:javax.persistence' testImplementation('org.hibernate:hibernate-entitymanager') { + exclude group: 'javax.activation', module: 'javax.activation-api' + exclude group: 'javax.persistence', module: 'javax.persistence-api' + exclude group: 'javax.xml.bind', module: 'jaxb-api' exclude group: 'org.jboss.spec.javax.transaction', module: 'jboss-transaction-api_1.2_spec' } testImplementation 'org.hsqldb:hsqldb' From cca35bdd93514996c9434c0b652e4342d3aeb2ab Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Mon, 13 Dec 2021 16:33:23 -0300 Subject: [PATCH 142/589] Make Saml2AuthenticationRequests serializable Closes gh-10550 --- .../AbstractSaml2AuthenticationRequest.java | 8 ++- .../Saml2PostAuthenticationRequestTests.java | 56 +++++++++++++++++++ ...ml2RedirectAuthenticationRequestTests.java | 56 +++++++++++++++++++ 3 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequestTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequestTests.java diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java index 028ecd6bae..cb2df00a8a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/AbstractSaml2AuthenticationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,10 @@ package org.springframework.security.saml2.provider.service.authentication; +import java.io.Serializable; import java.nio.charset.Charset; +import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.util.Assert; @@ -34,7 +36,9 @@ import org.springframework.util.Assert; * @see Saml2AuthenticationRequestFactory#createPostAuthenticationRequest(Saml2AuthenticationRequestContext) * @see Saml2AuthenticationRequestFactory#createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext) */ -public abstract class AbstractSaml2AuthenticationRequest { +public abstract class AbstractSaml2AuthenticationRequest implements Serializable { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final String samlRequest; diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequestTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequestTests.java new file mode 100644 index 0000000000..748bfcdc66 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequestTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.SerializationUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2PostAuthenticationRequestTests { + + private static final String IDP_SSO_URL = "https://sso-url.example.com/IDP/SSO"; + + @Test + void serializeWhenDeserializeThenSameFields() { + Saml2PostAuthenticationRequest authenticationRequest = getAuthenticationRequestBuilder().build(); + byte[] bytes = SerializationUtils.serialize(authenticationRequest); + Saml2PostAuthenticationRequest deserializedAuthenticationRequest = (Saml2PostAuthenticationRequest) SerializationUtils + .deserialize(bytes); + assertThat(deserializedAuthenticationRequest).usingRecursiveComparison().isEqualTo(authenticationRequest); + } + + @Test + void serializeWhenDeserializeAndCompareToOtherThenNotSame() { + Saml2PostAuthenticationRequest authenticationRequest = getAuthenticationRequestBuilder().build(); + Saml2PostAuthenticationRequest otherAuthenticationRequest = getAuthenticationRequestBuilder() + .relayState("relay").build(); + byte[] bytes = SerializationUtils.serialize(otherAuthenticationRequest); + Saml2PostAuthenticationRequest deserializedAuthenticationRequest = (Saml2PostAuthenticationRequest) SerializationUtils + .deserialize(bytes); + assertThat(deserializedAuthenticationRequest).usingRecursiveComparison().isNotEqualTo(authenticationRequest); + } + + private Saml2PostAuthenticationRequest.Builder getAuthenticationRequestBuilder() { + return Saml2PostAuthenticationRequest + .withAuthenticationRequestContext( + TestSaml2AuthenticationRequestContexts.authenticationRequestContext().build()) + .samlRequest("request").authenticationRequestUri(IDP_SSO_URL); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequestTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequestTests.java new file mode 100644 index 0000000000..e2878455d8 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequestTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.SerializationUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2RedirectAuthenticationRequestTests { + + private static final String IDP_SSO_URL = "https://sso-url.example.com/IDP/SSO"; + + @Test + void serializeWhenDeserializeThenSameFields() { + Saml2RedirectAuthenticationRequest authenticationRequest = getAuthenticationRequestBuilder().build(); + byte[] bytes = SerializationUtils.serialize(authenticationRequest); + Saml2RedirectAuthenticationRequest deserializedAuthenticationRequest = (Saml2RedirectAuthenticationRequest) SerializationUtils + .deserialize(bytes); + assertThat(deserializedAuthenticationRequest).usingRecursiveComparison().isEqualTo(authenticationRequest); + } + + @Test + void serializeWhenDeserializeAndCompareToOtherThenNotSame() { + Saml2RedirectAuthenticationRequest authenticationRequest = getAuthenticationRequestBuilder().build(); + Saml2RedirectAuthenticationRequest otherAuthenticationRequest = getAuthenticationRequestBuilder() + .relayState("relay").build(); + byte[] bytes = SerializationUtils.serialize(otherAuthenticationRequest); + Saml2RedirectAuthenticationRequest deserializedAuthenticationRequest = (Saml2RedirectAuthenticationRequest) SerializationUtils + .deserialize(bytes); + assertThat(deserializedAuthenticationRequest).usingRecursiveComparison().isNotEqualTo(authenticationRequest); + } + + private Saml2RedirectAuthenticationRequest.Builder getAuthenticationRequestBuilder() { + return Saml2RedirectAuthenticationRequest + .withAuthenticationRequestContext( + TestSaml2AuthenticationRequestContexts.authenticationRequestContext().build()) + .samlRequest("request").authenticationRequestUri(IDP_SSO_URL); + } + +} From d538423f98da1b01e0371c692f021ad785f4af0f Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 9 Dec 2020 12:48:52 -0700 Subject: [PATCH 143/589] Add Saml2AuthenticationRequestResolver Closes gh-10355 --- .../saml2/Saml2LoginConfigurer.java | 128 ++++++----- .../saml2/Saml2LoginConfigurerTests.java | 139 +++++++++++- .../saml2/login/authentication-requests.adoc | 115 ++-------- .../Saml2PostAuthenticationRequest.java | 14 +- .../Saml2RedirectAuthenticationRequest.java | 15 +- ...aml2WebSsoAuthenticationRequestFilter.java | 99 ++++++--- ...OpenSamlAuthenticationRequestResolver.java | 163 ++++++++++++++ .../authentication/OpenSamlSigningUtils.java | 173 +++++++++++++++ .../OpenSamlVerificationUtils.java | 207 ++++++++++++++++++ .../Saml2AuthenticationRequestResolver.java | 34 +++ .../web/authentication/Saml2Utils.java | 79 +++++++ ...penSaml3AuthenticationRequestResolver.java | 113 ++++++++++ ...penSaml4AuthenticationRequestResolver.java | 110 ++++++++++ ...ebSsoAuthenticationRequestFilterTests.java | 33 ++- ...amlAuthenticationRequestResolverTests.java | 169 ++++++++++++++ 15 files changed, 1404 insertions(+), 187 deletions(-) create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlVerificationUtils.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2AuthenticationRequestResolver.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/Saml2Utils.java create mode 100644 saml2/saml2-service-provider/src/opensaml3Main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml3AuthenticationRequestResolver.java create mode 100644 saml2/saml2-service-provider/src/opensaml4Main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSaml4AuthenticationRequestResolver.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolverTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index ed594983c4..ee8e598ff1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,6 @@ package org.springframework.security.config.annotation.web.configurers.saml2; import java.util.LinkedHashMap; import java.util.Map; -import javax.servlet.Filter; - import org.opensaml.core.Version; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -50,6 +48,7 @@ import org.springframework.security.saml2.provider.service.web.RelyingPartyRegis import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationTokenConverter; +import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; @@ -115,9 +114,11 @@ public final class Saml2LoginConfigurer> private String loginPage; - private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; + private String authenticationRequestUri = "/saml2/authenticate/{registrationId}"; - private AuthenticationRequestEndpointConfig authenticationRequestEndpoint = new AuthenticationRequestEndpointConfig(); + private Saml2AuthenticationRequestResolver authenticationRequestResolver; + + private String loginProcessingUrl = Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; @@ -176,6 +177,20 @@ public final class Saml2LoginConfigurer> return this; } + /** + * Use this {@link Saml2AuthenticationRequestResolver} for generating SAML 2.0 + * Authentication Requests. + * @param authenticationRequestResolver + * @return the {@link Saml2LoginConfigurer} for further configuration + * @since 5.7 + */ + public Saml2LoginConfigurer authenticationRequestResolver( + Saml2AuthenticationRequestResolver authenticationRequestResolver) { + Assert.notNull(authenticationRequestResolver, "authenticationRequestResolver cannot be null"); + this.authenticationRequestResolver = authenticationRequestResolver; + return this; + } + /** * Specifies the URL to validate the credentials. If specified a custom URL, consider * specifying a custom {@link AuthenticationConverter} via @@ -200,7 +215,7 @@ public final class Saml2LoginConfigurer> /** * {@inheritDoc} - * + *

* Initializes this filter chain for SAML 2 Login. The following actions are taken: *

T^)@D;9f!^2oLgX{RNmKcwx9kZ9Q#aL{;>gveV;#wgYlV4@nO9GpUWbuQZG6)FbjBqYa1q-Hm~6?CM7vH zK*Pl-GREumglD*|sj2CoP#6v8-x24Za|bm5*jI7_f-_aXF#;{Ni9@5&5}Vd*49(|H zStq~2M#vnkQ&;CN7OJn3&!s7G+#j_VjM&rk>0Jn%#5$)UY0Psi-i^}OKVc2`#tX@D zI%<%R8`tOv4Qv#qPeDM!_eY_T;5ab<15}9qesl&E;YA>xsknQwgV7q{!c+Z=yU1lL zYqAwC7^+PI%wiloFO9m9Ws-i-!z5qYXbngtdFQ`K&lgywQ*Zkh8RVUU=6P zN=KZ>=11!ST+h7EO$!WyCUS&1%pt5d&SyMZ(Yu|*1{yz=bWY_g0M-tUs>{MIKknKPT>Z)7Q_;=L1Avoq>F2vM z`u+G$g~v=8GzC!!{}+JgSo_RW3)V%5U%nRZamWUe=54L?XK?dtoOZJ8uez;%f_uON zP(B+HFEQSUIg>_XVg9;T9IKFPxfs@^y*Rq}c4yBq$^S*fS@Deg$ap(!M z)th^_)-hks)~a%FK&?!4f7li4fQpQas;vrAWN&oY`+l^j-QkWu*u?!Pa)u~wYg-L} ztC6gb+VN+w7&062BkR22BXUnOs;8bd zlEXdycr|)?n7z@X$)nP6LNSpM_if7Y0||QoY1JJl^u00_6USox(cykeAUCTJseBv- z#B;aiR9Q7<*XeZACYKV;Z42BWb~M4cKFK2;`h}BD}_xIDVVyKyz#pq^CX` zmFHdl{0_D%{n;g%js1{_c1K9;FR)Q+6{=9N6F5T*f>r;cT-2t%RyuJvsX#*$ly7A|IXsKVyPuhh!ko=4oMZe#vezC`M?5-$p8L-88& zRx>Ial>4U#(0LFEF#Zy`zxvu$GBPndvld+S_rW#?dN~zzLB(XU3wwwI%>-hNddTF7 zA@ec5Cb^Wma6L1>2KRMZkVghx1M!djfvfmDp5VshV`E zN^AiEt=00f({)Ek`n18>H$rKl3E>;Y8JAz~!UbbkK|`_8N1&-|@fuvJtAoEtMt;}K z5Rlmvcksuw@1B z(Y+VF4DL^&i0`&60wXw0dJlHzm+)HitqG#J}(L%Wi>j^^UvBK7emlcpn}g$_8y?0f;jc`TCZ@kj+pl3%eC zUAlMu5wvMbLLAjuE}0d4hLf{v_po7Fs~v^hDs{TnWB(jDM0=_oO}TcP;w@)qXTP-| zre-XFHpZ+=EhMRDbJSj~OVsCZRp+Dq#uO_DyqdYm!Lu35g+S9qQ9d9FtRzsO{t6P~_u?iM2W2$hT_W_(ekd0-KeB3)-XRwKkWH zVVNZ?1n`famGp~olw}wQ8SA;{Y@4m}R8CF=gFfd2^EeKb<44Zk>w%u6h1B)f2(G@( z13BjU=GAI?RSPL5CT3bF&5cS7Y+F7qUop(D%_DtD8LstvNx0d`q;cK*axi^ZQTfB8=8>j}c-@{uqse=gf;9g1s=m;8C0Tg|g>SfQ z)}BoULy-qwo}RT%3~6(8KGM;tl6Z)Z?8Y;N0nGmQbk%x|W)>FYQJRj|*T0^WCY&sk z?YKgp0WNv3@h5NgCNRxVOHlA48Q+Hm=&i~9FRG5OZMpqpBqw$wtBtjQuk2Gw_exgD zq^Bo{VwpLtjz3$^K;9q9jE!9l!V%Uu7p9Xpb2~S7TMpE8wzt5>Z{bJHRa_M04jatH zJGa=Y8u25_sPD!;m+H3Y?((r;ff-9vj_A-9gKOM|}!$wy@2l_!mU0{yvcedn! zIGbxXRU zDZ6PyM;!_FY;>TKn6df_VcPWGYZ_+Y>OJ@8esu;Ho?F1ou~cF~Ipbj={$nntfFZH*?6e7bYsWn&%6|^0}Bl8R}3_ z(lx8YO6{EABW)RY(S*8cvok`$Fuf}KEd-4wGx~m8JXov}(u=iW`#z0I1tkl9T8)Ss z*Pz-;7Ug?<_$zBj0&N2k)TsP~ELOXgJoZE1`gbGOtiAThxz8C=oWceDoJU7tWJ)_Y zp~h+?JC%MWNs*K0OZX3)Gw9yQkx&Iok}rmQ`kq z)zu9r{AZ@8TNve$-|`E3xG|04us~@nmEd10JBAqv7uwZZKkXQ^Q@}VQ94io61*dN>%p_MM6cbZ<0mCfBjt+%M%96pR3K zKYZWI=I$Act$G@WQkli~xrkhET*FXoRT8a0t2}3Z9MWV-H-R++G+8oeHA6{|)z@^0 zxe~59``=C_uWE>XJY2pT31Lfz!jBei^{ar{$y$)NseI{&_j{-Nt@%m27=<~^WqZsN z?EPb-bhZ7vl4(61Xs6_uJ6;_Z!6$hp(y8347%&RHL#78r)B#wMeWCcdM$af~zenpl z%Aq1il5=cuc=}vAw(<7kCB9E%&x{kxZ>$Z8bKbDY-aL?*?&@-pu^8da(O{ns-+nKp zW42VcnTU+?%=%*>p76(+Y4xlO5fZ#=MV`7rk_*qrs|!!wA=g zhC8JwCN|sa+saX2rz^M{^~d&g(!6+wS4rG`&{50)nE*tlsS~BzlcU~J1G!T~1%#~) zZmG~D!Gd24>}7MRRW5t2l#4~eXUeaT%V23DK=~VMZ^Ovb7pz0*!c|v_wcqgT2xag# zK6QG>Q}?!JjzX^Z32ijJ!{9hd$x!zTEqyM(m%EGPk$9Z>g{3}wW`1MwiC32#k$CfS zqb<+w`B||!n@>kVS_Jr<5OU5HA~l!qu*=_FRhu2dRr`EC#oC5b5)Z?})bljF8;DIw znzkBSbfg%+??ej2n1|lLy*g^C*ORk_!z0uVo>s$6jerkK{Z_uUl96HSe8GQ-82M*` zG0~d2cBKlE=6DurKO@X{dE>RG4jhvvWId!N0KfK-4m~uu<`WF_sP6Xg&EBTLL3f=5 zIgIxW-g*T$Bo6CNjvp-l5w)K(y{W7p`92zKBtWNgatsz!g4Wi|DvS+ubtIS=30u?^&+YTegt zVTZ_p{u7w{=>sGWi9SskyWdRp+4gNap}`R=IMcnyTgwVDBF&Jj{m}3%z8*Iz-bcuLA8+P>~&gXbuPykLGKB9MFf5e@brfpqXAx7YaqlhxT-<0=1KwUMj)N+Av~5 zs5~m$hHq1UcV0YWedQR&I=Cc(>Kc}$cTPHb?4)tc19!&$uiiiuaP-HM)8R%V%_bf5 z_T#vVj1p%drZ>>0gB%GGH__FAn{JkNS%4HM|oe z$*=`{y79NG&K5AIQh|&4^i~AE_hb4lNhgCldS63~kjYPnj;n58&jDTT^~EQLM6;^l zfGL6Jbz!p{90--~iMSX&@2z><*ez8EWjEb~UXgFO96M?TeXU{I^vIv1pMcgGn;EAY z>SfPw)I$ox*J~}8h&;5SWUzOSS!M~G@y0&!-lKNgyj6C=sRE-h`hOuK!?X){Q&qUG zRwonPGr+5~#E5Av4r&89coK#+h3}r1&fE#P^3#+ft}bGAtc&5=6`+RsOIYux3d6LW zOeU9Lt+r}pM$}s6;@nw!#8jZz{X7^KG|4xeOg!9X|?9@YD^-f zrVA2^N5w+o^kQ;vvT#w8@d;nHM)=jqGO@Q!k!FP7=#|-W5>4la`wbdWx;Rny}OXj+Y$vpwk zP0^Y~7mK*`?M+N`@bji9Us4(2ARv+kwufGW=5Bp6eq4T>eqVnmOnje2l`8m5Kp`@1yaOr zVqM~-LcmuWhJkAf9l_C%!=7yG+IZ4{A>f@#zCxUKw z@l><4h||{Fq+>f=M(|1oOUDMoNLmbdcf@YCXrc=xDsebKJd!$gV$y3shdN+iY^V50{wofbLr21_Bo*DuCv z^)TICVMMW*^j325$$WJYC_JdyMyBo%z0yUVHmb8E3yIq`G zf*t_TF_Omiq(0i)z~$WlgnIQvt%r{;I5{~v&IW_wS0ZB{mT^LYj4Mk^K~EnxauRH8 zO?lVD>TS+vh6GVVLR-ux6V07~aS={x05KKSC_)7n7gq@ThnD#0zi#6sQJVQ8fI=7) z?(1tNuh+5fb#86++lNJ5jAcCZ95j^zK}+#^v^7QeG1?sUvWlV)KR^F#Ol2{hEWwAM zSW)D9Sjg=wF>(1MlyE7+a!}OVwYFgk9&?_TxOg{VslrY`0n~VmC=YjXUaOj-)QZRAjT(a(_mw{7C@} zg^QAOulLI4zOjI_0CH6Mt43&BoPW<`b;FmZ85-TI_wQM9H!APl)0FV=R9(zQNUFn})7FAdk(H-HY=9HD0lQF8RRqp*A6gpdMx0Oro+ zQIdnA#IYPZEtH92IJQE%Xac3Zh7K>C0P0hdcOSX5(t}44G{Rvm_+2^yQqf=)Sd;^7 z_1P5Ip%Jk!QrXwd-dVWKwPon}z}WHcvfX+3=t!QhY0w4VVTz)70+f01`IF$18gC&p z<-YxN+eRX6IuCr3n9`)Bd3ddc2ar=(3HskbF~%F&XRA84b6t!#+37_m@qbxdPta8t zgfX95&maz3!1Yuv_AWE4gNrn*a!%5VY|qw77^EJNgy&r3pQk8iygeD`^KPlaFc_ny zuum;-rAFixLUV!)g5vz^(YH4nT;epJ=+hk?x}sdWd1vpf#<$}RqGl`X2YxTD^O17^ zS(02-l?THB)G302p+L2RjNla&ji2`0>kB;Z3r6qnC>~!FvR)NG5KT@iNR&`Xb#9&} zZg>LgMZ{p-+PFbz-s^aXPYkAm*u?~Tur6d8-|@x!Uz6E%m*1dXTG$bgyfq;)!h(I> z<|4i)v-ci~kF#ATTWG+^rWq5WhJ?m#7G)pr5+Wi(WlJHHtbH99A!8!S zFVZMTcEJqaH6s;FOMGlS>XvX?=}RrW;NZC~!vMicE3`sB_df!UQ#VCceV{5sI9tu?nSR0L%kj^#xs~O;6AbNX(rwC%gOjR5|;Wu>B_a~3l>ejTg&4d`t8 zbt|;NOGkQwY1G>fM(1Pvt%6Gtg}e+rJlw@GGJ{Z=H{R_K))P=&Y7#NX=b9cc-2T`ap-&6ho2uq0H-h=C)@ zb=_@-m#vwbMNt$8dCE2N0(apjE=p4v2O}zZMT*uPnqvb$JZ_;;nu+mD9*>e;iiYHE zKY+L-DK0Kv2uehax@GbzoWu*CCqXSyf%+>YWho7?tiYFRB)|$M27=V#wjbfZ7*L$_ zk({!7n{_5`SbA`%myf+%@S zO-)I@na{$=c!UGQ1mRz}!Y(_Onu;q5;&&|lPwJ#E?Xcw2{=B4PUg2Z~a~O3lZP+8cSy z)wQ*&FMuloR6q>;ODlO+cB{Re9V`+otli)(8Q+Hw2jDmOac;UK>GAE_+_C$tL=G}a z?Hn7)PZq0K3qqV?U5hQ!T)HMEn4uY*|3(3R9_LRfKdC7KBv&5;Irdu=!v-$K{6Awm zx~-3{kD`@3j5myrdLN>;OFX%MClF#eBqk;$HAn64#4v!*ErR~v(Pnpop*xR10rPi4 zB|<7`2X!yvWGGox|7+Uor}X?&{47^M`BS@@70AYVh(N+l%(kNO zXR7XlP9vjv5ll#S?~i%v*-auQBsY5YdqxrbjK($#x2Ow=c)lLQM7<%j1W7-P4lh)`(ob2oAw#$r7ftT{u`W^5StER1OD&&TKJA?z%1=Z-it#+G{SHk0{hkIRW6V-G)1>N* z*@)*2S!O)haCBH07W7Uqheh;SI%UNI-mVlxP!^+_dPmu=Y%iewZgu))K74 zLYw;HeL=c;9XiGd|BZxJ; zse`%hY*gTOc~-^Xu^@u8 z_9qo$NmFd&oznOOxZHu9PA=QovYo?^sSgWUrYjA~8G5s~BQV=H;7yKDV z6H)=)##!LgZxr3Y3kU8tw9^xbP*`k-*Ba+6i*l7D0#5x(M4Dp(yLSV#XnRw^e)e1H z6|qp6(!==H6;s9NFtjHg{j&N2h{JzA1<5Hvv_qs^8E#{ma&CgWz#s+Q?qmNA@eco4 zp`_RfO!ViPxM{%8uxwNjLV>A!fu%aFW92{CSJN3lh zP3z0ioTl9}#V(%cgjEb*{iD<1JK1>awbl`kFcNW z8ZZb&t?uN1?7C?jt!4w^K2t$R3`O9wRUR$Y7z7wkq_=3nQhmn$usner(aCrCp~vuO z2%J*ezZRiGJ5znQ1YFMLMYT@7y5htfk#V^N1*8kz8l(|`QR=S{WooU~8;6nG?T_>z zQDoB%aw9^27P7R%f~f7M_SMbd)K2pUv?flxgcPPT$+aH~ShxR<+NHfV{)!-a2L>=cs$TYm`okrSnVTpQgUCOWO_2#?+#l5v_rz4@;rEY^=;1a zjNIg}RP5}oL}-zZLkm==*Z*<@_RR&JN01209l)kRqto-T$`n81^$PI53LTx7z^4f3 z$R){2|Hu#uAHF!t8~;vikW2|h|2$=&IUUHY=cnUfrqETK^Lul1Nc7OaM(_S3l>@o$ z2E1@K#*k~K<83um~~rQd4}9LJg|(*+y6FtUp; zEf)c%D4CurzJJrV;N{z~-npf+o-^5Y&By$x3jJf;?%o=**>G=Y6~2RMnBB>eFSRJ4 z=2gkDeh>Ou=uulrpu5?J@e_k^``}U2g|l7TyPsWV9U>{f>YMS3H)X+(O+oU7+QnUD zn^Ew2ajnO-VZAyz1!v!$Hos%9)pvq#%@J!Hs;}V+#KJt_cY?6{Wcu2@GHGU|8e=Y?1a(C56>R_VUF%ujmDbx3zjVUhfOG}qm zm61ZEDBKSMTJZgfroLQVs&~kLL&kVwcA(q?qI&HzeM(btc%Y- z^Un=%KOA;}dU= zL%ss)A5z8OI1sRLTaJxPILpu7N!5@j*yoV%PjW(X z!6NPFoX&vH>8Yq71Vh36o6Is4f?6nXe^rg0_X4DqmR-ACpW>bl1gUOyM)I(`uLF)s zm=_*hVL4vYrF#|_=o!AC6~q6fnq-`U^e$%&uWt0K1eNMGI+x+h==u0UC1(VsA;vda;!pO* zY#GDE6g0v31>y#;tH8tAcokx1`6ARkmyDmnJ=yoo!LoWi7W)%ucjk6VwT-&r)J|g% z1RD)dSD!q+>B%!+&tTTdqRkOT6<3!za55c$;>iyM0>1C3@tLo>@EnSeo(B5NMYylo zYo@!Z=CJZ+vh8uQV3c_AzyrGw?+7BTskv`G>KBOfldZG==lTs4gcWO*0J&!q|G9aC z$TKZ!p=w%%K6Dywio(9dk_7O~#=R11A-Q>|d^m2k3%=O`Crc^p-NSSCFl*jP8;&Cqi9%+7)*7ed!}u&N4^ zj+J@#*0^Yt*^s`pl06U`)4lb#d(#R+Q;@5V)o+#s)sY4ELG0|r5tDE+Mhcf9d)arl z=4|tx`uWM)0WusvZ|kK77NsE&v4}OpcZ*ub*ZHrr+Ht#P)jBap`Ss>Wf6ko~!6acn zsym9{QUALwyyVK7uP>JzL$jQMD)SwZms}(MEIdnM)$e?T_p@K9wTleZrEE88XGote zc!=J{*i4m&ptZ?BxYtN<0#{Cca;57GVQ86_<8)sh%~uP6p3UjUTRuvCqcch~S=Cvx z$_rSv!puWp;t`YkBXZ@I*L?pd>ohxrO_rmX*hUqaZ=-7%$I+=(rlb~orsnwbDF+2U zxA!D3^XN!lA;;@)__+@Jo#^n>U)r$p0foCBeafX@7n%Hf$A9`ygi~r^vd??}IGWD* zDpiZA*RaWyw95>Xd84KoBdsDZlg`_mbs)?Y>p*SIc4lE#;TNa35P)G47JA1rCUy7U z#m0jPP?*r?X&0aj7l)b8dD%Bi!Fb~z9VS9!qi;g|Idszov!xZ;sq?yX+TrHl<* z`?&C_MmMAz@DZIx&vwGT2?UL%7=i6hLghzUA>Go3KZ2fVRF0!3c`{pM_;7i`gd>@h zdn}Juz8`_J_BqzbHw|2wO>^RprrW$vf1RawNEbxhkUR;7X6of)DoOuy*GlB^xU{$Q z`rUcu`|yG@oJ<7^}7KaX&3#C%vF%B1&G`^_Cl?2<|~7 z#q)pf6quZcH`hWEw%ar)%>01RrZT@0P}c60=T5147X843{@4WS+Ji@ZZ1Zh#`~Gdk zH}$RO0PCxK)=9_JzdfG71-`4B$UY5fp?wvpi>G>4dUVByP!K` zC&vl`{RvH03mKMjs;b`m78f7#0N49U01P0NNF3M0`#b{s7l40mxAy=$;F302L6%O) zr;pAuRcdwn67^Q&$&7D+=P5zhb#{v-kXPx`rtr1d0tF50wN9@#%Gi^O3qBwT<&cO@ zFO*7K7q)-CJ!v9L2)K?1M16$f!>fb4}oSh;v^ujs$8(hGF^j=Pjg1nK&Sso6#CXUJD?zG@Aq&F6@ zdIZ()Jp>E%9&a^lZ4aIrkWgNNObDaf2Hri?|Kz~ zjMps<2bITN4S@W>fwF~?DOn#N4`s>6L6mx{8P*p|hI8bEjj!RdZy|f1|5A$plzjyR zs6jY``(zA7^7Weav-(ve9qtSP^%nar5B@zO1l1qv{?;3=dN1Av_1^kR$s;?7r`plO zv|q8>Oc#1dzowd`{E^1^IDvn>!{pc>FO*+*vuWJl+Rt8=8|XdKH5eGO-1a}A!+&#FvySuvucXxLP?hu^d z?lMjGK6mEaZ{GT$x~sdYy6TtpuhrF{a{sCtRUEUov)+L5nLnOcoHXEb32C+rlp3v` z;qZ}Y1NTIW;rFKVF;ybp2~JZL5m>#GW5spu8&3DjX_I2}Nh{~cOoXGKB``3bNOMK1 z9}2FY-tQK8?eeX2=6@A@JxtFv#O@(t4UA*?shr|rq56wg!|I_RjDJgm+i3s|- zB-EGR!xf_+LrsbBF%@KxaldKi=_K>t;!J?`-MZaa-?K!W`T33(4K_LjB_>X3Qm{B4 z>Up6B8_8EmmE~Ix ztXBlL1b&mVX2c!Qxz%i&+z_D- zSD9*EK543o^_ytMy~eDfB1pb^=LE#&Ly6J!A%>$}^7!8t$U^}Y%P|i!DUt{tgZiOa zMIlI6KTLB=2KGX)kD8(+nT%{!%>s5*joM?B>W~OWM}V@2NVMAO3RrC z*SFvrn577P6Nps%T8C5x@rRCj{?B?(_@rnpXUTZ4px!pp@4uT+Cs`G{=#wCBbv{VrQ|cvCMk#4H6g1{Moru3Q!|lA?Ie&sNy^R9trlY zT`*wWKDi|@L(Y0cp`88h(nq5+6nZtcx>%(%X7GRhZOKn*GO7ZkBlBw6gr@AW3Ahl= zuWOzbC?y1SvT-Q=tE|)x&rdv5GSO;s$Z@GBLmyMAl~VnY;oJWjkHk`yU%upj6wc+oJxb?1lw17gi=_|muVEbJwnMi$3SxPC#0$*hKuaVMri^E?$X&KE9-= z-}YpwCabZrQST#7e-)o?EB|}f-=&)azUbm(fA*(h`q0jPeS2MPH<`*BLM4g@TKutY zj;Djncll30@E_XUhaUxm3B9>FR{*P(u+>`gG$;=xru3H2$0sRm>*6Ln-5$T+Uu^Y3 z#`XOf6OJdBIm?+!5Ec^3fD;o2zWP7C)x6wqdf!ghn#~q9S+BRw({h*;0s*IJ%A@rb z`(lY{(~zEopa1EW_pk(7z0q`fd+@{msskcQyH5MJh<5S``9zRP%>S9g0{^Gv9HWo& zu{DR%vSRj_|g^BjD$b9dra4-*D2mE z_2m5Xei!cwBCpVv;{9cnLnROR?fCM41Jd9U042JVCYQdI+hYz{$(f&UFLGHIyL%kQ zD7F@Q;nV#~57X*FMq73lee@IN8v7Or>hO^9&eidXptf1n&Kl~%WACik^zO_z9!EYP zCNwG*rvyv~!ScV9f_!%QjZYAQA^0K#r-Nl&huoTG?RY$&|ascPXbKVHyIhc-naqrJCTA{hpt^eKGq#%izz5-eJ{EGYn zPYovFe2XIfxSG}Xr-F1ZN4)N94DL1n0r1fkySM7W> z0`$yqq!kBNQvQ2Jz2K}6wt8|10XA0XN*`JdTEBNkF;`|g9W0fliz22om1B+@8JU1|NUa@eCV+n=lxvo<@58Z;(|>jUW6y|djdrI*RN-3xf^_NNPH zRp~QzB*5NAR_X+mk}@~joSdLg7o6_FHVXgBSB-7Z{#DfX{(8-=Qv52%ufYni-c1Qr zSiwUI`)4K~k0veS?C0)Xu%{(2G>biemWt;cID_k6oA*h zwB8uK2cu6*E2@C4qUBZK#bX4sHhF0xx>V(mspN>x1Z9i_8Tpn-M#vUBWiUpv(s}x= zCqEP4VI^RdrJFFAi9c9I%MO=^GO+KI43C^W-Q70R5VxmBJY1UBQWj!FiR4tpX@%VY zQgEDb89mc$@z2*6doOCwwJ5SeH6BdU7k#Pv8s+G1b(xCQgQx#5@{NY%e~@qF#q5a> zF0PiZZgp7_QSJLjY$kN-jjTH!otI*|bPEdkn&x6C#Jp{MlP zJ4uX}rN2;&jBGoO@KdpOIA@Z&pT|9p82`7FN>sZ+?4`+o(!_x*yli#f!At~}Y-mXm z(yx$2`=%IX-_!o+x=brl%cZwPa|o zQU}KwE=&1)Ygkec!!?99Scw626MbzFoL$Xz~#-UTwk1vN5_-0q)z%w z+1YnIdJ4f2YW+D(dy>o$0tN=NoQqWk@8id8k1d?53nO=m;8G$IEepDGjb-8=Kto0{ z%i3tF%C{@t@(V3a zhmwgC0IPvRLA6l|;(fTlsKkH`@(ay01Kvyw0ysEDh?hI%mu6h^6@gJ(HACv*K{~Kj@`X7`XCne=C z0p`39Qw8Dk=Y}?qM>~zMG4ZUfuJ@D=X%4;JeR87G1dFFpKob|aOS~?h8aG7EU->B}Oe{NU1pGCN)y#M{GC}g<@#{sulkc6 zdV70+CRX@U`<$YPrv7uKw@l5<3Mn-{LM^dCVa)eW9?5=Gu(j@re$P0%cA%>i5rDA- zeA%(>(RR#tyALf9MHnUc|7M}WR?)oH0YvA6Bf`h?L?F5|XBkC&4U4}^d{8Cqp~qdx zxPQ;~a=+JU-wZXo8nk>qfE4ZL9;eU!T;Umxn&zO|!PXHvbj&i*;fRw0?&jXzU1kEo$5FdtN<-b)W zR&>K?KBygNX|y}}sYvyP^)KIi=_%Awa%#|g-rzF`I@g1A&7eDg(_3vx^PrsZ&auP_ z&n=_l;jm7L&7_IvU=qr)aV`LS5QZ>w@%?_D$5OIF)}9_~{`C1uSzMRpOTQw&Z|v9d zgI$fb4YokxI3ts6i~u=ai}7D=hqqVqASKpge&8D;Zxs!#=UsNZfwY^MaGUaPi3V*I zcW~{TGX=)aO+8JrEkCBYM|@W4eE}1XK^+NM(~mACZ!1BlXk*^UVM*h9II`1MB_I6q zIkIk32kC9rjV`O}b`CrfHj+3BO21oQ z>J+aI>PtqQ*)MY@@V`d?v!y{@lUWMD_s1Ud$9@t+wy@yKhT3FED+9P4eqbYP@1&gbw3 zi_iV`NE9#PpF#In*PWmj*a|sE7GnT;LY2UH*XO zISf85@Y9k$mhndOs@o?@{kgV4&b@RU2%^Og3$}P}{Y?K}dI16@Cx?1$GuEVC^=DJI zgS|Tg_kI!17#TZiB*~x>@>PaYnu|}OJRGB{zC)=d^zE7acIf!FEuCA%if)RE{Ohhi5bP>PLm&13T0XxJ$62O-#Yg6jOe)2}6&_ zuJ8UB-^23ninBbPmE2c;Kx0`$TAp-l2`Iwl2~2>Z7s?&@9{A?aHv8&PEwKq<0J?i4 z$^5SVV09D=frZB+!j`*^I;av(c!qmN;d$#cjqqj8qXu$Ae=n~U_m2Q@vB*lW%w1aI zkhf{=QKmIc(xI$YM2r{@Lz=oKK8na88xjJk$wW}Ub?}mntNapB!GJ1zi;}`}U;{ z@tj8561AQKRiUzEv1*4B+cY*>x35Lmv&Egg0zg4%R$IjnMiXf)?WBOS_hZ;WM5Az9 z8i27EfTI?ht@-T0+Xf<;Z2{GFH}Ag~o+WhqsP`8~hUjRWGo;x8LKIir_jJDo@V(lHAL>97pqE{hPXw4>@fBtrkq8x!<4?Zn4v_F3pKgHE1bm%P5k6&js) z!LN*`^efkpQi8Iq)M5}6hqY&pcOBOP;Sus8$w@5ps$eN>34A|OaRmgDAZ<^uIRc|E&R7dW)(H8)=Vj%>1X5anZdN* zbz+2bC8dc9RDN?e8VdMpo$pvNu3vPNLha*4j37J z%2qFnsdz8Z3=Xu8k9U4>WwOeFx~}(SK?k#r7)xLwN!-{1=JzS?cMDgsN_x}%XiS#{ z{3#3XqH#hC+en%0*rr+%^hTSkFAuk6r$^~ET_ZFCJndP94Wnk>3LWKGo0f^WvdQ22 zBF!#k^L3l_B<-N+&(t@c zLOtOl{!Hb;9VqtdQY%uzh*lz+65vTwHAi%NaIKGbr#tgMxrrahTQr-{sJz0o=$RKU zpV8ZH&>+F7{Nw_LCHJ!yfOa4`uQ5R6(|#O&9uHr&EO~>aw9$~YZo`@ZgVrPV zK!ffsoK-n;3O6VBGku83gOWRwDVmjDANLD~+UOaT5&p!NjPdfC&|zihmJF4PxRjVC ze9uhFS@TBL?sbWq2JtB&k9IeEbHpKTPxu~4>4UEI^5uG@mwr;j*w9?U z#jhgL%Pw{+36YF4+#5OS(f83p?%(4QO?GEI;Y8^JA!aVqk|w;hU0+HBty=efs2bv~ zdK&*p10PLwb1-)lJ5~B!({o~h(`H9R+~&Ci)cFnr2UFCj6%V9R%7*S)#pg?(Tsvr`}0X@{<%&ZlQ-Gra~sQrHaYBY;e7ewiS_n3 zwL8oCpr4_?&if0)3b9nv^q98bR~L?pi&StcfhZq~RYivtJM^@RO$H}7S$bB-(yzf^ zJhppBDByCM@)4GzG2ylF3BV0X&ty9;{FbVpJ^`(#@5t2Ot9Rs z(36dyOb89uqvIQso=n>;0Q(<<^=q~dy=F5b=R0zM{EC--($do%vaHk!$CD841oQn~ z%--`sGlv>3zE9I1;wz>cG0$m?;DcO3)cJ6z<~~0z68l85b14ixp8$fzC^CpVFv9SuCVPZVxOLNx+zM4 z&q?*Q)2J)LLEa+gF~u6h<2NiIxvD_^5#xp(Zi`&)w#lz;kQZnagSCL?pwJFq01qJV zK}wiDe^yGd={`KZRLCrMx8fNqV6*PnHr`GAdjs*l(Ee1H&fxXUJukp1vY@tIwclB$ ziIw?Lyd*# z3jR|C@OcK%Us5Hnmaz%v_0WGH&S_ZSr2m0_kM)oN8(xE=c>6n;R)|Z{j_CYE_``-Q z&U$qv0$PrDsV-i7bH^mQ%9OohC_;lKHtSUOro2#UKx=8y*(MkS>QJ_3p>GPW+__UK z(?9@R`S;V?x!;lJSnUQbmZF(f)j9zMa~@sWX*;HEZ@#eXt#+(%e{k)QJkNE4Q|p&j zEUR;OS}&J+`EE91Lgh2CkRDW!54!;Ntrg0D1W(z0ou$|B);z!^(uWV5eFrZh~9*Chfbm?7W(3I)Au02`6?7zvtiF{Kq{b=G044 zU(~OBo@WdoBL;;X{CZQIl!fKTqAv7W0?yKoXIh+@9?lrarUcU5n8R*y#6r;P_wTsx zMY1~>h0rQTW8ZUYT$Hx;NWew~&Hh_w>>v=|FwtDRl2X@)EI|~U-;>)F_rycFOU#w| zBe0kEvn*Tv%cA8w-Ie#<87RFuM+De|HHJenWr(jnjS~&Kle04ox|6d3hm&)Y{?O(b zIvRvX!SmT&7hVRwjhHz6?xPp~kY(HHdfj>ypZg|6Tez(w;<|csjZrW{`8QiZBk-`-Sj`i9pw?gLS;W zEOk3Q-TWwLB0Lt7^1sjnFGAxIqzP{)_yS1j>R9h7OBMEoZ{xLvDG206*&$)bQmmmUl$S4ejDxhWx27I z?Q^hnE2LSyVN(H zj`;@HBds`_+7G2agZbV!$S-imiEP&}7tI_-k6puDqdopG`O=EX>eR*CR(@DMI9<{b zd=UZ+Q+H1fQBNkmC9ktPZF*92u&KDpLWldkdK!OSW8<9yhvsai>1?>s`Rw!3#!@l& ziC44qo+b?5uMHIN6{9|<*5p&9*?g#yQIiN;K5sY>;#d9WitWUX{f>e)Y@?~8DU_x8 zl$;a1Mx%FmBWIk&<+0UIV~MX?(SgqA*zORsd*bY3cF6<3-d1R6+t??+@Tm`$u7!W= zf09zG*bHBAJ6D@8OPTweeofr^l(CsTijnlHgg;jK%(pR*s5ZHoKR`qBh9=yE^v_}$Polsy13XlYz%T4f+b_{e0gei{WsYp20gp@B~Ft$l~$xnM~00YkK17>o*X3B z8=0>#^P^?fFMN>mm4-6)u_UZ3Laew*v(~zkGSw>7~6x)kPC9?H&k5Q@<&(bn%=%XN1&!O?kDl~!pO;T9R)cB zzmWJ-P8w<))$RGFH{N|^Ss4!g_038@78Vu+OrVgN;!bK>+7pGzn@0o%awsVvOxq(7 znY?iPjmyezQ0naJjRvT&^%_Yf<0B^`<&%#SVkUFT!5V>&)0h>8mLNzdKoo)|B2~&G z4g9!BMJA&P04_!nC?$x13-O|F60jc^%w&>^&_n^GM|)))11oa;H#K1x42;4`cX=LggdXmlGmf#hT4a7i>@NF zVhWXoim4gfWC6*8B69q=4%$Pe8#P+aW3r z|3;ZgQ$5q1%r~Kd12;jhcR-n1p}yGhOTkvy~U#hJzl z4R7WEqZn88<169xG((O>&X_ahvihTFV*fx{i>s&GuK5keEXPsTVd^H&9#Ha_6JN!Z zv;O_G`TQK8O}+8GCvVGgEaGj|1x6wG_LQB)xUf4=BXfMCKcJS2!*hn^uD~k zrE@!)D^VVO)7{B)Vx+rm+KTxZ_R&%4dWXBSxQk0O*QRYueLkIj@2<7m2%bPw;`D04 z5HYFIzh1D(DBrWxz>XZsx*(w9SSnLf9~l;9_ks(YrAeQ2Kfaurh=^#xW9aLQc1PG3 zs4*uEG)M1MT{^v;Z3TaZN+3JiJb?+i4hj0}hmSw^)@tlkVBJPzZB=SRuZOo3z=g#>;Mw_4u4k~ zj-8T>Qu_x{!od}yqbE5^x#pM-M|>sJeK!S6sRS%Ga;F~u;NFf^U@li9^$}WHyI{Ox z#~<_LO+NHd#z=*XUGoTeA9B=}<|N+Kmmt2oE37vnqdd7F?SILJ)(UPkWie&^Ms#jt zeED@ZdoMa@px+xyX3dhk@Vh~>$b)W}J$`kbVv6k>eZZAuTb32NtPBqr=6 zBDVi9anC2m}k@{bafIeN%0E)A8CC*E0p*K8$AaKF@45 zjA1*CSWxdOqQC!kbR}REQqIdBG#n_&S4x)00x~q3_P$eL`Gi>-Ar0O zZm>To-RonqUdTRqfJ(}3`UPQz7n3aZte+xliY=&k>^F3RFPymf3u2Jykvv3%Hoyuk zK0=d6+7GHmmxOu8=xNLSNw1?E*B@(RDn#nUa^KqWw$@wNCl>u&62x~3e=%xEn06rG z3wF@UcsxS^o@o^haUCtVSV<0h@Nnn;LUcNAa~BugPKx8w3nxfc+8c{YS(pE&iRNaHYvJgEGdPhFYcE>%wDck+TW-_g7Pf2~$B~eeyen?Et zVG+Q6ZT)8=!u-LQ>CL8{eCVXR+CHW zJGlE#26C>=fVyLq?`2wO6JtXjMQ84Yb6y1bjLQ;U3E4wvEKQUB6zMe8k-k z1lXr@#`tLH(zV3K;z+R=JJJ zH$w!awX3Rtw|v|hr%}ZlPV^{0(M#sHLz9&6U9{uJ&ACRNWt=c!t(ZpG3yyXQz@NCa zzF}MTd+UK0y87FjdT{rSWp~s|VUbWk=xpZfq~vs=iH71^^bh#I&na@HvI2qBTt+Um zn-UOfys$TLJ+lPUlssmk4zfd8I`3ivYu<4i(0D=~WE~m6 zsAvX5ut<9F0@&|ciZHcZ7d%ri>Y*e@oPS)A?!7&V{hF@Q%rnX)Sdo5<)`pWs8|$et zQKnUIfz(#y*quYfQA{adgzsawT6-`wk57mwP_-sl>9F$ggyOYv6SaH{7d+kL|E2WM zxN?4(A9OB^lIt6|{kmEao?=4<8hzjx#v$5kIc7Jlkl|$I^ErH~$I$@~_k9Uwb}xNr z#~CE-viv98xvbI?9>vmI-sCT>GvdUByo3RE-VDQF1a+qrr@KP^(uW%1@LX0R@rza^*AKdV<0u^C(P;FBC&)K6e=j>IfYR@CJZvnw%Jnhz-&E7ZMVK-8nw08tvYl># zuG)p&Gn&yq#{SLL^8Y>yE-`FL>~lXw@Q2<)@vSb&b#kCm21mh21R5K|(>K(SnzW1- z2M1}xZ4`A3qwHuo zuIS{MFV#+^`3wHMCqdW9+nt8&uQ5ec>AwO5ETXvm`H zu~Kh)jxs?o=L&5~y9{K;CR$i3iPgWD`UqRB?*g>ab2L}ZQ{(y(l0)Mpf0AFSDv4Em z9b3S6_Uch~9@|U@TG7c_2IoF3n_d!_y9|z@dRy-xMo~*$qLYYhL8wbUHwrry3(ym{ zNEyDgyy{zy0nXY#=V8u*XX;m|#_VTw#|Z1(bP6Qi(pWIa*tUF}>fx8e6Zpk``+^)N zc%!!(Z_iGpN63WFxI!N9`=L_q$=x-l!<+`}t8OswY)-x7vXSytjwBD>@WMMpiD=oL z7n}$>3oOe<^IBf0s%dP4iSE&t=GSx~*PneWF62NH@hQmE)zzQe+}(@Uvq&dK7^OO` zR-@A)z)C2bA~yZfq}um3^GjCF6;AJ&R?v)vAO*T;dvXgwTyBYrMo6UJk{(bn3>I8o zEX!1aixAP%zVP*Uq}*~nn>d_GrnH;U8Q)_0r1Ev0= zCM?G>yxJ;$zqXFA>-wTD?OIUy{&sl_d!Is(`?VRI@Jj&_&7Dp-seC(azO{`N0-XxU z5d(wY_VeT5wp?`lKH(GTeU?`qb2K$lh>@*@x86x8?G27p3X0E$*PfQNXxN`hRjzT z@BYUNkw}0)gX+vtwV=z0$ndpGeT&HQhWh81i05H~pI1aY9+(@9KH+&l#aDb7Yk*r4 z)^qW?`S68U| zl%%5g0-2oRkD{=^hif_kvkW@d-!)b1Dr^&pFU1~(GrfgUVv_Y?FZ>YAm05q%XV(v9 z)M@XkFu+AFw*ml~Xv0znZ* zYAjUThb8IrbL*~83dl?-am!dZW`Som%b4?)atEW&6gD zld)9?2wWJ8s+8d_;_7Q7iLeiNCzx12 zce;ynJAXNTMu3K6eJWrw7OD-gv_jjz!K|5USbxCh9#kpvetlrR0yL5s?j9aI?z31* zdOSJ}h=7;eBFYWpUas2cS)lA)Pf{Q53{5IoI~MItV4ZuqWDSTkBi0LO#vrYsx9~X+ zQ^&NQPS)r$WM|r8{&C4!Fc=f3zBmx^gZfwNgV*kDY}k+x7EH<^;%ih-?H?`UTz7R2 z_CuG${@lQMu_kPL%jsO`g~~emYo;P9;}$fM@u78RjCiRKdTYCuMz!WVx7B-4F(jN@ zd0UzeK1*qK2VTszgJkKvEh2CxR*p)2jKXt<9%oD1o-%(4nD21>uC8OOX z@80mgjICID@l3;n_P#=kO zu$6yXmJ`;!lo>qQGeq3zQG6(B*J?Yc)p@41YD3TJCZnWyHG0HOWz4D=4?GV}n)GxH zxpB`!SVIgI32S?FUQsVa7}Jh)9%u=besUGT&t3m5$C52+ZY#rdepVzYL3a_Pe2uJX4;Q zV<@t}zi%Ce-Z16>154yB1wJT##9-9W>zTEi!eVeo+I0K;Jcaw)Wnf%T#Y)k&w#GbM z6{B*1BH2fQOPzakT{Bs&_Q_JmRUOO(Wgms(CMJ81f8SoB*2=MNd;5xg1Z!$zZ^^hv zyzVm}dMCcObHUU&0kf$bXM5iHITG;{wvF7=cdc?$cJ7`p0!*QF#d%6?UeL)oHkm?# z8SZct+GjmI9nJS{29Va>PfV2r9R=)+7+vaI?PtAi>rW@hw3a;Iv0OgE)r6)oW3KND zeoaApXnRaPV6-#se~cVv?4I`<@5)^Fcwu?#XyhA-O40NMT;VeQzrUsZDRt(L%3L z=P?x)Gq*<@2qMDHtLXF77^dz4YBN#5Q#=qbH4hrtgNgMZES2rAt50Z(caTK z#}Vz0Nciw_T{5w=zZqIsxTl}H03VyD-4Zax?p`<0$O~7#){&B{54P#`;3{?2Q4R89 z=h(FFYBQ01sVWpRHu6a)`QEgxceY6KB5s+49RAd@>Gb!g97^<4%^lrDsx#lufT>2e zW7NbEp@6GQDjZj9X9Zo>B_Dg^E!qI8j`(VCLT4*!>A>OkIw@|tagLXrKU^ugzfI%C zPGCApI=O1>@WD{jCLd;5mtBl->lzL(oX}OOvYsB7P&tsi?yr7mPD~puk`EI1s~)+} zQP;SNZgy#gxLYp|so7SJ7RY3yH3-GhDt>$@CAkykq{xN5;|JrI#dS3CDtvf5DttOgo12Q% z`Xu-_5#nC$b9!C*=ABjKBI4{#e>&>tzz2Mpt5N)l);jCZl-rYb*zMWt z-RtFxhwrBrh1lAoB&fjwxTf=r4P&B!9q*Cunqh&YRqj0CL3c z*K@&it8?WEcXR2pbldGl9c>$jES-2t#H+mlm*9pe))@IOGOEI{2d$7{ljSw&EGA)0 zc0VIFa{kMM_&HuyI+KjQ`hPhcTo_^oe4ll4ESjI0PNR!%+N@TOgITRS_5McUf7>5}`+*5qGdc~(`ddNaX* z4wIL#qo%IjDPHD^+&h9SA(`Bv^Xv@rbd3i^MCxi>>0Ryq9!r78-}TZDgf5k97&f$e zRc>kytQJyBNy{|Drv*0F_>8blUYRE^7JVWIF41C9_A4~F8l~oQOH!eT(q3d0_koJa zBzFQHrxTC)GoUaN9v&8!dAoHglSlP^t1q|^SfcGL%*=|<-M(O!)XJp}Z2+f)YO~p{ zTD^6ZcE=i9V&Tf^+1bX};^Jc5ai`s$Wx02Y9xE7 zh>4jQ7Ms)NPugz6n9T9sp3(94_V(bwXyQ7+53-~AbBz~}BYg5ZJ}BDSABarOuxl~$ ze1GfU!+^SaCaWgrzb8%^>H8ak z?sNqH)<_;JWFbT`G#MI&+26$oY=dRJFn(DAV#+l+CE6#y_jqQdVj`j2 zp$Ld4Ggln$(%Wcv-D$qJ-5H$N;X#k{a>~BWa5LI8>g*%5tD;}O{1%Dr&1MP6fc2zP zIA2vW!$``yNATW@er?-l9xxH1*y!t zU)r%jjapUi`?ym*D-TJTaX8mkoa{&Swy3HnjLqXxwzZIWoF$^~{ihkbpZlw&n&xR_ zZF<@KmbNB`DqL)u-RyCjWSL#!D%K;1=i5um;7~My#d^#3X}>cq>EP>F^k=5i^SA`j zR}8*;cW2@0S3`BGLz(C6TdzGdh1tA}!Bh;rshSdlnVqmMenatWUtL0Mh{PGfYCY~9 zLrN%!sV@>&{2qM*SWa_m*XN4ll)IZmk+3qH@-zf~#2W^RY9wnR&X>O`lPE{VBw!?{ z8YcMOso9z5#O-zxW7(KD z-r!MVp*0xsAA$x3W*1B(J0#1(cigt@y#1t4Cu!2@ArMwt0#m)O=B3$U32Lv$5;jg` ziINR7wY;L6kObCEVta~GKKqsz`Vvfx3f6jxrmE_b?Y_9j0e&gZ>_pErRhaB?4U%a z62J4T%C9NCG3SN0lOZ~)4Oh!NrH|+rcnpx>I-m}`Pdsh4dwx9?u7ApSgDyFT!tJy_ zpMs9Vpj0hM&#N=_Q?wZ~Yvl?78rzwl(k)Ha#QKR<)cz^*_jGhSqTANaY%5<%Db%1e|+$cHv=Td;4qO+=o5Nqx}ew^N~!Ki+YSp!&O>Q>SpcsvpF z)opN>cgm#jKHqB$f}I5G$EmubE-`Gn8L%xhRDaYL$$xjdhU7imbt4IQQFzIdPSERXc8IXIQBT({ZpnN-O_v5e1l zk0|8AvTO#a;-Sy9efWSXM>8I;bXl3r0u}bq`So^?;Uch2IGl0+nt(yyY2#V+T+S=E zrnq9PbG>ipaPSvrCqh3yM@^Z+i_0Z|qsSz&XQywQ6=dn4og3%r@_Hh@9DwPq^v~st;XRyGgjiG1^o(w$=`Z!k zG0#yA`$&<{R=}h7;Xl?iQ+Ov1>*b_0Fe@N&&viE{7}XS=N=JB`2KqRUxQmt+xgy5l z8I|qhi!L4}tH3QvKeHNzOXF&&FVz}JA)IxHMdNEcL-|K>|L)M_Z?V%8M)kgYS)2Cm}7(6Zy!%{Uix+`Na zcT7AKq?Ph*K{y%7L+OLfNz?Xo(M)KCYN15pYJn0{khNzY5U_g zd(WZGKUP#Iv@&6v(b2($Ot4&MLJ{*=;McMh(k&^;8)YouxS0{{{l9OhPZ(iDzZm5joc|ba+l^%avS*Hwt4LUBM?3* zA0C%o0mOZl)}-%fRBQIft-<{w3(4QZRd_SJwS_pAaaGq)$NQMP>{&!oKcu5c2z?=CY{2l_oa`K zwxVe_H!Z>Y!%12E^G6q_b7y#R_dUG8&H*3sv6zzGbj7tG<0WJdu-@hiB7h_-e{%T+ zo|5rmo=7;!N9tKDmDz}$-{YZL)ztJ?qK8{4QU%}Xd-;*Rp=8Kh{5)JP_(YrB#H~9(ppH(Tfp#Mx7 z1H!-OIui1=u#)n_UDB}yDJd!77r0_c3i+auWk*3AW`HoLbC#s&P<$f5S=W4t2L>#* za^ctlcp#-px9Mw$rdp+4k$a+|z1*j@*QfJM4QhS~2}qQ~n4kR@s*zk43ok6e2;7(O zKa14-1n@-)z(01-e=v`a(}EI)9PmJp=hlRo;s5VP0N9#7d1PszEg67?F~r=PZT!3M z3jjdkfH!GwX#w*04y#uMiS$}eTlr$qxXQs)>0^hD521iEEaMgrB+!dmFtJ2vNXYLk zo|y9O4~riwNN)#Wv)R^wfPZ^L5;&^>T#2N-{ge);?e;g6Ghe?^uMfk1qr=^Lo297y z&u}aWUGqI4!Hv7cf&B46Ut)>eOtDc`Ah%XfQc}{DLA6MLiD^3S3``Y(wSfh;lmCtt zFb8`tFE8T|puXka?{CM=X{en$Bd~`7YQS@-a>W%DTdyCM%OJ~4=wz}(YGbI_P8b(X z%-*YEl%M|}#=a^nj;7rZ`l}bk$VY3ldxlm1P2BNqF+mjz>7$Yj*c4LgnI*z zIt%=*@y>TfpPHH)uT( z$*^AQEc@>}?eYsoqwaEZ0#hQ(yTMK=BO?PnXjvdHS~o&okn7%y`p=9a2}EaF?yMai zYA7x|Lgwi2Ly^NC%*4gT-~9KjUOM$=*(UyFIv!^5jhGNZ8dy;?EP^7V-pOewZbDeU zfG*JQHHv;meL?i$TkOpfd8lX7P4fy#a(h`-e!bgi7hhd3X^Y|XUkk!yYz&gPaj29d zzgO}S+MSm*A&BW8L_3Fk%*_9DJoJ-r^`P6j>{t;{NsFm6jk^q8sb1TdY~ ztAvyYGxo#22q1pvhW_b(4s8hIrCVo)>4(=E#-LSC$(O-%jrFEiNm0BD_8mpl37Mvt z?%?ri#+&%fkb#!PvQ~d|^)LwB6Jh86!M(?OQ@y zF|5poh`t4wV5qGYcY@iorz1;N8IKt=7jb{IVr1URCVUvl>l(Tl4BxyaLTsM$OKdgj zjR^NXeqqO$iV-ez49Ov<=qNxt(D>GOQ$$M!u(~yAtB>dyBc82beqVUzBA> zp(G;Od^?us+ROyrY|$5W`-EP-Rhp11Fl}A+6BWh|^il#wm@*~Mj*ib3+(tO~&E5wh6y0@~cZoJF}$N9+#4bil&bEzuiS0e!= zTvEl&aAT7|;rU%DlMRMuWmEj5g zQ|=8LaUZk+Qo_NO*2bG|!(hzE)scvA0ssoMr@4#<+q-qWdUi9L-AjTrt(COeX7f8q zsnF}0ts>XUpt&1!8Hk=+L(Ps*v8g2C z!=#2-%G{k-R@ZL&aPwAgc$je>K??j{abupnxYx~Q;3p4X=JYC5FV#a{q%$_%GGkQL z!6w=n$q(38jRaJWK|My=spv!-Gg{GjMk}^&Vp7hYcs8CyNr*yt4#)V{*P?R*vt|-7 zy*#pESXo&=94+_s^w2IF2?_&MlAw}W+o~CB=|$6ir#w8WSR_1-pEczROUDvx5?;UN zFf@ zQKzWU8bI)A(IqnU8(;%fQK=Q~rrvaAA7gpzV11uZr;b`H@_!ZhMt3nSgk8}&d4d5A zaLH~)z`|D!KotWlup1C&3_rnzBeKC9GV!UC;8#T+`1^Y*T}65&+Zwg6 z-8%-P)Q|>r-CMG^Jm(*1i538BX`#wU3g)N<5I1(aK@5Bt0%UCz5&{2UA{(f?(ZRa9 z{FT33L*9BnRN>H2_oQ|1i?wk`>+E}Y^&5{O!^A`t=yPl;qJ;ork<*Kd;UPOb65QW* zX~Kq%dK&auo#|dIdv`a<4`5`7 z{PgjINd$z1$bF8#^|x6K#nW=tJ2h^m0=A58cLNlA11KVf+089mOIW4PAK`=f{%py8 z+%emYwzl@4DL_{nWBCJkV&YVW!ygFNZxUv|{Qaqu2OKH2s-H#hg`K|nzG>hBd%wsdrK!FeBSgWyRi2bio@nr*-!=Ih8|8X&;E zG(3EeP^q;rQfbaekRkb!VtZMWl;=;6ZdpHmys|KOF4yTQq|s`9AWLU3cxS&(GXOtr zk!P_~yFA86;ivBwOwt47v&bu=HigiRQ&LiL4TSkszC{plamwHO`uJ2=*VIHP0yP$h zq=*cDa=M(Ui;P^ng?_J`tO&R`X-O;=uIi-)m0B*7tVASw!0Nu0bV3-j(Z@M+VAfhb z3C9P~U0z(+5B{;P#3-!ztkMH~(+CuofT6kvzVEyV4U9YpPI8kK_T*<)T;TG0dhi6_ z1KQbh=O0jsNJ?H1=#)($hyCi1F=g0+QT`-W+$LF#v}^_ysc1 zl(|j73oFyr-u@|Dt;j-Iiup#;1>VaMExhQP>bL1xDnyVxhm zt)ItYG>*dYdqjhK;CI-8+&A2kIUvluH^Ail$3^=6Z{+MBZv^=^&;9;74gwJuX20S|euuuQTmpNDlfIDDKgEnE3==T7t z;N8m#0CGYuxBtUuh++g(JC^2vQW_EfvKm_53t$hRt^y5#RcC3Uk^2DZ2VB;j$k{Ex zOu&~+P5PW?~tLbE_NXURmt(3O+|Q}D8HfoN7bO`jx2 z^z?R_wOGX(4?=gF+eh>4^Xgbj(x*XEJT?wNyiaT|U8+a|HvoNE0(1Gv%8E$q(XJpV zS#iYhrXuIxcq>}9Gnv=|>Z7%e^5`l=fhr1s%10z}-NJ^q!HU_drX$N`pg&@f`A;5a zbgyonkH_4#Xa8xGkNZrg0MjZ|*kRVkf|34kS~e&gayi0ayojVZ6r%0ui=%wAeLN)M zp@P%Y67A&+>}|k%xW1B_lEQiOo~)(9o5%c7u=+MyaSM_D7j8^3moze=WDdJ z=wo^7Hg&%eowcCviK|Y|gAn)w0@#i(5{92;qMuLvU8p*bbp;YJ zl+#KpR<#-l5D^15#OY&JA${p_`TDvCmo(nd$f)wEs!$0%ajV}oVvDy zVPlTFEI>{_w9nN0quEpUost!K!wK8#@p3Az=+QA|5@nSX7cI(2?g0B_UF@wZoXV+kqzv{eqd3+7n%V ze7k-6Y&B*%KW)JTeem(45e|p=@sA2b+s}3WENL1pKNef0Ix_gg)RX1V)Gr0w`v%JU zS<%rl2w0Byf^x%qZ_avg5tA)@%jOv~YiKL(jfJM;%b<5mhfcrv`OkCEj~-ftWNydN><^%5-6dQ z4(AwSH#H}1XML*v7B>Vl5n>U{RdUHt;P;9tt(OE}q1R*)+VD@l5cs%uAAbarJmqO` zHhipr5yibaUQq)gxT!LobIJciZaZMqnd0$-`86rZN%mHHT)(JJnip0!uN@v}94Xv? zm?#*wciI!TVoei!v^zwk%_yD6z(c14TWzDeRWd(O3OBDd<$E1Fvd(xhS93fjH0~8v zNO;xm(l9}pWQuo{nfRNe#X|-9ptBi?&tm#Z!Xvd}lfc$8un$7!os3adgW}S|7q>ai z-aTf$C|zH6nV=Xp%R!iWU^;zRmUrhk!Bn)c+<-t($8YZlhY`|vTU%RBt$m~!o<>~E zi1v8cbER3Jrk-MSHQ#0QR@zF;c)NLVC1Rw!-u=Mq@j8U;?eU+Tf+Xb`L6>S;Y4O$S zN5ls7!hzfo0+v6kB*T~14!y~w(GTm{Ak`@?mzL;kxUAj__-k8pL z1kyX(l~YRA5A)?@{dn%Px$z{YdN)L?C8#2j*f-i$y2B@6C$?Dxldp6Rxwd#`y1NQx6Wh5qTJj|k9P(dFnY`1?34#$l9SGbHYOiM z%A?B=93ah;KZJs}r1z!uIZER!{K`Os8&8=I41p@4uO;7Wh8V6Y(=>3sL%5FcYvNRq zK$*gysq{PXm?m7Uv+x&qOaE}khU!GZBW=^6c|)5DZ&$%$7_k&eKf6)UY@vsN>xDd~ z7qKT>*FvV*x@f|(YkV_WE!T1weIalyLWCpef~leif*D*~`RJQx-}!@)16#w4EZjYU zJ=(#<@HZR$^)nl@VfXqOpGF1!#a0)=VMB7JOFp&O{-v^}CgRZoVSW9^dsV5~2j4JP z_tz_xg%sm?xZ+kkU57T1&88N=xrcG}5)Z?SCX}5F^!+zpuIHcO`)IMN6KpAZ^v@R%WrhO2|T-l$u z5gL*Rc^)Us|MfP2>Rs;E)yFpX znkis>Cv}%d_ozon`&BGagf9ggYZ z5U477rqNitwl8{@P}PwUCs%h|@_6@jw?nDwI`lJRLXu7DT31%D!RO0g_MuG;mnxWwH?>Zmz7G+_NSRaUPq_3c4;N*JDI&&qd`mRc zA)^|po!b=8S_A2s(_#?j83Lmr7L~0(lL|AST6x7T#kgs;?|n?3ah;dUGyQC1h%y>i$I z>!1pFO&xSAWNJyl71(P2|mUvmc6lR71}bT2i(TQ1-UJOBhZdi324M z*z$evvyy4rdw0A>bw!SB$7{r zOE>Y2&-_#0w}Tf;2IDL+HwclwGdh;O0Vi7^3qMX=+haqyP-Tr?tL@52!bIMzPbPnp zmtAm13!^;ncL$k-$r@}9*>;Pe4lRw$MZ^E8n>H^?GENhGbn4A@1V$48Wau9Pqe)dc z$&_j9uv=VT8OQ|Wax+s6wGYp!t=;7C>5#TiPcpvJoQ(yDNrO^67~i#Vwsl$zdYZSQ zLqRHWZJw;j-MwVroQFdd#13YT`USG_WI@qcnRtKh0Jj+uan8kk8+{T(i>OV-Q?uYIeF6w`smfzEc+bfN)eo`%D8YKOz938JQW1Z1 zWg4kl`bJL3ISoN*fY5jS-ipHU{cTf?ud_GqM^>(27?|Ys8Gp@ug;jhk z{{-EkE4rIoOSBa(&xrU#$BpSi+2FI>BdZX(AwPy^PIKztNjEgbgH0RLD~=}~ns7>^ z+`M^FAO6^U-j0GY5WMV)q&}}W%Xz;gc&VhmkfmP6q>QC$)$*k{3%7O^N&bD?7rM2^ zdX;R->V}VRZC=;MqiK65*|4m_c}sJTW}=rWXRB{jS|={hi_h9Rt)Rd&St65{cMXg3 zex0aq-kBLSYFEMeIOMb~HOi4??aLJ0;lYX+%x?A*b>rTbuAHXobCkuOq(LV)6jC=6 z&DFUNrJbL2)ZCr9a5~ake>ZL@3h3H1b#NG+>X&5c&WFbMTI)DuXHp{b$_X$L=NWpd z?0E2_k`-q$9-X~kF{FYMZ_ z+NHKn{n%41)Qf~)@-C5^B2YKUBMT9ePvg0M4@l9!h**pstiQ5wC zL%$+2wtgs0tN*ROw8r)-N>FAr0VSlews!ltaT(*T)`(nuIz^JZq=QCWZf}**DE$G` z3Gdgl(zK0g{_f~p-iO>m_bdHthRw(}UA&G;{vmkY)W$OO?mvqT%<6GmVuszf2W<^S zdK}rU{R-BuJ|;p=Dq$0YX_Q{!cv)IpycxO?PodbDegua~02I#t^q0+@gk#ag!-EgB z16JQ{v#7r_wcydpy2BTh6m9D_aI-zBsb7|(Z`eY=TP6;9c1(K;hz|wMW4*R=?ef!! zX#wZDJ;$=Dbe)+djWu*k>7bPj81Nk%{PdC#)?NArK4yu!q^&bs@w}xz`U0G})=~9K zKL-|}vR=^>49<0Rt+rG#_hQu1{CZ|(o72QFQm*f#Ox(Vgdt28Ox$lU5J-ObttjU~E z=J|1C3HpTk*Am?t5QP~?W-TKJ+~fD&(zkW8^ik$?dJkyE%*x}5*iAD7Mk zYfLV2+v~3BSv71SC^YqUhi?+e!$?J`*7V5xtE$-cc?J{!CFo4tycyn2){l29MF%{^ z=0U}45glJI6`|t&xQgp#$=65jzq}gQ_tT|(;DGE4rEB}AfiDZWf75A6{h9#o<|Vbn zSg8QuV?K~}{Obq{2driFj`rHs^;MdmW(LtqJ&3(8H_tn<~aO@fGeJ5prKYu1= zV{4n#>-2tel~_vFc41+`V*}`l%ho{54m{A6qW?gANT?_0=8$)QtYT%6`3#uvrUp=l zA^^KNx{rSvXao=?*g!fhTH~=)L7=&rPVK8ZYuQnx7bT ze|MMv3?F8o8S2gJROQrgPjX5y(w0?I1Ab(s+szBz*{pV+C ziGMDNG$##4Wmse^Xoe_an5l?_rhCbWZgyND+Akh}>}Y&yEDFQQ3c^JHC(EanM!;Q& zV{XQz0rK#|2+3h-E&om*n#5}CayI-(j~Oy&?yxaGfG=TDgAy&zE+4}S+Xf&dNpRuX zzBCtNystO#(!Ym{$8saI)w>6iRDBO zhpX0PDv4Z@aCL#C?&|0dy=*~i$*UmlwM5f@2S)$%BdBEcmCfGIU!-Z8$#SvUUr%^9Sxay^nuaR;!IIS9-z7I!<*d*26k?c!(!w@wOOxA}!!JRAiyR_wV%7;{! zFg$@3x<}Nld6k6Q{9=&?ZsOJIeaketXMS3K3~JQitzWYJOX#-{mLs>biiqLbvEt<^ zrT0ZG!tAqMC0R>z_|>{G5|=A=R>)`%p2oZTiW02lD4+PT3UlbSq<_s(fpm_D(haqO zO)GOtXIr~xKg?vKbkYM0V}Nd6s}sMqveUrGLW`}mCu1ZgSjyAp(~lY}z0PJScV51$ z{qG4Z%&JZJyH$sS<4-f9X+3W@cesD43LmRpj()y|sHdvAnJO?NWq ztI(z|b+dnNEqbfsks(dca&bB76W}k@TEKKWKB;8UoqUK7;cPv*ZR{EnP`DRZAyBIo zkLYn5rbcuw|E*OpV`veWDj&~!BdZVTpxR1LX|!j%UdgPS9m!V2(eM%fX9*>rI39xo z-udEh1dpP@BfJawHBhO!Tw7*lSFE<&orh7WFtqgC=deYoajDYa-dPt_AtD-f=>0iC zagMw4x95qDMP6LpRZA3yB*o1^+o4uADNQ3jh|}tu9P4j<%QE!ZCFe{p4LDc0O(Kd- zhx6p77XJ#jhlZ{E>8J1MqR!Ti+8qtvj6a>+Pw9yw$tm@c8!X>-vvFjs@za^8C`h@j z{>Y(q)0v3yw7aKQfrUM}olSyw{c-WMd8r4?OF50_f-hcJS&0jty2Wlr1lPyiF6sgY z!GB-iNb}6D$f~ncmJniMGHG_2yYBWyMteOdFBQW>_{HE&0ez$ARJVvw-J1Cy_hjl$ zzX~xesr!ngWgpzLr1Vd^ zEq_x|+Tuj>65d=G2&t2Gqw}74tq&ge*%IY>L(olm-l7(^qV8imDwR~0hO_Z2Wh?97 zEk)SzlShFEtYpKl8aL{| z_(AGB>pSLTITz31eGLBtf5KC7-48@k%JoUHd2^Nu!CHkaLo*V^2rN1JDlfVOT7ce4 zG$Lo>q9b+!DqeHrTtgw2XZQOs-P)rmOV-lm6vWTRQdBB}A-4`Yhs17me3u{(^~Hv! zs80?c%>Ab|4GhGA`bmF?Cb5xAU61DY_!#o6-MMWC;FP;KsVaCCyy z2*kSYiq=Ek9PG^4G=GEnmVCIdW?k?o`0>~RSHawJ$siqre5KIvJ9=<%*$5D0oG`DJ zj#Yl33JnW;8$%F+60P}n>of)(-^#g&7tggeB9FZDSb#N z-XSOku{yP02lYCbN9u6aZLCmoNTx+{p)8S-&wP0SpV@i!jem=odMkzxj=qm(I@^$X-QmmYl;8?frZ7 z{^v{q4%`3ivA_{r`0q*sZt$On0lsaN`ri)T7Xt0eap#AE3841Yz(dp1(^{WU`hk+& zZVU7C?#qo1W=^eopGlftgh6=a0anF!`%9we=Kh{K4fYWbU48>7ffsi=sr%^*{q1!y zP;~=&JsM+1CyrXFwMU85Pv7XDA8r!x=kjGXsI-9m)bihx{WO+RZrfWOr}X~jbVK4c z{3Ye|Vlf06ZS;nD)bsm8SO9{@I*0(s8vzFh5lI?gx$<4za~U)FeY`tUy}m=#iT%s0N__rfJFJ+57DPYk2?E@7cr+KPD z2Q$ZIHd`#>ad)xMPfF|FCm`(jSNy2Evby>ru1fw&D(S1qEC3Dc1>{XE)JoMGT2FCs zi56Z=Arzm)j~Fvh0w&;kn{xoU_O(&3FTAP2=;j0!Xn_f|h%}Ed8&1V9%O$7jD zDVzmBZs#dFssi;NLuX)Ce(bx*FaUr{0Mdf)%79Qp#5Q0R%L#T13Ofn`Yk*`) z)Y2#OtNDES7WgeJE{i#ku55KDoMK|(h5h?4R6uE$6-p85SZKtL)viFT_D4YT$8KU} z<;lafTM#zr``??al}?{tU5(z>O$vRow@uAQy{Mq*(GXW#tr=_wN(v_4f99R{`~H0E$sNv+Y}ST-HyY+WkOl zjzIsqn5ejUD?u;oav>=yN(RNrQgpS|gSG!sfprvMSq|5uTLAbzPAUMcO9Fa1nxYSJ zu(AEDnMuHii}j#^LeH!79VrAl_S>Zxb2uWeh?m72H8nMbpw?i!6$y0MA_mfncz$_V zvzg|evN#k6H2VAM2lbFrffYai0C%MUa8{z!om7`^!cd}Z|GsGO8XC*iM^EJHi11FN z$S*7XA3OSV7w{VD8yo5VfRO&}IFP%f{QDO8MT!G-55N}p(-#GJG;LTe+26MhY8TU%E1F+qkZ06MuFFx;WAq>l z6x$KYq_rq%rET|#*&syJ!K=pJWDB)N09IUreZXd7L(o@;nsc<7?JMQziQ6~duBO?F zQn~FC>qA)wW!Jtaj|y>2gwq{0Pg%8@4Bv2%!`ecV(nEKFxOhg z@(x+%q%hrz;ae(dn_f$0phjEBL`O@Q-_TL7^aZ_C0{--=#PqlG`mz+braw3edicTo2JGo_Ar-4 zmW%RRO4vp4(-B7x8=wr7O*r%4SG89Us@Rp^J46Qf=KCAxK+t8TEgMjGR1HmPhb(NC{JYMe{ef^mg zp@4hKiYNDcuMN(Uxpe7pT?MDLnd2Ryxy?og96>6Fl&j2k$Q}!Qmx`R_e573)M{&MU zBdeud#HN}EUkRZUpE7~~Q7Fo_56P?n;t9U_k8x?^kEor!=2l(3)#_={k@vJMHlHPZ zh_4^=V8{rP{(L@%pbJ~IymoT7SX~#F3s>wBOt>m9Hg|l!l-5atZf#1A@}1@ z@_2n|xJj(6N~3qCH$Xf`KtXF08M|Kh+gEhh(NqYhueCU%-5;{S6+jGY*c~d-VoXky z!Y+2klhqRzX)*iZH&HD?Td3z?Jxsc)IC#e2@X<}kiz^C22l+iew1IDSar5u_GesVR zGLG`ZD+!4q#jLKI>xipSIr}AeZ!FBgwT7>2&a89S)5-F!?WHOng${QROc7WSR8iT= z&%L~=(tdyD+QNsPZvzp|hR`v(1e57tQV$f8FPE%H%Z;x+%AWJz97_ChdjFQ?ZX$GD zh}eJ(Y5fOERJ(j-q08JQb*E&v32!4C#q&WPQTYv--lZ)g?6EebyBrYwv0Etfz}kVv zNbVw~fN?iKDxpoREA8o?i^1LBLr_ZQUIsCamhXq8ZS35vJ8Ru`9FzE?KRl-Xo@-{R zbCj}>WmJF!l0=SeO=HDO^qPV9m5-cauFZZe!i1+bxle{Zg(;G8Ibg&T9f7Gk!7XjV z+*Z|kZO8qTz)-#CBRa`S-~un){lyQzCH?K`0m2+U@7D!InL7dmq_2slkfxXp6P-@|7^hN@Y<4y z$59XgVH=;-zPGq`>O<|K2OYZ&-GYN219jLFQ~PfY&bvpy@moLZ-@MuA7W5Y$+s z{z%$+7PG`3I{LjXgRT;3b?%+hND63NM%xgtd)+2K6b2ejQbue9Ju2O_1}EO2l_swU zH=Fz6&Iw#H(;xw#eEl*IZ@*R+2`MPrC74GngM4`P^fR_MF+;&;9;%jh{J6p_gA}nX&Zj+^aU9(*e8__?is|P5OPuVdR<3*i zR97t8QZ9l2(D3Rio3hPfN)Nizp)z>I@_rpD&~p&JmWJn?h#H4hlgX$MQi%x(2tQJG zNQU&&o#(99?#Qx6{OmVw>HmE^Xe`~x57+E`#p!5B{18pvuhDnd;kcSe#OTtNU}f#? z&K{;kVw8%?D;xN-m7U3{gW1}Uuj!ro!g|6o2yxmE&$!};kA1y|wu#SLHFvy|6`hh> zZ7qGlK63-Kj@nL3sW2}Zt)x&5Deq4lTq0_zIV}pI=znqP@-|_`lh;{1tyzyJI2aoZ zG!rmijTdjmn_20dT|i4GLuzrX3EnkZ>1-b*hD4iF1?!kK;VzkP|6V$CV?f_k*~`_o zh}7SY6T305VpTIAs%jrx4#A<4tX=spZ2@|5hXTgg zJktuPqlzb?sT~1j9_uls=r!|^d3@|je#Qjllb9vr;$CcOVVE2oHGTJ;zg(=4^L_%}O4H+-aZBFkM*PqRuHkQ5>5@qjFh^Qmu^eT3IP+Qc6D0&xU1s8($BLRz zJQs^L(nV$d=C*LBTLWjuo-^J-ToZ<~+r|PCK}Zl6d+RRnh*GLyB0j^CGM!O6lZPGc zs}-hjwH^;O71)*D*naKPmSczdB=oSQ*jCH_Ds-X!*cn4lstxV#hOFA!*=3prEA{5( z`KY19md_w69<$b~+xhNzTB}RVgCRX8?4L!vST=aP87k%y@V-Y##GwrjM)!qN9#rhL83kk)Qn^&SFvvvHBs;?Luap73e7EUhF5Ru@Ss`wz#_VN9sDnZi ztr=skRMlxq_()3NF7BBh$wx*Ns8Y>_kYMJbuIp+9$Ge_v8ZaMB2-ff;1*rl0%d)_T z-dk2(<(r$hwiut%v%8cJJT@!32i+IW<@*c8B{>}1l8c|A5D#>^ve1IO?#B=H=*o6= zTk9>6XcU_91Cf~&kq$XC~xS?G6i6vM51M8--zah}ewKYil_nx#CL{mr|wERyg|cwI5Pc<$R% zXJeL>p?dvu0WHE8X0ZhVeDI(R=gc^2Co&-R60d8LdF|l1YKp&eQM9tgFa)Te5S$(F ztu}9B9Q6Q%G?W)h9x#YS{uhH7$gTWeWO7w{Lq5<4V^J#}T%-S~Z~9?-Of!_b%d!h3 zU@D(h=jk>pITU|H7L6HZcz;xuq(x{vT zsf!pq0~JUDPjHZL>+smDe}|b`;P;J;jQHnS!wy`%49Xx-8+r+gN`AMd(yRT)YqQ(X zzz4PInOQ)$xRI2bh!gPD^(m2%kgQ^tiKB!&jc7JkM>%*lwYK;cloLtie^%<9`T!{#~DeJEKtjX9|(2EmxyxLZ` zqq{x0rn?dEZ??WSSGMZP!^2}|+1NcOd$>JIq(o5Xu70cD0KVIdPglFXy1F`kQFp+) zPPJLZpYwRQIn4#f2?4*mZr>>k0bzm5_13=F_~hB8S4yRgNw4VyOYycEw`&0wzjZ~v zXQ0RkpToU^2KoN1gXtWc5*=%(8hPPXu6}Ahb;AAA3sg21642yB!`#Mt#M<%5Qtx1_ zuaAN8?9_p{QF|V1-UfE+2Lvff?wX{RJ>|~a9&+~k&_>(|s%L>{O=lW0gscUHXw zO=ORZ0R^X`azLsZHKb<(PE zAl_}w)4nFRYs+_l3Xl*bWddkW-qB7deSc8GlGVw+0MLRk1X4)qGcojXa+a!AZ%8LmOwTgi!9qt=R{NifkcPSxa1^<==sKNQ_A`zzcxxG2QIrBPB6quZ@T(1ck5W9 z27I&hqphut5_Sy;lyGi@%hx+`$co_1K>3p(&)H`j^*SX57#R&hkv3&LOTDOk|JR#l zWbenz7@WI^RwzJ}H2zH9$to)AEI3m@l4Wc<%fQ5RPioXFBu--YpX;JWCI+Ga@c|%! zls171KR^L69A$%_cu-qqfbr#WyR!eY0*`it^A+dp%6s z0dV<#A#Q@3v0ea_0KhQ>LPle|uMPk9am~^nsGbv9>}@IyyGkLlhI6fse6&0;FyOl* z-Om%gv_?^MtYE`TIp9IlRYS|fftXaQJ3C~R(Ga3!;_TMd+LAh?7Q~(BQ0)3f5KU*A z`5RXG2yw}fV&8vUW`!$+ZrA9-k-HR54{a4(k56(u7N9fjduR@et5kNZjzyN4-nnWT zH4VG3L^yzVpJpQxEKbJsbg?%zz1sDJFxq1zzPZ}X^;uLSx|5WCKtz`6Bb@sYbQL>P zBRg@S0+q{s?+l*;^(Km!X&vPJPYb?X-acO8SdppI5EHRBKJMkW1PF3WMe~Cdl}g#^ z3|JKw>DF@=#pBo)j6%3yhO$wXqEe z4bAY~-c(-P4Y2zxO{abxQmM1lc+Sp!Y^YK|p!;6Hc|YNkj4m3cfMlgh^kD6%tE6Oq z0f1v+cCHB7?bpJCzVnK`|5fEN*xWj!WYgtkZf}{Mb-cipo-ONZ;t-4vb~sk5!4V>` zw=L1LGgo6;E?JHcvft>{!$!sJ5%ueRLnf*c6Fp!3!w;27x8V2w`hC=#Q2fr|7ql-E zM0fbU1|3Zp*Pd`LZp2>8y&h8BS5XUBziQm>f^}*>n5d3sfWndY^?EpW8hOgB_tT8# z<7h39voV`ozShchqdbi^3x*S0B|QDKH7^LGPl``K-*X^UdW24uT7AOaA7AXTm?>pn z7V2~FWDJ3>QD$@-`sKMznxf^Z4%*i*@<-_rnpjayO~obPvA=XF!4oiRUa&hd-wP5#+%~>JsOT(J{9ns`4~$mlu!|Gz2Mb z)26)e%WyRxvPJW58$Jh!Z&DWA%fkE>9xmx8P8RZl2=IubRlD+(-JnUa0V|~O{sH^R zqALWZ-%yT}9sK`f+t>~dF9{S5zMe&n&NS5Y&5n3f$H^PcTR-)vd7f zgg>OC6>&s#=hA-qW~^%?rbSO{tn9p*%n^f3>oFtPys%O+x`B{h25MH9uhEhn!b2s< z$e)>*L-(W9+f}b0$M51b;B>$iMpx5ZNEm5wIB@l-E#$s9>Y%)J`+7XCH`b9wpTy6d zu2r8KH4ZV&R|vm9=EmaUs_!fYbEtmLsHCVM&}Z~GRM!zdEw!Ro*HSvan@oG8QZsY$ zj2wIqWoBAe%H6F8$!sxdQeeD1FIfG(VdA@_6URq~Sdir7fGMlI1U|0uV@PUyv`()> z7Y%o(@TSZ%LdiD4|A#-Lq@wBHB(c>EL>bMm@HaFFOPRNa_{#_g&mJ1 zsMrOa(0VEITHGK4@4ix1b)8>qa=3gPX_oHn{Wj5^Wii)J{V*lclr*#|W#T-UJyT6r z!zu~!2~%fbJPy$6s!$LgBXQFZSuwU-?xBtiFJVWo$bG~AD6ao#Gcpn6E-u^%Sbat> ztahN*#K0-GU*tncxO5pIVJD% zyi3YU&N-Xqw8$fKgTIyrq##@ViDpe+HxMyPijLh(iCO0;gM_L`uxJ&px$51*T+ssmgY{_U6nbk2pWrFiLtZpJ*tmyGRCebk1k{GEjbKZ!uF{iFOenlxhnf#i1N? zvfo_eT4pUI_rUz8FEY`k*K&ova7p+=!NE;cIvBVgt2=x13ia0Oy9| z(yY82q~MyU2jerK_vcU?wi;Fx|GcUEtL=*?&c~pGtopj0Yk~ymgZ2_?FPwUQdhsa| zbvygYA#xJ@7c1{Yy-Zg{acp*$WyWB^{^*By4YKxQ4vTVI17mn?<1c8`TybmXVb4Ds zd{TIK7pQ?(;NCH0beqEg?8HU-Z=y~k6H;^?B&#_X;vH1m8RD|M#82IJk8;CcEB<9~ z0YQLYOlMhK?8|InNfjL%OE0##PC$}hsvg@%m?3Aoql10be6Z#)uUNX3yU)=Mbd?>A@tFwS z|D=!O2nN}H7u-1Vv`HTqNt@O4(0J|`)v}avmd2woQcVf;M51!%7SKN8H!XFC}I z`TG32L!kSc=%aJ|fR>sP&Xdyn@i!rAjyP?6kybx;vPHIx6lRRvEon+8jAEv4r&d|6 zEI?cR_u+M22JBS9>F?nANr~ZWl0Dp)>I0@$xt(R=lNGjP*YoTKAt|ZOTGCd1dv+u6 zIDWTY2s&XnkG36@Ar0crnnb)@bv4H9WwR@u$8+y@yT1fIlf)%_ntGIl)p|J-8yaqb zq}U+-S~iCmJI4&(7G9cOVKt(;=$t>DB{An!)e%HcTZPNR-nQeY+#{^Nx z1VkT-djK6r@7DXM!Z$)X&jDw5sHx<93<@&+ew8($_vRy0`8Glk3yb0qe48d>_q|>o z!+|v{R8Vo2ZrAzw`qKH3>BUpL(T(xU$2-%%#kz%xR7zBF#NYnQF35rl% zS?*nl=vaHstBhWUs6{?i3G#kG7Vw$RHkap4LUwl#wN8+$cUa5y@b0m&$E>!cV&f#u zkyIRt8@#i`Ehfs6y6kij8I@`%w6_zaY<*#H&oBaiEGD@Cqls5&Dp!3NQjujeb{v^W z@~$3#-9)a$cp-4(9!a?dQkyrmvF^H4D*OhMRy>n9OACg3Osn2;>UAHaE|?qa_z+JD zGIAn}k^eOiHGc+=faLMvtn}NHYnW@QG3&R+()-v$yX8CwnG}0>F8(Al$<p7vk)X0%i^Ft@jA)`p{ zJr7Gd?+UyX5uF_28$rSv{BC6?SB=b|4qU0*VRJu<#E_2Q7TS*fPV2KhrRF$7&AJpN zskCgo0Ka=y%1holhCZ3eB`l>t)ef0f2X5)d>w^D7+FORj5p3a~Bm_txf#4Q2xVys; z+}$-maQEQB-QC^Y-CY8~ox$C0aJISk{&zp^e%bi|PtU^)-Bs09)#tqD_YyTcwLT0R z=*iFS(B*7L7tQ5S{Jl={Nv>YyOrQ*`p-{FZcsi@^o&N4&zfy-V~P&nxU;3)tuEWh6-}q`M4>aS!|6m@d!d*7Andqhf4c2cX9j z0BBxrV?BTI=KBv}56orbVOCwI%3qfTwMbphy6OA5akpVke>LNBg>dk zPYGt&?=_SApCa84`g59*=l|vi$(vtWcF1%%%c_A0<4ZtQsJp|(Yz;E~%|wmST_*Y{!Wj1)HQOaBrq+kz)8S@FNR4hfR)PQ&D&ez)$n%gj04zZVQ-Ay?!Z zX9N`#v7zFlY#Y2!Eajn{*~RMQoPT+@!E$7WIluPjaNBP!91rrb2pOV1xaQiU3oZ`c z`My1CKcUt!03NIW66}??@3|YTj;Wqs5*0QO&4CU63+?|y6dfSR35~tnnk9hs*9d1>k+qI0KW<|V>wP)|8 z{T7ao+uYYSX$fc3>CkF%b^p3+G5IL@K*EX^vf$Bwbwz(#W8TY)$6f4jIR6}4KZ>ZS zIEGwuPMJv?YW<%pu#9vC+F63s7fChKySp zq&{e@@+g%o?YW#ppOljAJU(&5NXBq?yh%}92L#0wfOSS42&Fmqvq3&TFEd?24nH(@ zEN^~30w0lS;8lb;YL;%;T$v}i(SO_0hU=9+d%q@ZvT^T(6js)Xy_$L3gBe+o)V%ZM z7(STM^U3k}CMNqi3^~ct4$&KSccEdO#K;Qw&p(x;iyxXF*{-Rdt}yyZG0p2zA)obK z5^8^T8}z$N-x%O{!YdC2!e2f+R#IC2q>jp}*b?ZESj``P=g_{2aTd|o-6IHIH`$4N zccnX^O&I^o*IU#k+OCSb+(YNa@bZIvMO_oG)+z{HJW)zh_rfX<7ge@n%0-@*96^Ne zkIT4w)V-_s@)Kyy`dumtF452y5-2jDiHC;VjP^Cl$)h;(Sy{^cL8VBZ56Q@?0w7Fk zmOWGHE@cwzjB25-7O;zURb#5Xh#0Ept*Jed;XDVh`{p)AZlZ)Hl+UIqr3T#KrkF_r zMu8?%0Fnw&?(o;VMT!zI{_+y6H2zd+0~C&QT_j+C@blF&9e>Y}Y-bk@`1*t3BLJDZ zo6p0bY$9wp!jTHqRdnE74Z)r&c?tt>rQ`1C}3woqqItbz@S zv~_PIeij(WOo9C~HS)i#anMQ3h?zjBS&gqbtIjVK$&oK5g*15=t(&ThuB;LW@6 zVizbP0Nn+|uLQ(!emcZ@$ktdy7&>5i`?Y$BJ;{zl^40F1_Q3ky1yM0N-h2S3or*Z8 z0J3CcNGxb{%BGimUeV(i!%ooGxxvI}W@les7rWG;`wza$4>78@-DsW3DbbT{2DgAM z-Df-HuI!B0?AhJEQ}A%5)a**`DxqIE2kN67H@4!0!qY}iP5?CcZ(_6EMAGp2ZifN&|&3^lrWQpdfAiDB7YR~J0_6CB7=3>bs0qAk7$EJoKiA|X`QlV{J z6A_*v4<5mF>iN}>XRE)8T8I^+3<{>nrW5t}GSoN)V8)N(V2R@84)EOE-^4;arBT*M_FU2}jaXr9%TjPU zm~cCp&nzD>z`x3LWMDV&X#A~5mh0W<9_uGIpStvG0g*{zW0?uL`R-0okA8o$d=ANJ z8ob~Cm$D&ut#t%7ZmG0Wrip`LUQ1|g`fShj=Ne!aD|oPn?&9Yp2>Rq5Kic{PGwmt)uEEpXd^P}tay)^!5wpVGxZ^w;O zyjaPuA|AzEius71;}u9bDa1YPb}nqQri9aeNy^c@ea|B13V~C24JoEo14j{=aw6()1e2Cd&c3AC;MNmqy-x{=$Qbx>dz3%^dit(=f3ZB63 zdF})=L!b~^t@U`JOIUfSZ-tq^)ia3chrGUn<_D^A<6^LChM%QC{&1{@d#W_Iq2a1N;vk)Dq)r+iH1j}Ul-%ZCzV=GE}|#9^Umrl zhpPOUgjU;da&j?cAb%JCEiv$Mc=ze~S^7S;_0RJycm|%!un<&3VB_={SpTKv^RZRy z)p`p3R;3?dV(hyPy0b|q|3c~bb<$cbM^fg!WK~lrGKup;Z-0!h*yen3&5Y^kiDX zCh9!PRYgXpiv+(Tm`DK?Cb&S7MR9&bvAk`g(cSWq^q~w{AV!Y(<twH`sn` z#OtIxUv+iUU&9FWWs=(O(z!nx0b#0RSf5out196~W^D&Rkx;_7UheI$ZJYQL+-4&4 z%}F)w7RSGjyE|?}aVxzT0!mp?c1@mgPq>f*2oo1J9^|c zO=X^#aVxZ_>$)4;|F_}b`Y=)Tn%`wA&|~>#5S_fdVCn(iMTOnoi&deztU1L6X^hsr zZujsUK4LmOrY-k4T07k>scywqtuVik?aNGj4ayrxFr2j~EWChgI^PQMu%Y zUGf156v{_tkD25(R!r#5%1X1k({$=q#7|b>!Qpw&E98b}qxw&aq`Kld^;f9d@gpo` zwt)N5sO_O*LVAho#IJA9XzDC4aS|m+3QF8kN_1MR!F71AWU|64PKmIE6>DVzGFc>lqYX&m zgA5z2eiX`9W@yu)zxHk475%Thc>hCza(*k-vnC-a9R|wk;^V?5PHf_4AtiH_x?t<$ zh3ZcPCvh+)oYpNpHL3f}g2VmWQ6Gebj>w#^fqa`y;Y-#V)M|>JLV)FM?g6X;RO?i^ zxw!Z?Ingd7Q$b!YYL4RYiEY1TN&NHZWo0MY{?G1BrBEhaQan;v?68>IG$GcO{BKNI z06;l>OIHN*6Uwc`1?yoIn}LW9c6tMm?g0RlTjrY%c>{R$wR3ZFx&Sb)HwD3l_~}Zs zwapoTs#Vh2bp8Vl8tSajZgqN`N#_~W_8Eh;D~Rm^#SNPKk3JTbcWINe}G=Ocj`ZNfR6UP&8XO{jzA=}wl*;_u{<<16qkJ! z5WbtSSuR~S0eSFBKwVu4;AuP7ADT@8WYhQN3k3x_%t64k_2kVJLH9JRz5V&|t;SG^ zmc6)UWo1Q+`1|+z=KtG2H*IEQ{h~G?l%i*MteBG>pM7#G`6<^a^3jV&?wAsb>LKR5Tvq3gTXqAfonL953 z1n@-c_J&3?P6^0Iv;Q$%cul=YnQ7?nHv&@FamilhC4l?EAL>YIc2eMp{}k|FMwk?z z^^+w|kly^X2S^;ya&mI&8Y~v41Xqp!>tt+^q;dt1gft%xFkr#`#afwWcewkYR6yb7OZ8vj%8uBtf4p|H<}>B5e5$XPtX-!oldk|CSJBBJ%^64P zQrh@FVoP^dZTv-XIEIb>a%ZNMy-ISinTspOro%wt;pI8d(f36X93WZ^dqzj&OT)D} z5xA*U#BKGRP1+$KeI1!Bkup+!A}?elp0JX9Me-b$!JwkH!f$ zZ1Gyg+EZM8k-WjnOVBw5*EmqG9EzVlVwvZ~@OHVwC<3`ZXU#YL-5TjYquPav+GsRe zP*?CO42fR53pI4$4dfX7OPflYtc@^c?^ya19{Ok)7*F-uEb+jae2ddd^qIRwYLztC z?$0i^PUs}a-PMBrL-S7Ph-GJ?Q?5E3yv_tVBAmT4qV}9qMGQPkAbH5W_*O$>M!7|m zlN6xgyh?dZOj&s9S?JLs4LM4Uo0B`46F!B#NN6@tf0~6Xu4c7G3{W^-iRR1 zOu_dIP~X_V76h2E<)J6G?xnWfJ}stcG)`D(%(WPZ3^I;dUOl18<^=F8gJ6Fu%Mwod zly9JZrXw7%1Yu{bqAhEQkd}_^SMh7MQ- zafp&5gy|5(f6DoZi;Pca^RFcmTk`(Vn@Nj~ah>C|fBqO5r)F9r1l+GG@d@-16SMm2*h2XQTixxH6w&MZIV?te@jnY{R4>t( zN8-C@22q47Ev>jK2L$Ka8zv0yJ%n0o(wp@%`8-OBO+&3R;ZnM{e z?e6-D=vj@6aCo9NKf^kR@_*6eIxCfC;jBwot+^$KDaGt!diDDv66r| zS3T%jD$Cpi)+6+(ib@WL&A+58TLVvRpZS15u>dn%)_hd|s)Ic!p}*4_8QQx86~~gp zf28@3ZeZ%q*%8U0s1j`1Lc$Wd-%xuol;COc3a5dxVz{q=(W#w8j(kPs1}KWpM0~7M zM-2YnVy-*czF&^sZ#1N{GtOQLra`0n&p?0B_q{a$FU)?$q(&$&gzUhfD17*4?yHSB z@vq!gCeLw+?y&{_)I(&$;1{2g_RELx5Mu{N=@C+lTBj2{tYiaF1|O2YaD0SK)!!!Q zkue<1%r~Yu0Cfjj?~KF?c6dA{NOHQrm3r+0I03%;9rIBC4avY+OV}CV^-I3pR{AzO zPTMOFXK{zPF1fD{+F$Xo1{k6a4-P~>Ueok}1Yqv5MY`CE)4a?dx%?Aw4@~b1*#g44 ziLc$cX_DdCSlN+~PK-pcm+^@d$GG)ovCt)iwo)|B|Uk*b|LRk{DI zQlhu;s)N@7#yi?roE2nzSJVXnc6Zu46&JIr_27t}d*7C#j&NpO%3;Bey;)DI>~n_BHD z@z?H=Iy&A(1xxFtb&^TcrN1vsK@4FwC#G-cT~QY`(D>TC?~o?%Wv3{#%H!`Wi@#o2 zNs!autTd8Hid+l#z*W|L*xZVYM(Mk`^`#XhL-_kue!;>`Tfg#)OhL*Y z9iOo5JOz9xW3SwvXotap%%0`4_pjd;<13Ot9w6d4V;Y75o3tAk%ToBSzpXjPt+gW) zt!d>9p3&o8{~=M&zUVBFc$x>wO_yRK>OKa8YmxW)D`y<8b&h?$UD}?>l!YrgyKi?A z&|-0w_pVdo5nk^52{R)S2{49ZHF%umc#*@Ia6uxf_XFleB?!4xdwLhH0Tbq%SNdJA z210ePGYek@Ww_-dnCK9KP4d|4COs z1-p5}8X=d>zuosuE*m^;9iF0SrOG26DeV7tq$CV~qi+Bq0EkPyZDEuuen zuE*f;ofb785*@YHPZeVVGTfJDhoE>^!MILLGo=&Xma>4$*sx|Ek%Qn@tq8Qh4NN)yU&}(Q7``+H+y#4c%~Z z61n#X+gp^6lHVH6cLpOU~=`=CzYGk$-m(~{OZBDGz>LW4d^ls8SmSkbIO_|G_^rw4U)GP(`7(88xkZrY z(_OQc%ZfVP$|9W>JG+$2ZfH#=ZenUcY67+3;za!4zrLxM>=QsBe-00@@yJ`MHwQa8 zI}<>_Ua#>c%MB+XfOs18X72U`oC?-NGIL>_dowh-RMPps(NRgSDL^k^X=y1qSiy14 zdoo{xaf!qiERuP&IRg!PYGe3dzFmp+8Eds|NY$Akt>xO+194|Q$_M;C_6a-)ewvsp zX~v74xE-jD_+q<9-nbFaWiJRbmWBsAT5}UsXv4VQd0(N1!AtJ^r89B+34AG#jeu9> zFplg;T`^Db2i_O<`!0DJ!A@XgckF3x*4_ge-jUiLz0UQyXw>R>3cF1^iVY>gV2O7C zOtAvTS`GFoB3A2m+Y5XT%hx;e4s>DXDSXU@Y7hI#Tn@g0^ZSMygtA8%UEX>169@WK zBrW&Bv#}lNgHD(iP;0>B7sJ>5jl|;1XRGEiw3$ML-s0vVW*_p&~x< z_HM9LO1%4wY;oD~;s5L~?U_D&vkKSVyUJb*2(C&H9*UK<`fWOs%=V4|cD#@Mlk?)l zyJLGtznhZDs`V=s(lAns&|Nrth`7{y91N^!h0}HzReH}z>;aL@69G*rjUujWo@!5f zJl&q{PemOqKB)8{)`hbpTZ^mJB#fT}AMzihUp%6QCQA+@Y)b}3a&Sy)YDHEAKJ5Nu zkE^`P4CzG_^7~LoM*8d`gSa|8)RCf(03etpl|<@eG(;Wy^r z?WBR}>8pD|K_BrdxLH_~yp|g*R{`q`wx^q;d4Cq^X#jtVSCV*ZgHBPBX&;ZhBB=;c zz3*<1d)87f*|9~Z_|F^OJ#TNoz(W*nfr0e&9{6pKP5~fzWwh9xYt<{>1FRuh{E>-f zcr?RK_tL8Ax|L#Lx)3VKj_i;&MV#{w)^un6)6}BpyNhmqfDd@!R_;zE(Y7QBV+Yb-DrIs27wY3BVRt^Qj8fV-+xG=U zJ2F4*){5&ZAp)I7!99ptPGuN#v}PE?QN9<9Pcy8uwL65vaYMnM!GxfvW(unTu@3wO z>lqY7#(A#IgujXloHg{1+jRN_D>heO1>4iV>x4l)b#l0DSchD*#9&UlPXu4 z^U05lR6dUsl7jc>!s2;c7OsqYCa~F#{<@kP+6EKz>h{Z5;rwyvT4-qUhKI|GI^v|z zV|(z5-1hen{~ZQrcx$tYp3qqh!1w!knIlz7*7WrBwWk@ZdF}Pv{eYFqnKEWbof73Y z0j_r;nSp#|faP(^TN8IaypDEdX@TNKoZ;Em0*3|T*9X^J)q2x<|GdOUm%Ofpy^}&v z;zQ06ba+MlTid659< zlB-BqKPxrHI?5yH8*n5X<{~K>qO~#Lf4kg=@Axj3tA{@GEb@HpF}8X+j)f_@Nv-C+bL<<-(~i zSX3{{V>#@|2YJ7>nfmi44GQ$?QtO6U?N`k~DQ{yrmJTbb$ox#U>_T(5q~ga1eYafV&rtRjS+TeD|d`YDQB537%9tz<325Fj1Trcwri7goW%1y&`Y1LhGq z`h~SvKUr|do$$IUdo{(LQYk zlg8RIY1*}D6Pca;z2eOmY{s?=oZ-UHIfd1R0})A&pKVAk?E;;A##2s)oLhO>&fFhb zVKjGZVV7MICJncEIKu9O4m_+jUNQ`S+t`!Nv1Y&q4}BJyyVp~%;tFBG_XENsgssfq zMhgqup4@Q^oP-^Z{w_2h-(@o8T5n2aPJK*yCP>3`{K@zh6Bs*cyLajG?a(4* zT@Jf%fKxB66zh`ni?1RzIToi>?;l5(oh!8fO!~ErLpjZrMyijgHPU*P2A{e`t%d#P zD!=1Z$s5DDI+uRf&>hT&S?-8r%zgIYS!u_DR57&u~q}Km=;NfUpb%l%1+N?m}1fZY4VxYj9414La^+&a?@Lw-1L*H`7 zi3YLR23EeF5xncLDhY!rq|RNc`(9*J$wLxHw5|kK zCbSS3^T$ZDb(rEBwzs4cWd{C;SROe98xg!)cw! zN8me9FizrZQ&0+1p~lYdOWGXx&@n*iO)Hu^A!;TP!|ASffNfJG2Pk4SbXsT zM&9%kS4qNwL~z@z1@U>i@`FYLf4HBZ*L2NUKtU*QoXKG@Q*3xhZsz)N3svTv{MYmd z1t`kR6kk-$c2PI6l5;lzYgz2d0bXBo+q#7C`QN&Qb z;Fa#4`r@LmSk0P*8nwI1L{w=@hcbzjV*sIDI*!o_2rir#Ops7d?`_@)fk+k6OKT_FDMIVeu+HU)0{Vn+AqmvRATJ}(%1 z0-h9FB@rKQWrT-_{pwL83wJV5Qh3cZAhp(p*IRtx(9$H=!Dy7B`piq=MQXh^Vj8m& z8Lt~RcEaWHe;$2-n3Pgf_bWdy4M-j;{DBSD%^81O=WF&u2%L>wmgZHI*Z*dJAPDwR z9Oj+WvR2;=TXnl~YIe^@9cW?_?f->(nd*nCK8lbO#QA?!{!AT;5&Qw=EwUAL98?29 z`~moeS;ra3KV^?;kNdL#6M7~&J(#^h^^OSO?*YMTv_fLx%dL!^m+;e7q_p```ONHbp0j5*BG@j?AC8yM4O?f^}MN? zryO=2vwuY^M}9wi*#4XtEc`&m`Ti1}_Nv6pmu=7O+Nw3bEq>qrNu1erk?FaG#4pNA zD<1_G>6GUm^>H;M##ixi$vsf;~nO4TQ()8REbrs6*^WfG**+iqWT?w!1*)7E-)uSdVX!T>e$@NUnr zl3%5r!HAhJl|wSV#GcIB6hwsGx0P7U3kcF%M|_QsNV#9{42TUu;GMGAeNSC%;5(vv zX+oqnO_4(kvv$B7MS^FpKjp%Sg6wW?cHi>F2)`3|Y&QtIhd;|4T|Vo#|NNZm)Xz?& ziIWU%UZtChc)?i}*?pFDAI@kwi^UT0lsbgw$%g{#3e4%0} zA6%is8*tJ;3E9?*$q0$^3N5BM+$f$66qF9H_k99Mp)-62H6sH(?|kVi`02`KCyh?j zYvdRmIwZ`^<7haIYDefUpKH@Kx$$l+2HoQYb$fQgI`h?6@Rt}Gg5dqD^$y4;0jN)6 z0x#v`2H-c@%-`3~)3<(%4jwBI%b)+kjRa;eVLwD)eL>FgWO|v#FwUr=p|CW1yKalGUYbYD=)kRyzS{-9%wKKmJ^6`{%E+;#Ikwg+-9VR*V(h#A)+Iz>#PVpkEW<^hm!@F-5$N{O zg(oHw_p+V>w7xm4yXG<>b9~6vyF0UI%6ctBqDJD)KQPq$?Ac#_d1s1SknwaT7|%Z- z6xArzJJ?<@mI11X`1=X@g@;P}2L_>gjhFDWp=QKw%jLOcb7f}_ZI(H{o@(7?X~3$; zG0t-1!$TUeA8C8WEDa>T+p(^)IbO<%2ADJmV~1mu)wNsv7F%;Q(+XfdxZr#P*|cx= zAAv`M>V<03n!^Nj+aL6hv5B%JtS@CL<+{e^hvZ(5LKD~g&6I0F;A-V!%Ry%+-XUkL z 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