diff --git a/.github/ISSUE_REPLY_TEMPLATES.md b/.github/ISSUE_REPLY_TEMPLATES.md deleted file mode 100644 index 4d554f74c41..00000000000 --- a/.github/ISSUE_REPLY_TEMPLATES.md +++ /dev/null @@ -1,10 +0,0 @@ - -### Complete and Minimal Sample - -It would be very helpful if you could provide a complete and minimal sample that reproduces the issue and share it via a GitHub repository. This will allow us to efficiently troubleshoot and help resolve the issue. The sample should contain the minimum amount of code to reproduce the issue along with detailed steps on how to reproduce. Please see the following references for what a complete and minimal sample should consist of. - -- https://stackoverflow.com/help/mcve - -### Post questions on StackOverflow not GitHub - -Thanks for getting in touch, but it feels like this is a question that would be better suited to [Stack Overflow](https://stackoverflow.com/). As mentioned in [the guidelines for contributing](https://github.com/spring-projects/spring-security/blob/master/CONTRIBUTING.md#using-github-issues), we prefer to use GitHub issues only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add some more details if you feel this is a genuine bug. diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 00000000000..b02bd314cee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,24 @@ +--- +name: Bug +about: Create a bug report to help us improve +title: '' +labels: 'status: waiting-for-triage, type: bug' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Sample** + +A link to a GitHub repository with a [minimal, reproducible sample](https://stackoverflow.com/help/minimal-reproducible-example). + +Reports that include a sample will take priority over reports that do not. +At times, we may require a sample, so it is good to try and include a sample up front. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..4ba41d1b38b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Support + url: https://stackoverflow.com/questions/tagged/spring-security + about: Please ask and answer questions on StackOverflow with the tag `spring-security`. diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 00000000000..14d9fb49273 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,25 @@ +--- +name: Enhancement +about: Suggest an enhancement for this project +title: '' +labels: 'status: waiting-for-triage, type: enhancement' +assignees: '' + +--- + +**Expected Behavior** + + + +**Current Behavior** + + + +**Context** + + diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml new file mode 100644 index 00000000000..db4f6ccbcf7 --- /dev/null +++ b/.github/workflows/pr-build-workflow.yml @@ -0,0 +1,22 @@ +name: PR Build + +on: pull_request + +jobs: + build: + name: Build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: '8' + - name: Cache Gradle packages + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + - name: Build with Gradle + run: ./gradlew clean build --continue diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1ab282c5779..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: java - -jdk: - - openjdk8 - -os: - - linux - -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ - -script: ./gradlew build --continue diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc deleted file mode 100644 index 17783c7c066..00000000000 --- a/CODE_OF_CONDUCT.adoc +++ /dev/null @@ -1,44 +0,0 @@ -= Contributor Code of Conduct - -As contributors and maintainers of this project, and in the interest of fostering an open -and welcoming community, we pledge to respect all people who contribute through reporting -issues, posting feature requests, updating documentation, submitting pull requests or -patches, and other activities. - -We are committed to making participation in this project a harassment-free experience for -everyone, regardless of level of experience, gender, gender identity and expression, -sexual orientation, disability, personal appearance, body size, race, ethnicity, age, -religion, or nationality. - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery -* Personal attacks -* Trolling or insulting/derogatory comments -* Public or private harassment -* Publishing other's private information, such as physical or electronic addresses, - without explicit permission -* Other unethical or unprofessional conduct - -Project maintainers have the right and responsibility to remove, edit, or reject comments, -commits, code, wiki edits, issues, and other contributions that are not aligned to this -Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors -that they deem inappropriate, threatening, offensive, or harmful. - -By adopting this Code of Conduct, project maintainers commit themselves to fairly and -consistently applying these principles to every aspect of managing this project. Project -maintainers who do not follow or enforce the Code of Conduct may be permanently removed -from the project team. - -This Code of Conduct applies both within project spaces and in public spaces when an -individual is representing the project or its community. - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by -contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will -be reviewed and investigated and will result in a response that is deemed necessary and -appropriate to the circumstances. Maintainers are obligated to maintain confidentiality -with regard to the reporter of an incident. - -This Code of Conduct is adapted from the -https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at -https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/] diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 00000000000..16ba203531e --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,170 @@ +_Have something you'd like to contribute to the framework? We welcome pull requests, but ask that you carefully read this document first to understand how best to submit them; what kind of changes are likely to be accepted; and what to expect from the Spring Security team when evaluating your submission._ + +_Please refer back to this document as a checklist before issuing any pull request; this will save time for everyone!_ + += Code of Conduct + +Please see our https://github.com/spring-projects/.github/blob/master/CODE_OF_CONDUCT.md[code of conduct] + += Similar but different + +Each Spring module is slightly different than another in terms of team size, number of issues, etc. Therefore each project is managed slightly different. You will notice that this document is very similar to the https://github.com/spring-projects/spring-framework/wiki/Contributor-guidelines[Spring Framework Contributor guidelines]. However, there are some subtle differences between the two documents, so please be sure to read this document thoroughly. + += Importing into IDE + +The following provides information on setting up a development environment that can run the sample in https://www.springsource.org/sts[Spring Tool Suite 3.6.0+]. Other IDE's should work using Gradle's IDE support, but have not been tested. + +* IDE Setup +* Install Spring Tool Suite 3.6.0+ +* You will need the following plugins installed (can be found on the Extensions Page) + * Gradle Eclipse + * Groovy Eclipse +* Importing the project into Spring Tool Suite +* File->Import…->Gradle Project + +As of new versions of Spring Tool Suite, you might need to install Groovy Eclipse pointing directly to the updates plugin location. To install Groovy Eclipse on Spring Tool Suite based on Eclipse Oxigen you must do the following steps: + +Help->Install New Software…->Add the following URL into _Work with_ field: +https://dist.springsource.org/snapshot/GRECLIPSE/e4.7/[https://dist.springsource.org/snapshot/GRECLIPSE/e4.7/] + += Understand the basics + +Not sure what a pull request is, or how to submit one? Take a look at GitHub's excellent https://help.github.com/articles/using-pull-requests[help documentation first]. + += Search GitHub issues; create an issue if necessary + +Is there already an issue that addresses your concern? Do a bit of searching in our https://github.com/spring-projects/spring-security/issues[GitHub issues ] to see if you can find something similar. If not, please create a new issue before submitting a pull request unless the change is not a user facing issue. + += Discuss non-trivial contribution ideas with committers + +If you're considering anything more than correcting a typo or fixing a minor bug, please discuss it on the https://gitter.im/spring-projects/spring-security[Spring Security Gitter] before submitting a pull request. We're happy to provide guidance but please spend an hour or two researching the subject on your own including searching the forums for prior discussions. + += Sign the Contributor License Agreement + +If you have not previously done so, please fill out and +submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement]. + += Create your branch from master + +Create your topic branch to be submitted as a pull request from master. The Spring team will consider your pull request for backporting on a case-by-case basis; you don't need to worry about submitting anything for backporting. + += Use short branch names + +Branches used when submitting pull requests should preferably be named according to GitHub issues, e.g. 'gh-1234' or 'gh-1234-fix-npe'. Otherwise, use succinct, lower-case, dash (-) delimited names, such as 'fix-warnings', 'fix-typo', etc. This is important, because branch names show up in the merge commits that result from accepting pull requests, and should be as expressive and concise as possible. + += Keep commits focused + +Remember each ticket should be focused on a single item of interest since the tickets are used to produce the changelog. Since each commit should be tied to a single GitHub issue, ensure that your commits are focused. For example, do not include an update to a transitive library in your commit unless the GitHub is to update the library. Reviewing your commits is essential before sending a pull request. + += Mind the whitespace + +Please carefully follow the whitespace and formatting conventions already present in the framework. + +. Tabs, not spaces +. Unix (LF), not dos (CRLF) line endings +. Eliminate all trailing whitespace +. Aim to wrap code at 120 characters, but favor readability over wrapping +. Preserve existing formatting; i.e. do not reformat code for its own sake +. Search the codebase using git grep and other tools to discover common naming conventions, etc. +. UTF-8 encoding for Java sources and XML files + +Whitespace management tips + +. You can use the https://marketplace.eclipse.org/content/anyedit-tools[AnyEdit Eclipse plugin] to ensure spaces are used and to clean up trailing whitespaces. +. Use git's pre-commit.sample hook to prevent invalid whitespace from being pushed out. You can enable it by moving ~/spring-security/.git/hooks/pre-commit.sample to ~/spring-security/.git/hooks/pre-commit and ensuring it is executable. For more information on hooks refer to https://git-scm.com/book/cs/ch7-3.html[Pro Git's Pre-Commit Hook's section] + += Add Apache license header to all new classes + += Update Apache license header to modified files as necessary + +Always check the date range in the license header. For example, if you've modified a file in 2012 whose header still reads +
+ * Copyright 2002-2011 the original author or authors.
+
+then be sure to update it to 2012 appropriately +
+ * Copyright 2002-2012 the original author or authors.
+
+ += Use @since tags for newly-added public API types and methods + +e.g. +
+/**
+ * …
+ *
+ * @author First Last
+ * @since 3.2
+ * @see …
+ */
+
+ += Submit JUnit test cases for all behavior changes + +Search the codebase to find related unit tests and add additional `@Test` methods within. + +. Any new tests should end in the name Tests (note this is plural). For example, a valid name would be `FilterChainProxyTests`. An invalid name would be `FilterChainProxyTest`. +. New test methods should not start with test. This is an old JUnit3 convention and is not necessary since the method is annotated with @Test. + += Update spring-security-x.y.rnc for schema changes + +Update the https://www.relaxng.org[RELAX NG] schema `spring-security-x.y.rnc` instead of `spring-security-x.y.xsd` if you contribute changes to supported XML configuration. The XML schema file can be generated the following Gradle task: + +Changes to the XML schema will be overwritten by the Gradle build task. + += Squash commits + +Use git rebase –interactive, git add –patch and other tools to "squash" multiple commits into atomic changes. In addition to the man pages for git, there are many resources online to help you understand how these tools work. Here is one: https://book.git-scm.com/4_interactive_rebasing.html[https://book.git-scm.com/4_interactive_rebasing.html]. + += Use real name in git commits + +Please configure git to use your real first and last name for any commits you intend to submit as pull requests. For example, this is not acceptable: + +Rather, please include your first and last name, properly capitalized, as submitted against the SpringSource contributor license agreement: +
+Author: First Last <link:mailto:user@mail.com&gt[user@mail.com&gt];
+
+This helps ensure traceability against the CLA, and also goes a long way to ensuring useful output from tools like git shortlog and others. + +You can configure this globally via the account admin area GitHub (useful for fork-and-edit cases); globally with + +or locally for the spring-security repository only by omitting the '–global' flag: +
+cd spring-security
+git config user.name "First Last"
+git config user.email link:mailto:user@mail.com[user@mail.com]
+
+ += Format commit messages + +. Keep the subject line to 50 characters or less if possible +. Do not end the subject line with a period +. In the body of the commit message, explain how things worked before this commit, what has changed, and how things work now +. Include Closes gh- at the end if this fixes a GitHub issue +. Avoid markdown, including back-ticks identifying code + += Run all tests prior to submission + += Submit your pull request + +Subject line: + +Follow the same conventions for pull request subject lines as mentioned above for commit message subject lines. + +In the body: + +. Explain your use case. What led you to submit this change? Why were existing mechanisms in the framework insufficient? Make a case that this is a general-purpose problem and that yours is a general-purpose solution, etc +. Add any additional information and ask questions; start a conversation, or continue one from GitHub Issues +. Mention any GitHub Issues +. Also mention that you have submitted the CLA as described above +Note that for pull requests containing a single commit, GitHub will default the subject line and body of the pull request to match the subject line and body of the commit message. This is fine, but please also include the items above in the body of the request. + += Mention your pull request on the associated GitHub issue + +Add a comment to the associated GitHub issue(s) linking to your new pull request. + += Expect discussion and rework + +The Spring team takes a very conservative approach to accepting contributions to the framework. This is to keep code quality and stability as high as possible, and to keep complexity at a minimum. Your changes, if accepted, may be heavily modified prior to merging. You will retain "Author:" attribution for your Git commits granted that the bulk of your changes remain intact. You may be asked to rework the submission for style (as explained above) and/or substance. Again, we strongly recommend discussing any serious submissions with the Spring Framework team prior to engaging in serious development work. + +Note that you can always force push (git push -f) reworked / rebased commits against the branch used to submit your pull request. i.e. you do not need to issue a new pull request when asked to make changes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 7336e501bdb..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,223 +0,0 @@ -_Have something you'd like to contribute to the framework? We welcome pull requests, but ask that you carefully read this document first to understand how best to submit them; what kind of changes are likely to be accepted; and what to expect from the Spring Security team when evaluating your submission._ - -_Please refer back to this document as a checklist before issuing any pull request; this will save time for everyone!_ - -# Code of Conduct -This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.adoc). -By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. - -# Similar but different - -Each Spring module is slightly different than another in terms of team size, number of issues, etc. Therefore each project is managed slightly different. You will notice that this document is very similar to the [Spring Framework Contributor guidelines](https://github.com/spring-projects/spring-framework/wiki/Contributor-guidelines). However, there are some subtle differences between the two documents, so please be sure to read this document thoroughly. - -# Importing into IDE - -The following provides information on setting up a development environment that can run the sample in [Spring Tool Suite 3.6.0+](https://www.springsource.org/sts). Other IDE's should work using Gradle's IDE support, but have not been tested. - -* IDE Setup - * Install Spring Tool Suite 3.6.0+ - * You will need the following plugins installed (can be found on the Extensions Page) - * Gradle Eclipse - * Groovy Eclipse -* Importing the project into Spring Tool Suite - * File->Import...->Gradle Project - -As of new versions of Spring Tool Suite, you might need to install Groovy Eclipse pointing directly to the updates plugin location. To install Groovy Eclipse on Spring Tool Suite based on Eclipse Oxigen you must do the following steps: - -Help->Install New Software...->Add the following URL into _Work with_ field: -https://dist.springsource.org/snapshot/GRECLIPSE/e4.7/ - -# Understand the basics -Not sure what a pull request is, or how to submit one? Take a look at GitHub's excellent [help documentation first](https://help.github.com/articles/using-pull-requests). - -# Search GitHub issues; create an issue if necessary -Is there already an issue that addresses your concern? Do a bit of searching in our [GitHub issues ](https://github.com/spring-projects/spring-security/issues) to see if you can find something similar. If not, please create a new issue before submitting a pull request unless the change is not a user facing issue. - -# Using GitHub Issues -We use GitHub issues to track bugs and enhancements. -If you have a general usage question please ask on [Stack Overflow](https://stackoverflow.com). -The Spring Security team and the broader community monitor the [`spring-security`](https://stackoverflow.com/tags/spring-security) tag. - -If you are reporting a bug, please help to speed up problem diagnosis by providing as much -information as possible. Ideally, that would include a small -[sample project](https://github.com/spring-projects/spring-boot-issues) that reproduces the -problem. - - - -# Discuss non-trivial contribution ideas with committers -If you're considering anything more than correcting a typo or fixing a minor bug, please discuss it on the [Spring Security Gitter](https://gitter.im/spring-projects/spring-security) before submitting a pull request. We're happy to provide guidance but please spend an hour or two researching the subject on your own including searching the forums for prior discussions. - -# Sign the Contributor License Agreement - -If you have not previously done so, please fill out and -submit the [Contributor License Agreement](https://cla.pivotal.io/sign/spring). - -# Create your branch from master -Create your topic branch to be submitted as a pull request from master. The Spring team will consider your pull request for backporting on a case-by-case basis; you don't need to worry about submitting anything for backporting. - -# Use short branch names -Branches used when submitting pull requests should preferably be named according to GitHub issues, e.g. 'gh-1234' or 'gh-1234-fix-npe'. Otherwise, use succinct, lower-case, dash (-) delimited names, such as 'fix-warnings', 'fix-typo', etc. This is important, because branch names show up in the merge commits that result from accepting pull requests, and should be as expressive and concise as possible. - -# Keep commits focused - -Remember each ticket should be focused on a single item of interest since the tickets are used to produce the changelog. Since each commit should be tied to a single GitHub issue, ensure that your commits are focused. For example, do not include an update to a transitive library in your commit unless the GitHub is to update the library. Reviewing your commits is essential before sending a pull request. - -# Mind the whitespace -Please carefully follow the whitespace and formatting conventions already present in the framework. - -1. Tabs, not spaces -1. Unix (LF), not dos (CRLF) line endings -1. Eliminate all trailing whitespace -1. Aim to wrap code at 120 characters, but favor readability over wrapping -1. Preserve existing formatting; i.e. do not reformat code for its own sake -1. Search the codebase using git grep and other tools to discover common naming conventions, etc. -1. UTF-8 encoding for Java sources - -Whitespace management tips - -1. You can use the [AnyEdit Eclipse plugin](https://marketplace.eclipse.org/content/anyedit-tools) to ensure spaces are used and to clean up trailing whitespaces. -1. Use git's pre-commit.sample hook to prevent invalid whitespace from being pushed out. You can enable it by moving ~/spring-security/.git/hooks/pre-commit.sample to ~/spring-security/.git/hooks/pre-commit and ensuring it is executable. For more information on hooks refer to [Pro Git's Pre-Commit Hook's section](https://git-scm.com/book/cs/ch7-3.html) - -# Add Apache license header to all new classes - -
-/*
- * Copyright 2002-2012 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 ...;
-
-# Update Apache license header to modified files as necessary -Always check the date range in the license header. For example, if you've modified a file in 2012 whose header still reads -
- * Copyright 2002-2011 the original author or authors.
-
-then be sure to update it to 2012 appropriately -
- * Copyright 2002-2012 the original author or authors.
-
-# Use @since tags for newly-added public API types and methods -e.g. -
-/**
- * ...
- *
- * @author First Last
- * @since 3.2
- * @see ...
- */
-
- -# Submit JUnit test cases for all behavior changes -Search the codebase to find related unit tests and add additional `@Test` methods within. - -1. Any new tests should end in the name Tests (note this is plural). For example, a valid name would be `FilterChainProxyTests`. An invalid name would be `FilterChainProxyTest`. -2. New test methods should not start with test. This is an old JUnit3 convention and is not necessary since the method is annotated with @Test. - -# Update spring-security-x.y.rnc for schema changes -Update the [RELAX NG](https://relaxng.org/) schema `spring-security-x.y.rnc` instead of `spring-security-x.y.xsd` if you contribute changes to supported XML configuration. The XML schema file can be generated the following Gradle task: - -
-./gradlew spring-security-config:rncToXsd
-
- -Changes to the XML schema will be overwritten by the Gradle build task. - -# Squash commits -Use `git rebase --interactive`, `git add --patch` and other tools to "squash" multiple commits into atomic changes. In addition to the man pages for git, there are many resources online to help you understand how these tools work. Here from the [Git SCM Book](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History). - -# Use real name in git commits -Please configure git to use your real first and last name for any commits you intend to submit as pull requests. For example, this is not acceptable: - -
-Author: Nickname <user@mail.com>
-
-Rather, please include your first and last name, properly capitalized, as submitted against the SpringSource contributor license agreement: -
-Author: First Last <user@mail.com>
-
-This helps ensure traceability against the CLA, and also goes a long way to ensuring useful output from tools like git shortlog and others. - -You can configure this globally via the account admin area GitHub (useful for fork-and-edit cases); globally with - -
-git config --global user.name "First Last"
-git config --global user.email user@mail.com
-
- -or locally for the spring-security repository only by omitting the '--global' flag: -
-cd spring-security
-git config user.name "First Last"
-git config user.email user@mail.com
-
- -# Format commit messages - -
-Short (50 chars or less) summary of changes
-
-More detailed explanatory text, if necessary.  Wrap it to about 72
-characters or so.  In some contexts, the first line is treated as the
-subject of an email and the rest of the text as the body.  The blank
-line separating the summary from the body is critical (unless you omit
-the body entirely); tools like rebase can get confused if you run the
-two together.
-
-Further paragraphs come after blank lines.
-
- - Bullet points are okay, too
-
- - Typically a hyphen or asterisk is used for the bullet, preceded by a
-   single space, with blank lines in between, but conventions vary here
-
-Fixes gh-123
-
- - -1. Keep the subject line to 50 characters or less if possible -2. Do not end the subject line with a period -3. In the body of the commit message, explain how things worked before this commit, what has changed, and how things work now -3. Include Fixes gh- at the end if this fixes a GitHub issue -5. Avoid markdown, including back-ticks identifying code - -# Run all tests prior to submission - -
-cd spring-security
-./gradlew clean build integrationTest
-
- -# Submit your pull request -Subject line: - -Follow the same conventions for pull request subject lines as mentioned above for commit message subject lines. - -In the body: - -1. Explain your use case. What led you to submit this change? Why were existing mechanisms in the framework insufficient? Make a case that this is a general-purpose problem and that yours is a general-purpose solution, etc -2. Add any additional information and ask questions; start a conversation, or continue one from GitHub Issues -3. Mention any GitHub Issues -4. Also mention that you have submitted the CLA as described above -Note that for pull requests containing a single commit, GitHub will default the subject line and body of the pull request to match the subject line and body of the commit message. This is fine, but please also include the items above in the body of the request. - -# Mention your pull request on the associated GitHub issue -Add a comment to the associated GitHub issue(s) linking to your new pull request. - -# Expect discussion and rework -The Spring team takes a very conservative approach to accepting contributions to the framework. This is to keep code quality and stability as high as possible, and to keep complexity at a minimum. Your changes, if accepted, may be heavily modified prior to merging. You will retain "Author:" attribution for your Git commits granted that the bulk of your changes remain intact. You may be asked to rework the submission for style (as explained above) and/or substance. Again, we strongly recommend discussing any serious submissions with the Spring Framework team prior to engaging in serious development work. - -Note that you can always force push (git push -f) reworked / rebased commits against the branch used to submit your pull request. i.e. you do not need to issue a new pull request when asked to make changes. diff --git a/Jenkinsfile b/Jenkinsfile index 0b04ac6ded3..8bcfaebb375 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -89,7 +89,7 @@ try { "GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USERNAME}", "GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PASSWORD}", "GRADLE_ENTERPRISE_ACCESS_KEY=${GRADLE_ENTERPRISE_ACCESS_KEY}"]) { - sh "./gradlew test -PforceMavenRepositories=snapshot -PspringVersion='5.+' -PreactorVersion=Dysprosium-BUILD-SNAPSHOT -PspringDataVersion=Lovelace-BUILD-SNAPSHOT -PlocksDisabled --stacktrace" + sh "./gradlew test -PforceMavenRepositories=snapshot -PspringVersion='5.+' -PreactorVersion=20+ -PspringDataVersion=Lovelace-BUILD-SNAPSHOT -PlocksDisabled --stacktrace" } } } catch(Exception e) { @@ -99,50 +99,6 @@ try { } } }, - jdk9: { - stage('JDK 9') { - node { - checkout scm - sh "git clean -dfx" - try { - withCredentials([GRADLE_ENTERPRISE_CACHE_USER, - GRADLE_ENTERPRISE_SECRET_ACCESS_KEY]) { - withEnv([jdkEnv("jdk9"), - "GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USERNAME}", - "GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PASSWORD}", - "GRADLE_ENTERPRISE_ACCESS_KEY=${GRADLE_ENTERPRISE_ACCESS_KEY}"]) { - sh "./gradlew test --stacktrace" - } - } - } catch(Exception e) { - currentBuild.result = 'FAILED: jdk9' - throw e - } - } - } - }, - jdk10: { - stage('JDK 10') { - node { - checkout scm - sh "git clean -dfx" - try { - withCredentials([GRADLE_ENTERPRISE_CACHE_USER, - GRADLE_ENTERPRISE_SECRET_ACCESS_KEY]) { - withEnv([jdkEnv("jdk10"), - "GRADLE_ENTERPRISE_CACHE_USERNAME=${GRADLE_ENTERPRISE_CACHE_USERNAME}", - "GRADLE_ENTERPRISE_CACHE_PASSWORD=${GRADLE_ENTERPRISE_CACHE_PASSWORD}", - "GRADLE_ENTERPRISE_ACCESS_KEY=${GRADLE_ENTERPRISE_ACCESS_KEY}"]) { - sh "./gradlew test --stacktrace" - } - } - } catch(Exception e) { - currentBuild.result = 'FAILED: jdk10' - throw e - } - } - } - }, jdk11: { stage('JDK 11') { node { diff --git a/license.txt b/LICENSE.txt similarity index 99% rename from license.txt rename to LICENSE.txt index 20e4bd85661..823c1c8e982 100644 --- a/license.txt +++ b/LICENSE.txt @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ @@ -178,7 +179,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" + boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -198,4 +199,4 @@ 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. + limitations under the License. \ No newline at end of file diff --git a/README.adoc b/README.adoc index bbc983ce2fb..43c13973c67 100644 --- a/README.adoc +++ b/README.adoc @@ -1,7 +1,5 @@ image::https://badges.gitter.im/Join%20Chat.svg[Gitter,link=https://gitter.im/spring-projects/spring-security?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge] -image:https://travis-ci.org/spring-projects/spring-security.svg?branch=master["Build Status", link="https://travis-ci.org/spring-projects/spring-security"] - = Spring Security Spring Security provides security services for the https://docs.spring.io[Spring IO Platform]. Spring Security 5.0 requires Spring 5.0 as @@ -10,8 +8,7 @@ a minimum and also requires Java 8. For a detailed list of features and access to the latest release, please visit https://spring.io/projects[Spring projects]. == Code of Conduct -This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct]. -By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. +Please see our https://github.com/spring-projects/.github/blob/master/CODE_OF_CONDUCT.md[code of conduct] == Downloading Artifacts See https://github.com/spring-projects/spring-framework/wiki/Downloading-Spring-artifacts[downloading Spring artifacts] for Maven repository information. @@ -59,7 +56,7 @@ Check out the https://stackoverflow.com/questions/tagged/spring-security[Spring https://spring.io/services[Commercial support] is available too. == Contributing -https://help.github.com/articles/creating-a-pull-request[Pull requests] are welcome; see the https://github.com/spring-projects/spring-security/blob/master/CONTRIBUTING.md[contributor guidelines] for details. +https://help.github.com/articles/creating-a-pull-request[Pull requests] are welcome; see the https://github.com/spring-projects/spring-security/blob/master/CONTRIBUTING.adoc[contributor guidelines] for details. == License Spring Security is Open Source software released under the diff --git a/acl/src/main/java/org/springframework/security/acls/domain/AccessControlEntryImpl.java b/acl/src/main/java/org/springframework/security/acls/domain/AccessControlEntryImpl.java index f39dbd30b54..fb41f101d6c 100644 --- a/acl/src/main/java/org/springframework/security/acls/domain/AccessControlEntryImpl.java +++ b/acl/src/main/java/org/springframework/security/acls/domain/AccessControlEntryImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * Copyright 2002-2016 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. @@ -131,10 +131,9 @@ public boolean equals(Object arg0) { @Override public int hashCode() { - int result = this.acl.hashCode(); - result = 31 * result + this.permission.hashCode(); + int result = this.permission.hashCode(); result = 31 * result + (this.id != null ? this.id.hashCode() : 0); - result = 31 * result + this.sid.hashCode(); + result = 31 * result + (this.sid.hashCode()); result = 31 * result + (this.auditFailure ? 1 : 0); result = 31 * result + (this.auditSuccess ? 1 : 0); result = 31 * result + (this.granting ? 1 : 0); diff --git a/acl/src/main/java/org/springframework/security/acls/jdbc/AclClassIdUtils.java b/acl/src/main/java/org/springframework/security/acls/jdbc/AclClassIdUtils.java index 677dbb5cb8c..65b5d80f549 100644 --- a/acl/src/main/java/org/springframework/security/acls/jdbc/AclClassIdUtils.java +++ b/acl/src/main/java/org/springframework/security/acls/jdbc/AclClassIdUtils.java @@ -118,7 +118,7 @@ private T convertFromStringTo(String identifier, Class< */ private Long convertToLong(Serializable identifier) { Long idAsLong; - if (canConvertFromStringTo(Long.class)) { + if (conversionService.canConvert(identifier.getClass(), Long.class)) { idAsLong = conversionService.convert(identifier, Long.class); } else { idAsLong = Long.valueOf(identifier.toString()); 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 03789529b93..f2cb89f9092 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 @@ -16,7 +16,7 @@ package org.springframework.security.acls.jdbc; import java.io.Serializable; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -99,7 +99,7 @@ public List findChildren(ObjectIdentity parentIdentity) { return new ObjectIdentityImpl(javaType, identifier); }); - if (objects.size() == 0) { + if (objects.isEmpty()) { return null; } @@ -108,7 +108,7 @@ public List findChildren(ObjectIdentity parentIdentity) { public Acl readAclById(ObjectIdentity object, List sids) throws NotFoundException { - Map map = readAclsById(Arrays.asList(object), sids); + Map map = readAclsById(Collections.singletonList(object), sids); Assert.isTrue(map.containsKey(object), () -> "There should have been an Acl entry for ObjectIdentity " + object); diff --git a/acl/src/test/java/org/springframework/security/acls/domain/AclImplTests.java b/acl/src/test/java/org/springframework/security/acls/domain/AclImplTests.java index f91d42a1bdd..7313048d20b 100644 --- a/acl/src/test/java/org/springframework/security/acls/domain/AclImplTests.java +++ b/acl/src/test/java/org/springframework/security/acls/domain/AclImplTests.java @@ -577,6 +577,25 @@ public void maskPermissionGrantingStrategy() { assertThat(acl.isGranted(permissions, sids, false)).isTrue(); } + @Test + public void hashCodeWithoutStackOverFlow() throws Exception { + //given + Sid sid = new PrincipalSid("pSid"); + ObjectIdentity oid = new ObjectIdentityImpl("type", 1); + AclAuthorizationStrategy authStrategy = new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("role")); + PermissionGrantingStrategy grantingStrategy = new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger()); + + AclImpl acl = new AclImpl(oid, 1L, authStrategy, grantingStrategy, null, null, false, sid); + AccessControlEntryImpl ace = new AccessControlEntryImpl(1L, acl, sid, BasePermission.READ, true, true, true); + + Field fieldAces = FieldUtils.getField(AclImpl.class, "aces"); + fieldAces.setAccessible(true); + List aces = (List) fieldAces.get(acl); + aces.add(ace); + //when - then none StackOverFlowError been raised + ace.hashCode(); + } + // ~ Inner Classes // ================================================================================================== diff --git a/acl/src/test/java/org/springframework/security/acls/jdbc/AclClassIdUtilsTest.java b/acl/src/test/java/org/springframework/security/acls/jdbc/AclClassIdUtilsTest.java index 65e7fd83fc3..5dd2e36f311 100644 --- a/acl/src/test/java/org/springframework/security/acls/jdbc/AclClassIdUtilsTest.java +++ b/acl/src/test/java/org/springframework/security/acls/jdbc/AclClassIdUtilsTest.java @@ -24,6 +24,7 @@ import org.springframework.core.convert.ConversionService; import java.io.Serializable; +import java.math.BigInteger; import java.sql.ResultSet; import java.sql.SQLException; import java.util.UUID; @@ -39,6 +40,7 @@ public class AclClassIdUtilsTest { private static final Long DEFAULT_IDENTIFIER = 999L; + private static final BigInteger BIGINT_IDENTIFIER = new BigInteger("999"); private static final String DEFAULT_IDENTIFIER_AS_STRING = DEFAULT_IDENTIFIER.toString(); @Mock @@ -62,6 +64,15 @@ public void shouldReturnLongIfIdentifierIsLong() throws SQLException { assertThat(newIdentifier).isEqualTo(DEFAULT_IDENTIFIER); } + @Test + public void shouldReturnLongIfIdentifierIsBigInteger() throws SQLException { + // when + Serializable newIdentifier = aclClassIdUtils.identifierFrom(BIGINT_IDENTIFIER, resultSet); + + // then + assertThat(newIdentifier).isEqualTo(DEFAULT_IDENTIFIER); + } + @Test public void shouldReturnLongIfClassIdTypeIsNull() throws SQLException { // given diff --git a/build.gradle b/build.gradle index 2f3dd3a9120..ace768def7d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ buildscript { dependencies { - classpath 'io.spring.gradle:spring-build-conventions:0.0.31.RELEASE' + classpath 'io.spring.gradle:spring-build-conventions:0.0.33.RELEASE' classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" - classpath 'io.spring.nohttp:nohttp-gradle:0.0.2.RELEASE' - classpath "io.freefair.gradle:aspectj-plugin:4.0.2" + classpath 'io.spring.nohttp:nohttp-gradle:0.0.5.RELEASE' + classpath "io.freefair.gradle:aspectj-plugin:5.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } repositories { @@ -39,3 +39,7 @@ subprojects { options.encoding = "UTF-8" } } + +nohttp { + allowlistFile = project.file("etc/nohttp/allowlist.lines") +} diff --git a/config/spring-security-config.gradle b/config/spring-security-config.gradle index e5d631121c2..d641eda0ab3 100644 --- a/config/spring-security-config.gradle +++ b/config/spring-security-config.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + apply plugin: 'io.spring.convention.spring-module' apply plugin: 'trang' apply plugin: 'kotlin' @@ -39,6 +41,7 @@ dependencies { testCompile project(path : ':spring-security-core', configuration : 'tests') testCompile project(path : ':spring-security-oauth2-client', configuration : 'tests') testCompile project(path : ':spring-security-oauth2-resource-server', configuration : 'tests') + testCompile project(path : ':spring-security-saml2-service-provider', configuration : 'tests') testCompile project(path : ':spring-security-web', configuration : 'tests') testCompile apachedsDependencies testCompile powerMock2Dependencies @@ -85,11 +88,11 @@ rncToXsd { xslFile = new File(rncDir, 'spring-security.xsl') } -compileKotlin { - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs = ["-Xjsr305=strict"] - } +tasks.withType(KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = "1.8" + freeCompilerArgs = ["-Xjsr305=strict"] + } } build.dependsOn rncToXsd diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderBuilderSecurityBuilderTests.java b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderBuilderSecurityBuilderTests.java index 34a4be0934d..9ff0675bc20 100644 --- a/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderBuilderSecurityBuilderTests.java +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderBuilderSecurityBuilderTests.java @@ -42,7 +42,7 @@ import java.io.IOException; import java.net.ServerSocket; import java.util.List; - +import javax.naming.directory.SearchControls; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; @@ -67,6 +67,8 @@ public void defaultConfiguration() { assertThat(authoritiesPopulator).hasFieldOrPropertyWithValue("groupRoleAttribute", "cn"); assertThat(authoritiesPopulator).hasFieldOrPropertyWithValue("groupSearchBase", ""); assertThat(authoritiesPopulator).hasFieldOrPropertyWithValue("groupSearchFilter", "(uniqueMember={0})"); + assertThat(authoritiesPopulator).extracting("searchControls").hasFieldOrPropertyWithValue("searchScope", + SearchControls.ONELEVEL_SCOPE); assertThat(ReflectionTestUtils.getField(getAuthoritiesMapper(provider), "prefix")).isEqualTo("ROLE_"); } @@ -124,6 +126,29 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { // @formatter:on } + @Test + public void groupSubtreeSearchCustom() { + this.spring.register(GroupSubtreeSearchConfig.class).autowire(); + LdapAuthenticationProvider provider = ldapProvider(); + + assertThat(ReflectionTestUtils.getField(getAuthoritiesPopulator(provider), "searchControls")) + .extracting("searchScope").isEqualTo(SearchControls.SUBTREE_SCOPE); + } + + @EnableWebSecurity + static class GroupSubtreeSearchConfig extends BaseLdapProviderConfig { + // @formatter:off + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .contextSource(contextSource()) + .userDnPatterns("uid={0},ou=people") + .groupSearchFilter("ou=groupName") + .groupSearchSubtree(true); + } + // @formatter:on + } + @Test public void rolePrefixCustom() { this.spring.register(RolePrefixConfig.class).autowire(); diff --git a/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderConfigurerTests.java b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderConfigurerTests.java index a2cdc44e881..3cf29d63386 100644 --- a/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderConfigurerTests.java +++ b/config/src/integration-test/java/org/springframework/security/config/annotation/authentication/ldap/LdapAuthenticationProviderConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -18,11 +18,14 @@ import org.junit.Rule; import org.junit.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.ldap.LdapAuthenticationProviderBuilderSecurityBuilderTests.BaseLdapProviderConfig; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.web.servlet.MockMvc; @@ -61,6 +64,23 @@ public void authenticationManagerSupportMultipleLdapContextWithCustomRolePrefix( .andExpect(authenticated().withUsername("bob").withAuthorities(singleton(new SimpleGrantedAuthority("ROL_DEVELOPERS")))); } + @Test + public void authenticationManagerWhenPortZeroThenAuthenticates() throws Exception { + this.spring.register(LdapWithRandomPortConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("bob").password("bobspassword")) + .andExpect(authenticated().withUsername("bob")); + } + + @Test + public void authenticationManagerWhenSearchSubtreeThenNestedGroupFound() throws Exception { + this.spring.register(GroupSubtreeSearchConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("ben").password("benspassword")) + .andExpect(authenticated().withUsername("ben").withAuthorities( + AuthorityUtils.createAuthorityList("ROLE_SUBMANAGERS", "ROLE_MANAGERS", "ROLE_DEVELOPERS"))); + } + @EnableWebSecurity static class MultiLdapAuthenticationProvidersConfig extends WebSecurityConfigurerAdapter { // @formatter:off @@ -98,4 +118,32 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } // @formatter:on } + + @EnableWebSecurity + static class LdapWithRandomPortConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .groupSearchBase("ou=groups") + .groupSearchFilter("(member={0})") + .userDnPatterns("uid={0},ou=people") + .contextSource() + .port(0); + } + } + + @EnableWebSecurity + static class GroupSubtreeSearchConfig extends BaseLdapProviderConfig { + // @formatter:off + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .ldapAuthentication() + .groupSearchBase("ou=groups") + .groupSearchFilter("(member={0})") + .groupSearchSubtree(true) + .userDnPatterns("uid={0},ou=people"); + } + // @formatter:on + } } diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapProviderBeanDefinitionParserTests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapProviderBeanDefinitionParserTests.java index eefa4e2f77e..f2a954fed19 100644 --- a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapProviderBeanDefinitionParserTests.java +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapProviderBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -16,8 +16,11 @@ package org.springframework.security.config.ldap; +import java.text.MessageFormat; + import org.junit.After; import org.junit.Test; + import org.springframework.context.ApplicationContextException; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; @@ -29,8 +32,6 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper; -import java.text.MessageFormat; - import static org.assertj.core.api.Assertions.assertThat; public class LdapProviderBeanDefinitionParserTests { @@ -46,7 +47,7 @@ public void closeAppContext() { @Test public void simpleProviderAuthenticatesCorrectly() { - appCtx = new InMemoryXmlApplicationContext("" + appCtx = new InMemoryXmlApplicationContext("" + "" + " " + "" @@ -60,7 +61,7 @@ public void simpleProviderAuthenticatesCorrectly() { @Test public void multipleProvidersAreSupported() { - appCtx = new InMemoryXmlApplicationContext("" + appCtx = new InMemoryXmlApplicationContext("" + "" + " " + " " @@ -84,7 +85,7 @@ public void missingServerEltCausesConfigException() { @Test public void supportsPasswordComparisonAuthentication() { - appCtx = new InMemoryXmlApplicationContext("" + appCtx = new InMemoryXmlApplicationContext("" + "" + " " + " " @@ -100,7 +101,7 @@ public void supportsPasswordComparisonAuthentication() { @Test public void supportsPasswordComparisonAuthenticationWithPasswordEncoder() { - appCtx = new InMemoryXmlApplicationContext("" + appCtx = new InMemoryXmlApplicationContext("" + "" + " " + " " @@ -120,7 +121,7 @@ public void supportsPasswordComparisonAuthenticationWithPasswordEncoder() { // SEC-2472 @Test public void supportsCryptoPasswordEncoder() { - appCtx = new InMemoryXmlApplicationContext("" + appCtx = new InMemoryXmlApplicationContext("" + "" + " " + " " @@ -139,7 +140,7 @@ public void supportsCryptoPasswordEncoder() { @Test public void inetOrgContextMapperIsSupported() { - appCtx = new InMemoryXmlApplicationContext("" + appCtx = new InMemoryXmlApplicationContext("" + "" + " " + "" diff --git a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParserTests.java b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParserTests.java index 9b2c07cb174..564352400b2 100644 --- a/config/src/integration-test/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParserTests.java +++ b/config/src/integration-test/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -15,13 +15,12 @@ */ package org.springframework.security.config.ldap; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.IOException; import java.net.ServerSocket; import org.junit.After; import org.junit.Test; + import org.springframework.ldap.core.LdapTemplate; import org.springframework.security.config.BeanIds; import org.springframework.security.config.util.InMemoryXmlApplicationContext; @@ -29,6 +28,8 @@ import org.springframework.security.ldap.server.ApacheDSContainer; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Luke Taylor * @author Rob Winch @@ -47,7 +48,7 @@ public void closeAppContext() { @Test public void embeddedServerCreationContainsExpectedContextSourceAndData() { appCtx = new InMemoryXmlApplicationContext( - ""); + ""); DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx .getBean(BeanIds.CONTEXT_SOURCE); @@ -82,7 +83,7 @@ public void useOfUrlAttributeCreatesCorrectContextSource() throws Exception { @Test public void loadingSpecificLdifFileIsSuccessful() { appCtx = new InMemoryXmlApplicationContext( - ""); + ""); DefaultSpringSecurityContextSource contextSource = (DefaultSpringSecurityContextSource) appCtx .getBean(BeanIds.CONTEXT_SOURCE); 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 d3bb38032bf..fc044e24e24 100644 --- a/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -20,6 +20,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.xml.BeanDefinitionDecorator; @@ -45,8 +48,6 @@ import org.springframework.security.config.websocket.WebSocketMessageBrokerSecurityBeanDefinitionParser; import org.springframework.security.core.SpringSecurityCoreVersion; import org.springframework.util.ClassUtils; -import org.w3c.dom.Element; -import org.w3c.dom.Node; /** * Parses elements from the "security" namespace @@ -87,7 +88,7 @@ public BeanDefinition parse(Element element, ParserContext pc) { 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.3. Please update your schema declarations to the 5.3 schema.", + + "with Spring Security 5.4. Please update your schema declarations to the 5.4 schema.", element); } String name = pc.getDelegate().getLocalName(element); @@ -223,7 +224,7 @@ private boolean namespaceMatchesVersion(Element element) { private boolean matchesVersionInternal(Element element) { String schemaLocation = element.getAttributeNS( "http://www.w3.org/2001/XMLSchema-instance", "schemaLocation"); - return schemaLocation.matches("(?m).*spring-security-5\\.3.*.xsd.*") + return schemaLocation.matches("(?m).*spring-security-5\\.4.*.xsd.*") || schemaLocation.matches("(?m).*spring-security.xsd.*") || !schemaLocation.matches("(?m).*spring-security.*"); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java index 478076c7d39..9c6e2bae855 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/ldap/LdapAuthenticationProviderConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -21,7 +21,6 @@ import org.springframework.ldap.core.support.BaseLdapPathContextSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder; @@ -29,6 +28,7 @@ import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.ldap.DefaultSpringSecurityContextSource; import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator; import org.springframework.security.ldap.authentication.BindAuthenticator; @@ -61,6 +61,7 @@ public class LdapAuthenticationProviderConfigurer { private String groupRoleAttribute = "cn"; private String groupSearchBase = ""; + private boolean groupSearchSubtree = false; private String groupSearchFilter = "(uniqueMember={0})"; private String rolePrefix = "ROLE_"; private String userSearchBase = ""; // only for search @@ -130,6 +131,7 @@ private LdapAuthoritiesPopulator getLdapAuthoritiesPopulator() { contextSource, groupSearchBase); defaultAuthoritiesPopulator.setGroupRoleAttribute(groupRoleAttribute); defaultAuthoritiesPopulator.setGroupSearchFilter(groupSearchFilter); + defaultAuthoritiesPopulator.setSearchSubtree(groupSearchSubtree); defaultAuthoritiesPopulator.setRolePrefix(this.rolePrefix); this.ldapAuthoritiesPopulator = defaultAuthoritiesPopulator; @@ -320,6 +322,19 @@ public LdapAuthenticationProviderConfigurer groupSearchBase(String groupSearc return this; } + /** + * If set to true, a subtree scope search will be performed for group membership. If false a + * single-level search is used. + * + * @param searchSubtree set to true to enable searching of the entire tree below the + * groupSearchBase. + * @return the {@link LdapAuthenticationProviderConfigurer} for further customizations + */ + public LdapAuthenticationProviderConfigurer groupSearchSubtree(boolean groupSearchSubtree) { + this.groupSearchSubtree = groupSearchSubtree; + return this; + } + /** * The LDAP filter to search for groups. Defaults to "(uniqueMember={0})". The * substituted parameter is the DN of the user. @@ -427,14 +442,20 @@ private PasswordCompareConfigurer() { * embedded LDAP instance. * * @author Rob Winch + * @author Evgeniy Cheban * @since 3.2 */ public final class ContextSourceBuilder { + private static final String APACHEDS_CLASSNAME = "org.apache.directory.server.core.DefaultDirectoryService"; + 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 String ldif = "classpath*:*.ldif"; private String managerPassword; private String managerDn; private Integer port; - private static final int DEFAULT_PORT = 33389; private String root = "dc=springframework,dc=org"; private String url; @@ -478,6 +499,9 @@ public ContextSourceBuilder managerPassword(String managerPassword) { /** * 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 * @return the {@link ContextSourceBuilder} for further customization */ @@ -522,6 +546,10 @@ public LdapAuthenticationProviderConfigurer and() { } private DefaultSpringSecurityContextSource build() throws Exception { + if (this.url == null) { + startEmbeddedLdapServer(); + } + DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource( getProviderUrl()); if (managerDn != null) { @@ -533,20 +561,25 @@ private DefaultSpringSecurityContextSource build() throws Exception { contextSource.setPassword(managerPassword); } contextSource = postProcess(contextSource); - if (url != null) { - return contextSource; - } - if (ClassUtils.isPresent("org.apache.directory.server.core.DefaultDirectoryService", getClass().getClassLoader())) { - ApacheDSContainer apacheDsContainer = new ApacheDSContainer(root, ldif); + return contextSource; + } + + private void startEmbeddedLdapServer() throws Exception { + if (ClassUtils.isPresent(APACHEDS_CLASSNAME, getClass().getClassLoader())) { + ApacheDSContainer apacheDsContainer = new ApacheDSContainer(this.root, this.ldif); apacheDsContainer.setPort(getPort()); postProcess(apacheDsContainer); + this.port = apacheDsContainer.getLocalPort(); } - else if (ClassUtils.isPresent("com.unboundid.ldap.listener.InMemoryDirectoryServer", getClass().getClassLoader())) { - UnboundIdContainer unboundIdContainer = new UnboundIdContainer(root, ldif); + else if (ClassUtils.isPresent(UNBOUNDID_CLASSNAME, getClass().getClassLoader())) { + UnboundIdContainer unboundIdContainer = new UnboundIdContainer(this.root, this.ldif); unboundIdContainer.setPort(getPort()); postProcess(unboundIdContainer); + this.port = unboundIdContainer.getPort(); + } + else { + throw new IllegalStateException("Embedded LDAP server is not provided"); } - return contextSource; } private int getPort() { @@ -557,29 +590,10 @@ private int getPort() { } private int getDefaultPort() { - ServerSocket serverSocket = null; - try { - try { - serverSocket = new ServerSocket(DEFAULT_PORT); - } - catch (IOException e) { - try { - serverSocket = new ServerSocket(0); - } - catch (IOException e2) { - return DEFAULT_PORT; - } - } + try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { return serverSocket.getLocalPort(); - } - finally { - if (serverSocket != null) { - try { - serverSocket.close(); - } - catch (IOException e) { - } - } + } catch (IOException e) { + return RANDOM_PORT; } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java index e169bed3a25..3c61557c4c1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/configuration/ObjectPostProcessorConfiguration.java @@ -16,8 +16,10 @@ package org.springframework.security.config.annotation.configuration; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -34,9 +36,11 @@ * @since 3.2 */ @Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class ObjectPostProcessorConfiguration { @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public ObjectPostProcessor objectPostProcessor( AutowireCapableBeanFactory beanFactory) { return new AutowireBeanFactoryObjectPostProcessor(beanFactory); diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java index e18e23a27da..eb3207f823c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.*; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; @@ -80,6 +81,7 @@ * @see EnableGlobalMethodSecurity */ @Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class GlobalMethodSecurityConfiguration implements ImportAware, SmartInitializingSingleton, BeanFactoryAware { private static final Log logger = LogFactory diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MetadataSourceConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MetadataSourceConfiguration.java index bfb920d90d0..5c98bf48fe1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MetadataSourceConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/Jsr250MetadataSourceConfiguration.java @@ -15,14 +15,18 @@ */ package org.springframework.security.config.annotation.method.configuration; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; import org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource; @Configuration(proxyBeanMethods = false) +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) class Jsr250MetadataSourceConfiguration { @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public Jsr250MethodSecurityMetadataSource jsr250MethodSecurityMetadataSource() { return new Jsr250MethodSecurityMetadataSource(); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java index 8c95784c982..b1ba9ae5d82 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/ReactiveMethodSecurityConfiguration.java @@ -54,6 +54,7 @@ public MethodSecurityMetadataSourceAdvisor methodSecurityInterceptor(AbstractMet } @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public DelegatingMethodSecurityMetadataSource methodMetadataSource(MethodSecurityExpressionHandler methodSecurityExpressionHandler) { ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory( methodSecurityExpressionHandler); @@ -74,6 +75,7 @@ public PrePostAdviceReactiveMethodInterceptor securityMethodInterceptor(Abstract } @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); if (this.grantedAuthorityDefaults != null) { 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 83e363ad9c7..bda590ade01 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 @@ -233,7 +233,9 @@ private ApplicationContext getContext() { * * * @return the {@link OpenIDLoginConfigurer} for further customizations. - * + * @deprecated 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. * @throws Exception * @see OpenIDLoginConfigurer */ @@ -355,6 +357,9 @@ public OpenIDLoginConfigurer openidLogin() throws Exception { * * @param openidLoginCustomizer the {@link Customizer} to provide more options for * the {@link OpenIDLoginConfigurer} + * @deprecated 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. * @return the {@link HttpSecurity} for further customizations * @throws Exception */ @@ -1105,6 +1110,7 @@ public HttpSecurity rememberMe(Customizer> re /** * Allows restricting access based upon the {@link HttpServletRequest} using + * {@link RequestMatcher} implementations (i.e. via URL patterns). * *

Example Configurations

* @@ -1471,7 +1477,7 @@ public HttpSecurity servletApi(Customizer> se * } * * - * @return the {@link ServletApiConfigurer} for further customizations + * @return the {@link CsrfConfigurer} for further customizations * @throws Exception */ public CsrfConfigurer csrf() throws Exception { 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 e08b1eae971..e1abe8b5dbc 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-2018 the original author or authors. + * 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. @@ -31,6 +31,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.config.annotation.AbstractConfiguredSecurityBuilder; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; @@ -49,8 +50,9 @@ import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.debug.DebugFilter; -import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.firewall.HttpFirewall; +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.util.Assert; @@ -74,6 +76,7 @@ * @see WebSecurityConfiguration * * @author Rob Winch + * @author Evgeniy Cheban * @since 3.2 */ public final class WebSecurity extends @@ -91,6 +94,8 @@ public final class WebSecurity extends private HttpFirewall httpFirewall; + private RequestRejectedHandler requestRejectedHandler; + private boolean debugEnabled; private WebInvocationPrivilegeEvaluator privilegeEvaluator; @@ -295,6 +300,9 @@ protected Filter performBuild() throws Exception { if (httpFirewall != null) { filterChainProxy.setFirewall(httpFirewall); } + if (requestRejectedHandler != null) { + filterChainProxy.setRequestRejectedHandler(requestRejectedHandler); + } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; @@ -383,6 +391,11 @@ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.defaultWebSecurityExpressionHandler .setApplicationContext(applicationContext); + + try { + this.defaultWebSecurityExpressionHandler.setRoleHierarchy(applicationContext.getBean(RoleHierarchy.class)); + } catch (NoSuchBeanDefinitionException e) {} + try { this.defaultWebSecurityExpressionHandler.setPermissionEvaluator(applicationContext.getBean( PermissionEvaluator.class)); @@ -392,5 +405,8 @@ public void setApplicationContext(ApplicationContext applicationContext) try { this.httpFirewall = applicationContext.getBean(HttpFirewall.class); } catch(NoSuchBeanDefinitionException e) {} + try { + this.requestRejectedHandler = applicationContext.getBean(RequestRejectedHandler.class); + } catch(NoSuchBeanDefinitionException e) {} } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index b29212d79ef..892fb394f0d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -20,6 +20,7 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.type.AnnotationMetadata; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; @@ -33,7 +34,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.List; -import java.util.Optional; /** * {@link Configuration} for OAuth 2.0 Client support. @@ -67,47 +67,69 @@ static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer private ClientRegistrationRepository clientRegistrationRepository; private OAuth2AuthorizedClientRepository authorizedClientRepository; private OAuth2AccessTokenResponseClient accessTokenResponseClient; + private OAuth2AuthorizedClientManager authorizedClientManager; @Override public void addArgumentResolvers(List argumentResolvers) { - if (this.clientRegistrationRepository != null && this.authorizedClientRepository != null) { - OAuth2AuthorizedClientProviderBuilder authorizedClientProviderBuilder = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .password(); - if (this.accessTokenResponseClient != null) { - authorizedClientProviderBuilder.clientCredentials(configurer -> - configurer.accessTokenResponseClient(this.accessTokenResponseClient)); - } else { - authorizedClientProviderBuilder.clientCredentials(); - } - OAuth2AuthorizedClientProvider authorizedClientProvider = authorizedClientProviderBuilder.build(); - DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( - this.clientRegistrationRepository, this.authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + OAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager(); + if (authorizedClientManager != null) { argumentResolvers.add(new OAuth2AuthorizedClientArgumentResolver(authorizedClientManager)); } } @Autowired(required = false) - public void setClientRegistrationRepository(List clientRegistrationRepositories) { + void setClientRegistrationRepository(List clientRegistrationRepositories) { if (clientRegistrationRepositories.size() == 1) { this.clientRegistrationRepository = clientRegistrationRepositories.get(0); } } @Autowired(required = false) - public void setAuthorizedClientRepository(List authorizedClientRepositories) { + void setAuthorizedClientRepository(List authorizedClientRepositories) { if (authorizedClientRepositories.size() == 1) { this.authorizedClientRepository = authorizedClientRepositories.get(0); } } - @Autowired - public void setAccessTokenResponseClient( - Optional> accessTokenResponseClient) { - accessTokenResponseClient.ifPresent(client -> this.accessTokenResponseClient = client); + @Autowired(required = false) + void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient accessTokenResponseClient) { + this.accessTokenResponseClient = accessTokenResponseClient; + } + + @Autowired(required = false) + void setAuthorizedClientManager(List authorizedClientManagers) { + if (authorizedClientManagers.size() == 1) { + this.authorizedClientManager = authorizedClientManagers.get(0); + } + } + + private OAuth2AuthorizedClientManager getAuthorizedClientManager() { + if (this.authorizedClientManager != null) { + return this.authorizedClientManager; + } + + OAuth2AuthorizedClientManager authorizedClientManager = null; + if (this.clientRegistrationRepository != null && this.authorizedClientRepository != null) { + if (this.accessTokenResponseClient != null) { + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials(configurer -> + configurer.accessTokenResponseClient(this.accessTokenResponseClient)) + .password() + .build(); + DefaultOAuth2AuthorizedClientManager defaultAuthorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + this.clientRegistrationRepository, this.authorizedClientRepository); + defaultAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + authorizedClientManager = defaultAuthorizedClientManager; + } else { + authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( + this.clientRegistrationRepository, this.authorizedClientRepository); + } + } + return authorizedClientManager; } } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java index 8943fa6dd99..597edc90a21 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java @@ -187,7 +187,7 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { /** * Creates the {@link HttpSecurity} or returns the current instance * - * ] * @return the {@link HttpSecurity} + * @return the {@link HttpSecurity} * @throws Exception */ @SuppressWarnings({ "rawtypes", "unchecked" }) 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 e53be5d8bba..5d6d2c22f07 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 @@ -21,6 +21,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.GenericApplicationListenerAdapter; @@ -678,6 +679,9 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) { } private SessionRegistry getSessionRegistry(H http) { + if (this.sessionRegistry == null) { + this.sessionRegistry = getBeanOrNull(SessionRegistry.class); + } if (this.sessionRegistry == null) { SessionRegistryImpl sessionRegistry = new SessionRegistryImpl(); registerDelegateApplicationListener(http, sessionRegistry); @@ -688,15 +692,10 @@ private SessionRegistry getSessionRegistry(H http) { private void registerDelegateApplicationListener(H http, ApplicationListener delegate) { - ApplicationContext context = http.getSharedObject(ApplicationContext.class); - if (context == null) { + DelegatingApplicationListener delegating = getBeanOrNull(DelegatingApplicationListener.class); + if (delegating == null) { return; } - if (context.getBeansOfType(DelegatingApplicationListener.class).isEmpty()) { - return; - } - DelegatingApplicationListener delegating = context - .getBean(DelegatingApplicationListener.class); SmartApplicationListener smartListener = new GenericApplicationListenerAdapter( delegate); delegating.addListener(smartListener); @@ -717,4 +716,17 @@ private boolean isConcurrentSessionControlEnabled() { private static SessionAuthenticationStrategy createDefaultSessionFixationProtectionStrategy() { return new ChangeSessionIdAuthenticationStrategy(); } + + private T getBeanOrNull(Class type) { + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + if (context == null) { + return null; + } + try { + return context.getBean(type); + } + catch (NoSuchBeanDefinitionException e) { + return null; + } + } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index b5c0db06bfd..af2f56e0cd5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -79,6 +79,7 @@ * * * @author Joe Grandja + * @author Parikshit Dutta * @since 5.1 * @see OAuth2AuthorizationRequestRedirectFilter * @see OAuth2AuthorizationCodeGrantFilter @@ -256,6 +257,10 @@ private OAuth2AuthorizationCodeGrantFilter createAuthorizationCodeGrantFilter(B if (this.authorizationRequestRepository != null) { authorizationCodeGrantFilter.setAuthorizationRequestRepository(this.authorizationRequestRepository); } + RequestCache requestCache = builder.getSharedObject(RequestCache.class); + if (requestCache != null) { + authorizationCodeGrantFilter.setRequestCache(requestCache); + } return authorizationCodeGrantFilter; } 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 4e2ef7062a2..e28f27e2f7c 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 @@ -123,6 +123,7 @@ * * * @author Josh Cummings + * @author Evgeniy Cheban * @since 5.1 * @see BearerTokenAuthenticationFilter * @see JwtAuthenticationProvider @@ -280,8 +281,7 @@ public class JwtConfigurer { private AuthenticationManager authenticationManager; private JwtDecoder decoder; - private Converter jwtAuthenticationConverter = - new JwtAuthenticationConverter(); + private Converter jwtAuthenticationConverter; JwtConfigurer(ApplicationContext context) { this.context = context; @@ -315,6 +315,14 @@ public OAuth2ResourceServerConfigurer and() { } Converter getJwtAuthenticationConverter() { + if (this.jwtAuthenticationConverter == null) { + if (this.context.getBeanNamesForType(JwtAuthenticationConverter.class).length > 0) { + this.jwtAuthenticationConverter = this.context.getBean(JwtAuthenticationConverter.class); + } else { + this.jwtAuthenticationConverter = new JwtAuthenticationConverter(); + } + } + return this.jwtAuthenticationConverter; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java index 76c24f7e0d4..4fa74f00538 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java @@ -118,6 +118,9 @@ * * * @author Rob Winch + * @deprecated 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. * @since 3.2 */ public final class OpenIDLoginConfigurer> extends 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 b9535203658..73f33203c18 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 @@ -16,6 +16,10 @@ package org.springframework.security.config.annotation.web.configurers.saml2; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.servlet.Filter; + import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; @@ -37,10 +41,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; -import java.util.LinkedHashMap; -import java.util.Map; -import javax.servlet.Filter; - import static org.springframework.util.StringUtils.hasText; /** @@ -323,10 +323,9 @@ private AuthenticationRequestEndpointConfig() { private Filter build(B http) { Saml2AuthenticationRequestFactory authenticationRequestResolver = getResolver(http); - Saml2WebSsoAuthenticationRequestFilter authenticationRequestFilter = - new Saml2WebSsoAuthenticationRequestFilter(Saml2LoginConfigurer.this.relyingPartyRegistrationRepository); - authenticationRequestFilter.setAuthenticationRequestFactory(authenticationRequestResolver); - return authenticationRequestFilter; + return postProcess(new Saml2WebSsoAuthenticationRequestFilter( + Saml2LoginConfigurer.this.relyingPartyRegistrationRepository, + authenticationRequestResolver)); } private Saml2AuthenticationRequestFactory getResolver(B http) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index a6c7f4783f6..b7d4b3e3c7c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -18,6 +18,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -87,11 +88,11 @@ void setUserDetailsPasswordService(ReactiveUserDetailsPasswordService userDetail @Bean public WebFluxConfigurer authenticationPrincipalArgumentResolverConfigurer( - AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver) { + ObjectProvider authenticationPrincipalArgumentResolver) { return new WebFluxConfigurer() { @Override public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { - configurer.addCustomResolver(authenticationPrincipalArgumentResolver); + configurer.addCustomResolver(authenticationPrincipalArgumentResolver.getObject()); } }; } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index fcfcbb5af18..3e82001c40b 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -15,18 +15,8 @@ */ package org.springframework.security.config.http; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import javax.servlet.http.HttpServletRequest; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.w3c.dom.Element; - import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; @@ -63,8 +53,18 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +import javax.servlet.http.HttpServletRequest; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; import static org.springframework.security.config.http.SecurityFilters.ANONYMOUS_FILTER; import static org.springframework.security.config.http.SecurityFilters.BASIC_AUTH_FILTER; @@ -160,12 +160,16 @@ final class AuthenticationConfigBuilder { private String openIDLoginPage; + private boolean oauth2LoginEnabled; + private boolean defaultAuthorizedClientRepositoryRegistered; private String oauth2LoginFilterId; private BeanDefinition oauth2AuthorizationRequestRedirectFilter; private BeanDefinition oauth2LoginEntryPoint; private BeanReference oauth2LoginAuthenticationProviderRef; private BeanReference oauth2LoginOidcAuthenticationProviderRef; private BeanDefinition oauth2LoginLinks; + + private boolean oauth2ClientEnabled; private BeanDefinition authorizationRequestRedirectFilter; private BeanDefinition authorizationCodeGrantFilter; private BeanReference authorizationCodeAuthenticationProviderRef; @@ -196,8 +200,7 @@ final class AuthenticationConfigBuilder { createBasicFilter(authenticationManager); createBearerTokenAuthenticationFilter(authenticationManager); createFormLoginFilter(sessionStrategy, authenticationManager); - createOAuth2LoginFilter(sessionStrategy, authenticationManager); - createOAuth2ClientFilter(requestCache, authenticationManager); + createOAuth2ClientFilters(sessionStrategy, requestCache, authenticationManager); createOpenIDLoginFilter(sessionStrategy, authenticationManager); createX509Filter(authenticationManager); createJeeFilter(authenticationManager); @@ -274,15 +277,27 @@ void createFormLoginFilter(BeanReference sessionStrategy, BeanReference authMana } } + void createOAuth2ClientFilters(BeanReference sessionStrategy, BeanReference requestCache, + BeanReference authenticationManager) { + createOAuth2LoginFilter(sessionStrategy, authenticationManager); + createOAuth2ClientFilter(requestCache, authenticationManager); + registerOAuth2ClientPostProcessors(); + } + void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager) { Element oauth2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_LOGIN); if (oauth2LoginElt == null) { return; } + this.oauth2LoginEnabled = true; OAuth2LoginBeanDefinitionParser parser = new OAuth2LoginBeanDefinitionParser(requestCache, portMapper, portResolver, sessionStrategy, allowSessionCreation); BeanDefinition oauth2LoginFilterBean = parser.parse(oauth2LoginElt, this.pc); + + BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository(); + registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository); + oauth2LoginFilterBean.getPropertyValues().addPropertyValue("authenticationManager", authManager); // retrieve the other bean result @@ -319,11 +334,15 @@ void createOAuth2ClientFilter(BeanReference requestCache, BeanReference authenti if (oauth2ClientElt == null) { return; } + this.oauth2ClientEnabled = true; OAuth2ClientBeanDefinitionParser parser = new OAuth2ClientBeanDefinitionParser( requestCache, authenticationManager); parser.parse(oauth2ClientElt, this.pc); + BeanDefinition defaultAuthorizedClientRepository = parser.getDefaultAuthorizedClientRepository(); + registerDefaultAuthorizedClientRepositoryIfNecessary(defaultAuthorizedClientRepository); + this.authorizationRequestRedirectFilter = parser.getAuthorizationRequestRedirectFilter(); String authorizationRequestRedirectFilterId = pc.getReaderContext() .generateBeanName(this.authorizationRequestRedirectFilter); @@ -344,57 +363,35 @@ void createOAuth2ClientFilter(BeanReference requestCache, BeanReference authenti this.authorizationCodeAuthenticationProviderRef = new RuntimeBeanReference(authorizationCodeAuthenticationProviderId); } + void registerDefaultAuthorizedClientRepositoryIfNecessary(BeanDefinition defaultAuthorizedClientRepository) { + if (!this.defaultAuthorizedClientRepositoryRegistered && defaultAuthorizedClientRepository != null) { + String authorizedClientRepositoryId = pc.getReaderContext() + .generateBeanName(defaultAuthorizedClientRepository); + this.pc.registerBeanComponent(new BeanComponentDefinition( + defaultAuthorizedClientRepository, authorizedClientRepositoryId)); + this.defaultAuthorizedClientRepositoryRegistered = true; + } + } + + private void registerOAuth2ClientPostProcessors() { + if (!this.oauth2LoginEnabled && !this.oauth2ClientEnabled) { + return; + } + + boolean webmvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", getClass().getClassLoader()); + if (webmvcPresent) { + this.pc.getReaderContext().registerWithGeneratedName( + new RootBeanDefinition(OAuth2ClientWebMvcSecurityPostProcessor.class)); + } + } + void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager) { Element openIDLoginElt = DomUtils.getChildElementByTagName(httpElt, Elements.OPENID_LOGIN); RootBeanDefinition openIDFilter = null; if (openIDLoginElt != null) { - FormLoginBeanDefinitionParser parser = new FormLoginBeanDefinitionParser( - "/login/openid", null, - OPEN_ID_AUTHENTICATION_PROCESSING_FILTER_CLASS, requestCache, - sessionStrategy, allowSessionCreation, portMapper, portResolver); - - parser.parse(openIDLoginElt, pc); - openIDFilter = parser.getFilterBean(); - openIDEntryPoint = parser.getEntryPointBean(); - openidLoginProcessingUrl = parser.getLoginProcessingUrl(); - openIDLoginPage = parser.getLoginPage(); - - List attrExElts = DomUtils.getChildElementsByTagName(openIDLoginElt, - Elements.OPENID_ATTRIBUTE_EXCHANGE); - - if (!attrExElts.isEmpty()) { - // Set up the consumer with the required attribute list - BeanDefinitionBuilder consumerBldr = BeanDefinitionBuilder - .rootBeanDefinition(OPEN_ID_CONSUMER_CLASS); - BeanDefinitionBuilder axFactory = BeanDefinitionBuilder - .rootBeanDefinition(OPEN_ID_ATTRIBUTE_FACTORY_CLASS); - ManagedMap> axMap = new ManagedMap<>(); - - for (Element attrExElt : attrExElts) { - String identifierMatch = attrExElt.getAttribute("identifier-match"); - - if (!StringUtils.hasText(identifierMatch)) { - if (attrExElts.size() > 1) { - pc.getReaderContext().error( - "You must supply an identifier-match attribute if using more" - + " than one " - + Elements.OPENID_ATTRIBUTE_EXCHANGE - + " element", attrExElt); - } - // Match anything - identifierMatch = ".*"; - } - - axMap.put(identifierMatch, parseOpenIDAttributes(attrExElt)); - } - axFactory.addConstructorArgValue(axMap); - - consumerBldr.addConstructorArgValue(axFactory.getBeanDefinition()); - openIDFilter.getPropertyValues().addPropertyValue("consumer", - consumerBldr.getBeanDefinition()); - } + openIDFilter = parseOpenIDFilter(sessionStrategy, openIDLoginElt); } if (openIDFilter != null) { @@ -412,6 +409,65 @@ void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authMa } } + /** + * Parses OpenID 1.0 and 2.0 - related parts of configuration xmls + * @deprecated 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. + * @param sessionStrategy sessionStrategy + * @param openIDLoginElt the element from the xml file + * @return the parsed filter as rootBeanDefinition + */ + private RootBeanDefinition parseOpenIDFilter( BeanReference sessionStrategy, Element openIDLoginElt ) { + RootBeanDefinition openIDFilter; + FormLoginBeanDefinitionParser parser = new FormLoginBeanDefinitionParser( + "/login/openid", null, + OPEN_ID_AUTHENTICATION_PROCESSING_FILTER_CLASS, requestCache, + sessionStrategy, allowSessionCreation, portMapper, portResolver); + + parser.parse(openIDLoginElt, pc); + openIDFilter = parser.getFilterBean(); + openIDEntryPoint = parser.getEntryPointBean(); + openidLoginProcessingUrl = parser.getLoginProcessingUrl(); + openIDLoginPage = parser.getLoginPage(); + + List attrExElts = DomUtils.getChildElementsByTagName(openIDLoginElt, + Elements.OPENID_ATTRIBUTE_EXCHANGE); + + if (!attrExElts.isEmpty()) { + // Set up the consumer with the required attribute list + BeanDefinitionBuilder consumerBldr = BeanDefinitionBuilder + .rootBeanDefinition(OPEN_ID_CONSUMER_CLASS); + BeanDefinitionBuilder axFactory = BeanDefinitionBuilder + .rootBeanDefinition(OPEN_ID_ATTRIBUTE_FACTORY_CLASS); + ManagedMap> axMap = new ManagedMap<>(); + + for (Element attrExElt : attrExElts) { + String identifierMatch = attrExElt.getAttribute("identifier-match"); + + if (!StringUtils.hasText(identifierMatch)) { + if (attrExElts.size() > 1) { + pc.getReaderContext().error( + "You must supply an identifier-match attribute if using more" + + " than one " + + Elements.OPENID_ATTRIBUTE_EXCHANGE + + " element", attrExElt); + } + // Match anything + identifierMatch = ".*"; + } + + axMap.put(identifierMatch, parseOpenIDAttributes(attrExElt)); + } + axFactory.addConstructorArgValue(axMap); + + consumerBldr.addConstructorArgValue(axFactory.getBeanDefinition()); + openIDFilter.getPropertyValues().addPropertyValue("consumer", + consumerBldr.getBeanDefinition()); + } + return openIDFilter; + } + private ManagedList parseOpenIDAttributes(Element attrExElt) { ManagedList attributes = new ManagedList<>(); for (Element attElt : DomUtils.getChildElementsByTagName(attrExElt, diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index 256cae6dcb4..d9f4a74ee26 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java @@ -24,14 +24,19 @@ import org.w3c.dom.Element; import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ListFactoryBean; import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; @@ -393,7 +398,8 @@ else if (StringUtils.hasText(before)) { } static void registerFilterChainProxyIfNecessary(ParserContext pc, Object source) { - if (pc.getRegistry().containsBeanDefinition(BeanIds.FILTER_CHAIN_PROXY)) { + BeanDefinitionRegistry registry = pc.getRegistry(); + if (registry.containsBeanDefinition(BeanIds.FILTER_CHAIN_PROXY)) { return; } // Not already registered, so register the list of filter chains and the @@ -412,10 +418,46 @@ static void registerFilterChainProxyIfNecessary(ParserContext pc, Object source) BeanDefinition fcpBean = fcpBldr.getBeanDefinition(); pc.registerBeanComponent(new BeanComponentDefinition(fcpBean, BeanIds.FILTER_CHAIN_PROXY)); - pc.getRegistry().registerAlias(BeanIds.FILTER_CHAIN_PROXY, + registry.registerAlias(BeanIds.FILTER_CHAIN_PROXY, BeanIds.SPRING_SECURITY_FILTER_CHAIN); + + BeanDefinitionBuilder requestRejected = BeanDefinitionBuilder.rootBeanDefinition(RequestRejectedHandlerPostProcessor.class); + requestRejected.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + requestRejected.addConstructorArgValue("requestRejectedHandler"); + requestRejected.addConstructorArgValue(BeanIds.FILTER_CHAIN_PROXY); + requestRejected.addConstructorArgValue("requestRejectedHandler"); + AbstractBeanDefinition requestRejectedBean = requestRejected.getBeanDefinition(); + String requestRejectedPostProcessorName = pc.getReaderContext().generateBeanName(requestRejectedBean); + registry.registerBeanDefinition(requestRejectedPostProcessorName, requestRejectedBean); + } + +} + +class RequestRejectedHandlerPostProcessor implements BeanDefinitionRegistryPostProcessor { + private final String beanName; + + private final String targetBeanName; + + private final String targetPropertyName; + + RequestRejectedHandlerPostProcessor(String beanName, String targetBeanName, String targetPropertyName) { + this.beanName = beanName; + this.targetBeanName = targetBeanName; + this.targetPropertyName = targetPropertyName; } + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + if (registry.containsBeanDefinition(this.beanName)) { + BeanDefinition beanDefinition = registry.getBeanDefinition(this.targetBeanName); + beanDefinition.getPropertyValues().add(this.targetPropertyName, new RuntimeBeanReference(this.beanName)); + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + } } class OrderDecorator implements Ordered { diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java index 269143ede08..71cd14661cc 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java @@ -23,27 +23,30 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.createAuthorizedClientRepository; +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.createDefaultAuthorizedClientRepository; +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.getAuthorizedClientRepository; +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.getAuthorizedClientService; +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.getClientRegistrationRepository; + /** * @author Joe Grandja * @since 5.3 */ final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser { private static final String ELT_AUTHORIZATION_CODE_GRANT = "authorization-code-grant"; - private static final String ATT_CLIENT_REGISTRATION_REPOSITORY_REF = "client-registration-repository-ref"; - private static final String ATT_AUTHORIZED_CLIENT_REPOSITORY_REF = "authorized-client-repository-ref"; - private static final String ATT_AUTHORIZED_CLIENT_SERVICE_REF = "authorized-client-service-ref"; private static final String ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF = "authorization-request-repository-ref"; private static final String ATT_AUTHORIZATION_REQUEST_RESOLVER_REF = "authorization-request-resolver-ref"; private static final String ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF = "access-token-response-client-ref"; private final BeanReference requestCache; private final BeanReference authenticationManager; + private BeanDefinition defaultAuthorizedClientRepository; private BeanDefinition authorizationRequestRedirectFilter; private BeanDefinition authorizationCodeGrantFilter; private BeanDefinition authorizationCodeAuthenticationProvider; @@ -58,8 +61,16 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { Element authorizationCodeGrantElt = DomUtils.getChildElementByTagName(element, ELT_AUTHORIZATION_CODE_GRANT); BeanMetadataElement clientRegistrationRepository = getClientRegistrationRepository(element); - BeanMetadataElement authorizedClientRepository = getAuthorizedClientRepository( - element, clientRegistrationRepository); + BeanMetadataElement authorizedClientRepository = getAuthorizedClientRepository(element); + if (authorizedClientRepository == null) { + BeanMetadataElement authorizedClientService = getAuthorizedClientService(element); + if (authorizedClientService == null) { + this.defaultAuthorizedClientRepository = createDefaultAuthorizedClientRepository(clientRegistrationRepository); + authorizedClientRepository = this.defaultAuthorizedClientRepository; + } else { + authorizedClientRepository = createAuthorizedClientRepository(authorizedClientService); + } + } BeanMetadataElement authorizationRequestRepository = getAuthorizationRequestRepository( authorizationCodeGrantElt); @@ -95,41 +106,6 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { return null; } - private BeanMetadataElement getClientRegistrationRepository(Element element) { - BeanMetadataElement clientRegistrationRepository; - String clientRegistrationRepositoryRef = element.getAttribute(ATT_CLIENT_REGISTRATION_REPOSITORY_REF); - if (!StringUtils.isEmpty(clientRegistrationRepositoryRef)) { - clientRegistrationRepository = new RuntimeBeanReference(clientRegistrationRepositoryRef); - } else { - clientRegistrationRepository = new RuntimeBeanReference(ClientRegistrationRepository.class); - } - return clientRegistrationRepository; - } - - private BeanMetadataElement getAuthorizedClientRepository(Element element, - BeanMetadataElement clientRegistrationRepository) { - BeanMetadataElement authorizedClientRepository; - String authorizedClientRepositoryRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_REPOSITORY_REF); - if (!StringUtils.isEmpty(authorizedClientRepositoryRef)) { - authorizedClientRepository = new RuntimeBeanReference(authorizedClientRepositoryRef); - } else { - BeanMetadataElement authorizedClientService; - String authorizedClientServiceRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_SERVICE_REF); - if (!StringUtils.isEmpty(authorizedClientServiceRef)) { - authorizedClientService = new RuntimeBeanReference(authorizedClientServiceRef); - } else { - authorizedClientService = BeanDefinitionBuilder - .rootBeanDefinition( - "org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService") - .addConstructorArgValue(clientRegistrationRepository).getBeanDefinition(); - } - authorizedClientRepository = BeanDefinitionBuilder.rootBeanDefinition( - "org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository") - .addConstructorArgValue(authorizedClientService).getBeanDefinition(); - } - return authorizedClientRepository; - } - private BeanMetadataElement getAuthorizationRequestRepository(Element element) { BeanMetadataElement authorizationRequestRepository; String authorizationRequestRepositoryRef = element != null ? @@ -158,6 +134,10 @@ private BeanMetadataElement getAccessTokenResponseClient(Element element) { return accessTokenResponseClient; } + BeanDefinition getDefaultAuthorizedClientRepository() { + return this.defaultAuthorizedClientRepository; + } + BeanDefinition getAuthorizationRequestRedirectFilter() { return this.authorizationRequestRedirectFilter; } diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserUtils.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserUtils.java new file mode 100644 index 00000000000..4ff56b11474 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserUtils.java @@ -0,0 +1,79 @@ +/* + * 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.config.http; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * @author Joe Grandja + * @since 5.4 + */ +final class OAuth2ClientBeanDefinitionParserUtils { + private static final String ATT_CLIENT_REGISTRATION_REPOSITORY_REF = "client-registration-repository-ref"; + private static final String ATT_AUTHORIZED_CLIENT_REPOSITORY_REF = "authorized-client-repository-ref"; + private static final String ATT_AUTHORIZED_CLIENT_SERVICE_REF = "authorized-client-service-ref"; + + static BeanMetadataElement getClientRegistrationRepository(Element element) { + BeanMetadataElement clientRegistrationRepository; + String clientRegistrationRepositoryRef = element.getAttribute(ATT_CLIENT_REGISTRATION_REPOSITORY_REF); + if (!StringUtils.isEmpty(clientRegistrationRepositoryRef)) { + clientRegistrationRepository = new RuntimeBeanReference(clientRegistrationRepositoryRef); + } else { + clientRegistrationRepository = new RuntimeBeanReference(ClientRegistrationRepository.class); + } + return clientRegistrationRepository; + } + + static BeanMetadataElement getAuthorizedClientRepository(Element element) { + String authorizedClientRepositoryRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_REPOSITORY_REF); + if (!StringUtils.isEmpty(authorizedClientRepositoryRef)) { + return new RuntimeBeanReference(authorizedClientRepositoryRef); + } + return null; + } + + static BeanMetadataElement getAuthorizedClientService(Element element) { + String authorizedClientServiceRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_SERVICE_REF); + if (!StringUtils.isEmpty(authorizedClientServiceRef)) { + return new RuntimeBeanReference(authorizedClientServiceRef); + } + return null; + } + + static BeanMetadataElement createAuthorizedClientRepository(BeanMetadataElement authorizedClientService) { + return BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository") + .addConstructorArgValue(authorizedClientService) + .getBeanDefinition(); + } + + static BeanDefinition createDefaultAuthorizedClientRepository(BeanMetadataElement clientRegistrationRepository) { + BeanDefinition authorizedClientService = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService") + .addConstructorArgValue(clientRegistrationRepository) + .getBeanDefinition(); + return BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository") + .addConstructorArgValue(authorizedClientService) + .getBeanDefinition(); + } +} diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientWebMvcSecurityPostProcessor.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientWebMvcSecurityPostProcessor.java new file mode 100644 index 00000000000..a6535ea064f --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientWebMvcSecurityPostProcessor.java @@ -0,0 +1,91 @@ +/* + * 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.config.http; + +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +/** + * @author Joe Grandja + * @since 5.4 + */ +final class OAuth2ClientWebMvcSecurityPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware { + private static final String CUSTOM_ARGUMENT_RESOLVERS_PROPERTY = "customArgumentResolvers"; + private BeanFactory beanFactory; + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + String[] clientRegistrationRepositoryBeanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + (ListableBeanFactory) this.beanFactory, ClientRegistrationRepository.class, false, false); + String[] authorizedClientRepositoryBeanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + (ListableBeanFactory) this.beanFactory, OAuth2AuthorizedClientRepository.class, false, false); + + if (clientRegistrationRepositoryBeanNames.length != 1 || authorizedClientRepositoryBeanNames.length != 1) { + return; + } + + for (String beanName : registry.getBeanDefinitionNames()) { + BeanDefinition beanDefinition = registry.getBeanDefinition(beanName); + if (RequestMappingHandlerAdapter.class.getName().equals(beanDefinition.getBeanClassName())) { + PropertyValue currentArgumentResolvers = + beanDefinition.getPropertyValues().getPropertyValue(CUSTOM_ARGUMENT_RESOLVERS_PROPERTY); + ManagedList argumentResolvers = new ManagedList<>(); + if (currentArgumentResolvers != null) { + argumentResolvers.addAll((ManagedList) currentArgumentResolvers.getValue()); + } + + String[] authorizedClientManagerBeanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + (ListableBeanFactory) this.beanFactory, OAuth2AuthorizedClientManager.class, false, false); + + BeanDefinitionBuilder beanDefinitionBuilder = + BeanDefinitionBuilder.genericBeanDefinition(OAuth2AuthorizedClientArgumentResolver.class); + if (authorizedClientManagerBeanNames.length == 1) { + beanDefinitionBuilder.addConstructorArgReference(authorizedClientManagerBeanNames[0]); + } else { + beanDefinitionBuilder.addConstructorArgReference(clientRegistrationRepositoryBeanNames[0]); + beanDefinitionBuilder.addConstructorArgReference(authorizedClientRepositoryBeanNames[0]); + } + argumentResolvers.add(beanDefinitionBuilder.getBeanDefinition()); + beanDefinition.getPropertyValues().add(CUSTOM_ARGUMENT_RESOLVERS_PROPERTY, argumentResolvers); + break; + } + } + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } +} diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java index 0d2c7c44106..b5fcdda6807 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java @@ -15,13 +15,6 @@ */ package org.springframework.security.config.http; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; @@ -66,6 +59,19 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy; import org.w3c.dom.Element; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.createAuthorizedClientRepository; +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.createDefaultAuthorizedClientRepository; +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.getAuthorizedClientRepository; +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.getAuthorizedClientService; +import static org.springframework.security.config.http.OAuth2ClientBeanDefinitionParserUtils.getClientRegistrationRepository; + /** * @author Ruby Hartono * @since 5.3 @@ -77,9 +83,6 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { private static final String ELT_CLIENT_REGISTRATION = "client-registration"; private static final String ATT_REGISTRATION_ID = "registration-id"; - private static final String ATT_CLIENT_REGISTRATION_REPOSITORY_REF = "client-registration-repository-ref"; - private static final String ATT_AUTHORIZED_CLIENT_REPOSITORY_REF = "authorized-client-repository-ref"; - private static final String ATT_AUTHORIZED_CLIENT_SERVICE_REF = "authorized-client-service-ref"; private static final String ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF = "authorization-request-repository-ref"; private static final String ATT_AUTHORIZATION_REQUEST_RESOLVER_REF = "authorization-request-resolver-ref"; private static final String ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF = "access-token-response-client-ref"; @@ -98,6 +101,8 @@ final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser { private final BeanReference sessionStrategy; private final boolean allowSessionCreation; + private BeanDefinition defaultAuthorizedClientRepository; + private BeanDefinition oauth2AuthorizationRequestRedirectFilter; private BeanDefinition oauth2LoginAuthenticationEntryPoint; @@ -128,8 +133,16 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { // configure filter BeanMetadataElement clientRegistrationRepository = getClientRegistrationRepository(element); - BeanMetadataElement authorizedClientRepository = getAuthorizedClientRepository(element, - clientRegistrationRepository); + BeanMetadataElement authorizedClientRepository = getAuthorizedClientRepository(element); + if (authorizedClientRepository == null) { + BeanMetadataElement authorizedClientService = getAuthorizedClientService(element); + if (authorizedClientService == null) { + this.defaultAuthorizedClientRepository = createDefaultAuthorizedClientRepository(clientRegistrationRepository); + authorizedClientRepository = this.defaultAuthorizedClientRepository; + } else { + authorizedClientRepository = createAuthorizedClientRepository(authorizedClientService); + } + } BeanMetadataElement accessTokenResponseClient = getAccessTokenResponseClient(element); BeanMetadataElement oauth2UserService = getOAuth2UserService(element); BeanMetadataElement authorizationRequestRepository = getAuthorizationRequestRepository(element); @@ -251,41 +264,6 @@ private BeanMetadataElement getAuthorizationRequestRepository(Element element) { return authorizationRequestRepository; } - private BeanMetadataElement getAuthorizedClientRepository(Element element, - BeanMetadataElement clientRegistrationRepository) { - BeanMetadataElement authorizedClientRepository; - String authorizedClientRepositoryRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_REPOSITORY_REF); - if (!StringUtils.isEmpty(authorizedClientRepositoryRef)) { - authorizedClientRepository = new RuntimeBeanReference(authorizedClientRepositoryRef); - } else { - BeanMetadataElement authorizedClientService; - String authorizedClientServiceRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_SERVICE_REF); - if (!StringUtils.isEmpty(authorizedClientServiceRef)) { - authorizedClientService = new RuntimeBeanReference(authorizedClientServiceRef); - } else { - authorizedClientService = BeanDefinitionBuilder - .rootBeanDefinition( - "org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService") - .addConstructorArgValue(clientRegistrationRepository).getBeanDefinition(); - } - authorizedClientRepository = BeanDefinitionBuilder.rootBeanDefinition( - "org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository") - .addConstructorArgValue(authorizedClientService).getBeanDefinition(); - } - return authorizedClientRepository; - } - - private BeanMetadataElement getClientRegistrationRepository(Element element) { - BeanMetadataElement clientRegistrationRepository; - String clientRegistrationRepositoryRef = element.getAttribute(ATT_CLIENT_REGISTRATION_REPOSITORY_REF); - if (!StringUtils.isEmpty(clientRegistrationRepositoryRef)) { - clientRegistrationRepository = new RuntimeBeanReference(clientRegistrationRepositoryRef); - } else { - clientRegistrationRepository = new RuntimeBeanReference(ClientRegistrationRepository.class); - } - return clientRegistrationRepository; - } - private BeanDefinition getOidcAuthProvider(Element element, BeanMetadataElement accessTokenResponseClient, String userAuthoritiesMapperRef) { @@ -353,6 +331,10 @@ private BeanMetadataElement getAccessTokenResponseClient(Element element) { return accessTokenResponseClient; } + BeanDefinition getDefaultAuthorizedClientRepository() { + return this.defaultAuthorizedClientRepository; + } + BeanDefinition getOAuth2AuthorizationRequestRedirectFilter() { return oauth2AuthorizationRequestRedirectFilter; } diff --git a/config/src/main/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParser.java index a84ad1eb222..cbdda20de6f 100644 --- a/config/src/main/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/ldap/LdapServerBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -18,31 +18,32 @@ import java.io.IOException; import java.net.ServerSocket; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.security.config.BeanIds; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; import org.springframework.security.ldap.server.ApacheDSContainer; import org.springframework.security.ldap.server.UnboundIdContainer; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; -import org.w3c.dom.Element; /** * @author Luke Taylor * @author Eddú Meléndez + * @author Evgeniy Cheban */ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser { private static final String CONTEXT_SOURCE_CLASS = "org.springframework.security.ldap.DefaultSpringSecurityContextSource"; - private final Log logger = LogFactory.getLog(getClass()); - /** * Defines the Url of the ldap server to use. If not specified, an embedded apache DS * instance will be created @@ -66,8 +67,8 @@ public class LdapServerBeanDefinitionParser implements BeanDefinitionParser { /** Defines the port the LDAP_PROVIDER server should run on */ public static final String ATT_PORT = "port"; + private static final String RANDOM_PORT = "0"; private static final int DEFAULT_PORT = 33389; - public static final String OPT_DEFAULT_PORT = String.valueOf(DEFAULT_PORT); private static final String APACHEDS_CLASSNAME = "org.apache.directory.server.core.DefaultDirectoryService"; private static final String UNBOUNID_CLASSNAME = "com.unboundid.ldap.listener.InMemoryDirectoryServer"; @@ -136,23 +137,22 @@ private RootBeanDefinition createEmbeddedServer(Element element, suffix = OPT_DEFAULT_ROOT_SUFFIX; } - String port = element.getAttribute(ATT_PORT); - - if (!StringUtils.hasText(port)) { - port = getDefaultPort(); - if (logger.isDebugEnabled()) { - logger.debug("Using default port of " + port); - } - } - - String url = "ldap://127.0.0.1:" + port + "/" + suffix; - BeanDefinitionBuilder contextSource = BeanDefinitionBuilder .rootBeanDefinition(CONTEXT_SOURCE_CLASS); - contextSource.addConstructorArgValue(url); + contextSource.addConstructorArgValue(suffix); contextSource.addPropertyValue("userDn", "uid=admin,ou=system"); contextSource.addPropertyValue("password", "secret"); + BeanDefinition embeddedLdapServerConfigBean = BeanDefinitionBuilder + .rootBeanDefinition(EmbeddedLdapServerConfigBean.class).getBeanDefinition(); + String embeddedLdapServerConfigBeanName = parserContext.getReaderContext() + .generateBeanName(embeddedLdapServerConfigBean); + + parserContext.registerBeanComponent(new BeanComponentDefinition(embeddedLdapServerConfigBean, + embeddedLdapServerConfigBeanName)); + + contextSource.setFactoryMethodOnBean("createEmbeddedContextSource", embeddedLdapServerConfigBeanName); + String mode = element.getAttribute("mode"); RootBeanDefinition ldapContainer = getRootBeanDefinition(mode); ldapContainer.setSource(source); @@ -164,9 +164,7 @@ private RootBeanDefinition createEmbeddedServer(Element element, } ldapContainer.getConstructorArgumentValues().addGenericArgumentValue(ldifs); - ldapContainer.getPropertyValues().addPropertyValue("port", port); - - logger.info("Embedded LDAP server bean definition created for URL: " + url); + ldapContainer.getPropertyValues().addPropertyValue("port", getPort(element)); if (parserContext.getRegistry() .containsBeanDefinition(BeanIds.EMBEDDED_APACHE_DS) || @@ -212,31 +210,46 @@ private boolean isUnboundidEnabled(String mode) { return "unboundid".equals(mode) || ClassUtils.isPresent(UNBOUNID_CLASSNAME, getClass().getClassLoader()); } + private String getPort(Element element) { + String port = element.getAttribute(ATT_PORT); + return (StringUtils.hasText(port) ? port : getDefaultPort()); + } + private String getDefaultPort() { - ServerSocket serverSocket = null; - try { - try { - serverSocket = new ServerSocket(DEFAULT_PORT); - } - catch (IOException e) { - try { - serverSocket = new ServerSocket(0); - } - catch (IOException e2) { - return String.valueOf(DEFAULT_PORT); - } - } + try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) { return String.valueOf(serverSocket.getLocalPort()); + } catch (IOException e) { + return RANDOM_PORT; + } + } + + private static class EmbeddedLdapServerConfigBean implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; } - finally { - if (serverSocket != null) { - try { - serverSocket.close(); - } - catch (IOException e) { - } + + @SuppressWarnings("unused") + private DefaultSpringSecurityContextSource createEmbeddedContextSource(String suffix) { + int port; + if (ClassUtils.isPresent(APACHEDS_CLASSNAME, getClass().getClassLoader())) { + ApacheDSContainer apacheDSContainer = this.applicationContext.getBean(ApacheDSContainer.class); + port = apacheDSContainer.getLocalPort(); + } + else if (ClassUtils.isPresent(UNBOUNID_CLASSNAME, getClass().getClassLoader())) { + UnboundIdContainer unboundIdContainer = this.applicationContext.getBean(UnboundIdContainer.class); + port = unboundIdContainer.getPort(); + } + else { + throw new IllegalStateException("Embedded LDAP server is not provided"); } + + String providerUrl = "ldap://127.0.0.1:" + port + "/" + suffix; + + return new DefaultSpringSecurityContextSource(providerUrl); } } - } diff --git a/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java b/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java index 07950784125..cc1f37d4370 100644 --- a/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java +++ b/config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -41,6 +41,7 @@ public Builder getBuilder(String registrationId) { builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth"); builder.tokenUri("https://www.googleapis.com/oauth2/v4/token"); builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs"); + builder.issuerUri("https://accounts.google.com"); builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo"); builder.userNameAttributeName(IdTokenClaimNames.SUB); builder.clientName("Google"); 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 3b92be824fc..91866bff42f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -31,6 +31,9 @@ import java.util.function.Function; import java.util.function.Supplier; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import reactor.core.publisher.Mono; import reactor.util.context.Context; @@ -235,6 +238,7 @@ * @author Rafiullah Hamedy * @author Eddú Meléndez * @author Joe Grandja + * @author Parikshit Dutta * @since 5.0 */ public class ServerHttpSecurity { @@ -1056,8 +1060,11 @@ private ReactiveAuthenticationManager getAuthenticationManager() { private ReactiveAuthenticationManager createDefault() { ReactiveOAuth2AccessTokenResponseClient client = getAccessTokenResponseClient(); - ReactiveAuthenticationManager result = new OAuth2LoginReactiveAuthenticationManager(client, getOauth2UserService()); - + OAuth2LoginReactiveAuthenticationManager oauth2Manager = new OAuth2LoginReactiveAuthenticationManager(client, getOauth2UserService()); + GrantedAuthoritiesMapper authoritiesMapper = getBeanOrNull(GrantedAuthoritiesMapper.class); + if (authoritiesMapper != null) { + oauth2Manager.setAuthoritiesMapper(authoritiesMapper); + } boolean oidcAuthenticationProviderEnabled = ClassUtils.isPresent( "org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader()); if (oidcAuthenticationProviderEnabled) { @@ -1069,9 +1076,12 @@ private ReactiveAuthenticationManager createDefault() { if (jwtDecoderFactory != null) { oidc.setJwtDecoderFactory(jwtDecoderFactory); } - result = new DelegatingReactiveAuthenticationManager(oidc, result); + if (authoritiesMapper != null) { + oidc.setAuthoritiesMapper(authoritiesMapper); + } + return new DelegatingReactiveAuthenticationManager(oidc, oauth2Manager); } - return result; + return oauth2Manager; } /** @@ -1086,9 +1096,14 @@ public OAuth2LoginSpec authenticationConverter(ServerAuthenticationConverter aut private ServerAuthenticationConverter getAuthenticationConverter(ReactiveClientRegistrationRepository clientRegistrationRepository) { if (this.authenticationConverter == null) { - ServerOAuth2AuthorizationCodeAuthenticationTokenConverter authenticationConverter = new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository); - authenticationConverter.setAuthorizationRequestRepository(getAuthorizationRequestRepository()); + ServerOAuth2AuthorizationCodeAuthenticationTokenConverter delegate = + new ServerOAuth2AuthorizationCodeAuthenticationTokenConverter(clientRegistrationRepository); + delegate.setAuthorizationRequestRepository(getAuthorizationRequestRepository()); + ServerAuthenticationConverter authenticationConverter = exchange -> + delegate.convert(exchange).onErrorMap(OAuth2AuthorizationException.class, + e -> new OAuth2AuthenticationException(e.getError(), e.getError().toString())); this.authenticationConverter = authenticationConverter; + return authenticationConverter; } return this.authenticationConverter; } @@ -1180,22 +1195,54 @@ protected void configure(ServerHttpSecurity http) { authenticationFilter.setAuthenticationFailureHandler(getAuthenticationFailureHandler()); authenticationFilter.setSecurityContextRepository(this.securityContextRepository); - MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher( - MediaType.TEXT_HTML); - htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + setDefaultEntryPoints(http); + + http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); + http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); + } + + private void setDefaultEntryPoints(ServerHttpSecurity http) { + String defaultLoginPage = "/login"; Map urlToText = http.oauth2Login.getLinks(); - String authenticationEntryPointRedirectPath; + String providerLoginPage = null; if (urlToText.size() == 1) { - authenticationEntryPointRedirectPath = urlToText.keySet().iterator().next(); - } else { - authenticationEntryPointRedirectPath = "/login"; + providerLoginPage = urlToText.keySet().iterator().next(); } - RedirectServerAuthenticationEntryPoint entryPoint = new RedirectServerAuthenticationEntryPoint(authenticationEntryPointRedirectPath); - entryPoint.setRequestCache(http.requestCache.requestCache); - http.defaultEntryPoints.add(new DelegateEntry(htmlMatcher, entryPoint)); - http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); - http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); + MediaTypeServerWebExchangeMatcher htmlMatcher = new MediaTypeServerWebExchangeMatcher( + MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), + MediaType.TEXT_HTML, MediaType.TEXT_PLAIN); + htmlMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + + ServerWebExchangeMatcher xhrMatcher = exchange -> { + if (exchange.getRequest().getHeaders().getOrEmpty("X-Requested-With").contains("XMLHttpRequest")) { + return ServerWebExchangeMatcher.MatchResult.match(); + } + return ServerWebExchangeMatcher.MatchResult.notMatch(); + }; + ServerWebExchangeMatcher notXhrMatcher = new NegatedServerWebExchangeMatcher(xhrMatcher); + + ServerWebExchangeMatcher defaultEntryPointMatcher = new AndServerWebExchangeMatcher( + notXhrMatcher, htmlMatcher); + + if (providerLoginPage != null) { + ServerWebExchangeMatcher loginPageMatcher = new PathPatternParserServerWebExchangeMatcher(defaultLoginPage); + ServerWebExchangeMatcher faviconMatcher = new PathPatternParserServerWebExchangeMatcher("/favicon.ico"); + ServerWebExchangeMatcher defaultLoginPageMatcher = new AndServerWebExchangeMatcher( + new OrServerWebExchangeMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); + + ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher( + notXhrMatcher, new NegatedServerWebExchangeMatcher(defaultLoginPageMatcher)); + RedirectServerAuthenticationEntryPoint entryPoint = + new RedirectServerAuthenticationEntryPoint(providerLoginPage); + entryPoint.setRequestCache(http.requestCache.requestCache); + http.defaultEntryPoints.add(new DelegateEntry(matcher, entryPoint)); + } + + RedirectServerAuthenticationEntryPoint defaultEntryPoint = + new RedirectServerAuthenticationEntryPoint(defaultLoginPage); + defaultEntryPoint.setRequestCache(http.requestCache.requestCache); + http.defaultEntryPoints.add(new DelegateEntry(defaultEntryPointMatcher, defaultEntryPoint)); } private ServerAuthenticationSuccessHandler getAuthenticationSuccessHandler(ServerHttpSecurity http) { @@ -1472,10 +1519,17 @@ protected void configure(ServerHttpSecurity http) { OAuth2AuthorizationCodeGrantWebFilter codeGrantWebFilter = new OAuth2AuthorizationCodeGrantWebFilter( authenticationManager, authenticationConverter, authorizedClientRepository); codeGrantWebFilter.setAuthorizationRequestRepository(getAuthorizationRequestRepository()); + if (http.requestCache != null) { + codeGrantWebFilter.setRequestCache(http.requestCache.requestCache); + } OAuth2AuthorizationRequestRedirectWebFilter oauthRedirectFilter = new OAuth2AuthorizationRequestRedirectWebFilter( clientRegistrationRepository); oauthRedirectFilter.setAuthorizationRequestRepository(getAuthorizationRequestRepository()); + if (http.requestCache != null) { + oauthRedirectFilter.setRequestCache(http.requestCache.requestCache); + } + http.addFilterAt(codeGrantWebFilter, SecurityWebFiltersOrder.OAUTH2_AUTHORIZATION_CODE); http.addFilterAt(oauthRedirectFilter, SecurityWebFiltersOrder.HTTP_BASIC); } @@ -2614,7 +2668,7 @@ public AuthorizeExchangeSpec hasAnyRole(String... roles) { /** * Require a specific authority. - * @param authority the authority to require (i.e. "USER" woudl require authority of "USER"). + * @param authority the authority to require (i.e. "USER" would require authority of "USER"). * @return the {@link AuthorizeExchangeSpec} to configure */ public AuthorizeExchangeSpec hasAuthority(String authority) { @@ -3706,7 +3760,8 @@ private HeaderSpec() { */ public final class LogoutSpec { private LogoutWebFilter logoutWebFilter = new LogoutWebFilter(); - private List logoutHandlers = new ArrayList<>(Arrays.asList(new SecurityContextServerLogoutHandler())); + private final SecurityContextServerLogoutHandler DEFAULT_LOGOUT_HANDLER = new SecurityContextServerLogoutHandler(); + private List logoutHandlers = new ArrayList<>(Arrays.asList(this.DEFAULT_LOGOUT_HANDLER)); /** * Configures the logout handler. Default is {@code SecurityContextServerLogoutHandler} @@ -3770,6 +3825,10 @@ public ServerHttpSecurity disable() { } private ServerLogoutHandler createLogoutHandler() { + ServerSecurityContextRepository securityContextRepository = ServerHttpSecurity.this.securityContextRepository; + if (securityContextRepository != null) { + this.DEFAULT_LOGOUT_HANDLER.setSecurityContextRepository(securityContextRepository); + } if (this.logoutHandlers.isEmpty()) { return null; } else if (this.logoutHandlers.size() == 1) { 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 new file mode 100644 index 00000000000..8df31aaca51 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDsl.kt @@ -0,0 +1,136 @@ +/* + * 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.config.web.server + +import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager +import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager +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.util.matcher.ServerWebExchangeMatcher +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers +import org.springframework.security.web.util.matcher.RequestMatcher +import reactor.core.publisher.Mono + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] exchange authorization using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@ServerSecurityMarker +class AuthorizeExchangeDsl { + private val authorizationRules = mutableListOf() + + /** + * Adds an exchange authorization rule for an endpoint matching the provided + * matcher. + * + * @param matcher the [RequestMatcher] to match incoming requests against + * @param access the [ReactiveAuthorizationManager] which determines the access + * to the specific matcher. + * Some predefined shortcuts have already been created, such as + * [hasAnyAuthority], [hasAnyRole], [permitAll], [authenticated] and more + */ + fun authorize(matcher: ServerWebExchangeMatcher = ServerWebExchangeMatchers.anyExchange(), + access: ReactiveAuthorizationManager = authenticated) { + authorizationRules.add(MatcherExchangeAuthorizationRule(matcher, access)) + } + + /** + * Adds an exchange authorization rule for an endpoint matching the provided + * ant pattern. + * + * @param antPattern the ant ant pattern to match incoming requests against. + * @param access the [ReactiveAuthorizationManager] which determines the access + * to the specific matcher. + * Some predefined shortcuts have already been created, such as + * [hasAnyAuthority], [hasAnyRole], [permitAll], [authenticated] and more + */ + fun authorize(antPattern: String, access: ReactiveAuthorizationManager = authenticated) { + authorizationRules.add(PatternExchangeAuthorizationRule(antPattern, access)) + } + + /** + * Matches any exchange. + */ + val anyExchange: ServerWebExchangeMatcher = ServerWebExchangeMatchers.anyExchange() + + /** + * Allow access for anyone. + */ + val permitAll: ReactiveAuthorizationManager = + ReactiveAuthorizationManager { _: Mono, _: AuthorizationContext -> Mono.just(AuthorizationDecision(true)) } + + /** + * Deny access for everyone. + */ + val denyAll: ReactiveAuthorizationManager = + ReactiveAuthorizationManager { _: Mono, _: AuthorizationContext -> Mono.just(AuthorizationDecision(false)) } + + /** + * Require a specific role. This is a shortcut for [hasAuthority]. + */ + fun hasRole(role: String): ReactiveAuthorizationManager = + AuthorityReactiveAuthorizationManager.hasRole(role) + + /** + * Require any specific role. This is a shortcut for [hasAnyAuthority]. + */ + fun hasAnyRole(vararg roles: String): ReactiveAuthorizationManager = + AuthorityReactiveAuthorizationManager.hasAnyRole(*roles) + + /** + * Require a specific authority. + */ + fun hasAuthority(authority: String): ReactiveAuthorizationManager = + AuthorityReactiveAuthorizationManager.hasAuthority(authority) + + /** + * Require any authority. + */ + fun hasAnyAuthority(vararg authorities: String): ReactiveAuthorizationManager = + AuthorityReactiveAuthorizationManager.hasAnyAuthority(*authorities) + + /** + * Require an authenticated user. + */ + val authenticated: ReactiveAuthorizationManager = + AuthenticatedReactiveAuthorizationManager.authenticated() + + internal fun get(): (ServerHttpSecurity.AuthorizeExchangeSpec) -> Unit { + return { requests -> + authorizationRules.forEach { rule -> + when (rule) { + is MatcherExchangeAuthorizationRule -> requests.matchers(rule.matcher).access(rule.rule) + is PatternExchangeAuthorizationRule -> requests.pathMatchers(rule.pattern).access(rule.rule) + } + } + } + } + + private data class MatcherExchangeAuthorizationRule(val matcher: ServerWebExchangeMatcher, + override val rule: ReactiveAuthorizationManager) : ExchangeAuthorizationRule(rule) + + private data class PatternExchangeAuthorizationRule(val pattern: String, + override val rule: ReactiveAuthorizationManager) : ExchangeAuthorizationRule(rule) + + private abstract class ExchangeAuthorizationRule(open val rule: ReactiveAuthorizationManager) +} + diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerAnonymousDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerAnonymousDsl.kt new file mode 100644 index 00000000000..6f532691f2b --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerAnonymousDsl.kt @@ -0,0 +1,62 @@ +/* + * 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.config.web.server + +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] anonymous authentication using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property key the key to identify tokens created for anonymous authentication + * @property principal the principal for [Authentication] objects of anonymous users + * @property authorities the [Authentication.getAuthorities] for anonymous users + * @property authenticationFilter the [AnonymousAuthenticationWebFilter] used to populate + * an anonymous user. + */ +@ServerSecurityMarker +class ServerAnonymousDsl { + var key: String? = null + var principal: Any? = null + var authorities: List? = null + var authenticationFilter: AnonymousAuthenticationWebFilter? = null + + private var disabled = false + + /** + * Disables anonymous authentication + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.AnonymousSpec) -> Unit { + return { anonymous -> + key?.also { anonymous.key(key) } + principal?.also { anonymous.principal(principal) } + authorities?.also { anonymous.authorities(authorities) } + authenticationFilter?.also { anonymous.authenticationFilter(authenticationFilter) } + if (disabled) { + anonymous.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCacheControlDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCacheControlDsl.kt new file mode 100644 index 00000000000..76899260a3e --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCacheControlDsl.kt @@ -0,0 +1,44 @@ +/* + * 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.config.web.server + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] cache control headers using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@ServerSecurityMarker +class ServerCacheControlDsl { + private var disabled = false + + /** + * Disables cache control response headers + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.CacheSpec) -> Unit { + return { cacheControl -> + if (disabled) { + cacheControl.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerContentSecurityPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerContentSecurityPolicyDsl.kt new file mode 100644 index 00000000000..53dba4d8d71 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerContentSecurityPolicyDsl.kt @@ -0,0 +1,41 @@ +/* + * 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.config.web.server + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] Content-Security-Policy header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@ServerSecurityMarker +class ServerContentSecurityPolicyDsl { + var policyDirectives: String? = null + var reportOnly: Boolean? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.ContentSecurityPolicySpec) -> Unit { + return { contentSecurityPolicy -> + policyDirectives?.also { + contentSecurityPolicy.policyDirectives(policyDirectives) + } + reportOnly?.also { + contentSecurityPolicy.reportOnly(reportOnly!!) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerContentTypeOptionsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerContentTypeOptionsDsl.kt new file mode 100644 index 00000000000..3ddadb686ad --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerContentTypeOptionsDsl.kt @@ -0,0 +1,44 @@ +/* + * 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.config.web.server + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] the content type options header + * using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@ServerSecurityMarker +class ServerContentTypeOptionsDsl { + private var disabled = false + + /** + * Disables content type options response header + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.ContentTypeOptionsSpec) -> Unit { + return { contentTypeOptions -> + if (disabled) { + contentTypeOptions.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCorsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCorsDsl.kt new file mode 100644 index 00000000000..897e6a3d6cc --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCorsDsl.kt @@ -0,0 +1,50 @@ +/* + * 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.config.web.server + +import org.springframework.web.cors.reactive.CorsConfigurationSource + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] CORS headers using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property configurationSource the [CorsConfigurationSource] to use. + */ +@ServerSecurityMarker +class ServerCorsDsl { + var configurationSource: CorsConfigurationSource? = null + + private var disabled = false + + /** + * Disables CORS support within Spring Security. + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.CorsSpec) -> Unit { + return { cors -> + configurationSource?.also { cors.configurationSource(configurationSource) } + if (disabled) { + cors.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCsrfDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCsrfDsl.kt new file mode 100644 index 00000000000..d1cdd139df2 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerCsrfDsl.kt @@ -0,0 +1,64 @@ +/* + * 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.config.web.server + +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler +import org.springframework.security.web.server.csrf.CsrfWebFilter +import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] CSRF protection using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property accessDeniedHandler the [ServerAccessDeniedHandler] used when a CSRF token is invalid. + * @property csrfTokenRepository the [ServerCsrfTokenRepository] used to persist the CSRF token. + * @property requireCsrfProtectionMatcher the [ServerWebExchangeMatcher] used to determine when CSRF protection + * is enabled. + * @property tokenFromMultipartDataEnabled if true, the [CsrfWebFilter] should try to resolve the actual CSRF + * token from the body of multipart data requests. + */ +@ServerSecurityMarker +class ServerCsrfDsl { + var accessDeniedHandler: ServerAccessDeniedHandler? = null + var csrfTokenRepository: ServerCsrfTokenRepository? = null + var requireCsrfProtectionMatcher: ServerWebExchangeMatcher? = null + var tokenFromMultipartDataEnabled: Boolean? = null + + private var disabled = false + + /** + * Disables CSRF protection + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.CsrfSpec) -> Unit { + return { csrf -> + accessDeniedHandler?.also { csrf.accessDeniedHandler(accessDeniedHandler) } + csrfTokenRepository?.also { csrf.csrfTokenRepository(csrfTokenRepository) } + requireCsrfProtectionMatcher?.also { csrf.requireCsrfProtectionMatcher(requireCsrfProtectionMatcher) } + tokenFromMultipartDataEnabled?.also { csrf.tokenFromMultipartDataEnabled(tokenFromMultipartDataEnabled!!) } + if (disabled) { + csrf.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDsl.kt new file mode 100644 index 00000000000..d4e4d72cd4a --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDsl.kt @@ -0,0 +1,44 @@ +/* + * 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.config.web.server + +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] exception handling using idiomatic Kotlin + * code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationEntryPoint the [ServerAuthenticationEntryPoint] to use when + * the application request authentication + * @property accessDeniedHandler the [ServerAccessDeniedHandler] to use when an + * authenticated user does not hold a required authority + */ +@ServerSecurityMarker +class ServerExceptionHandlingDsl { + var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null + var accessDeniedHandler: ServerAccessDeniedHandler? = null + + internal fun get(): (ServerHttpSecurity.ExceptionHandlingSpec) -> Unit { + return { exceptionHandling -> + authenticationEntryPoint?.also { exceptionHandling.authenticationEntryPoint(authenticationEntryPoint) } + accessDeniedHandler?.also { exceptionHandling.accessDeniedHandler(accessDeniedHandler) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerFormLoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerFormLoginDsl.kt new file mode 100644 index 00000000000..89ccc633bd4 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerFormLoginDsl.kt @@ -0,0 +1,86 @@ +/* + * 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.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler +import org.springframework.security.web.server.context.ReactorContextWebFilter +import org.springframework.security.web.server.context.ServerSecurityContextRepository +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] form login using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to authenticate. + * @property loginPage the url to redirect to which provides a form to log in (i.e. "/login"). + * If this is customized: + * - The default log in & log out page are no longer provided + * - The application must render a log in page at the provided URL + * - The application must render an authentication error page at the provided URL + "?error" + * - Authentication will occur for POST to the provided URL + * @property authenticationEntryPoint configures how to request for authentication. + * @property requiresAuthenticationMatcher configures when authentication is performed. + * @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] used after + * authentication success. + * @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] used to handle + * a failed authentication. + * @property securityContextRepository the [ServerSecurityContextRepository] used to save + * the [Authentication]. For the [SecurityContext] to be loaded on subsequent requests the + * [ReactorContextWebFilter] must be configured to be able to load the value (they are not + * implicitly linked). + */ +@ServerSecurityMarker +class ServerFormLoginDsl { + var authenticationManager: ReactiveAuthenticationManager? = null + var loginPage: String? = null + var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null + var requiresAuthenticationMatcher: ServerWebExchangeMatcher? = null + var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null + var authenticationFailureHandler: ServerAuthenticationFailureHandler? = null + var securityContextRepository: ServerSecurityContextRepository? = null + + private var disabled = false + + /** + * Disables HTTP basic authentication + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.FormLoginSpec) -> Unit { + return { formLogin -> + authenticationManager?.also { formLogin.authenticationManager(authenticationManager) } + loginPage?.also { formLogin.loginPage(loginPage) } + authenticationEntryPoint?.also { formLogin.authenticationEntryPoint(authenticationEntryPoint) } + requiresAuthenticationMatcher?.also { formLogin.requiresAuthenticationMatcher(requiresAuthenticationMatcher) } + authenticationSuccessHandler?.also { formLogin.authenticationSuccessHandler(authenticationSuccessHandler) } + authenticationFailureHandler?.also { formLogin.authenticationFailureHandler(authenticationFailureHandler) } + securityContextRepository?.also { formLogin.securityContextRepository(securityContextRepository) } + if (disabled) { + formLogin.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerFrameOptionsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerFrameOptionsDsl.kt new file mode 100644 index 00000000000..cf95d55887a --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerFrameOptionsDsl.kt @@ -0,0 +1,52 @@ +/* + * 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.config.web.server + +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] X-Frame-Options header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property mode the X-Frame-Options mode to set in the response header. + */ +@ServerSecurityMarker +class ServerFrameOptionsDsl { + var mode: XFrameOptionsServerHttpHeadersWriter.Mode? = null + + private var disabled = false + + /** + * Disables the X-Frame-Options response header + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.FrameOptionsSpec) -> Unit { + return { frameOptions -> + mode?.also { + frameOptions.mode(mode) + } + if (disabled) { + frameOptions.disable() + } + } + } +} 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 new file mode 100644 index 00000000000..b6c435c2ffe --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHeadersDsl.kt @@ -0,0 +1,181 @@ +/* + * 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.config.web.server + +import org.springframework.security.web.server.header.* + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] headers using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@ServerSecurityMarker +class ServerHeadersDsl { + private var contentTypeOptions: ((ServerHttpSecurity.HeaderSpec.ContentTypeOptionsSpec) -> Unit)? = null + private var xssProtection: ((ServerHttpSecurity.HeaderSpec.XssProtectionSpec) -> Unit)? = null + private var cacheControl: ((ServerHttpSecurity.HeaderSpec.CacheSpec) -> Unit)? = null + private var hsts: ((ServerHttpSecurity.HeaderSpec.HstsSpec) -> Unit)? = null + private var frameOptions: ((ServerHttpSecurity.HeaderSpec.FrameOptionsSpec) -> Unit)? = null + private var contentSecurityPolicy: ((ServerHttpSecurity.HeaderSpec.ContentSecurityPolicySpec) -> Unit)? = null + private var referrerPolicy: ((ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit)? = null + private var featurePolicyDirectives: String? = null + + private var disabled = false + + /** + * Configures the [ContentTypeOptionsServerHttpHeadersWriter] which inserts the X-Content-Type-Options header + * + * @param contentTypeOptionsConfig the customization to apply to the header + */ + fun contentTypeOptions(contentTypeOptionsConfig: ServerContentTypeOptionsDsl.() -> Unit) { + this.contentTypeOptions = ServerContentTypeOptionsDsl().apply(contentTypeOptionsConfig).get() + } + + /** + * Note this is not comprehensive XSS protection! + * + *

+ * Allows customizing the [XXssProtectionServerHttpHeadersWriter] which adds the X-XSS-Protection header + *

+ * + * @param xssProtectionConfig the customization to apply to the header + */ + fun xssProtection(xssProtectionConfig: ServerXssProtectionDsl.() -> Unit) { + this.xssProtection = ServerXssProtectionDsl().apply(xssProtectionConfig).get() + } + + /** + * Allows customizing the [CacheControlServerHttpHeadersWriter]. Specifically it adds + * the following headers: + *
    + *
  • Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  • + *
  • Pragma: no-cache
  • + *
  • Expires: 0
  • + *
+ * + * @param cacheControlConfig the customization to apply to the headers + */ + fun cache(cacheControlConfig: ServerCacheControlDsl.() -> Unit) { + this.cacheControl = ServerCacheControlDsl().apply(cacheControlConfig).get() + } + + /** + * Allows customizing the [StrictTransportSecurityServerHttpHeadersWriter] which provides support + * for HTTP Strict Transport Security + * (HSTS). + * + * @param hstsConfig the customization to apply to the header + */ + fun hsts(hstsConfig: ServerHttpStrictTransportSecurityDsl.() -> Unit) { + this.hsts = ServerHttpStrictTransportSecurityDsl().apply(hstsConfig).get() + } + + /** + * Allows customizing the [XFrameOptionsServerHttpHeadersWriter] which add the X-Frame-Options + * header. + * + * @param frameOptionsConfig the customization to apply to the header + */ + fun frameOptions(frameOptionsConfig: ServerFrameOptionsDsl.() -> Unit) { + this.frameOptions = ServerFrameOptionsDsl().apply(frameOptionsConfig).get() + } + + /** + * Allows configuration for Content Security Policy (CSP) Level 2. + * + * @param contentSecurityPolicyConfig the customization to apply to the header + */ + fun contentSecurityPolicy(contentSecurityPolicyConfig: ServerContentSecurityPolicyDsl.() -> Unit) { + this.contentSecurityPolicy = ServerContentSecurityPolicyDsl().apply(contentSecurityPolicyConfig).get() + } + + /** + * Allows configuration for Referrer Policy. + * + *

+ * Configuration is provided to the [ReferrerPolicyServerHttpHeadersWriter] which support the writing + * of the header as detailed in the W3C Technical Report: + *

+ *
    + *
  • Referrer-Policy
  • + *
+ * + * @param referrerPolicyConfig the customization to apply to the header + */ + fun referrerPolicy(referrerPolicyConfig: ServerReferrerPolicyDsl.() -> Unit) { + this.referrerPolicy = ServerReferrerPolicyDsl().apply(referrerPolicyConfig).get() + } + + /** + * Allows configuration for Feature + * Policy. + * + *

+ * Calling this method automatically enables (includes) the Feature-Policy + * header in the response using the supplied policy directive(s). + *

+ * + * @param policyDirectives policyDirectives the security policy directive(s) + */ + fun featurePolicy(policyDirectives: String) { + this.featurePolicyDirectives = policyDirectives + } + + /** + * Disables HTTP response headers. + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec) -> Unit { + return { headers -> + contentTypeOptions?.also { + headers.contentTypeOptions(contentTypeOptions) + } + xssProtection?.also { + headers.xssProtection(xssProtection) + } + cacheControl?.also { + headers.cache(cacheControl) + } + hsts?.also { + headers.hsts(hsts) + } + frameOptions?.also { + headers.frameOptions(frameOptions) + } + contentSecurityPolicy?.also { + headers.contentSecurityPolicy(contentSecurityPolicy) + } + featurePolicyDirectives?.also { + headers.featurePolicy(featurePolicyDirectives) + } + referrerPolicy?.also { + headers.referrerPolicy(referrerPolicy) + } + if (disabled) { + headers.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDsl.kt new file mode 100644 index 00000000000..91b157c2644 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDsl.kt @@ -0,0 +1,66 @@ +/* + * 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.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.context.ReactorContextWebFilter +import org.springframework.security.web.server.context.ServerSecurityContextRepository + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] basic authorization using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to authenticate. + * @property securityContextRepository the [ServerSecurityContextRepository] used to save + * the [Authentication]. For the [SecurityContext] to be loaded on subsequent requests the + * [ReactorContextWebFilter] must be configured to be able to load the value (they are not + * implicitly linked). + * @property authenticationEntryPoint the [ServerAuthenticationEntryPoint] to be + * populated on [BasicAuthenticationFilter] in the event that authentication fails. + */ +@ServerSecurityMarker +class ServerHttpBasicDsl { + var authenticationManager: ReactiveAuthenticationManager? = null + var securityContextRepository: ServerSecurityContextRepository? = null + var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null + + private var disabled = false + + /** + * Disables HTTP basic authentication + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HttpBasicSpec) -> Unit { + return { httpBasic -> + authenticationManager?.also { httpBasic.authenticationManager(authenticationManager) } + securityContextRepository?.also { httpBasic.securityContextRepository(securityContextRepository) } + authenticationEntryPoint?.also { httpBasic.authenticationEntryPoint(authenticationEntryPoint) } + if (disabled) { + httpBasic.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt new file mode 100644 index 00000000000..8f09f5589a2 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDsl.kt @@ -0,0 +1,529 @@ +/* + * 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.config.web.server + +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.web.server.ServerWebExchange + +/** + * Configures [ServerHttpSecurity] using a [ServerHttpSecurity Kotlin DSL][ServerHttpSecurityDsl]. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * authorizeExchange { + * exchange("/public", permitAll) + * exchange(anyExchange, authenticated) + * } + * } + * } + * } + * ``` + * + * @author Eleftheria Stein + * @since 5.4 + * @param httpConfiguration the configurations to apply to [ServerHttpSecurity] + */ +operator fun ServerHttpSecurity.invoke(httpConfiguration: ServerHttpSecurityDsl.() -> Unit): SecurityWebFilterChain = + ServerHttpSecurityDsl(this, httpConfiguration).build() + +/** + * A [ServerHttpSecurity] Kotlin DSL created by [`http { }`][invoke] + * in order to configure [ServerHttpSecurity] using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @param init the configurations to apply to the provided [ServerHttpSecurity] + */ +@ServerSecurityMarker +class ServerHttpSecurityDsl(private val http: ServerHttpSecurity, private val init: ServerHttpSecurityDsl.() -> Unit) { + + /** + * Allows configuring the [ServerHttpSecurity] to only be invoked when matching the + * provided [ServerWebExchangeMatcher]. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**")) + * formLogin { + * loginPage = "/log-in" + * } + * } + * } + * } + * ``` + * + * @param securityMatcher a [ServerWebExchangeMatcher] used to determine whether this + * configuration should be invoked. + */ + fun securityMatcher(securityMatcher: ServerWebExchangeMatcher) { + this.http.securityMatcher(securityMatcher) + } + + /** + * Enables form based authentication. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * formLogin { + * loginPage = "/log-in" + * } + * } + * } + * } + * ``` + * + * @param formLoginConfiguration custom configuration to apply to the form based + * authentication + * @see [ServerFormLoginDsl] + */ + fun formLogin(formLoginConfiguration: ServerFormLoginDsl.() -> Unit) { + val formLoginCustomizer = ServerFormLoginDsl().apply(formLoginConfiguration).get() + this.http.formLogin(formLoginCustomizer) + } + + /** + * Allows restricting access based upon the [ServerWebExchange] + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * authorizeExchange { + * exchange("/public", permitAll) + * exchange(anyExchange, authenticated) + * } + * } + * } + * } + * ``` + * + * @param authorizeExchangeConfiguration custom configuration that specifies + * access for an exchange + * @see [AuthorizeExchangeDsl] + */ + fun authorizeExchange(authorizeExchangeConfiguration: AuthorizeExchangeDsl.() -> Unit) { + val authorizeExchangeCustomizer = AuthorizeExchangeDsl().apply(authorizeExchangeConfiguration).get() + this.http.authorizeExchange(authorizeExchangeCustomizer) + } + + /** + * Enables HTTP basic authentication. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * httpBasic { } + * } + * } + * } + * ``` + * + * @param httpBasicConfiguration custom configuration to be applied to the + * HTTP basic authentication + * @see [ServerHttpBasicDsl] + */ + fun httpBasic(httpBasicConfiguration: ServerHttpBasicDsl.() -> Unit) { + val httpBasicCustomizer = ServerHttpBasicDsl().apply(httpBasicConfiguration).get() + this.http.httpBasic(httpBasicCustomizer) + } + + /** + * Allows configuring response headers. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * headers { + * referrerPolicy { + * policy = ReferrerPolicy.SAME_ORIGIN + * } + * frameOptions { + * mode = Mode.DENY + * } + * } + * } + * } + * } + * ``` + * + * @param headersConfiguration custom configuration to be applied to the + * response headers + * @see [ServerHeadersDsl] + */ + fun headers(headersConfiguration: ServerHeadersDsl.() -> Unit) { + val headersCustomizer = ServerHeadersDsl().apply(headersConfiguration).get() + this.http.headers(headersCustomizer) + } + + /** + * Allows configuring CORS. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * cors { + * configurationSource = customConfigurationSource + * } + * } + * } + * } + * ``` + * + * @param corsConfiguration custom configuration to be applied to the + * CORS headers + * @see [ServerCorsDsl] + */ + fun cors(corsConfiguration: ServerCorsDsl.() -> Unit) { + val corsCustomizer = ServerCorsDsl().apply(corsConfiguration).get() + this.http.cors(corsCustomizer) + } + + /** + * Allows configuring HTTPS redirection rules. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * redirectToHttps { + * httpsRedirectWhen { + * it.request.headers.containsKey("X-Requires-Https") + * } + * } + * } + * } + * } + * ``` + * + * @param httpsRedirectConfiguration custom configuration for the HTTPS redirect + * rules. + * @see [ServerHttpsRedirectDsl] + */ + fun redirectToHttps(httpsRedirectConfiguration: ServerHttpsRedirectDsl.() -> Unit) { + val httpsRedirectCustomizer = ServerHttpsRedirectDsl().apply(httpsRedirectConfiguration).get() + this.http.redirectToHttps(httpsRedirectCustomizer) + } + + /** + * Allows configuring exception handling. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * exceptionHandling { + * authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/auth") + * } + * } + * } + * } + * ``` + * + * @param exceptionHandlingConfiguration custom configuration to apply to + * exception handling + * @see [ServerExceptionHandlingDsl] + */ + fun exceptionHandling(exceptionHandlingConfiguration: ServerExceptionHandlingDsl.() -> Unit) { + val exceptionHandlingCustomizer = ServerExceptionHandlingDsl().apply(exceptionHandlingConfiguration).get() + this.http.exceptionHandling(exceptionHandlingCustomizer) + } + + /** + * Adds X509 based pre authentication to an application using a certificate provided by a client. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * x509 { } + * } + * } + * } + * ``` + * + * @param x509Configuration custom configuration to apply to the X509 based pre authentication + * @see [ServerX509Dsl] + */ + fun x509(x509Configuration: ServerX509Dsl.() -> Unit) { + val x509Customizer = ServerX509Dsl().apply(x509Configuration).get() + this.http.x509(x509Customizer) + } + + /** + * Allows configuring request cache which is used when a flow is interrupted (i.e. due to requesting credentials) + * so that the request can be replayed after authentication. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * requestCache { } + * } + * } + * } + * ``` + * + * @param requestCacheConfiguration custom configuration to apply to the request cache + * @see [ServerRequestCacheDsl] + */ + fun requestCache(requestCacheConfiguration: ServerRequestCacheDsl.() -> Unit) { + val requestCacheCustomizer = ServerRequestCacheDsl().apply(requestCacheConfiguration).get() + this.http.requestCache(requestCacheCustomizer) + } + + /** + * Enables CSRF protection. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * csrf { } + * } + * } + * } + * ``` + * + * @param csrfConfiguration custom configuration to apply to the CSRF protection + * @see [ServerCsrfDsl] + */ + fun csrf(csrfConfiguration: ServerCsrfDsl.() -> Unit) { + val csrfCustomizer = ServerCsrfDsl().apply(csrfConfiguration).get() + this.http.csrf(csrfCustomizer) + } + + /** + * Provides logout support. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * logout { + * logoutUrl = "/sign-out" + * } + * } + * } + * } + * ``` + * + * @param logoutConfiguration custom configuration to apply to logout + * @see [ServerLogoutDsl] + */ + fun logout(logoutConfiguration: ServerLogoutDsl.() -> Unit) { + val logoutCustomizer = ServerLogoutDsl().apply(logoutConfiguration).get() + this.http.logout(logoutCustomizer) + } + + /** + * Enables and configures anonymous authentication. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * anonymous { + * authorities = listOf(SimpleGrantedAuthority("ROLE_ANON")) + * } + * } + * } + * } + * ``` + * + * @param anonymousConfiguration custom configuration to apply to anonymous authentication + * @see [ServerAnonymousDsl] + */ + fun anonymous(anonymousConfiguration: ServerAnonymousDsl.() -> Unit) { + val anonymousCustomizer = ServerAnonymousDsl().apply(anonymousConfiguration).get() + this.http.anonymous(anonymousCustomizer) + } + + /** + * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + * A [ReactiveClientRegistrationRepository] is required and must be registered as a Bean or + * configured via [ServerOAuth2LoginDsl.clientRegistrationRepository]. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * oauth2Login { + * clientRegistrationRepository = getClientRegistrationRepository() + * } + * } + * } + * } + * ``` + * + * @param oauth2LoginConfiguration custom configuration to configure the OAuth 2.0 Login + * @see [ServerOAuth2LoginDsl] + */ + fun oauth2Login(oauth2LoginConfiguration: ServerOAuth2LoginDsl.() -> Unit) { + val oauth2LoginCustomizer = ServerOAuth2LoginDsl().apply(oauth2LoginConfiguration).get() + this.http.oauth2Login(oauth2LoginCustomizer) + } + + /** + * Configures OAuth2 client support. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * oauth2Client { + * clientRegistrationRepository = getClientRegistrationRepository() + * } + * } + * } + * } + * ``` + * + * @param oauth2ClientConfiguration custom configuration to configure the OAuth 2.0 client + * @see [ServerOAuth2ClientDsl] + */ + fun oauth2Client(oauth2ClientConfiguration: ServerOAuth2ClientDsl.() -> Unit) { + val oauth2ClientCustomizer = ServerOAuth2ClientDsl().apply(oauth2ClientConfiguration).get() + this.http.oauth2Client(oauth2ClientCustomizer) + } + + /** + * Configures OAuth2 resource server support. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * oauth2ResourceServer { + * jwt { } + * } + * } + * } + * } + * ``` + * + * @param oauth2ResourceServerConfiguration custom configuration to configure the OAuth 2.0 resource server + * @see [ServerOAuth2ResourceServerDsl] + */ + fun oauth2ResourceServer(oauth2ResourceServerConfiguration: ServerOAuth2ResourceServerDsl.() -> Unit) { + val oauth2ResourceServerCustomizer = ServerOAuth2ResourceServerDsl().apply(oauth2ResourceServerConfiguration).get() + this.http.oauth2ResourceServer(oauth2ResourceServerCustomizer) + } + + /** + * Apply all configurations to the provided [ServerHttpSecurity] + */ + internal fun build(): SecurityWebFilterChain { + init() + return this.http.build() + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpStrictTransportSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpStrictTransportSecurityDsl.kt new file mode 100644 index 00000000000..cb77fded423 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpStrictTransportSecurityDsl.kt @@ -0,0 +1,57 @@ +/* + * 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.config.web.server + +import java.time.Duration + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] HTTP Strict Transport Security + * header using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property maxAge he value for the max-age directive of the Strict-Transport-Security + * header. + * @property includeSubdomains if true, subdomains should be considered HSTS Hosts too. + * @property preload if true, preload will be included in HSTS Header. + */ +@ServerSecurityMarker +class ServerHttpStrictTransportSecurityDsl { + var maxAge: Duration? = null + var includeSubdomains: Boolean? = null + var preload: Boolean? = null + + private var disabled = false + + /** + * Disables the X-Frame-Options response header + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.HstsSpec) -> Unit { + return { hsts -> + maxAge?.also { hsts.maxAge(maxAge) } + includeSubdomains?.also { hsts.includeSubdomains(includeSubdomains!!) } + preload?.also { hsts.preload(preload!!) } + if (disabled) { + hsts.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDsl.kt new file mode 100644 index 00000000000..4cebc72fe34 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDsl.kt @@ -0,0 +1,71 @@ +/* + * 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.config.web.server + +import org.springframework.security.web.PortMapper +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.web.server.ServerWebExchange + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] HTTPS redirection rules using idiomatic + * Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property portMapper the [PortMapper] that specifies a custom HTTPS port to redirect to. + */ +@ServerSecurityMarker +class ServerHttpsRedirectDsl { + var portMapper: PortMapper? = null + + private var redirectMatchers: Array? = null + private var redirectMatcherFunction: ((ServerWebExchange) -> Boolean)? = null + + /** + * Configures when this filter should redirect to https. + * If invoked multiple times, whether a matcher or a function is provided, only the + * last redirect rule will apply and all previous rules will be overridden. + * + * @param redirectMatchers the list of conditions that, when any are met, the + * filter should redirect to https. + */ + fun httpsRedirectWhen(vararg redirectMatchers: ServerWebExchangeMatcher) { + this.redirectMatcherFunction = null + this.redirectMatchers = redirectMatchers + } + + /** + * Configures when this filter should redirect to https. + * If invoked multiple times, whether a matcher or a function is provided, only the + * last redirect rule will apply and all previous rules will be overridden. + * + * @param redirectMatcherFunction the condition in which the filter should redirect to + * https. + */ + fun httpsRedirectWhen(redirectMatcherFunction: (ServerWebExchange) -> Boolean) { + this.redirectMatchers = null + this.redirectMatcherFunction = redirectMatcherFunction + } + + internal fun get(): (ServerHttpSecurity.HttpsRedirectSpec) -> Unit { + return { httpsRedirect -> + portMapper?.also { httpsRedirect.portMapper(portMapper) } + redirectMatchers?.also { httpsRedirect.httpsRedirectWhen(*redirectMatchers!!) } + redirectMatcherFunction?.also { httpsRedirect.httpsRedirectWhen(redirectMatcherFunction) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerJwtDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerJwtDsl.kt new file mode 100644 index 00000000000..0ba0501c66a --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerJwtDsl.kt @@ -0,0 +1,82 @@ +/* + * 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.config.web.server + +import org.springframework.core.convert.converter.Converter +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder +import reactor.core.publisher.Mono +import java.security.interfaces.RSAPublicKey + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] JWT Resource Server support using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided + * [Authentication] can be authenticated. + * @property jwtAuthenticationConverter the [Converter] to use for converting a [Jwt] into an + * [AbstractAuthenticationToken]. + * @property jwtDecoder the [ReactiveJwtDecoder] to use. + * @property publicKey configures a [ReactiveJwtDecoder] that leverages the provided [RSAPublicKey] + * @property jwkSetUri configures a [ReactiveJwtDecoder] using a + * JSON Web Key (JWK) URL + */ +@ServerSecurityMarker +class ServerJwtDsl { + private var _jwtDecoder: ReactiveJwtDecoder? = null + private var _publicKey: RSAPublicKey? = null + private var _jwkSetUri: String? = null + + var authenticationManager: ReactiveAuthenticationManager? = null + var jwtAuthenticationConverter: Converter>? = null + + var jwtDecoder: ReactiveJwtDecoder? + get() = _jwtDecoder + set(value) { + _jwtDecoder = value + _publicKey = null + _jwkSetUri = null + } + var publicKey: RSAPublicKey? + get() = _publicKey + set(value) { + _publicKey = value + _jwtDecoder = null + _jwkSetUri = null + } + var jwkSetUri: String? + get() = _jwkSetUri + set(value) { + _jwkSetUri = value + _jwtDecoder = null + _publicKey = null + } + + internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.JwtSpec) -> Unit { + return { jwt -> + authenticationManager?.also { jwt.authenticationManager(authenticationManager) } + jwtAuthenticationConverter?.also { jwt.jwtAuthenticationConverter(jwtAuthenticationConverter) } + jwtDecoder?.also { jwt.jwtDecoder(jwtDecoder) } + publicKey?.also { jwt.publicKey(publicKey) } + jwkSetUri?.also { jwt.jwkSetUri(jwkSetUri) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerLogoutDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerLogoutDsl.kt new file mode 100644 index 00000000000..021fb770d7f --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerLogoutDsl.kt @@ -0,0 +1,62 @@ +/* + * 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.config.web.server + +import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler +import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] logout support using idiomatic Kotlin + * code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property logoutHandler a [ServerLogoutHandler] that is invoked when logout occurs. + * @property logoutUrl the URL that triggers logout to occur. + * @property requiresLogout the [ServerWebExchangeMatcher] that triggers logout to occur. + * @property logoutSuccessHandler the [ServerLogoutSuccessHandler] to use after logout has + * occurred. + */ +@ServerSecurityMarker +class ServerLogoutDsl { + var logoutHandler: ServerLogoutHandler? = null + var logoutUrl: String? = null + var requiresLogout: ServerWebExchangeMatcher? = null + var logoutSuccessHandler: ServerLogoutSuccessHandler? = null + + private var disabled = false + + /** + * Disables logout + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.LogoutSpec) -> Unit { + return { logout -> + logoutHandler?.also { logout.logoutHandler(logoutHandler) } + logoutUrl?.also { logout.logoutUrl(logoutUrl) } + requiresLogout?.also { logout.requiresLogout(requiresLogout) } + logoutSuccessHandler?.also { logout.logoutSuccessHandler(logoutSuccessHandler) } + if (disabled) { + logout.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDsl.kt new file mode 100644 index 00000000000..6751d242963 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDsl.kt @@ -0,0 +1,58 @@ +/* + * 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.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.web.server.ServerWebExchange + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] OAuth 2.0 client using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided + * [Authentication] can be authenticated. + * @property authenticationConverter the [ServerAuthenticationConverter] used for converting from a [ServerWebExchange] + * to an [Authentication]. + * @property clientRegistrationRepository the repository of client registrations. + * @property authorizedClientRepository the repository for authorized client(s). + * @property authorizationRequestRepository the repository to use for storing [OAuth2AuthorizationRequest]s. + */ +@ServerSecurityMarker +class ServerOAuth2ClientDsl { + var authenticationManager: ReactiveAuthenticationManager? = null + var authenticationConverter: ServerAuthenticationConverter? = null + var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null + var authorizedClientRepository: ServerOAuth2AuthorizedClientRepository? = null + var authorizationRequestRepository: ServerAuthorizationRequestRepository? = null + + internal fun get(): (ServerHttpSecurity.OAuth2ClientSpec) -> Unit { + return { oauth2Client -> + authenticationManager?.also { oauth2Client.authenticationManager(authenticationManager) } + authenticationConverter?.also { oauth2Client.authenticationConverter(authenticationConverter) } + clientRegistrationRepository?.also { oauth2Client.clientRegistrationRepository(clientRegistrationRepository) } + authorizedClientRepository?.also { oauth2Client.authorizedClientRepository(authorizedClientRepository) } + authorizationRequestRepository?.also { oauth2Client.authorizationRequestRepository(authorizationRequestRepository) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt new file mode 100644 index 00000000000..0c24340fbb3 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDsl.kt @@ -0,0 +1,84 @@ +/* + * 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.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizationRequestResolver +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler +import org.springframework.security.web.server.context.ServerSecurityContextRepository +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.web.server.ServerWebExchange + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] OAuth 2.0 login using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided + * [Authentication] can be authenticated. + * @property securityContextRepository the [ServerSecurityContextRepository] used to save the [Authentication]. + * @property authenticationSuccessHandler the [ServerAuthenticationSuccessHandler] used after authentication success. + * @property authenticationFailureHandler the [ServerAuthenticationFailureHandler] used after authentication failure. + * @property authenticationConverter the [ServerAuthenticationConverter] used for converting from a [ServerWebExchange] + * to an [Authentication]. + * @property clientRegistrationRepository the repository of client registrations. + * @property authorizedClientService the service responsible for associating an access token to a client and resource + * owner. + * @property authorizedClientRepository the repository for authorized client(s). + * @property authorizationRequestRepository the repository to use for storing [OAuth2AuthorizationRequest]s. + * @property authorizationRequestResolver the resolver used for resolving [OAuth2AuthorizationRequest]s. + * @property authenticationMatcher the [ServerWebExchangeMatcher] used for determining if the request is an + * authentication request. + */ +@ServerSecurityMarker +class ServerOAuth2LoginDsl { + var authenticationManager: ReactiveAuthenticationManager? = null + var securityContextRepository: ServerSecurityContextRepository? = null + var authenticationSuccessHandler: ServerAuthenticationSuccessHandler? = null + var authenticationFailureHandler: ServerAuthenticationFailureHandler? = null + var authenticationConverter: ServerAuthenticationConverter? = null + var clientRegistrationRepository: ReactiveClientRegistrationRepository? = null + var authorizedClientService: ReactiveOAuth2AuthorizedClientService? = null + var authorizedClientRepository: ServerOAuth2AuthorizedClientRepository? = null + var authorizationRequestRepository: ServerAuthorizationRequestRepository? = null + var authorizationRequestResolver: ServerOAuth2AuthorizationRequestResolver? = null + var authenticationMatcher: ServerWebExchangeMatcher? = null + + internal fun get(): (ServerHttpSecurity.OAuth2LoginSpec) -> Unit { + return { oauth2Login -> + authenticationManager?.also { oauth2Login.authenticationManager(authenticationManager) } + securityContextRepository?.also { oauth2Login.securityContextRepository(securityContextRepository) } + authenticationSuccessHandler?.also { oauth2Login.authenticationSuccessHandler(authenticationSuccessHandler) } + authenticationFailureHandler?.also { oauth2Login.authenticationFailureHandler(authenticationFailureHandler) } + authenticationConverter?.also { oauth2Login.authenticationConverter(authenticationConverter) } + clientRegistrationRepository?.also { oauth2Login.clientRegistrationRepository(clientRegistrationRepository) } + authorizedClientService?.also { oauth2Login.authorizedClientService(authorizedClientService) } + authorizedClientRepository?.also { oauth2Login.authorizedClientRepository(authorizedClientRepository) } + authorizationRequestRepository?.also { oauth2Login.authorizationRequestRepository(authorizationRequestRepository) } + authorizationRequestResolver?.also { oauth2Login.authorizationRequestResolver(authorizationRequestResolver) } + authenticationMatcher?.also { oauth2Login.authenticationMatcher(authenticationMatcher) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDsl.kt new file mode 100644 index 00000000000..ee48923469b --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDsl.kt @@ -0,0 +1,117 @@ +/* + * 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.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler +import org.springframework.web.server.ServerWebExchange + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] OAuth 2.0 resource server using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property accessDeniedHandler the [ServerAccessDeniedHandler] to use for requests authenticating with + * Bearer Tokens. + * @property authenticationEntryPoint the [ServerAuthenticationEntryPoint] to use for requests authenticating with + * Bearer Tokens. + * @property bearerTokenConverter the [ServerAuthenticationConverter] to use for requests authenticating with + * Bearer Tokens. + * @property authenticationManagerResolver the [ReactiveAuthenticationManagerResolver] to use. + */ +@ServerSecurityMarker +class ServerOAuth2ResourceServerDsl { + var accessDeniedHandler: ServerAccessDeniedHandler? = null + var authenticationEntryPoint: ServerAuthenticationEntryPoint? = null + var bearerTokenConverter: ServerAuthenticationConverter? = null + var authenticationManagerResolver: ReactiveAuthenticationManagerResolver? = null + + private var jwt: ((ServerHttpSecurity.OAuth2ResourceServerSpec.JwtSpec) -> Unit)? = null + private var opaqueToken: ((ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit)? = null + + /** + * Enables JWT-encoded bearer token support. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * oauth2ResourceServer { + * jwt { + * jwkSetUri = "https://example.com/oauth2/jwk" + * } + * } + * } + * } + * } + * ``` + * + * @param jwtConfig custom configurations to configure JWT resource server support + * @see [ServerJwtDsl] + */ + fun jwt(jwtConfig: ServerJwtDsl.() -> Unit) { + this.jwt = ServerJwtDsl().apply(jwtConfig).get() + } + + /** + * Enables opaque token support. + * + * Example: + * + * ``` + * @EnableWebFluxSecurity + * class SecurityConfig { + * + * @Bean + * fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + * return http { + * oauth2ResourceServer { + * opaqueToken { + * introspectionUri = "https://example.com/introspect" + * introspectionClientCredentials("client", "secret") + * } + * } + * } + * } + * } + * ``` + * + * @param opaqueTokenConfig custom configurations to configure JWT resource server support + * @see [ServerOpaqueTokenDsl] + */ + fun opaqueToken(opaqueTokenConfig: ServerOpaqueTokenDsl.() -> Unit) { + this.opaqueToken = ServerOpaqueTokenDsl().apply(opaqueTokenConfig).get() + } + + internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec) -> Unit { + return { oauth2ResourceServer -> + accessDeniedHandler?.also { oauth2ResourceServer.accessDeniedHandler(accessDeniedHandler) } + authenticationEntryPoint?.also { oauth2ResourceServer.authenticationEntryPoint(authenticationEntryPoint) } + bearerTokenConverter?.also { oauth2ResourceServer.bearerTokenConverter(bearerTokenConverter) } + authenticationManagerResolver?.also { oauth2ResourceServer.authenticationManagerResolver(authenticationManagerResolver!!) } + jwt?.also { oauth2ResourceServer.jwt(jwt) } + opaqueToken?.also { oauth2ResourceServer.opaqueToken(opaqueToken) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt new file mode 100644 index 00000000000..72d9bf103fb --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDsl.kt @@ -0,0 +1,67 @@ +/* + * 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.config.web.server + +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] Opaque Token Resource Server support using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property introspectionUri the URI of the Introspection endpoint. + * @property introspector the [ReactiveOpaqueTokenIntrospector] to use. + */ +@ServerSecurityMarker +class ServerOpaqueTokenDsl { + private var _introspectionUri: String? = null + private var _introspector: ReactiveOpaqueTokenIntrospector? = null + private var clientCredentials: Pair? = null + + var introspectionUri: String? + get() = _introspectionUri + set(value) { + _introspectionUri = value + _introspector = null + } + var introspector: ReactiveOpaqueTokenIntrospector? + get() = _introspector + set(value) { + _introspector = value + _introspectionUri = null + clientCredentials = null + } + + /** + * Configures the credentials for Introspection endpoint. + * + * @param clientId the clientId part of the credentials. + * @param clientSecret the clientSecret part of the credentials. + */ + fun introspectionClientCredentials(clientId: String, clientSecret: String) { + clientCredentials = Pair(clientId, clientSecret) + _introspector = null + } + + internal fun get(): (ServerHttpSecurity.OAuth2ResourceServerSpec.OpaqueTokenSpec) -> Unit { + return { opaqueToken -> + introspectionUri?.also { opaqueToken.introspectionUri(introspectionUri) } + clientCredentials?.also { opaqueToken.introspectionClientCredentials(clientCredentials!!.first, clientCredentials!!.second) } + introspector?.also { opaqueToken.introspector(introspector) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerReferrerPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerReferrerPolicyDsl.kt new file mode 100644 index 00000000000..2e247378def --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerReferrerPolicyDsl.kt @@ -0,0 +1,40 @@ +/* + * 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.config.web.server + +import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] referrer policy header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property policy the policy to be used in the response header. + */ +@ServerSecurityMarker +class ServerReferrerPolicyDsl { + var policy: ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy? = null + + internal fun get(): (ServerHttpSecurity.HeaderSpec.ReferrerPolicySpec) -> Unit { + return { referrerPolicy -> + policy?.also { + referrerPolicy.policy(policy) + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDsl.kt new file mode 100644 index 00000000000..59d25ad054a --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDsl.kt @@ -0,0 +1,51 @@ +/* + * 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.config.web.server + +import org.springframework.security.web.server.savedrequest.ServerRequestCache + +/** + * A Kotlin DSL to configure the request cache using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property requestCache allows explicit configuration of the [ServerRequestCache] to be used. + */ +@ServerSecurityMarker +class ServerRequestCacheDsl { + var requestCache: ServerRequestCache? = null + + private var disabled = false + + /** + * Disables the request cache. + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.RequestCacheSpec) -> Unit { + return { requestCacheConfig -> + requestCache?.also { + requestCacheConfig.requestCache(requestCache) + if (disabled) { + requestCacheConfig.disable() + } + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerSecurityMarker.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerSecurityMarker.kt new file mode 100644 index 00000000000..29fbdde03f6 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerSecurityMarker.kt @@ -0,0 +1,26 @@ +/* + * 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.config.web.server + +/** + * Marker annotation indicating that the annotated class is part of the security DSL for server configuration. + * + * @author Loïc Labagnara + * @since 5.4 + */ +@DslMarker +annotation class ServerSecurityMarker diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerX509Dsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerX509Dsl.kt new file mode 100644 index 00000000000..a970bd1b51d --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerX509Dsl.kt @@ -0,0 +1,43 @@ +/* + * 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.config.web.server + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor + +/** + * A Kotlin DSL to configure [ServerHttpSecurity] X509 based pre authentication using idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + * @property principalExtractor the [X509PrincipalExtractor] used to obtain the principal for use within the framework. + * @property authenticationManager the [ReactiveAuthenticationManager] used to determine if the provided + * [Authentication] can be authenticated. + */ +@ServerSecurityMarker +class ServerX509Dsl { + var principalExtractor: X509PrincipalExtractor? = null + var authenticationManager: ReactiveAuthenticationManager? = null + + internal fun get(): (ServerHttpSecurity.X509Spec) -> Unit { + return { x509 -> + authenticationManager?.also { x509.authenticationManager(authenticationManager) } + principalExtractor?.also { x509.principalExtractor(principalExtractor) } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDsl.kt new file mode 100644 index 00000000000..9166acf4fe7 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDsl.kt @@ -0,0 +1,44 @@ +/* + * 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.config.web.server + +/** + * A Kotlin DSL to configure the [ServerHttpSecurity] XSS protection header using + * idiomatic Kotlin code. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@ServerSecurityMarker +class ServerXssProtectionDsl { + private var disabled = false + + /** + * Disables cache control response headers + */ + fun disable() { + disabled = true + } + + internal fun get(): (ServerHttpSecurity.HeaderSpec.XssProtectionSpec) -> Unit { + return { xss -> + if (disabled) { + xss.disable() + } + } + } +} diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt index 3fcf38fa383..9e35287b5f5 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AbstractRequestMatcherDsl.kt @@ -16,6 +16,7 @@ package org.springframework.security.config.web.servlet +import org.springframework.http.HttpMethod import org.springframework.security.web.util.matcher.AnyRequestMatcher import org.springframework.security.web.util.matcher.RequestMatcher @@ -37,7 +38,8 @@ abstract class AbstractRequestMatcherDsl { protected data class PatternAuthorizationRule(val pattern: String, val patternType: PatternType, - val servletPath: String?, + val servletPath: String? = null, + val httpMethod: HttpMethod? = null, override val rule: String) : AuthorizationRule(rule) protected abstract class AuthorizationRule(open val rule: String) diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt index 5734c2caf68..eb0887d7bf5 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDsl.kt @@ -16,6 +16,7 @@ package org.springframework.security.config.web.servlet +import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer import org.springframework.security.web.util.matcher.AnyRequestMatcher @@ -35,6 +36,7 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { private val MVC_PRESENT = ClassUtils.isPresent( HANDLER_MAPPING_INTROSPECTOR, AuthorizeRequestsDsl::class.java.classLoader) + private val PATTERN_TYPE = if (MVC_PRESENT) PatternType.MVC else PatternType.ANT /** * Adds a request authorization rule. @@ -64,11 +66,32 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { * (i.e. "hasAuthority('ROLE_USER') and hasAuthority('ROLE_SUPER')") */ fun authorize(pattern: String, access: String = "authenticated") { - if (MVC_PRESENT) { - authorizationRules.add(PatternAuthorizationRule(pattern, PatternType.MVC, null, access)) - } else { - authorizationRules.add(PatternAuthorizationRule(pattern, PatternType.ANT, null, access)) - } + authorizationRules.add(PatternAuthorizationRule(pattern = pattern, + patternType = PATTERN_TYPE, + rule = access)) + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * If Spring MVC is not an the classpath, it will use an ant matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param method the HTTP method to match the income requests against. + * @param pattern the pattern to match incoming requests against. + * @param access the SpEL expression to secure the matching request + * (i.e. "hasAuthority('ROLE_USER') and hasAuthority('ROLE_SUPER')") + */ + fun authorize(method: HttpMethod, pattern: String, access: String = "authenticated") { + authorizationRules.add(PatternAuthorizationRule(pattern = pattern, + patternType = PATTERN_TYPE, + httpMethod = method, + rule = access)) } /** @@ -89,11 +112,36 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { * (i.e. "hasAuthority('ROLE_USER') and hasAuthority('ROLE_SUPER')") */ fun authorize(pattern: String, servletPath: String, access: String = "authenticated") { - if (MVC_PRESENT) { - authorizationRules.add(PatternAuthorizationRule(pattern, PatternType.MVC, servletPath, access)) - } else { - authorizationRules.add(PatternAuthorizationRule(pattern, PatternType.ANT, servletPath, access)) - } + authorizationRules.add(PatternAuthorizationRule(pattern = pattern, + patternType = PATTERN_TYPE, + servletPath = servletPath, + rule = access)) + } + + /** + * Adds a request authorization rule for an endpoint matching the provided + * pattern. + * If Spring MVC is on the classpath, it will use an MVC matcher. + * If Spring MVC is not an the classpath, it will use an ant matcher. + * The MVC will use the same rules that Spring MVC uses for matching. + * For example, often times a mapping of the path "/path" will match on + * "/path", "/path/", "/path.html", etc. + * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as an ant pattern will be used. + * + * @param method the HTTP method to match the income requests against. + * @param pattern the pattern to match incoming requests against. + * @param servletPath the servlet path to match incoming requests against. This + * only applies when using an MVC pattern matcher. + * @param access the SpEL expression to secure the matching request + * (i.e. "hasAuthority('ROLE_USER') and hasAuthority('ROLE_SUPER')") + */ + fun authorize(method: HttpMethod, pattern: String, servletPath: String, access: String = "authenticated") { + authorizationRules.add(PatternAuthorizationRule(pattern = pattern, + patternType = PATTERN_TYPE, + servletPath = servletPath, + httpMethod = method, + rule = access)) } /** @@ -152,12 +200,10 @@ class AuthorizeRequestsDsl : AbstractRequestMatcherDsl() { is MatcherAuthorizationRule -> requests.requestMatchers(rule.matcher).access(rule.rule) is PatternAuthorizationRule -> { when (rule.patternType) { - PatternType.ANT -> requests.antMatchers(rule.pattern).access(rule.rule) - PatternType.MVC -> { - val mvcMatchersAuthorizeUrl = requests.mvcMatchers(rule.pattern) - rule.servletPath?.also { mvcMatchersAuthorizeUrl.servletPath(rule.servletPath) } - mvcMatchersAuthorizeUrl.access(rule.rule) - } + PatternType.ANT -> requests.antMatchers(rule.httpMethod, rule.pattern).access(rule.rule) + PatternType.MVC -> requests.mvcMatchers(rule.httpMethod, rule.pattern) + .apply { if(rule.servletPath != null) servletPath(rule.servletPath) } + .access(rule.rule) } } } 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 2b3abfe8ee3..ae1d9d945e6 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 @@ -40,6 +40,7 @@ class HeadersDsl { private var contentSecurityPolicy: ((HeadersConfigurer.ContentSecurityPolicyConfig) -> Unit)? = null private var referrerPolicy: ((HeadersConfigurer.ReferrerPolicyConfig) -> Unit)? = null private var featurePolicyDirectives: String? = null + private var disabled = false var defaultsDisabled: Boolean? = null @@ -161,6 +162,15 @@ class HeadersDsl { this.featurePolicyDirectives = policyDirectives } + /** + * Disable all HTTP security headers. + * + * @since 5.4 + */ + fun disable() { + disabled = true + } + internal fun get(): (HeadersConfigurer) -> Unit { return { headers -> defaultsDisabled?.also { @@ -195,6 +205,9 @@ class HeadersDsl { featurePolicyDirectives?.also { headers.featurePolicy(featurePolicyDirectives) } + if (disabled) { + headers.disable() + } } } } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt index 6c672a8d1f2..137d459ce1c 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDsl.kt @@ -669,6 +669,134 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu this.http.addFilterAt(filter, atFilter) } + /** + * Adds the [Filter] at the location of the specified [Filter] class. + * Variant that is leveraging Kotlin reified type parameters. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * addFilterAt(CustomFilter()) + * } + * } + * } + * ``` + * + * @param filter the [Filter] to register + * @param T the location of another [Filter] that is already registered + * (i.e. known) with Spring Security. + */ + inline fun addFilterAt(filter: Filter) { + this.addFilterAt(filter, T::class.java) + } + + /** + * Adds the [Filter] after the location of the specified [Filter] class. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * addFilterAfter(CustomFilter(), UsernamePasswordAuthenticationFilter::class.java) + * } + * } + * } + * ``` + * + * @param filter the [Filter] to register + * @param afterFilter the location of another [Filter] that is already registered + * (i.e. known) with Spring Security. + */ + fun addFilterAfter(filter: Filter, afterFilter: Class) { + this.http.addFilterAfter(filter, afterFilter) + } + + /** + * Adds the [Filter] after the location of the specified [Filter] class. + * Variant that is leveraging Kotlin reified type parameters. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * addFilterAfter(CustomFilter()) + * } + * } + * } + * ``` + * + * @param filter the [Filter] to register + * @param T the location of another [Filter] that is already registered + * (i.e. known) with Spring Security. + */ + inline fun addFilterAfter(filter: Filter) { + this.addFilterAfter(filter, T::class.java) + } + + /** + * Adds the [Filter] before the location of the specified [Filter] class. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * addFilterBefore(CustomFilter(), UsernamePasswordAuthenticationFilter::class.java) + * } + * } + * } + * ``` + * + * @param filter the [Filter] to register + * @param beforeFilter the location of another [Filter] that is already registered + * (i.e. known) with Spring Security. + */ + fun addFilterBefore(filter: Filter, beforeFilter: Class) { + this.http.addFilterBefore(filter, beforeFilter) + } + + /** + * Adds the [Filter] before the location of the specified [Filter] class. + * Variant that is leveraging Kotlin reified type parameters. + * + * Example: + * + * ``` + * @EnableWebSecurity + * class SecurityConfig : WebSecurityConfigurerAdapter() { + * + * override fun configure(http: HttpSecurity) { + * http { + * addFilterBefore(CustomFilter()) + * } + * } + * } + * ``` + * + * @param filter the [Filter] to register + * @param T the location of another [Filter] that is already registered + * (i.e. known) with Spring Security. + */ + inline fun addFilterBefore(filter: Filter) { + this.addFilterBefore(filter, T::class.java) + } + /** * Apply all configurations to the provided [HttpSecurity] */ diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDsl.kt index 2febca5e679..a7149014c2e 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/RequiresChannelDsl.kt @@ -40,6 +40,7 @@ class RequiresChannelDsl : AbstractRequestMatcherDsl() { private val MVC_PRESENT = ClassUtils.isPresent( HANDLER_MAPPING_INTROSPECTOR, RequiresChannelDsl::class.java.classLoader) + private val PATTERN_TYPE = if (MVC_PRESENT) PatternType.MVC else PatternType.ANT var channelProcessors: List? = null @@ -71,11 +72,9 @@ class RequiresChannelDsl : AbstractRequestMatcherDsl() { * (i.e. "REQUIRES_SECURE_CHANNEL") */ fun secure(pattern: String, attribute: String = "REQUIRES_SECURE_CHANNEL") { - if (MVC_PRESENT) { - channelSecurityRules.add(PatternAuthorizationRule(pattern, PatternType.MVC, null, attribute)) - } else { - channelSecurityRules.add(PatternAuthorizationRule(pattern, PatternType.ANT, null, attribute)) - } + channelSecurityRules.add(PatternAuthorizationRule(pattern = pattern, + patternType = PATTERN_TYPE, + rule = attribute)) } /** @@ -96,11 +95,10 @@ class RequiresChannelDsl : AbstractRequestMatcherDsl() { * (i.e. "REQUIRES_SECURE_CHANNEL") */ fun secure(pattern: String, servletPath: String, attribute: String = "REQUIRES_SECURE_CHANNEL") { - if (MVC_PRESENT) { - channelSecurityRules.add(PatternAuthorizationRule(pattern, PatternType.MVC, servletPath, attribute)) - } else { - channelSecurityRules.add(PatternAuthorizationRule(pattern, PatternType.ANT, servletPath, attribute)) - } + channelSecurityRules.add(PatternAuthorizationRule(pattern = pattern, + patternType = PATTERN_TYPE, + servletPath = servletPath, + rule = attribute)) } /** diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CacheControlDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CacheControlDsl.kt index 7525348bd00..316015b0160 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CacheControlDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/CacheControlDsl.kt @@ -18,7 +18,6 @@ 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.config.web.servlet.SecurityMarker /** * A Kotlin DSL to configure the [HttpSecurity] cache control headers using idiomatic @@ -27,7 +26,7 @@ import org.springframework.security.config.web.servlet.SecurityMarker * @author Eleftheria Stein * @since 5.3 */ -@SecurityMarker +@HeadersSecurityMarker class CacheControlDsl { private var disabled = false diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDsl.kt index 1a27e5c197c..270b1d14b4d 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentSecurityPolicyDsl.kt @@ -18,7 +18,6 @@ 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.config.web.servlet.SecurityMarker /** * A Kotlin DSL to configure the [HttpSecurity] Content-Security-Policy header using @@ -29,7 +28,7 @@ import org.springframework.security.config.web.servlet.SecurityMarker * @property policyDirectives the security policy directive(s) to be used in the response header. * @property reportOnly includes the Content-Security-Policy-Report-Only header in the response. */ -@SecurityMarker +@HeadersSecurityMarker class ContentSecurityPolicyDsl { var policyDirectives: String? = null var reportOnly: Boolean? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDsl.kt index 5ef495a2c57..92014ae4063 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ContentTypeOptionsDsl.kt @@ -18,7 +18,6 @@ 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.config.web.servlet.SecurityMarker /** * A Kotlin DSL to configure [HttpSecurity] X-Content-Type-Options header using idiomatic @@ -27,7 +26,7 @@ import org.springframework.security.config.web.servlet.SecurityMarker * @author Eleftheria Stein * @since 5.3 */ -@SecurityMarker +@HeadersSecurityMarker class ContentTypeOptionsDsl { private var disabled = false diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDsl.kt index b16f2d0b233..3bf766ca98a 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/FrameOptionsDsl.kt @@ -18,7 +18,6 @@ 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.config.web.servlet.SecurityMarker /** * A Kotlin DSL to configure the [HttpSecurity] X-Frame-Options header using @@ -30,7 +29,7 @@ import org.springframework.security.config.web.servlet.SecurityMarker * application. * @property deny deny framing any content from this application. */ -@SecurityMarker +@HeadersSecurityMarker class FrameOptionsDsl { var sameOrigin: Boolean? = null var deny: Boolean? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HeadersSecurityMarker.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HeadersSecurityMarker.kt new file mode 100644 index 00000000000..67a97f56c05 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HeadersSecurityMarker.kt @@ -0,0 +1,26 @@ +/* + * 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.config.web.servlet.headers + +/** + * Marker annotation indicating that the annotated class is part of the headers security DSL. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@DslMarker +annotation class HeadersSecurityMarker diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDsl.kt index 5307351781e..74fbb6272a7 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpPublicKeyPinningDsl.kt @@ -18,7 +18,6 @@ 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.config.web.servlet.SecurityMarker /** * A Kotlin DSL to configure the [HttpSecurity] HTTP Public Key Pinning header using @@ -35,7 +34,7 @@ import org.springframework.security.config.web.servlet.SecurityMarker * the server. * @property reportUri the URI to which the browser should report pin validation failures. */ -@SecurityMarker +@HeadersSecurityMarker class HttpPublicKeyPinningDsl { var pins: Map? = null var maxAgeInSeconds: Long? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDsl.kt index a1e109f94e4..e23e6d36b85 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/HttpStrictTransportSecurityDsl.kt @@ -18,7 +18,6 @@ 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.config.web.servlet.SecurityMarker import org.springframework.security.web.util.matcher.RequestMatcher /** @@ -35,7 +34,7 @@ import org.springframework.security.web.util.matcher.RequestMatcher * @property includeSubDomains if true, subdomains should be considered HSTS Hosts too. * @property preload if true, preload will be included in HSTS Header. */ -@SecurityMarker +@HeadersSecurityMarker class HttpStrictTransportSecurityDsl { var maxAgeInSeconds: Long? = null var requestMatcher: RequestMatcher? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ReferrerPolicyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ReferrerPolicyDsl.kt index 944407dc129..1ac54d94c02 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ReferrerPolicyDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/ReferrerPolicyDsl.kt @@ -18,7 +18,6 @@ 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.config.web.servlet.SecurityMarker import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter /** @@ -29,7 +28,7 @@ import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWrite * @since 5.3 * @property policy the policy to be used in the response header. */ -@SecurityMarker +@HeadersSecurityMarker class ReferrerPolicyDsl { var policy: ReferrerPolicyHeaderWriter.ReferrerPolicy? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt index b023e8db354..a48a30af10a 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/headers/XssProtectionConfigDsl.kt @@ -18,7 +18,6 @@ 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.config.web.servlet.SecurityMarker /** * A Kotlin DSL to configure the [HttpSecurity] XSS protection header using @@ -30,7 +29,7 @@ import org.springframework.security.config.web.servlet.SecurityMarker * @property xssProtectionEnabled if true, the header value will contain a value of 1. * If false, will explicitly disable specify that X-XSS-Protection is disabled. */ -@SecurityMarker +@HeadersSecurityMarker class XssProtectionConfigDsl { var block: Boolean? = null var xssProtectionEnabled: Boolean? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDsl.kt index 6e04aae818b..b1ab6eca61f 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/AuthorizationCodeGrantDsl.kt @@ -18,7 +18,6 @@ package org.springframework.security.config.web.servlet.oauth2.client import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer -import org.springframework.security.config.web.servlet.SecurityMarker import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository @@ -35,7 +34,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ * @property accessTokenResponseClient the client used for requesting the access token credential * from the Token Endpoint. */ -@SecurityMarker +@OAuth2ClientSecurityMarker class AuthorizationCodeGrantDsl { var authorizationRequestResolver: OAuth2AuthorizationRequestResolver? = null var authorizationRequestRepository: AuthorizationRequestRepository? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/OAuth2ClientSecurityMarker.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/OAuth2ClientSecurityMarker.kt new file mode 100644 index 00000000000..3b6722a2590 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/client/OAuth2ClientSecurityMarker.kt @@ -0,0 +1,26 @@ +/* + * 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.config.web.servlet.oauth2.client + +/** + * Marker annotation indicating that the annotated class is part of the OAuth 2.0 client security DSL. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@DslMarker +annotation class OAuth2ClientSecurityMarker diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDsl.kt index c416adea912..27c7982c6db 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/AuthorizationEndpointDsl.kt @@ -18,7 +18,6 @@ package org.springframework.security.config.web.servlet.oauth2.login import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer -import org.springframework.security.config.web.servlet.SecurityMarker import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest @@ -33,7 +32,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ * @property authorizationRequestResolver the resolver used for resolving [OAuth2AuthorizationRequest]'s. * @property authorizationRequestRepository the repository used for storing [OAuth2AuthorizationRequest]'s. */ -@SecurityMarker +@OAuth2LoginSecurityMarker class AuthorizationEndpointDsl { var baseUri: String? = null var authorizationRequestResolver: OAuth2AuthorizationRequestResolver? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/OAuth2LoginSecurityMarker.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/OAuth2LoginSecurityMarker.kt new file mode 100644 index 00000000000..24ab0807d9d --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/OAuth2LoginSecurityMarker.kt @@ -0,0 +1,26 @@ +/* + * 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.config.web.servlet.oauth2.login + +/** + * Marker annotation indicating that the annotated class is part of the OAuth 2.0 login security DSL. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@DslMarker +annotation class OAuth2LoginSecurityMarker diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDsl.kt index a15d6e419ef..ac63d88c9c1 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/RedirectionEndpointDsl.kt @@ -18,7 +18,6 @@ package org.springframework.security.config.web.servlet.oauth2.login import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer -import org.springframework.security.config.web.servlet.SecurityMarker /** * A Kotlin DSL to configure the Authorization Server's Redirection Endpoint using @@ -28,7 +27,7 @@ import org.springframework.security.config.web.servlet.SecurityMarker * @since 5.3 * @property baseUri the URI where the authorization response will be processed. */ -@SecurityMarker +@OAuth2LoginSecurityMarker class RedirectionEndpointDsl { var baseUri: String? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDsl.kt index 0997c15e878..ddba776d551 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/TokenEndpointDsl.kt @@ -18,7 +18,6 @@ package org.springframework.security.config.web.servlet.oauth2.login import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer -import org.springframework.security.config.web.servlet.SecurityMarker import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest @@ -31,7 +30,7 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo * @property accessTokenResponseClient the client used for requesting the access token credential * from the Token Endpoint. */ -@SecurityMarker +@OAuth2LoginSecurityMarker class TokenEndpointDsl { var accessTokenResponseClient: OAuth2AccessTokenResponseClient? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDsl.kt index f2e30083d6d..3ef981e9584 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/login/UserInfoEndpointDsl.kt @@ -18,7 +18,6 @@ package org.springframework.security.config.web.servlet.oauth2.login import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer -import org.springframework.security.config.web.servlet.SecurityMarker import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest import org.springframework.security.oauth2.client.registration.ClientRegistration @@ -39,7 +38,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User * End-User from the UserInfo Endpoint. * @property userAuthoritiesMapper the [GrantedAuthoritiesMapper] used for mapping [OAuth2User.getAuthorities] */ -@SecurityMarker +@OAuth2LoginSecurityMarker class UserInfoEndpointDsl { var userService: OAuth2UserService? = null var oidcUserService: OAuth2UserService? = null @@ -58,6 +57,18 @@ class UserInfoEndpointDsl { customUserTypePair = Pair(customUserType, clientRegistrationId) } + /** + * Sets a custom [OAuth2User] type and associates it to the provided + * client [ClientRegistration.getRegistrationId] registration identifier. + * Variant that is leveraging Kotlin reified type parameters. + * + * @param T a custom [OAuth2User] type + * @param clientRegistrationId the client registration identifier + */ + inline fun customUserType(clientRegistrationId: String) { + customUserType(T::class.java, clientRegistrationId) + } + internal fun get(): (OAuth2LoginConfigurer.UserInfoEndpointConfig) -> Unit { return { userInfoEndpoint -> userService?.also { userInfoEndpoint.userService(userService) } diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDsl.kt index 2236aa4d101..e8d8008a974 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/JwtDsl.kt @@ -20,7 +20,6 @@ import org.springframework.core.convert.converter.Converter import org.springframework.security.authentication.AbstractAuthenticationToken import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer -import org.springframework.security.config.web.servlet.SecurityMarker import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.oauth2.jwt.JwtDecoder @@ -35,7 +34,7 @@ import org.springframework.security.oauth2.jwt.JwtDecoder * @property jwkSetUri configures a [JwtDecoder] using a * JSON Web Key (JWK) URL */ -@SecurityMarker +@OAuth2ResourceServerSecurityMarker class JwtDsl { private var _jwtDecoder: JwtDecoder? = null private var _jwkSetUri: String? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OAuth2ResourceServerSecurityMarker.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OAuth2ResourceServerSecurityMarker.kt new file mode 100644 index 00000000000..c561531ae97 --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OAuth2ResourceServerSecurityMarker.kt @@ -0,0 +1,26 @@ +/* + * 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.config.web.servlet.oauth2.resourceserver + +/** + * Marker annotation indicating that the annotated class is part of the OAuth 2.0 resource server security DSL. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@DslMarker +annotation class OAuth2ResourceServerSecurityMarker diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt index 062509cb0ec..f9123796548 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/oauth2/resourceserver/OpaqueTokenDsl.kt @@ -18,7 +18,6 @@ package org.springframework.security.config.web.servlet.oauth2.resourceserver import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer -import org.springframework.security.config.web.servlet.SecurityMarker import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector /** @@ -29,7 +28,7 @@ import org.springframework.security.oauth2.server.resource.introspection.OpaqueT * @property introspectionUri the URI of the Introspection endpoint. * @property introspector the [OpaqueTokenIntrospector] to use. */ -@SecurityMarker +@OAuth2ResourceServerSecurityMarker class OpaqueTokenDsl { private var _introspectionUri: String? = null private var _introspector: OpaqueTokenIntrospector? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDsl.kt index d673cccb69d..e0af442a9c0 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionConcurrencyDsl.kt @@ -18,7 +18,6 @@ package org.springframework.security.config.web.servlet.session import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer -import org.springframework.security.config.web.servlet.SecurityMarker import org.springframework.security.core.session.SessionRegistry import org.springframework.security.web.session.SessionInformationExpiredStrategy @@ -38,7 +37,7 @@ import org.springframework.security.web.session.SessionInformationExpiredStrateg * is allowed access and an existing user's session is expired. * @property sessionRegistry the [SessionRegistry] implementation used. */ -@SecurityMarker +@SessionSecurityMarker class SessionConcurrencyDsl { var maximumSessions: Int? = null var expiredUrl: String? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDsl.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDsl.kt index a5ee7188a00..b02a7d52746 100644 --- a/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDsl.kt @@ -18,7 +18,6 @@ package org.springframework.security.config.web.servlet.session import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer -import org.springframework.security.config.web.servlet.SecurityMarker import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpSession @@ -29,7 +28,7 @@ import javax.servlet.http.HttpSession * @author Eleftheria Stein * @since 5.3 */ -@SecurityMarker +@SessionSecurityMarker class SessionFixationDsl { private var strategy: SessionFixationStrategy? = null diff --git a/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionSecurityMarker.kt b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionSecurityMarker.kt new file mode 100644 index 00000000000..6e5ef671b7a --- /dev/null +++ b/config/src/main/kotlin/org/springframework/security/config/web/servlet/session/SessionSecurityMarker.kt @@ -0,0 +1,26 @@ +/* + * 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.config.web.servlet.session + +/** + * Marker annotation indicating that the annotated class is part of the session security DSL. + * + * @author Eleftheria Stein + * @since 5.4 + */ +@DslMarker +annotation class SessionSecurityMarker diff --git a/config/src/main/resources/META-INF/spring.schemas b/config/src/main/resources/META-INF/spring.schemas index 910737e6ae0..3ab72af7124 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.3.xsd +http\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.4.xsd +http\://www.springframework.org/schema/security/spring-security-5.4.xsd=org/springframework/security/config/spring-security-5.4.xsd http\://www.springframework.org/schema/security/spring-security-5.3.xsd=org/springframework/security/config/spring-security-5.3.xsd http\://www.springframework.org/schema/security/spring-security-5.2.xsd=org/springframework/security/config/spring-security-5.2.xsd http\://www.springframework.org/schema/security/spring-security-5.1.xsd=org/springframework/security/config/spring-security-5.1.xsd @@ -14,7 +15,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.3.xsd +https\://www.springframework.org/schema/security/spring-security.xsd=org/springframework/security/config/spring-security-5.4.xsd +https\://www.springframework.org/schema/security/spring-security-5.4.xsd=org/springframework/security/config/spring-security-5.4.xsd https\://www.springframework.org/schema/security/spring-security-5.3.xsd=org/springframework/security/config/spring-security-5.3.xsd https\://www.springframework.org/schema/security/spring-security-5.2.xsd=org/springframework/security/config/spring-security-5.2.xsd https\://www.springframework.org/schema/security/spring-security-5.1.xsd=org/springframework/security/config/spring-security-5.1.xsd diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.4.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.4.rnc new file mode 100644 index 00000000000..8307f525200 --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.4.rnc @@ -0,0 +1,1099 @@ +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? + +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? & 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 basic, post and none (public clients). + attribute client-authentication-method {"basic" | "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}? + +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:integer}? +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? & 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}? + +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" | "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.4.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.4.xsd new file mode 100644 index 00000000000..436820de820 --- /dev/null +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.4.xsd @@ -0,0 +1,3188 @@ + + + + + + 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 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 + basic, 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 + + + + + + + + + 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 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/annotation/rsocket/RSocketMessageHandlerITests.java b/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java index 6af06326bfd..19f5e1010ce 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/rsocket/RSocketMessageHandlerITests.java @@ -103,7 +103,9 @@ public void retrieveMonoWhenSecureThenDenied() throws Exception { .data(data) .retrieveMono(String.class) .block() - ).isInstanceOf(ApplicationErrorException.class); + ).isInstanceOf(ApplicationErrorException.class) + .hasMessageContaining("Access Denied"); + assertThat(this.controller.payloads).isEmpty(); } @@ -116,7 +118,9 @@ public void retrieveMonoWhenAuthenticationFailedThenException() throws Exception .data(data) .retrieveMono(String.class) .block() - ).isInstanceOf(ApplicationErrorException.class); + ).isInstanceOf(ApplicationErrorException.class) + .hasMessageContaining("Invalid Credentials"); + assertThat(this.controller.payloads).isEmpty(); } @@ -149,12 +153,13 @@ public void retrieveMonoWhenPublicThenGranted() throws Exception { @Test public void retrieveFluxWhenDataFluxAndSecureThenDenied() throws Exception { Flux data = Flux.just("a", "b", "c"); - assertThatCode(() -> this.requester.route("secure.secure.retrieve-flux") + assertThatCode(() -> this.requester.route("secure.retrieve-flux") .data(data, String.class) .retrieveFlux(String.class) .collectList() - .block()).isInstanceOf( - ApplicationErrorException.class); + .block() + ).isInstanceOf(ApplicationErrorException.class) + .hasMessageContaining("Access Denied"); assertThat(this.controller.payloads).isEmpty(); } @@ -179,8 +184,9 @@ public void retrieveFluxWhenDataStringAndSecureThenDenied() throws Exception { .data(data) .retrieveFlux(String.class) .collectList() - .block()).isInstanceOf( - ApplicationErrorException.class); + .block() + ).isInstanceOf(ApplicationErrorException.class) + .hasMessageContaining("Access Denied"); assertThat(this.controller.payloads).isEmpty(); } 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 60552a0e451..a096e39ec6a 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 @@ -36,6 +36,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; 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 static org.assertj.core.api.Assertions.assertThat; @@ -69,7 +71,7 @@ public void cleanup() { @Test public void ignoringMvcMatcher() throws Exception { - loadConfig(MvcMatcherConfig.class); + loadConfig(MvcMatcherConfig.class, LegacyMvcMatchingConfig.class); this.request.setRequestURI("/path"); this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); @@ -141,7 +143,7 @@ public String path() { @Test public void ignoringMvcMatcherServletPath() throws Exception { - loadConfig(MvcMatcherServletPathConfig.class); + loadConfig(MvcMatcherServletPathConfig.class, LegacyMvcMatchingConfig.class); this.request.setServletPath("/spring"); this.request.setRequestURI("/spring/path"); @@ -216,6 +218,14 @@ public String path() { } } + @Configuration + static class LegacyMvcMatchingConfig implements WebMvcConfigurer { + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(true); + } + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java index 8c42b037d7f..48eb3fd45d7 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -25,6 +25,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; @@ -32,6 +33,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.bind.annotation.GetMapping; @@ -41,7 +43,14 @@ import javax.servlet.http.HttpServletRequest; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientCredentials; import static org.springframework.security.oauth2.client.registration.TestClientRegistrations.clientRegistration; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; @@ -314,4 +323,71 @@ public OAuth2AccessTokenResponseClient acce return mock(OAuth2AccessTokenResponseClient.class); } } + + // gh-8700 + @Test + public void requestWhenAuthorizedClientManagerConfiguredThenUsed() throws Exception { + String clientRegistrationId = "client1"; + String principalName = "user1"; + TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password"); + + ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class); + OAuth2AuthorizedClientRepository authorizedClientRepository = mock(OAuth2AuthorizedClientRepository.class); + OAuth2AuthorizedClientManager authorizedClientManager = mock(OAuth2AuthorizedClientManager.class); + + ClientRegistration clientRegistration = clientRegistration().registrationId(clientRegistrationId).build(); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + clientRegistration, principalName, TestOAuth2AccessTokens.noScopes()); + + when(authorizedClientManager.authorize(any())).thenReturn(authorizedClient); + + OAuth2AuthorizedClientManagerRegisteredConfig.CLIENT_REGISTRATION_REPOSITORY = clientRegistrationRepository; + OAuth2AuthorizedClientManagerRegisteredConfig.AUTHORIZED_CLIENT_REPOSITORY = authorizedClientRepository; + OAuth2AuthorizedClientManagerRegisteredConfig.AUTHORIZED_CLIENT_MANAGER = authorizedClientManager; + this.spring.register(OAuth2AuthorizedClientManagerRegisteredConfig.class).autowire(); + + this.mockMvc.perform(get("/authorized-client").with(authentication(authentication))) + .andExpect(status().isOk()) + .andExpect(content().string("resolved")); + + verify(authorizedClientManager).authorize(any()); + verifyNoInteractions(clientRegistrationRepository); + verifyNoInteractions(authorizedClientRepository); + } + + @EnableWebMvc + @EnableWebSecurity + static class OAuth2AuthorizedClientManagerRegisteredConfig extends WebSecurityConfigurerAdapter { + static ClientRegistrationRepository CLIENT_REGISTRATION_REPOSITORY; + static OAuth2AuthorizedClientRepository AUTHORIZED_CLIENT_REPOSITORY; + static OAuth2AuthorizedClientManager AUTHORIZED_CLIENT_MANAGER; + + @Override + protected void configure(HttpSecurity http) { + } + + @RestController + public class Controller { + + @GetMapping("/authorized-client") + public String authorizedClient(@RegisteredOAuth2AuthorizedClient("client1") OAuth2AuthorizedClient authorizedClient) { + return authorizedClient != null ? "resolved" : "not-resolved"; + } + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return CLIENT_REGISTRATION_REPOSITORY; + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository() { + return AUTHORIZED_CLIENT_REPOSITORY; + } + + @Bean + public OAuth2AuthorizedClientManager authorizedClientManager() { + return AUTHORIZED_CLIENT_MANAGER; + } + } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java index c27fca6201a..c1d7d47b9fd 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -32,6 +32,8 @@ import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.AbstractSecurityExpressionHandler; import org.springframework.security.access.expression.SecurityExpressionHandler; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; @@ -69,6 +71,7 @@ * * @author Rob Winch * @author Joe Grandja + * @author Evgeniy Cheban */ public class WebSecurityConfigurationTests { @Rule @@ -290,6 +293,31 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void securityExpressionHandlerWhenRoleHierarchyBeanThenRoleHierarchyUsed() { + this.spring.register(WebSecurityExpressionHandlerRoleHierarchyBeanConfig.class).autowire(); + TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "notused", "ROLE_ADMIN"); + FilterInvocation invocation = new FilterInvocation(new MockHttpServletRequest("GET", ""), + new MockHttpServletResponse(), new MockFilterChain()); + + AbstractSecurityExpressionHandler handler = this.spring.getContext().getBean(AbstractSecurityExpressionHandler.class); + EvaluationContext evaluationContext = handler.createEvaluationContext(authentication, invocation); + Expression expression = handler.getExpressionParser() + .parseExpression("hasRole('ROLE_USER')"); + boolean granted = expression.getValue(evaluationContext, Boolean.class); + assertThat(granted).isTrue(); + } + + @EnableWebSecurity + static class WebSecurityExpressionHandlerRoleHierarchyBeanConfig extends WebSecurityConfigurerAdapter { + @Bean + RoleHierarchy roleHierarchy() { + RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); + roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER"); + return roleHierarchy; + } + } + @Test public void securityExpressionHandlerWhenPermissionEvaluatorBeanThenPermissionEvaluatorUsed() { this.spring.register(WebSecurityExpressionHandlerPermissionEvaluatorBeanConfig.class).autowire(); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java index b9fb86db21b..d10ea89dce3 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java @@ -46,6 +46,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; 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 static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.spy; @@ -292,7 +294,7 @@ public RoleHierarchy roleHiearchy() { @Test public void mvcMatcher() throws Exception { - loadConfig(MvcMatcherConfig.class); + loadConfig(MvcMatcherConfig.class, LegacyMvcMatchingConfig.class); this.request.setRequestURI("/path"); this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); @@ -350,7 +352,7 @@ public String path() { @Test public void requestWhenMvcMatcherDenyAllThenRespondsWithUnauthorized() throws Exception { - loadConfig(MvcMatcherInLambdaConfig.class); + loadConfig(MvcMatcherInLambdaConfig.class, LegacyMvcMatchingConfig.class); this.request.setRequestURI("/path"); this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); @@ -410,7 +412,7 @@ public String path() { @Test public void mvcMatcherServletPath() throws Exception { - loadConfig(MvcMatcherServletPathConfig.class); + loadConfig(MvcMatcherServletPathConfig.class, LegacyMvcMatchingConfig.class); this.request.setServletPath("/spring"); this.request.setRequestURI("/spring/path"); @@ -487,7 +489,7 @@ public String path() { @Test public void requestWhenMvcMatcherServletPathDenyAllThenMatchesOnServletPath() throws Exception { - loadConfig(MvcMatcherServletPathInLambdaConfig.class); + loadConfig(MvcMatcherServletPathInLambdaConfig.class, LegacyMvcMatchingConfig.class); this.request.setServletPath("/spring"); this.request.setRequestURI("/spring/path"); @@ -697,6 +699,14 @@ public String path() { } } + @Configuration + static class LegacyMvcMatchingConfig implements WebMvcConfigurer { + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(true); + } + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java index ef4d4a2e425..fec73febccb 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java @@ -36,6 +36,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; 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 static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.config.Customizer.withDefaults; @@ -71,7 +73,7 @@ public void cleanup() { @Test public void mvcMatcher() throws Exception { - loadConfig(MvcMatcherConfig.class); + loadConfig(MvcMatcherConfig.class, LegacyMvcMatchingConfig.class); this.request.setServletPath("/path"); this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); @@ -137,7 +139,7 @@ public String path() { @Test public void requestMatchersMvcMatcher() throws Exception { - loadConfig(RequestMatchersMvcMatcherConfig.class); + loadConfig(RequestMatchersMvcMatcherConfig.class, LegacyMvcMatchingConfig.class); this.request.setServletPath("/path"); this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); @@ -198,7 +200,7 @@ public String path() { @Test public void requestMatchersWhenMvcMatcherInLambdaThenPathIsSecured() throws Exception { - loadConfig(RequestMatchersMvcMatcherInLambdaConfig.class); + loadConfig(RequestMatchersMvcMatcherInLambdaConfig.class, LegacyMvcMatchingConfig.class); this.request.setServletPath("/path"); this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); @@ -377,6 +379,14 @@ public String path() { } } + @Configuration + static class LegacyMvcMatchingConfig implements WebMvcConfigurer { + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(true); + } + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); 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 dd3561d9fb8..7a9e5b1f943 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 @@ -22,6 +22,7 @@ import org.junit.Test; 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.AuthenticationManager; import org.springframework.security.authentication.AuthenticationTrustResolver; @@ -44,10 +45,14 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.ConfigurableWebApplicationContext; import javax.servlet.Filter; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -60,6 +65,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -329,6 +335,39 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void logoutServletApiWhenCsrfDisabled() throws Exception { + ConfigurableWebApplicationContext context = this.spring.register(CsrfDisabledConfig.class).getContext(); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()) + .build(); + MvcResult mvcResult = mockMvc.perform(get("/")) + .andReturn(); + assertThat(mvcResult.getRequest().getSession(false)).isNull(); + } + + @Configuration + @EnableWebSecurity + static class CsrfDisabledConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf().disable(); + // @formatter:on + } + + @RestController + static class LogoutController { + @GetMapping("/") + String logout(HttpServletRequest request) throws ServletException { + request.getSession().setAttribute("foo", "bar"); + request.logout(); + return "logout"; + } + } + } + private T getFilter(Class filterClass) { return (T) getFilters().stream() .filter(filterClass::isInstance) diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java index a5e2cdf1607..b1104f58d32 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java @@ -30,6 +30,7 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy; import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy; @@ -53,6 +54,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -483,4 +485,74 @@ protected void configure(HttpSecurity http) { // @formatter:on } } + + @Test + public void whenOneSessionRegistryBeanThenUseIt() throws Exception { + SessionRegistryOneBeanConfig.SESSION_REGISTRY = mock(SessionRegistry.class); + this.spring.register(SessionRegistryOneBeanConfig.class).autowire(); + + MockHttpSession session = new MockHttpSession(this.spring.getContext().getServletContext()); + this.mvc.perform(get("/").session(session)); + + verify(SessionRegistryOneBeanConfig.SESSION_REGISTRY) + .getSessionInformation(session.getId()); + } + + @Test + public void whenTwoSessionRegistryBeansThenUseNeither() throws Exception { + SessionRegistryTwoBeansConfig.SESSION_REGISTRY_ONE = mock(SessionRegistry.class); + SessionRegistryTwoBeansConfig.SESSION_REGISTRY_TWO = mock(SessionRegistry.class); + this.spring.register(SessionRegistryTwoBeansConfig.class).autowire(); + + MockHttpSession session = new MockHttpSession(this.spring.getContext().getServletContext()); + this.mvc.perform(get("/").session(session)); + + verifyNoInteractions(SessionRegistryTwoBeansConfig.SESSION_REGISTRY_ONE); + verifyNoInteractions(SessionRegistryTwoBeansConfig.SESSION_REGISTRY_TWO); + } + + @EnableWebSecurity + static class SessionRegistryOneBeanConfig extends WebSecurityConfigurerAdapter { + private static SessionRegistry SESSION_REGISTRY; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .sessionManagement() + .maximumSessions(1); + // @formatter:on + } + + @Bean + public SessionRegistry sessionRegistry() { + return SESSION_REGISTRY; + } + } + + @EnableWebSecurity + static class SessionRegistryTwoBeansConfig extends WebSecurityConfigurerAdapter { + private static SessionRegistry SESSION_REGISTRY_ONE; + + private static SessionRegistry SESSION_REGISTRY_TWO; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .sessionManagement() + .maximumSessions(1); + // @formatter:on + } + + @Bean + public SessionRegistry sessionRegistryOne() { + return SESSION_REGISTRY_ONE; + } + + @Bean + public SessionRegistry sessionRegistryTwo() { + return SESSION_REGISTRY_TWO; + } + } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java index c9d0fb9db87..9b86829ebc8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurerTests.java @@ -36,6 +36,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; 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 static org.assertj.core.api.Assertions.assertThat; @@ -71,7 +73,7 @@ public void cleanup() { @Test public void mvcMatcher() throws Exception { - loadConfig(MvcMatcherConfig.class); + loadConfig(MvcMatcherConfig.class, LegacyMvcMatchingConfig.class); this.request.setRequestURI("/path"); this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); @@ -129,7 +131,7 @@ public String path() { @Test public void mvcMatcherServletPath() throws Exception { - loadConfig(MvcMatcherServletPathConfig.class); + loadConfig(MvcMatcherServletPathConfig.class, LegacyMvcMatchingConfig.class); this.request.setServletPath("/spring"); this.request.setRequestURI("/spring/path"); @@ -222,6 +224,14 @@ public void configure(HttpSecurity http) throws Exception { } } + @Configuration + static class LegacyMvcMatchingConfig implements WebMvcConfigurer { + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(true); + } + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index 86e4b4e3c41..ffc06ee6b02 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -75,6 +75,7 @@ * Tests for {@link OAuth2ClientConfigurer}. * * @author Joe Grandja + * @author Parikshit Dutta */ public class OAuth2ClientConfigurerTests { private static ClientRegistrationRepository clientRegistrationRepository; @@ -208,6 +209,43 @@ public void configureWhenRequestCacheProvidedAndClientAuthorizationRequiredExcep verify(requestCache).saveRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); } + @Test + public void configureWhenRequestCacheProvidedAndClientAuthorizationSucceedsThenRequestCacheUsed() throws Exception { + this.spring.register(OAuth2ClientConfig.class).autowire(); + + // Setup the Authorization Request in the session + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, this.registration1.getRegistrationId()); + OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(this.registration1.getProviderDetails().getAuthorizationUri()) + .clientId(this.registration1.getClientId()) + .redirectUri("http://localhost/client-1") + .state("state") + .attributes(attributes) + .build(); + + AuthorizationRequestRepository authorizationRequestRepository = + new HttpSessionOAuth2AuthorizationRequestRepository(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); + MockHttpServletResponse response = new MockHttpServletResponse(); + authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response); + + MockHttpSession session = (MockHttpSession) request.getSession(); + + String principalName = "user1"; + TestingAuthenticationToken authentication = new TestingAuthenticationToken(principalName, "password"); + + this.mockMvc.perform(get("/client-1") + .param(OAuth2ParameterNames.CODE, "code") + .param(OAuth2ParameterNames.STATE, "state") + .with(authentication(authentication)) + .session(session)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/client-1")); + + verify(requestCache).getRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + // gh-5521 @Test public void configureWhenCustomAuthorizationRequestResolverSetThenAuthorizationRequestIncludesCustomParameters() throws Exception { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 51c64141c36..cf552a6ece9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -160,6 +160,7 @@ * Tests for {@link OAuth2ResourceServerConfigurer} * * @author Josh Cummings + * @author Evgeniy Cheban */ public class OAuth2ResourceServerConfigurerTests { private static final String JWT_TOKEN = "token"; @@ -1452,6 +1453,80 @@ public void configureWhenUsingBothAuthenticationManagerResolverAndOpaqueThenWiri .hasMessageContaining("authenticationManagerResolver"); } + @Test + public void getJwtAuthenticationConverterWhenNoConverterSpecifiedThenTheDefaultIsUsed() { + ApplicationContext context = + this.spring.context(new GenericWebApplicationContext()).getContext(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + + assertThat(jwtConfigurer.getJwtAuthenticationConverter()).isInstanceOf(JwtAuthenticationConverter.class); + } + + @Test + public void getJwtAuthenticationConverterWhenConverterBeanSpecified() { + JwtAuthenticationConverter converterBean = new JwtAuthenticationConverter(); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean(JwtAuthenticationConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + + assertThat(jwtConfigurer.getJwtAuthenticationConverter()).isEqualTo(converterBean); + } + + @Test + public void getJwtAuthenticationConverterWhenConverterBeanAndAnotherOnTheDslThenTheDslOneIsUsed() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + JwtAuthenticationConverter converterBean = new JwtAuthenticationConverter(); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean(JwtAuthenticationConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + jwtConfigurer.jwtAuthenticationConverter(converter); + + assertThat(jwtConfigurer.getJwtAuthenticationConverter()).isEqualTo(converter); + } + + @Test + public void getJwtAuthenticationConverterWhenDuplicateConverterBeansAndAnotherOnTheDslThenTheDslOneIsUsed() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + JwtAuthenticationConverter converterBean = new JwtAuthenticationConverter(); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("converterOne", JwtAuthenticationConverter.class, () -> converterBean); + context.registerBean("converterTwo", JwtAuthenticationConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + jwtConfigurer.jwtAuthenticationConverter(converter); + + assertThat(jwtConfigurer.getJwtAuthenticationConverter()).isEqualTo(converter); + } + + @Test + public void getJwtAuthenticationConverterWhenDuplicateConverterBeansThenThrowsException() { + JwtAuthenticationConverter converterBean = new JwtAuthenticationConverter(); + + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.registerBean("converterOne", JwtAuthenticationConverter.class, () -> converterBean); + context.registerBean("converterTwo", JwtAuthenticationConverter.class, () -> converterBean); + this.spring.context(context).autowire(); + + OAuth2ResourceServerConfigurer.JwtConfigurer jwtConfigurer = + new OAuth2ResourceServerConfigurer(context).jwt(); + + assertThatCode(jwtConfigurer::getJwtAuthenticationConverter) + .isInstanceOf(NoUniqueBeanDefinitionException.class); + } + // -- support @EnableWebSecurity diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java index 586781f5d5e..de9c20407e2 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurerTests.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.Collections; import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; import org.junit.After; import org.junit.Assert; @@ -55,9 +56,13 @@ import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; +import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter; +import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.context.HttpRequestResponseHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; @@ -66,10 +71,15 @@ import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.springframework.security.config.annotation.web.configurers.saml2.TestRelyingPartyRegistrations.saml2AuthenticationConfiguration; +import static org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationRequestContexts.authenticationRequestContext; +import static org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations.relyingPartyRegistration; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Tests for different Java configuration for {@link Saml2LoginConfigurer} @@ -133,6 +143,20 @@ public void saml2LoginWhenConfiguringAuthenticationDefaultsUsingCustomizerThenTh validateSaml2WebSsoAuthenticationFilterConfiguration(); } + @Test + public void saml2LoginWhenCustomAuthenticationRequestContextResolverThenUses() throws Exception { + this.spring.register(CustomAuthenticationRequestContextResolver.class).autowire(); + + Saml2AuthenticationRequestContext context = authenticationRequestContext().build(); + Saml2AuthenticationRequestContextResolver resolver = + CustomAuthenticationRequestContextResolver.resolver; + when(resolver.resolve(any(HttpServletRequest.class), any(RelyingPartyRegistration.class))) + .thenReturn(context); + this.mvc.perform(get("/saml2/authenticate/registration-id")) + .andExpect(status().isFound()); + verify(resolver).resolve(any(HttpServletRequest.class), any(RelyingPartyRegistration.class)); + } + private void validateSaml2WebSsoAuthenticationFilterConfiguration() { // get the OpenSamlAuthenticationProvider Saml2WebSsoAuthenticationFilter filter = getSaml2SsoFilter(this.springSecurityFilterChain); @@ -219,6 +243,38 @@ public O postProcess(O provider) { } } + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class CustomAuthenticationRequestContextResolver extends WebSecurityConfigurerAdapter { + private static final Saml2AuthenticationRequestContextResolver resolver = + mock(Saml2AuthenticationRequestContextResolver.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + ObjectPostProcessor processor + = new ObjectPostProcessor() { + @Override + public O postProcess(O filter) { + filter.setAuthenticationRequestContextResolver(resolver); + return filter; + } + }; + + http + .authorizeRequests(authz -> authz + .anyRequest().authenticated() + ) + .saml2Login(saml2 -> saml2 + .addObjectPostProcessor(processor) + ); + } + + @Bean + Saml2AuthenticationRequestContextResolver resolver() { + return resolver; + } + } + private static AuthenticationManager getAuthenticationManagerMock(String role) { return new AuthenticationManager() { @@ -253,9 +309,8 @@ SecurityContextRepository securityContextRepository() { @Bean RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { RelyingPartyRegistrationRepository repository = mock(RelyingPartyRegistrationRepository.class); - when(repository.findByRegistrationId(anyString())).thenReturn( - saml2AuthenticationConfiguration() - ); + when(repository.findByRegistrationId(anyString())) + .thenReturn(relyingPartyRegistration().build()); return repository; } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/TestRelyingPartyRegistrations.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/TestRelyingPartyRegistrations.java deleted file mode 100644 index b69456e2bcd..00000000000 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/TestRelyingPartyRegistrations.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2002-2019 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.annotation.web.configurers.saml2; - -import org.springframework.security.saml2.credentials.Saml2X509Credential; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; - -import static org.springframework.security.config.annotation.web.configurers.saml2.TestSaml2Credentials.signingCredential; -import static org.springframework.security.config.annotation.web.configurers.saml2.TestSaml2Credentials.verificationCertificate; - -/** - * Preconfigured test data for {@link RelyingPartyRegistration} objects - */ -public class TestRelyingPartyRegistrations { - - static RelyingPartyRegistration saml2AuthenticationConfiguration() { - //remote IDP entity ID - String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"; - //remote WebSSO Endpoint - Where to Send AuthNRequests to - String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php"; - //local registration ID - String registrationId = "simplesamlphp"; - //local entity ID - autogenerated based on URL - String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; - //local signing (and decryption key) - Saml2X509Credential signingCredential = signingCredential(); - //IDP certificate for verification of incoming messages - Saml2X509Credential idpVerificationCertificate = verificationCertificate(); - String acsUrlTemplate = "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; - return RelyingPartyRegistration.withRegistrationId(registrationId) - .providerDetails(c -> c.entityId(idpEntityId)) - .providerDetails(c -> c.webSsoUrl(webSsoEndpoint)) - .credentials(c -> c.add(signingCredential)) - .credentials(c -> c.add(idpVerificationCertificate)) - .localEntityIdTemplate(localEntityIdTemplate) - .assertionConsumerServiceUrlTemplate(acsUrlTemplate) - .build(); - } - - -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java index e317d8f6250..4be1c2b325b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java @@ -47,6 +47,7 @@ import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; +import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver; import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; @@ -59,6 +60,7 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.result.view.AbstractView; @@ -434,4 +436,23 @@ static class Child { Child() { } } + + @Test + // gh-8596 + public void resolveAuthenticationPrincipalArgumentResolverFirstDoesNotCauseBeanCurrentlyInCreationException() { + this.spring.register(EnableWebFluxSecurityConfiguration.class, + ReactiveAuthenticationTestConfiguration.class, + DelegatingWebFluxConfiguration.class).autowire(); + } + + @EnableWebFluxSecurity + @Configuration(proxyBeanMethods = false) + static class EnableWebFluxSecurityConfiguration { + /** + * It is necessary to Autowire AuthenticationPrincipalArgumentResolver because it triggers eager loading of + * AuthenticationPrincipalArgumentResolver bean which causes BeanCurrentlyInCreationException + */ + @Autowired + AuthenticationPrincipalArgumentResolver resolver; + } } 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 05bb9177332..cd00c06896e 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 @@ -15,19 +15,26 @@ */ package org.springframework.security.config.doc; -import org.apache.commons.lang.StringUtils; -import org.junit.After; -import org.junit.Test; -import org.springframework.core.io.ClassPathResource; -import org.springframework.security.config.http.SecurityFiltersAssertions; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.lang.StringUtils; +import org.junit.After; +import org.junit.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.config.http.SecurityFiltersAssertions; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -45,12 +52,17 @@ public class XsdDocumentedTests { "nsa-websocket-security", "nsa-ldap", "nsa-method-security", - "nsa-web"); + "nsa-web", + // deprecated and for removal + "nsa-frame-options-strategy", + "nsa-frame-options-ref", + "nsa-frame-options-value", + "nsa-frame-options-from-parameter"); String referenceLocation = "../docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc"; String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd"; - String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.3.xsd"; + String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.4.xsd"; XmlSupport xml = new XmlSupport(); @@ -142,8 +154,8 @@ public void sizeWhenReadingFilesystemThenIsCorrectNumberOfSchemaFiles() String[] schemas = resource.getFile().getParentFile().list((dir, name) -> name.endsWith(".xsd")); - assertThat(schemas.length).isEqualTo(15) - .withFailMessage("the count is equal to 15, if not then schemaDocument needs updating"); + assertThat(schemas.length).isEqualTo(16) + .withFailMessage("the count is equal to 16, if not then schemaDocument needs updating"); } /** diff --git a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java index c4903195ab0..4049067b0fe 100644 --- a/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/http/MiscHttpConfigTests.java @@ -94,6 +94,8 @@ import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.firewall.FirewalledRequest; import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.RequestRejectedException; +import org.springframework.security.web.firewall.RequestRejectedHandler; import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; @@ -754,6 +756,21 @@ public void reset() { } verify(firewall).getFirewalledResponse(any(HttpServletResponse.class)); } + @Test + public void getWhenUsingCustomRequestRejectedHandlerThenRequestRejectedHandlerIsInvoked() throws Exception { + this.spring.configLocations(xml("RequestRejectedHandler")).autowire(); + + HttpServletResponse response = new MockHttpServletResponse(); + + RequestRejectedException rejected = new RequestRejectedException("failed"); + HttpFirewall firewall = this.spring.getContext().getBean(HttpFirewall.class); + RequestRejectedHandler requestRejectedHandler = this.spring.getContext().getBean(RequestRejectedHandler.class); + when(firewall.getFirewalledRequest(any(HttpServletRequest.class))).thenThrow(rejected); + this.mvc.perform(get("/unprotected")); + + verify(requestRejectedHandler).handle(any(), any(), any()); + } + @Test public void getWhenUsingCustomAccessDecisionManagerThenAuthorizesAccordingly() throws Exception { this.spring.configLocations(xml("CustomAccessDecisionManager")).autowire(); diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java index 6774cfcdf2b..a7778d25d32 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java @@ -24,6 +24,7 @@ import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -31,6 +32,7 @@ import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -39,8 +41,11 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.ui.Model; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @@ -51,6 +56,7 @@ import static org.mockito.Mockito.when; import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses.accessTokenResponse; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -200,6 +206,32 @@ public void requestWhenCustomAuthorizedClientServiceThenCalled() throws Exceptio verify(this.authorizedClientService).saveAuthorizedClient(any(), any()); } + @WithMockUser + @Test + public void requestWhenAuthorizedClientFoundThenMethodArgumentResolved() throws Exception { + this.spring.configLocations(xml("AuthorizedClientArgumentResolver")).autowire(); + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google"); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + clientRegistration, "user", TestOAuth2AccessTokens.noScopes()); + when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())) + .thenReturn(authorizedClient); + + this.mvc.perform(get("/authorized-client")) + .andExpect(status().isOk()) + .andExpect(content().string("resolved")); + } + + @RestController + static class AuthorizedClientController { + + @GetMapping("/authorized-client") + String authorizedClient(Model model, @RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient) { + return authorizedClient != null ? "resolved" : "not-resolved"; + } + } + private static OAuth2AuthorizationRequest createAuthorizationRequest(ClientRegistration clientRegistration) { Map attributes = new HashMap<>(); attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java index 9470cd03b46..3c0c793b250 100644 --- a/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java @@ -17,6 +17,7 @@ import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; @@ -28,7 +29,9 @@ 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.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -40,6 +43,7 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -50,13 +54,23 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoderFactory; import org.springframework.security.oauth2.jwt.TestJwts; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.savedrequest.RequestCache; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.ui.Model; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -66,18 +80,17 @@ import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses.accessTokenResponse; import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses.oidcAccessTokenResponse; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - /** * Tests for {@link OAuth2LoginBeanDefinitionParser}. * * @author Ruby Hartono */ +@RunWith(SpringJUnit4ClassRunner.class) +@SecurityTestExecutionListeners public class OAuth2LoginBeanDefinitionParserTests { private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests"; @@ -489,6 +502,32 @@ public void requestWhenCustomAuthorizedClientServiceThenCalled() throws Exceptio verify(authorizedClientService).saveAuthorizedClient(any(), any()); } + @WithMockUser + @Test + public void requestWhenAuthorizedClientFoundThenMethodArgumentResolved() throws Exception { + this.spring.configLocations(xml("AuthorizedClientArgumentResolver")).autowire(); + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google-login"); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( + clientRegistration, "user", TestOAuth2AccessTokens.noScopes()); + when(this.authorizedClientRepository.loadAuthorizedClient(any(), any(), any())) + .thenReturn(authorizedClient); + + this.mvc.perform(get("/authorized-client")) + .andExpect(status().isOk()) + .andExpect(content().string("resolved")); + } + + @RestController + static class AuthorizedClientController { + + @GetMapping("/authorized-client") + String authorizedClient(Model model, @RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient) { + return authorizedClient != null ? "resolved" : "not-resolved"; + } + } + private String xml(String configName) { return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; } diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java index 606af24ae41..0a2eac541fa 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java @@ -165,6 +165,7 @@ public void parseWhenIssuerUriConfiguredThenRequestConfigFromIssuer() throws Exc .isEqualTo(AuthenticationMethod.HEADER); assertThat(googleProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("sub"); assertThat(googleProviderDetails.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); + assertThat(googleProviderDetails.getIssuerUri()).isEqualTo(serverUrl); } @Test @@ -195,6 +196,7 @@ public void parseWhenMultipleClientsConfiguredThenAvailableInRepository() { .isEqualTo(AuthenticationMethod.HEADER); assertThat(googleProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("sub"); assertThat(googleProviderDetails.getJwkSetUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/certs"); + assertThat(googleProviderDetails.getIssuerUri()).isEqualTo("https://accounts.google.com"); ClientRegistration githubRegistration = clientRegistrationRepository.findByRegistrationId("github-login"); assertThat(githubRegistration).isNotNull(); diff --git a/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java b/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java index 89111a9f544..8b8ec4f3abb 100644 --- a/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java +++ b/config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -47,6 +47,8 @@ public void getBuilderWhenGoogleShouldHaveGoogleSettings() { .isEqualTo(IdTokenClaimNames.SUB); assertThat(providerDetails.getJwkSetUri()) .isEqualTo("https://www.googleapis.com/oauth2/v3/certs"); + assertThat(providerDetails.getIssuerUri()) + .isEqualTo("https://accounts.google.com"); assertThat(registration.getClientAuthenticationMethod()) .isEqualTo(ClientAuthenticationMethod.BASIC); assertThat(registration.getAuthorizationGrantType()) diff --git a/config/src/test/java/org/springframework/security/config/test/SpringTestContext.java b/config/src/test/java/org/springframework/security/config/test/SpringTestContext.java index 4909d66107a..09d580acc7c 100644 --- a/config/src/test/java/org/springframework/security/config/test/SpringTestContext.java +++ b/config/src/test/java/org/springframework/security/config/test/SpringTestContext.java @@ -17,7 +17,6 @@ package org.springframework.security.config.test; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.mock.web.MockServletConfig; import org.springframework.mock.web.MockServletContext; import org.springframework.security.config.util.InMemoryXmlWebApplicationContext; @@ -113,8 +112,10 @@ private SpringTestContext addFilter(Filter filter) { return this; } - public ConfigurableApplicationContext getContext() { + public ConfigurableWebApplicationContext getContext() { if (!this.context.isRunning()) { + this.context.setServletContext(new MockServletContext()); + this.context.setServletConfig(new MockServletConfig()); this.context.refresh(); } return this.context; diff --git a/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java b/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java index 78422a06fc7..ac7b485f756 100644 --- a/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java +++ b/config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java @@ -41,7 +41,7 @@ public class InMemoryXmlApplicationContext extends AbstractXmlApplicationContext + "http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security-"; static final String BEANS_CLOSE = "\n"; - static final String SPRING_SECURITY_VERSION = "5.3"; + static final String SPRING_SECURITY_VERSION = "5.4"; Resource inMemoryXml; diff --git a/config/src/test/java/org/springframework/security/config/web/server/LogoutSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/LogoutSpecTests.java index e417a3cda56..723251e4cf2 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/LogoutSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/LogoutSpecTests.java @@ -21,6 +21,7 @@ import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder; import org.springframework.security.htmlunit.server.WebTestClientHtmlUnitDriverBuilder; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; @@ -200,4 +201,46 @@ public void logoutWhenDisabledThenPostToLogoutDoesNothing() { homePage .assertAt(); } + + + @Test + public void logoutWhenCustomSecurityContextRepositoryThenLogsOut() { + WebSessionServerSecurityContextRepository repository = new WebSessionServerSecurityContextRepository(); + repository.setSpringSecurityContextAttrName("CUSTOM_CONTEXT_ATTR"); + SecurityWebFilterChain securityWebFilter = this.http + .securityContextRepository(repository) + .authorizeExchange() + .anyExchange().authenticated() + .and() + .formLogin() + .and() + .logout() + .and() + .build(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(securityWebFilter) + .build(); + + WebDriver driver = WebTestClientHtmlUnitDriverBuilder + .webTestClientSetup(webTestClient) + .build(); + + FormLoginTests.DefaultLoginPage loginPage = FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class) + .assertAt(); + + FormLoginTests.HomePage homePage = loginPage.loginForm() + .username("user") + .password("password") + .submit(FormLoginTests.HomePage.class); + + homePage.assertAt(); + + FormLoginTests.DefaultLogoutPage.to(driver) + .assertAt() + .logout(); + + FormLoginTests.HomePage.to(driver, FormLoginTests.DefaultLoginPage.class) + .assertAt(); + } } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java index 4e07a65d785..0ea9c446da6 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ClientSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.security.config.web.server; +import java.net.URI; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -48,6 +50,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; +import org.springframework.security.web.server.savedrequest.ServerRequestCache; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.GetMapping; @@ -62,6 +65,7 @@ /** * @author Rob Winch + * @author Parikshit Dutta * @since 5.1 */ @RunWith(SpringRunner.class) @@ -146,6 +150,7 @@ public void oauth2ClientWhenCustomObjectsThenUsed() { ServerAuthenticationConverter converter = config.authenticationConverter; ReactiveAuthenticationManager manager = config.manager; ServerAuthorizationRequestRepository authorizationRequestRepository = config.authorizationRequestRepository; + ServerRequestCache requestCache = config.requestCache; OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() .redirectUri("/authorize/oauth2/code/registration-id") @@ -163,6 +168,7 @@ public void oauth2ClientWhenCustomObjectsThenUsed() { when(authorizationRequestRepository.loadAuthorizationRequest(any())).thenReturn(Mono.just(authorizationRequest)); when(converter.convert(any())).thenReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c"))); when(manager.authenticate(any())).thenReturn(Mono.just(result)); + when(requestCache.getRedirectUri(any())).thenReturn(Mono.just(URI.create("/saved-request"))); this.client.get() .uri(uriBuilder -> @@ -175,6 +181,7 @@ public void oauth2ClientWhenCustomObjectsThenUsed() { verify(converter).convert(any()); verify(manager).authenticate(any()); + verify(requestCache).getRedirectUri(any()); } @EnableWebFlux @@ -197,13 +204,17 @@ static class OAuth2ClientCustomConfig { ServerAuthorizationRequestRepository authorizationRequestRepository = mock(ServerAuthorizationRequestRepository.class); + ServerRequestCache requestCache = mock(ServerRequestCache.class); + @Bean public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { http .oauth2Client() .authenticationConverter(this.authenticationConverter) .authenticationManager(this.manager) - .authorizationRequestRepository(this.authorizationRequestRepository); + .authorizationRequestRepository(this.authorizationRequestRepository) + .and() + .requestCache(c -> c.requestCache(this.requestCache)); return http.build(); } } @@ -217,6 +228,7 @@ public void oauth2ClientWhenCustomObjectsInLambdaThenUsed() { ServerAuthenticationConverter converter = config.authenticationConverter; ReactiveAuthenticationManager manager = config.manager; ServerAuthorizationRequestRepository authorizationRequestRepository = config.authorizationRequestRepository; + ServerRequestCache requestCache = config.requestCache; OAuth2AuthorizationRequest authorizationRequest = TestOAuth2AuthorizationRequests.request() .redirectUri("/authorize/oauth2/code/registration-id") @@ -234,6 +246,7 @@ public void oauth2ClientWhenCustomObjectsInLambdaThenUsed() { when(authorizationRequestRepository.loadAuthorizationRequest(any())).thenReturn(Mono.just(authorizationRequest)); when(converter.convert(any())).thenReturn(Mono.just(new TestingAuthenticationToken("a", "b", "c"))); when(manager.authenticate(any())).thenReturn(Mono.just(result)); + when(requestCache.getRedirectUri(any())).thenReturn(Mono.just(URI.create("/saved-request"))); this.client.get() .uri(uriBuilder -> @@ -246,6 +259,7 @@ public void oauth2ClientWhenCustomObjectsInLambdaThenUsed() { verify(converter).convert(any()); verify(manager).authenticate(any()); + verify(requestCache).getRedirectUri(any()); } @Configuration @@ -256,6 +270,8 @@ static class OAuth2ClientInLambdaCustomConfig { ServerAuthorizationRequestRepository authorizationRequestRepository = mock(ServerAuthorizationRequestRepository.class); + ServerRequestCache requestCache = mock(ServerRequestCache.class); + @Bean public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { http @@ -263,8 +279,8 @@ public SecurityWebFilterChain springSecurityFilter(ServerHttpSecurity http) { oauth2Client .authenticationConverter(this.authenticationConverter) .authenticationManager(this.manager) - .authorizationRequestRepository(this.authorizationRequestRepository) - ); + .authorizationRequestRepository(this.authorizationRequestRepository)) + .requestCache(c -> c.requestCache(this.requestCache)); return http.build(); } } diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java index a783a8212af..b1e5662a3ef 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2LoginTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -24,6 +24,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; @@ -82,6 +83,7 @@ import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler; import org.springframework.security.web.server.context.ServerSecurityContextRepository; import org.springframework.security.web.server.savedrequest.ServerRequestCache; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; @@ -101,7 +103,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.springframework.security.oauth2.jwt.TestJwts.jwt; /** @@ -185,6 +190,22 @@ public void defaultLoginPageWithSingleClientRegistrationThenRedirect() { assertThat(driver.getCurrentUrl()).startsWith("https://github.com/login/oauth/authorize"); } + // gh-8118 + @Test + public void defaultLoginPageWithSingleClientRegistrationAndXhrRequestThenDoesNotRedirectForAuthorization() { + this.spring.register(OAuth2LoginWithSingleClientRegistrations.class, WebFluxConfig.class).autowire(); + + this.client.get() + .uri("/") + .header("X-Requested-With", "XMLHttpRequest") + .exchange() + .expectStatus().is3xxRedirection() + .expectHeader().valueEquals(HttpHeaders.LOCATION, "/login"); + } + + @EnableWebFlux + static class WebFluxConfig { } + @EnableWebFluxSecurity static class OAuth2LoginWithSingleClientRegistrations { @Bean @@ -665,7 +686,6 @@ private ReactiveJwtDecoder getJwtDecoder() { } } - @Test public void logoutWhenUsingOidcLogoutHandlerThenRedirects() { this.spring.register(OAuth2LoginConfigWithOidcLogoutSuccessHandler.class).autowire(); @@ -699,6 +719,8 @@ public SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { http .csrf().disable() .logout() + // avoid using mock ServerSecurityContextRepository for logout + .logoutHandler(new SecurityContextServerLogoutHandler()) .logoutSuccessHandler( new OidcClientInitiatedServerLogoutSuccessHandler( new InMemoryReactiveClientRegistrationRepository(this.withLogout))) @@ -719,6 +741,24 @@ ClientRegistration clientRegistration() { } } + // gh-8609 + @Test + public void oauth2LoginWhenAuthenticationConverterFailsThenDefaultRedirectToLogin() { + this.spring.register(OAuth2LoginWithMultipleClientRegistrations.class).autowire(); + + WebTestClient webTestClient = WebTestClientBuilder + .bindToWebFilters(this.springSecurity) + .build(); + + webTestClient.get() + .uri("/login/oauth2/code/google") + .exchange() + .expectStatus() + .is3xxRedirection() + .expectHeader() + .valueEquals("Location", "/login?error"); + } + static class GitHubWebFilter implements WebFilter { @Override diff --git a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java index b059c008802..943fe62d712 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/OAuth2ResourceServerSpecTests.java @@ -698,7 +698,7 @@ SecurityWebFilterChain springSecurity(ServerHttpSecurity http) { // @formatter:off http .authorizeExchange() - .pathMatchers("/**/message/**").hasAnyAuthority("SCOPE_message:read") + .pathMatchers("/*/message/**").hasAnyAuthority("SCOPE_message:read") .and() .oauth2ResourceServer() .authenticationManagerResolver(authenticationManagerResolver()); 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 new file mode 100644 index 00000000000..b39a0564e53 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/AuthorizeExchangeDslTests.kt @@ -0,0 +1,183 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +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.SpringTestRule +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.* + +/** + * Tests for [AuthorizeExchangeDsl] + * + * @author Eleftheria Stein + */ +class AuthorizeExchangeDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when secured by matcher then responds with unauthorized`() { + this.spring.register(MatcherAuthenticatedConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isUnauthorized + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class MatcherAuthenticatedConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + } + } + } + + @Test + fun `request when allowed by matcher then responds with ok`() { + this.spring.register(MatcherPermitAllConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class MatcherPermitAllConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, permitAll) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/") + fun path() { + } + } + } + + @Test + fun `request when secured by pattern then responds with unauthorized`() { + this.spring.register(PatternAuthenticatedConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `request when allowed by pattern then responds with ok`() { + this.spring.register(PatternAuthenticatedConfig::class.java).autowire() + + this.client.get() + .uri("/public") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PatternAuthenticatedConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize("/public", permitAll) + authorize("/**", authenticated) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/public") + fun public() { + } + } + } + + @Test + fun `request when missing required role then responds with forbidden`() { + this.spring.register(HasRoleConfig::class.java).autowire() + this.client + .get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + .expectStatus().isForbidden + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HasRoleConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, hasRole("ADMIN")) + } + httpBasic { } + } + } + + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerAnonymousDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerAnonymousDslTests.kt new file mode 100644 index 00000000000..24d13b1c869 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerAnonymousDslTests.kt @@ -0,0 +1,192 @@ +/* + * 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.config.web.server + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.security.authentication.AnonymousAuthenticationToken +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.reactive.server.expectBody +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono + +/** + * Tests for [ServerAnonymousDsl] + * + * @author Eleftheria Stein + */ +class ServerAnonymousDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `authentication when anonymous enabled then is of type anonymous authentication`() { + this.spring.register(AnonymousConfig::class.java, HttpMeController::class.java).autowire() + + this.client.get() + .uri("/principal") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("anonymousUser") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AnonymousConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + anonymous { } + } + } + } + + @Test + fun `anonymous when custom principal specified then custom principal is used`() { + this.spring.register(CustomPrincipalConfig::class.java, HttpMeController::class.java).autowire() + + this.client.get() + .uri("/principal") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("anon") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomPrincipalConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + anonymous { + principal = "anon" + } + } + } + } + + @Test + fun `anonymous when disabled then principal is null`() { + this.spring.register(AnonymousDisabledConfig::class.java, HttpMeController::class.java).autowire() + + this.client.get() + .uri("/principal") + .exchange() + .expectStatus().isOk + .expectBody().consumeWith { body -> assertThat(body.responseBody).isNull() } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AnonymousDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + anonymous { + disable() + } + } + } + } + + @Test + fun `anonymous when custom key specified then custom key used`() { + this.spring.register(CustomKeyConfig::class.java, HttpMeController::class.java).autowire() + + this.client.get() + .uri("/key") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("key".hashCode().toString()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomKeyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + anonymous { + key = "key" + } + } + } + } + + @Test + fun `anonymous when custom authorities specified then custom authorities used`() { + this.spring.register(CustomAuthoritiesConfig::class.java, HttpMeController::class.java).autowire() + + this.client.get() + .uri("/principal") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomAuthoritiesConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + anonymous { + authorities = listOf(SimpleGrantedAuthority("TEST")) + } + authorizeExchange { + authorize(anyExchange, hasAuthority("TEST")) + } + } + } + } + + @RestController + class HttpMeController { + @GetMapping("/principal") + fun principal(@AuthenticationPrincipal principal: String?): String? { + return principal + } + + @GetMapping("/key") + fun key(@AuthenticationPrincipal principal: Mono): Mono { + return principal + .map { it.keyHash } + .map { it.toString() } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCacheControlDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCacheControlDslTests.kt new file mode 100644 index 00000000000..b1c5d06d463 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCacheControlDslTests.kt @@ -0,0 +1,102 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerCacheControlDsl] + * + * @author Eleftheria Stein + */ +class ServerCacheControlDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when cache control configured then cache headers in response`() { + this.spring.register(CacheControlConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") + .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0") + .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CacheControlConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + cache { } + } + } + } + } + + @Test + fun `request when cache control disabled then no cache headers in response`() { + this.spring.register(CacheControlDisabledConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL) + .expectHeader().doesNotExist(HttpHeaders.EXPIRES) + .expectHeader().doesNotExist(HttpHeaders.PRAGMA) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CacheControlDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + cache { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerContentSecurityPolicyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerContentSecurityPolicyDslTests.kt new file mode 100644 index 00000000000..69699d95632 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerContentSecurityPolicyDslTests.kt @@ -0,0 +1,123 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +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.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerContentSecurityPolicyDsl] + * + * @author Eleftheria Stein + */ +class ServerContentSecurityPolicyDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when content security policy configured then content security policy header in response`() { + this.spring.register(ContentSecurityPolicyConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ContentSecurityPolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentSecurityPolicy { } + } + } + } + } + + @Test + fun `request when custom policy directives then custom policy directive in response header`() { + this.spring.register(CustomPolicyDirectivesConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY, "default-src 'self'; script-src trustedscripts.example.com") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomPolicyDirectivesConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentSecurityPolicy { + policyDirectives = "default-src 'self'; script-src trustedscripts.example.com" + } + } + } + } + } + + @Test + fun `request when report only configured then content security policy report only header in response`() { + this.spring.register(ReportOnlyConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY_REPORT_ONLY, "default-src 'self'") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ReportOnlyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentSecurityPolicy { + reportOnly = true + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerContentTypeOptionsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerContentTypeOptionsDslTests.kt new file mode 100644 index 00000000000..a27f4e65cc7 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerContentTypeOptionsDslTests.kt @@ -0,0 +1,98 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +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.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerContentTypeOptionsDsl] + * + * @author Eleftheria Stein + */ +class ServerContentTypeOptionsDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when content type options configured then header in response`() { + this.spring.register(ContentTypeOptionsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ContentTypeOptionsConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentTypeOptions { } + } + } + } + } + + @Test + fun `request when content type options disabled then no content type options header in response`() { + this.spring.register(ContentTypeOptionsDisabledConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ContentTypeOptionsDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + contentTypeOptions { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCorsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCorsDslTests.kt new file mode 100644 index 00000000000..90256ccb546 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCorsDslTests.kt @@ -0,0 +1,136 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.reactive.CorsConfigurationSource +import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerCorsDsl] + * + * @author Eleftheria Stein + */ +class ServerCorsDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when CORS configured using bean then Access-Control-Allow-Origin header in response`() { + this.spring.register(CorsBeanConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .header(HttpHeaders.ORIGIN, "https://origin.example.com") + .exchange() + .expectHeader().valueEquals("Access-Control-Allow-Origin", "*") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CorsBeanConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + cors { } + } + } + + @Bean + open fun corsConfigurationSource(): CorsConfigurationSource { + val source = UrlBasedCorsConfigurationSource() + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedOrigins = listOf("*") + source.registerCorsConfiguration("/**", corsConfiguration) + return source + } + } + + @Test + fun `request when CORS configured using source then Access-Control-Allow-Origin header in response`() { + this.spring.register(CorsSourceConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .header(HttpHeaders.ORIGIN, "https://origin.example.com") + .exchange() + .expectHeader().valueEquals("Access-Control-Allow-Origin", "*") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CorsSourceConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + val source = UrlBasedCorsConfigurationSource() + val corsConfiguration = CorsConfiguration() + corsConfiguration.allowedOrigins = listOf("*") + source.registerCorsConfiguration("/**", corsConfiguration) + return http { + cors { + configurationSource = source + } + } + } + } + + @Test + fun `request when CORS disabled then no Access-Control-Allow-Origin header in response`() { + this.spring.register(CorsDisabledConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .header(HttpHeaders.ORIGIN, "https://origin.example.com") + .exchange() + .expectHeader().doesNotExist("Access-Control-Allow-Origin") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CorsDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + cors { + disable() + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCsrfDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCsrfDslTests.kt new file mode 100644 index 00000000000..d63d9e27f9a --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerCsrfDslTests.kt @@ -0,0 +1,294 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.MediaType +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler +import org.springframework.security.web.server.csrf.CsrfToken +import org.springframework.security.web.server.csrf.DefaultCsrfToken +import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.reactive.function.BodyInserters.fromMultipartData +import reactor.core.publisher.Mono + +/** + * Tests for [ServerCsrfDsl] + * + * @author Eleftheria Stein + */ +class ServerCsrfDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private val token: CsrfToken = DefaultCsrfToken("csrf", "CSRF", "a") + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `post when CSRF protection enabled then requires CSRF token`() { + this.spring.register(CsrfConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + .expectStatus().isForbidden + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CsrfConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { } + } + } + } + + @Test + fun `post when CSRF protection disabled then CSRF token is not required`() { + this.spring.register(CsrfDisabledConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CsrfDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { + disable() + } + } + } + + @RestController + internal class TestController { + @PostMapping("/") + fun home() { + } + } + } + + @Test + fun `post when request matches CSRF matcher then CSRF token required`() { + this.spring.register(CsrfMatcherConfig::class.java).autowire() + + this.client.post() + .uri("/csrf") + .exchange() + .expectStatus().isForbidden + } + + @Test + fun `post when request does not match CSRF matcher then CSRF token is not required`() { + this.spring.register(CsrfMatcherConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CsrfMatcherConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { + requireCsrfProtectionMatcher = PathPatternParserServerWebExchangeMatcher("/csrf") + } + } + } + + @RestController + internal class TestController { + @PostMapping("/") + fun home() { + } + + @PostMapping("/csrf") + fun csrf() { + } + } + } + + @Test + fun `csrf when custom access denied handler then handler used`() { + this.spring.register(CustomAccessDeniedHandlerConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + + Mockito.verify(CustomAccessDeniedHandlerConfig.ACCESS_DENIED_HANDLER) + .handle(any(), any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomAccessDeniedHandlerConfig { + companion object { + var ACCESS_DENIED_HANDLER: ServerAccessDeniedHandler = mock(ServerAccessDeniedHandler::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { + accessDeniedHandler = ACCESS_DENIED_HANDLER + } + } + } + } + + @Test + fun `csrf when custom token repository then repository used`() { + `when`(CustomCsrfTokenRepositoryConfig.TOKEN_REPOSITORY.loadToken(any())) + .thenReturn(Mono.just(this.token)) + this.spring.register(CustomCsrfTokenRepositoryConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + + Mockito.verify(CustomCsrfTokenRepositoryConfig.TOKEN_REPOSITORY) + .loadToken(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomCsrfTokenRepositoryConfig { + companion object { + var TOKEN_REPOSITORY: ServerCsrfTokenRepository = mock(ServerCsrfTokenRepository::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { + csrfTokenRepository = TOKEN_REPOSITORY + } + } + } + } + + @Test + fun `csrf when multipart form data and not enabled then denied`() { + `when`(MultipartFormDataNotEnabledConfig.TOKEN_REPOSITORY.loadToken(any())) + .thenReturn(Mono.just(this.token)) + `when`(MultipartFormDataNotEnabledConfig.TOKEN_REPOSITORY.generateToken(any())) + .thenReturn(Mono.just(this.token)) + this.spring.register(MultipartFormDataNotEnabledConfig::class.java).autowire() + + this.client.post() + .uri("/") + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(fromMultipartData(this.token.parameterName, this.token.token)) + .exchange() + .expectStatus().isForbidden + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class MultipartFormDataNotEnabledConfig { + companion object { + var TOKEN_REPOSITORY: ServerCsrfTokenRepository = mock(ServerCsrfTokenRepository::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { + csrfTokenRepository = TOKEN_REPOSITORY + } + } + } + } + + @Test + fun `csrf when multipart form data and enabled then granted`() { + `when`(MultipartFormDataEnabledConfig.TOKEN_REPOSITORY.loadToken(any())) + .thenReturn(Mono.just(this.token)) + `when`(MultipartFormDataEnabledConfig.TOKEN_REPOSITORY.generateToken(any())) + .thenReturn(Mono.just(this.token)) + this.spring.register(MultipartFormDataEnabledConfig::class.java).autowire() + + this.client.post() + .uri("/") + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(fromMultipartData(this.token.parameterName, this.token.token)) + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class MultipartFormDataEnabledConfig { + companion object { + var TOKEN_REPOSITORY: ServerCsrfTokenRepository = mock(ServerCsrfTokenRepository::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + csrf { + csrfTokenRepository = TOKEN_REPOSITORY + tokenFromMultipartDataEnabled = true + } + } + } + + @RestController + internal class TestController { + @PostMapping("/") + fun home() { + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDslTests.kt new file mode 100644 index 00000000000..22b5de4b44c --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerExceptionHandlingDslTests.kt @@ -0,0 +1,126 @@ +/* + * 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.config.web.server + +import org.assertj.core.api.Assertions +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpStatus +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.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint +import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import java.util.* + +/** + * Tests for [ServerExceptionHandlingDsl] + * + * @author Eleftheria Stein + */ +class ServerExceptionHandlingDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `unauthenticated request when custom entry point then directed to custom entry point`() { + this.spring.register(EntryPointConfig::class.java).autowire() + + val result = this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + Assertions.assertThat(result.responseHeaders.location).hasPath("/auth") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class EntryPointConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + exceptionHandling { + authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/auth") + } + } + } + } + + @Test + fun `unauthorized request when custom access denied handler then directed to custom access denied handler`() { + this.spring.register(AccessDeniedHandlerConfig::class.java).autowire() + + this.client + .get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + .expectStatus().isSeeOther + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AccessDeniedHandlerConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, hasRole("ADMIN")) + } + httpBasic { } + exceptionHandling { + accessDeniedHandler = HttpStatusServerAccessDeniedHandler(HttpStatus.SEE_OTHER) + } + } + } + + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt new file mode 100644 index 00000000000..23afe0b8653 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFormLoginDslTests.kt @@ -0,0 +1,325 @@ +/* + * 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.config.web.server + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod +import org.springframework.security.authentication.ReactiveAuthenticationManager +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.SpringTestRule +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler +import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler +import org.springframework.security.web.server.context.ServerSecurityContextRepository +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers +import org.springframework.test.web.reactive.server.FluxExchangeResult +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.reactive.function.BodyInserters + +/** + * Tests for [ServerFormLoginDsl] + * + * @author Eleftheria Stein + */ +class ServerFormLoginDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when form login enabled then redirects to default login page`() { + this.spring.register(DefaultFormLoginConfig::class.java, UserDetailsConfig::class.java).autowire() + + val result: FluxExchangeResult = this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasPath("/login") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class DefaultFormLoginConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { } + } + } + } + + @Test + fun `request when custom login page then redirects to custom login page`() { + this.spring.register(CustomLoginPageConfig::class.java, UserDetailsConfig::class.java).autowire() + + val result: FluxExchangeResult = this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasPath("/log-in") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomLoginPageConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { + loginPage = "/log-in" + } + } + } + } + + @Test + fun `form login when custom authentication manager then manager used`() { + this.spring.register(CustomAuthenticationManagerConfig::class.java).autowire() + val data: MultiValueMap = LinkedMultiValueMap() + data.add("username", "user") + data.add("password", "password") + + this.client + .mutateWith(csrf()) + .post() + .uri("/login") + .body(BodyInserters.fromFormData(data)) + .exchange() + + verify(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER) + .authenticate(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomAuthenticationManagerConfig { + companion object { + var AUTHENTICATION_MANAGER: ReactiveAuthenticationManager = Mockito.mock(ReactiveAuthenticationManager::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { + authenticationManager = AUTHENTICATION_MANAGER + } + } + } + } + + @Test + fun `form login when custom authentication entry point then entry point used`() { + this.spring.register(CustomConfig::class.java, UserDetailsConfig::class.java).autowire() + + val result = this.client.get() + .uri("/") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasPath("/entry") + } + } + + @Test + fun `form login when custom requires authentication matcher then matching request logs in`() { + this.spring.register(CustomConfig::class.java, UserDetailsConfig::class.java).autowire() + val data: MultiValueMap = LinkedMultiValueMap() + data.add("username", "user") + data.add("password", "password") + + val result = this.client + .mutateWith(csrf()) + .post() + .uri("/log-in") + .body(BodyInserters.fromFormData(data)) + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasPath("/") + } + } + + @Test + fun `invalid login when custom failure handler then failure handler used`() { + this.spring.register(CustomConfig::class.java, UserDetailsConfig::class.java).autowire() + + val result = this.client + .mutateWith(csrf()) + .post() + .uri("/log-in") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasPath("/log-in-error") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { + authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/entry") + requiresAuthenticationMatcher = ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/log-in") + authenticationFailureHandler = RedirectServerAuthenticationFailureHandler("/log-in-error") + } + } + } + } + + @Test + fun `login when custom success handler then success handler used`() { + this.spring.register(CustomSuccessHandlerConfig::class.java, UserDetailsConfig::class.java).autowire() + val data: MultiValueMap = LinkedMultiValueMap() + data.add("username", "user") + data.add("password", "password") + + val result = this.client + .mutateWith(csrf()) + .post() + .uri("/login") + .body(BodyInserters.fromFormData(data)) + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasPath("/success") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomSuccessHandlerConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { + authenticationSuccessHandler = RedirectServerAuthenticationSuccessHandler("/success") + } + } + } + } + + @Test + fun `form login when custom security context repository then repository used`() { + this.spring.register(CustomSecurityContextRepositoryConfig::class.java, UserDetailsConfig::class.java).autowire() + val data: MultiValueMap = LinkedMultiValueMap() + data.add("username", "user") + data.add("password", "password") + + this.client + .mutateWith(csrf()) + .post() + .uri("/login") + .body(BodyInserters.fromFormData(data)) + .exchange() + + verify(CustomSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPOSITORY) + .save(Mockito.any(), Mockito.any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomSecurityContextRepositoryConfig { + companion object { + var SECURITY_CONTEXT_REPOSITORY: ServerSecurityContextRepository = Mockito.mock(ServerSecurityContextRepository::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { + securityContextRepository = SECURITY_CONTEXT_REPOSITORY + } + } + } + } + + @Configuration + open class UserDetailsConfig { + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFrameOptionsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFrameOptionsDslTests.kt new file mode 100644 index 00000000000..2a95dea8d8c --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerFrameOptionsDslTests.kt @@ -0,0 +1,124 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +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.config.test.SpringTestRule +import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerFrameOptionsDsl] + * + * @author Eleftheria Stein + */ +class ServerFrameOptionsDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when frame options configured then header in response`() { + this.spring.register(FrameOptionsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class FrameOptionsConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + frameOptions { } + } + } + } + } + + @Test + fun `request when frame options disabled then no frame options header in response`() { + this.spring.register(FrameOptionsDisabledConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class FrameOptionsDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + frameOptions { + disable() + } + } + } + } + } + + @Test + fun `request when frame options mode set then frame options response header has mode value`() { + this.spring.register(CustomModeConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN.name) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomModeConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + frameOptions { + mode = XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN + } + } + } + } + } +} 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 new file mode 100644 index 00000000000..6cebc2cbc3f --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHeadersDslTests.kt @@ -0,0 +1,134 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +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.StrictTransportSecurityServerHttpHeadersWriter +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerHeadersDsl] + * + * @author Eleftheria Stein + */ +class ServerHeadersDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when default headers configured then default headers are in the response`() { + this.spring.register(DefaultHeadersConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff") + .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains") + .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") + .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0") + .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache") + .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class DefaultHeadersConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { } + } + } + } + + @Test + fun `request when headers disabled then no security headers are in the response`() { + this.spring.register(HeadersDisabledConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS) + .expectHeader().doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS) + .expectHeader().doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY) + .expectHeader().doesNotExist(HttpHeaders.CACHE_CONTROL) + .expectHeader().doesNotExist(HttpHeaders.EXPIRES) + .expectHeader().doesNotExist(HttpHeaders.PRAGMA) + .expectHeader().doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HeadersDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + disable() + } + } + } + } + + @Test + fun `request when feature policy configured then feature policy header in response`() { + this.spring.register(FeaturePolicyConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("Feature-Policy", "geolocation 'self'") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class FeaturePolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + featurePolicy("geolocation 'self'") + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt new file mode 100644 index 00000000000..4d4c926d17b --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpBasicDslTests.kt @@ -0,0 +1,219 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.BDDMockito.given +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.Authentication +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.ServerAuthenticationEntryPoint +import org.springframework.security.web.server.context.ServerSecurityContextRepository +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 reactor.core.publisher.Mono +import java.util.* + +/** + * Tests for [ServerHttpBasicDsl] + * + * @author Eleftheria Stein + */ +class ServerHttpBasicDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `http basic when no authorization header then responds with unauthorized`() { + this.spring.register(HttpBasicConfig::class.java, UserDetailsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isUnauthorized + } + + @Test + fun `http basic when valid authorization header then responds with ok`() { + this.spring.register(HttpBasicConfig::class.java, UserDetailsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HttpBasicConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + httpBasic { } + } + } + + @RestController + internal class PathController { + @RequestMapping("/") + fun path() { + } + } + } + + @Test + fun `http basic when custom authentication manager then manager used`() { + given>(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER.authenticate(any())) + .willReturn(Mono.just(TestingAuthenticationToken("user", "password", "ROLE_USER"))) + + this.spring.register(CustomAuthenticationManagerConfig::class.java).autowire() + + this.client.get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + + verify(CustomAuthenticationManagerConfig.AUTHENTICATION_MANAGER) + .authenticate(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomAuthenticationManagerConfig { + companion object { + var AUTHENTICATION_MANAGER: ReactiveAuthenticationManager = mock(ReactiveAuthenticationManager::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + httpBasic { + authenticationManager = AUTHENTICATION_MANAGER + } + } + } + } + + @Test + fun `http basic when custom security context repository then repository used`() { + this.spring.register(CustomSecurityContextRepositoryConfig::class.java, UserDetailsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("user:password".toByteArray())) + .exchange() + + verify(CustomSecurityContextRepositoryConfig.SECURITY_CONTEXT_REPOSITORY) + .save(any(), any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomSecurityContextRepositoryConfig { + companion object { + var SECURITY_CONTEXT_REPOSITORY: ServerSecurityContextRepository = mock(ServerSecurityContextRepository::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + httpBasic { + securityContextRepository = SECURITY_CONTEXT_REPOSITORY + } + } + } + } + + @Test + fun `http basic when custom authentication entry point then entry point used`() { + this.spring.register(CustomAuthenticationEntryPointConfig::class.java, UserDetailsConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + + verify(CustomAuthenticationEntryPointConfig.ENTRY_POINT) + .commence(any(), any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomAuthenticationEntryPointConfig { + companion object { + var ENTRY_POINT: ServerAuthenticationEntryPoint = mock(ServerAuthenticationEntryPoint::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + httpBasic { + authenticationEntryPoint = ENTRY_POINT + } + } + } + } + + @Configuration + open class UserDetailsConfig { + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt new file mode 100644 index 00000000000..0b6135741f0 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpSecurityDslTests.kt @@ -0,0 +1,126 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +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.StrictTransportSecurityServerHttpHeadersWriter +import org.springframework.security.web.server.header.XFrameOptionsServerHttpHeadersWriter +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerHttpSecurityDsl] + * + * @author Eleftheria Stein + */ +class ServerHttpSecurityDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when it does not match the security matcher then the security rules do not apply`() { + this.spring.register(PatternMatcherConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isNotFound + } + + @Test + fun `request when it matches the security matcher then the security rules apply`() { + this.spring.register(PatternMatcherConfig::class.java).autowire() + + this.client.get() + .uri("/api") + .exchange() + .expectStatus().isUnauthorized + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PatternMatcherConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**")) + authorizeExchange { + authorize(anyExchange, authenticated) + } + } + } + } + + @Test + fun `post when default security configured then CSRF prevents the request`() { + this.spring.register(DefaultSecurityConfig::class.java).autowire() + + this.client.post() + .uri("/") + .exchange() + .expectStatus().isForbidden + } + + @Test + fun `request when default security configured then default headers are in the response`() { + this.spring.register(DefaultSecurityConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS, "nosniff") + .expectHeader().valueEquals(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS, XFrameOptionsHeaderWriter.XFrameOptionsMode.DENY.name) + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains") + .expectHeader().valueEquals(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate") + .expectHeader().valueEquals(HttpHeaders.EXPIRES, "0") + .expectHeader().valueEquals(HttpHeaders.PRAGMA, "no-cache") + .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class DefaultSecurityConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpStrictTransportSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpStrictTransportSecurityDslTests.kt new file mode 100644 index 00000000000..ccc62592939 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpStrictTransportSecurityDslTests.kt @@ -0,0 +1,176 @@ +/* + * 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.config.web.server.headers + +import org.junit.Rule +import org.junit.Test +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.config.web.server.invoke +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import java.time.Duration + +/** + * Tests for [ServerReferrerPolicyDsl] + * + * @author Eleftheria Stein + */ +class ServerHttpStrictTransportSecurityDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when hsts configured then hsts header in response`() { + this.spring.register(HstsConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HstsConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { } + } + } + } + } + + @Test + fun `request when hsts disabled then no hsts header in response`() { + this.spring.register(HstsDisabledConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HstsDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { + disable() + } + } + } + } + } + + @Test + fun `request when max age set then max age in response header`() { + this.spring.register(MaxAgeConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=1 ; includeSubDomains") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class MaxAgeConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { + maxAge = Duration.ofSeconds(1) + } + } + } + } + } + + @Test + fun `request when includeSubdomains false then includeSubdomains not in response header`() { + this.spring.register(IncludeSubdomainsConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class IncludeSubdomainsConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { + includeSubdomains = false + } + } + } + } + } + + @Test + fun `request when preload true then preload included in response header`() { + this.spring.register(PreloadConfig::class.java).autowire() + + this.client.get() + .uri("https://example.com") + .exchange() + .expectHeader().valueEquals(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains ; preload") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PreloadConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + hsts { + preload = true + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDslTests.kt new file mode 100644 index 00000000000..9de8fc77d42 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerHttpsRedirectDslTests.kt @@ -0,0 +1,199 @@ +/* + * 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.config.web.server + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +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.config.test.SpringTestRule +import org.springframework.security.web.PortMapperImpl +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import java.util.* + +/** + * Tests for [ServerHttpsRedirectDsl] + * + * @author Eleftheria Stein + */ +class ServerHttpsRedirectDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when matches redirect to HTTPS matcher then redirects to HTTPS`() { + this.spring.register(HttpRedirectMatcherConfig::class.java).autowire() + + val result = this.client.get() + .uri("/secure") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasScheme("https") + } + } + + @Test + fun `request when does not match redirect to HTTPS matcher then does not redirect`() { + this.spring.register(HttpRedirectMatcherConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isNotFound + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HttpRedirectMatcherConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + redirectToHttps { + httpsRedirectWhen(PathPatternParserServerWebExchangeMatcher("/secure")) + } + } + } + } + + @Test + fun `request when matches redirect to HTTPS function then redirects to HTTPS`() { + this.spring.register(HttpRedirectFunctionConfig::class.java).autowire() + + val result = this.client.get() + .uri("/") + .header("X-Requires-Https", "required") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasScheme("https") + } + } + + @Test + fun `request when does not match redirect to HTTPS function then does not redirect`() { + this.spring.register(HttpRedirectFunctionConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isNotFound + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HttpRedirectFunctionConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + redirectToHttps { + httpsRedirectWhen { + it.request.headers.containsKey("X-Requires-Https") + } + } + } + } + } + + @Test + fun `request when multiple rules configured then only the last rule applies`() { + this.spring.register(HttpRedirectMatcherAndFunctionConfig::class.java).autowire() + + this.client.get() + .uri("/secure") + .exchange() + .expectStatus().isNotFound + + val result = this.client.get() + .uri("/") + .header("X-Requires-Https", "required") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasScheme("https") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class HttpRedirectMatcherAndFunctionConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + redirectToHttps { + httpsRedirectWhen(PathPatternParserServerWebExchangeMatcher("/secure")) + httpsRedirectWhen { + it.request.headers.containsKey("X-Requires-Https") + } + } + } + } + } + + @Test + fun `request when port mapper configured then redirected to HTTPS port`() { + this.spring.register(PortMapperConfig::class.java).autowire() + + val result = this.client.get() + .uri("http://localhost:543") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location).hasScheme("https").hasPort(123) + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PortMapperConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + val customPortMapper = PortMapperImpl() + customPortMapper.setPortMappings(Collections.singletonMap("543", "123")) + return http { + redirectToHttps { + portMapper = customPortMapper + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt new file mode 100644 index 00000000000..ddb33b5323a --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerJwtDslTests.kt @@ -0,0 +1,270 @@ +/* + * 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.config.web.server + +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.core.convert.converter.Converter +import org.springframework.http.HttpHeaders +import org.springframework.security.authentication.AbstractAuthenticationToken +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames +import org.springframework.security.oauth2.jwt.Jwt +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono +import java.math.BigInteger +import java.security.KeyFactory +import java.security.interfaces.RSAPublicKey +import java.security.spec.RSAPublicKeySpec +import javax.annotation.PreDestroy + +/** + * Tests for [ServerJwtDsl] + * + * @author Eleftheria Stein + */ +class ServerJwtDslTests { + + private val expired = "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE1MzUwMzc4OTd9.jqZDDjfc2eysX44lHXEIr9XFd2S8vjIZHCccZU-dRWMRJNsQ1QN5VNnJGklqJBXJR4qgla6cmVqPOLkUHDb0sL0nxM5XuzQaG5ZzKP81RV88shFyAiT0fD-6nl1k-Fai-Fu-VkzSpNXgeONoTxDaYhdB-yxmgrgsApgmbOTE_9AcMk-FQDXQ-pL9kynccFGV0lZx4CA7cyknKN7KBxUilfIycvXODwgKCjj_1WddLTCNGYogJJSg__7NoxzqbyWd3udbHVjqYq7GsMMrGB4_2kBD4CkghOSNcRHbT_DIXowxfAVT7PAg7Q0E5ruZsr2zPZacEUDhJ6-wbvlA0FAOUg" + private val messageReadToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtb2NrLXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4ODY0MTQxM30.cRl1bv_dDYcAN5U4NlIVKj8uu4mLMwjABF93P4dShiq-GQ-owzaqTSlB4YarNFgV3PKQvT9wxN1jBpGribvISljakoC0E8wDV-saDi8WxN-qvImYsn1zLzYFiZXCfRIxCmonJpydeiAPRxMTPtwnYDS9Ib0T_iA80TBGd-INhyxUUfrwRW5sqKRbjUciRJhpp7fW2ZYXmi9iPt3HDjRQA4IloJZ7f4-spt5Q9wl5HcQTv1t4XrX4eqhVbE5cCoIkFQnKPOc-jhVM44_eazLU6Xk-CCXP8C_UT5pX0luRS2cJrVFfHp2IR_AWxC-shItg6LNEmNFD4Zc-JLZcr0Q86Q" + private val jwkSet = "{\n" + + " \"keys\":[\n" + + " {\n" + + " \"kty\":\"RSA\",\n" + + " \"e\":\"AQAB\",\n" + + " \"use\":\"sig\",\n" + + " \"kid\":\"one\",\n" + + " \"n\":\"0IUjrPZDz-3z0UE4ppcKU36v7hnh8FJjhu3lbJYj0qj9eZiwEJxi9HHUfSK1DhUQG7mJBbYTK1tPYCgre5EkfKh-64VhYUa-vz17zYCmuB8fFj4XHE3MLkWIG-AUn8hNbPzYYmiBTjfGnMKxLHjsbdTiF4mtn-85w366916R6midnAuiPD4HjZaZ1PAsuY60gr8bhMEDtJ8unz81hoQrozpBZJ6r8aR1PrsWb1OqPMloK9kAIutJNvWYKacp8WYAp2WWy72PxQ7Fb0eIA1br3A5dnp-Cln6JROJcZUIRJ-QvS6QONWeS2407uQmS-i-lybsqaH0ldYC7NBEBA5inPQ\"\n" + + " }\n" + + " ]\n" + + "}\n" + + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when JWT configured with public key and valid token then responds with ok`() { + this.spring.register(PublicKeyConfig::class.java, BaseController::class.java).autowire() + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth(messageReadToken) } + .exchange() + .expectStatus().isOk + } + + @Test + fun `request when JWT configured with public key and expired token then responds with unauthorized`() { + this.spring.register(PublicKeyConfig::class.java, BaseController::class.java).autowire() + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth(expired) } + .exchange() + .expectStatus().isUnauthorized + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PublicKeyConfig { + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + publicKey = publicKey() + } + } + } + } + } + + @Test + fun `jwt when using custom JWT decoded then custom decoded used`() { + this.spring.register(CustomDecoderConfig::class.java).autowire() + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth("token") } + .exchange() + + verify(CustomDecoderConfig.JWT_DECODER).decode("token") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomDecoderConfig { + companion object { + var JWT_DECODER: ReactiveJwtDecoder = mock(ReactiveJwtDecoder::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + jwtDecoder = JWT_DECODER + } + } + } + } + } + + @Test + fun `jwt when using custom JWK Set URI then custom URI used`() { + this.spring.register(CustomJwkSetUriConfig::class.java).autowire() + + CustomJwkSetUriConfig.MOCK_WEB_SERVER.enqueue(MockResponse().setBody(jwkSet)) + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth(messageReadToken) } + .exchange() + + val recordedRequest = CustomJwkSetUriConfig.MOCK_WEB_SERVER.takeRequest() + assertThat(recordedRequest.path).isEqualTo("/.well-known/jwks.json") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomJwkSetUriConfig { + companion object { + var MOCK_WEB_SERVER: MockWebServer = MockWebServer() + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + jwkSetUri = mockWebServer().url("/.well-known/jwks.json").toString() + } + } + } + } + + @Bean + open fun mockWebServer(): MockWebServer { + return MOCK_WEB_SERVER + } + + @PreDestroy + open fun shutdown() { + MOCK_WEB_SERVER.shutdown() + } + } + + + @Test + fun `opaque token when custom JWT authentication converter then converter used`() { + this.spring.register(CustomJwtAuthenticationConverterConfig::class.java).autowire() + `when`(CustomJwtAuthenticationConverterConfig.DECODER.decode(anyString())).thenReturn( + Mono.just(Jwt.withTokenValue("token") + .header("alg", "none") + .claim(IdTokenClaimNames.SUB, "user") + .build())) + `when`(CustomJwtAuthenticationConverterConfig.CONVERTER.convert(any())) + .thenReturn(Mono.just(TestingAuthenticationToken("test", "this", "ROLE"))) + + this.client.get() + .uri("/") + .headers { headers: HttpHeaders -> headers.setBearerAuth("token") } + .exchange() + + verify(CustomJwtAuthenticationConverterConfig.CONVERTER).convert(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomJwtAuthenticationConverterConfig { + companion object { + var CONVERTER: Converter> = mock(Converter::class.java) as Converter> + var DECODER: ReactiveJwtDecoder = mock(ReactiveJwtDecoder::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + jwtAuthenticationConverter = CONVERTER + } + } + } + } + + @Bean + open fun jwtDecoder(): ReactiveJwtDecoder { + return DECODER + } + } + + @RestController + internal class BaseController { + @GetMapping + fun index() { + } + } + + companion object { + private fun publicKey(): RSAPublicKey { + val modulus = "26323220897278656456354815752829448539647589990395639665273015355787577386000316054335559633864476469390247312823732994485311378484154955583861993455004584140858982659817218753831620205191028763754231454775026027780771426040997832758235764611119743390612035457533732596799927628476322029280486807310749948064176545712270582940917249337311592011920620009965129181413510845780806191965771671528886508636605814099711121026468495328702234901200169245493126030184941412539949521815665744267183140084667383643755535107759061065656273783542590997725982989978433493861515415520051342321336460543070448417126615154138673620797" + val exponent = "65537" + val spec = RSAPublicKeySpec(BigInteger(modulus), BigInteger(exponent)) + val factory = KeyFactory.getInstance("RSA") + return factory.generatePublic(spec) as RSAPublicKey + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerLogoutDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerLogoutDslTests.kt new file mode 100644 index 00000000000..4acdeea943e --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerLogoutDslTests.kt @@ -0,0 +1,244 @@ +/* + * 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.config.web.server + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.* +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.config.test.SpringTestRule +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler +import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono + +/** + * Tests for [ServerLogoutDsl] + * + * @author Eleftheria Stein + */ +class ServerLogoutDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `logout when defaults used then redirects to login page`() { + this.spring.register(LogoutConfig::class.java).autowire() + + val result = this.client + .mutateWith(csrf()) + .post() + .uri("/logout") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location) + .hasPath("/login") + .hasParameter("logout") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class LogoutConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + logout { } + } + } + } + + @Test + fun `logout when custom logout URL then custom URL redirects to login page`() { + this.spring.register(CustomUrlConfig::class.java).autowire() + + val result = this.client + .mutateWith(csrf()) + .post() + .uri("/custom-logout") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location) + .hasPath("/login") + .hasParameter("logout") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomUrlConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + logout { + logoutUrl = "/custom-logout" + } + } + } + } + + @Test + fun `logout when custom requires logout matcher then matching request redirects to login page`() { + this.spring.register(RequiresLogoutConfig::class.java).autowire() + + val result = this.client + .mutateWith(csrf()) + .post() + .uri("/custom-logout") + .exchange() + .expectStatus().is3xxRedirection + .returnResult(String::class.java) + + result.assertWithDiagnostics { + assertThat(result.responseHeaders.location) + .hasPath("/login") + .hasParameter("logout") + } + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class RequiresLogoutConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + logout { + requiresLogout = PathPatternParserServerWebExchangeMatcher("/custom-logout") + } + } + } + } + + @Test + fun `logout when custom logout handler then custom handler invoked`() { + this.spring.register(CustomLogoutHandlerConfig::class.java).autowire() + + `when`(CustomLogoutHandlerConfig.LOGOUT_HANDLER.logout(any(), any())) + .thenReturn(Mono.empty()) + + this.client + .mutateWith(csrf()) + .post() + .uri("/logout") + .exchange() + + verify(CustomLogoutHandlerConfig.LOGOUT_HANDLER) + .logout(any(), any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomLogoutHandlerConfig { + companion object { + var LOGOUT_HANDLER: ServerLogoutHandler = mock(ServerLogoutHandler::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + logout { + logoutHandler = LOGOUT_HANDLER + } + } + } + } + + @Test + fun `logout when custom logout success handler then custom handler invoked`() { + this.spring.register(CustomLogoutSuccessHandlerConfig::class.java).autowire() + + this.client + .mutateWith(csrf()) + .post() + .uri("/logout") + .exchange() + + verify(CustomLogoutSuccessHandlerConfig.LOGOUT_HANDLER) + .onLogoutSuccess(any(), any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomLogoutSuccessHandlerConfig { + companion object { + var LOGOUT_HANDLER: ServerLogoutSuccessHandler = mock(ServerLogoutSuccessHandler::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + logout { + logoutSuccessHandler = LOGOUT_HANDLER + } + } + } + } + + @Test + fun `logout when disabled then logout URL not found`() { + this.spring.register(LogoutDisabledConfig::class.java).autowire() + + this.client + .mutateWith(csrf()) + .post() + .uri("/logout") + .exchange() + .expectStatus().isNotFound + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class LogoutDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, permitAll) + } + logout { + disable() + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt new file mode 100644 index 00000000000..de317e59ee2 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ClientDslTests.kt @@ -0,0 +1,223 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono + +/** + * Tests for [ServerOAuth2ClientDsl] + * + * @author Eleftheria Stein + */ +class ServerOAuth2ClientDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `OAuth2 client when custom client registration repository then bean is not required`() { + this.spring.register(ClientRepoConfig::class.java).autowire() + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ClientRepoConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Client { + clientRegistrationRepository = InMemoryReactiveClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } + } + } + + @Test + fun `OAuth2 client when authorization request repository configured then custom repository used`() { + this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire() + + this.client.get() + .uri { + it.path("/") + .queryParam(OAuth2ParameterNames.CODE, "code") + .queryParam(OAuth2ParameterNames.STATE, "state") + .build() + } + .exchange() + + verify(AuthorizationRequestRepositoryConfig.AUTHORIZATION_REQUEST_REPOSITORY).loadAuthorizationRequest(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthorizationRequestRepositoryConfig { + companion object { + var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java) + as ServerAuthorizationRequestRepository + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Client { + authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY + } + } + } + } + + @Test + fun `OAuth2 client when authentication converter configured then custom converter used`() { + this.spring.register(AuthenticationConverterConfig::class.java, ClientConfig::class.java).autowire() + + `when`(AuthenticationConverterConfig.AUTHORIZATION_REQUEST_REPOSITORY.loadAuthorizationRequest(any())) + .thenReturn(Mono.just(OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri("https://example.com/login/oauth/authorize") + .clientId("clientId") + .redirectUri("/authorize/oauth2/code/google") + .build())) + + this.client.get() + .uri { + it.path("/authorize/oauth2/code/google") + .queryParam(OAuth2ParameterNames.CODE, "code") + .queryParam(OAuth2ParameterNames.STATE, "state") + .build() + } + .exchange() + + verify(AuthenticationConverterConfig.AUTHENTICATION_CONVERTER).convert(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthenticationConverterConfig { + companion object { + var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java) + as ServerAuthorizationRequestRepository + var AUTHENTICATION_CONVERTER: ServerAuthenticationConverter = mock(ServerAuthenticationConverter::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Client { + authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY + authenticationConverter = AUTHENTICATION_CONVERTER + } + } + } + } + + @Test + fun `OAuth2 client when authentication manager configured then custom manager used`() { + this.spring.register(AuthenticationManagerConfig::class.java, ClientConfig::class.java).autowire() + + `when`(AuthenticationManagerConfig.AUTHORIZATION_REQUEST_REPOSITORY.loadAuthorizationRequest(any())) + .thenReturn(Mono.just(OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri("https://example.com/login/oauth/authorize") + .clientId("clientId") + .redirectUri("/authorize/oauth2/code/google") + .build())) + `when`(AuthenticationManagerConfig.AUTHENTICATION_CONVERTER.convert(any())) + .thenReturn(Mono.just(TestingAuthenticationToken("a", "b", "c"))) + + this.client.get() + .uri { + it.path("/authorize/oauth2/code/google") + .queryParam(OAuth2ParameterNames.CODE, "code") + .queryParam(OAuth2ParameterNames.STATE, "state") + .build() + } + .exchange() + + verify(AuthenticationManagerConfig.AUTHENTICATION_MANAGER).authenticate(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthenticationManagerConfig { + companion object { + var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java) + as ServerAuthorizationRequestRepository + var AUTHENTICATION_CONVERTER: ServerAuthenticationConverter = mock(ServerAuthenticationConverter::class.java) + var AUTHENTICATION_MANAGER: ReactiveAuthenticationManager = mock(ReactiveAuthenticationManager::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Client { + authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY + authenticationConverter = AUTHENTICATION_CONVERTER + authenticationManager = AUTHENTICATION_MANAGER + } + } + } + } + + @Configuration + open class ClientConfig { + @Bean + open fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt new file mode 100644 index 00000000000..43aae3aecea --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2LoginDslTests.kt @@ -0,0 +1,201 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository +import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerOAuth2LoginDsl] + * + * @author Eleftheria Stein + */ +class ServerOAuth2LoginDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `oauth2Login when custom client registration repository then bean is not required`() { + this.spring.register(ClientRepoConfig::class.java).autowire() + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ClientRepoConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { + clientRegistrationRepository = InMemoryReactiveClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } + } + } + + @Test + fun `login page when OAuth2 login configured then default login page created`() { + this.spring.register(OAuth2LoginConfig::class.java, ClientConfig::class.java).autowire() + + this.client.get() + .uri("/login") + .exchange() + .expectStatus().isOk + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class OAuth2LoginConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + } + + @Test + fun `OAuth2 login when authorization request repository configured then custom repository used`() { + this.spring.register(AuthorizationRequestRepositoryConfig::class.java, ClientConfig::class.java).autowire() + + this.client.get() + .uri("/login/oauth2/code/google") + .exchange() + + verify(AuthorizationRequestRepositoryConfig.AUTHORIZATION_REQUEST_REPOSITORY).removeAuthorizationRequest(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthorizationRequestRepositoryConfig { + companion object { + var AUTHORIZATION_REQUEST_REPOSITORY = mock(ServerAuthorizationRequestRepository::class.java) + as ServerAuthorizationRequestRepository + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { + authorizationRequestRepository = AUTHORIZATION_REQUEST_REPOSITORY + } + } + } + } + + @Test + fun `OAuth2 login when authentication matcher configured then custom matcher used`() { + this.spring.register(AuthenticationMatcherConfig::class.java, ClientConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + + verify(AuthenticationMatcherConfig.AUTHENTICATION_MATCHER).matches(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthenticationMatcherConfig { + companion object { + var AUTHENTICATION_MATCHER: ServerWebExchangeMatcher = mock(ServerWebExchangeMatcher::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { + authenticationMatcher = AUTHENTICATION_MATCHER + } + } + } + } + + @Test + fun `OAuth2 login when authentication converter configured then custom converter used`() { + this.spring.register(AuthenticationConverterConfig::class.java, ClientConfig::class.java).autowire() + + this.client.get() + .uri("/login/oauth2/code/google") + .exchange() + + verify(AuthenticationConverterConfig.AUTHENTICATION_CONVERTER).convert(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthenticationConverterConfig { + companion object { + var AUTHENTICATION_CONVERTER: ServerAuthenticationConverter = mock(ServerAuthenticationConverter::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { + authenticationConverter = AUTHENTICATION_CONVERTER + } + } + } + } + + @Configuration + open class ClientConfig { + @Bean + open fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository( + CommonOAuth2Provider.GOOGLE + .getBuilder("google").clientId("clientId").clientSecret("clientSecret") + .build() + ) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDslTests.kt new file mode 100644 index 00000000000..2ddfa2b107c --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOAuth2ResourceServerDslTests.kt @@ -0,0 +1,204 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpStatus +import org.springframework.http.server.reactive.ServerHttpRequest +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint +import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.server.ServerWebExchange +import java.math.BigInteger +import java.security.KeyFactory +import java.security.interfaces.RSAPublicKey +import java.security.spec.RSAPublicKeySpec + +/** + * Tests for [ServerOAuth2ResourceServerDsl] + * + * @author Eleftheria Stein + */ +class ServerOAuth2ResourceServerDslTests { + private val validJwt = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtb2NrLXN1YmplY3QiLCJzY29wZSI6Im1lc3NhZ2U6cmVhZCIsImV4cCI6NDY4ODY0MTQxM30.cRl1bv_dDYcAN5U4NlIVKj8uu4mLMwjABF93P4dShiq-GQ-owzaqTSlB4YarNFgV3PKQvT9wxN1jBpGribvISljakoC0E8wDV-saDi8WxN-qvImYsn1zLzYFiZXCfRIxCmonJpydeiAPRxMTPtwnYDS9Ib0T_iA80TBGd-INhyxUUfrwRW5sqKRbjUciRJhpp7fW2ZYXmi9iPt3HDjRQA4IloJZ7f4-spt5Q9wl5HcQTv1t4XrX4eqhVbE5cCoIkFQnKPOc-jhVM44_eazLU6Xk-CCXP8C_UT5pX0luRS2cJrVFfHp2IR_AWxC-shItg6LNEmNFD4Zc-JLZcr0Q86Q" + + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when custom access denied handler configured then custom handler used`() { + this.spring.register(AccessDeniedHandlerConfig::class.java).autowire() + + this.client.get() + .uri("/") + .headers { it.setBearerAuth(validJwt) } + .exchange() + .expectStatus().isSeeOther + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AccessDeniedHandlerConfig { + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, hasAuthority("ADMIN")) + } + oauth2ResourceServer { + accessDeniedHandler = HttpStatusServerAccessDeniedHandler(HttpStatus.SEE_OTHER) + jwt { + publicKey = publicKey() + } + } + } + } + } + + @Test + fun `request when custom entry point configured then custom entry point used`() { + this.spring.register(AuthenticationEntryPointConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectStatus().isSeeOther + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthenticationEntryPointConfig { + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + authenticationEntryPoint = HttpStatusServerEntryPoint(HttpStatus.SEE_OTHER) + jwt { + publicKey = publicKey() + } + } + } + } + } + + @Test + fun `request when custom bearer token converter configured then custom converter used`() { + this.spring.register(BearerTokenConverterConfig::class.java).autowire() + + this.client.get() + .uri("/") + .headers { it.setBearerAuth(validJwt) } + .exchange() + + verify(BearerTokenConverterConfig.CONVERTER).convert(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class BearerTokenConverterConfig { + companion object { + val CONVERTER: ServerBearerTokenAuthenticationConverter = mock(ServerBearerTokenAuthenticationConverter::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + bearerTokenConverter = CONVERTER + jwt { + publicKey = publicKey() + } + } + } + } + } + + @Test + fun `request when custom authentication manager resolver configured then custom resolver used`() { + this.spring.register(AuthenticationManagerResolverConfig::class.java).autowire() + + this.client.get() + .uri("/") + .headers { it.setBearerAuth(validJwt) } + .exchange() + + verify(AuthenticationManagerResolverConfig.RESOLVER).resolve(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthenticationManagerResolverConfig { + companion object { + val RESOLVER: ReactiveAuthenticationManagerResolver = + mock(ReactiveAuthenticationManagerResolver::class.java) as ReactiveAuthenticationManagerResolver + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + authenticationManagerResolver = RESOLVER + } + } + } + } + + companion object { + private fun publicKey(): RSAPublicKey { + val modulus = "26323220897278656456354815752829448539647589990395639665273015355787577386000316054335559633864476469390247312823732994485311378484154955583861993455004584140858982659817218753831620205191028763754231454775026027780771426040997832758235764611119743390612035457533732596799927628476322029280486807310749948064176545712270582940917249337311592011920620009965129181413510845780806191965771671528886508636605814099711121026468495328702234901200169245493126030184941412539949521815665744267183140084667383643755535107759061065656273783542590997725982989978433493861515415520051342321336460543070448417126615154138673620797" + val exponent = "65537" + val spec = RSAPublicKeySpec(BigInteger(modulus), BigInteger(exponent)) + val factory = KeyFactory.getInstance("RSA") + return factory.generatePublic(spec) as RSAPublicKey + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDslTests.kt new file mode 100644 index 00000000000..d7807de6fc1 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerOpaqueTokenDslTests.kt @@ -0,0 +1,201 @@ +/* + * 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.config.web.server + +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.http.HttpHeaders +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.oauth2.server.resource.introspection.NimbusReactiveOpaqueTokenIntrospector +import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import javax.annotation.PreDestroy + +/** + * Tests for [ServerOpaqueTokenDsl] + * + * @author Eleftheria Stein + */ +class ServerOpaqueTokenDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `opaque token when using defaults then uses introspector bean`() { + this.spring.register(IntrospectorBeanConfig::class.java).autowire() + + IntrospectorBeanConfig.MOCK_WEB_SERVER.enqueue(MockResponse()) + + this.client.get() + .uri("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + .exchange() + + val recordedRequest = IntrospectorBeanConfig.MOCK_WEB_SERVER.takeRequest() + assertThat(recordedRequest.path).isEqualTo("/introspect") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class IntrospectorBeanConfig { + companion object { + var MOCK_WEB_SERVER: MockWebServer = MockWebServer() + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { } + } + } + } + + @Bean + open fun mockWebServer(): MockWebServer { + return MOCK_WEB_SERVER + } + + @PreDestroy + open fun shutdown() { + MOCK_WEB_SERVER.shutdown() + } + + @Bean + open fun tokenIntrospectionClient(): ReactiveOpaqueTokenIntrospector { + return NimbusReactiveOpaqueTokenIntrospector(mockWebServer().url("/introspect").toString(), "client", "secret") + } + } + + @Test + fun `opaque token when using custom introspector then introspector used`() { + this.spring.register(CustomIntrospectorConfig::class.java).autowire() + + CustomIntrospectorConfig.MOCK_WEB_SERVER.enqueue(MockResponse()) + + this.client.get() + .uri("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + .exchange() + + val recordedRequest = CustomIntrospectorConfig.MOCK_WEB_SERVER.takeRequest() + assertThat(recordedRequest.path).isEqualTo("/introspector") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomIntrospectorConfig { + companion object { + var MOCK_WEB_SERVER: MockWebServer = MockWebServer() + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspector = NimbusReactiveOpaqueTokenIntrospector(mockWebServer().url("/introspector").toString(), "client", "secret") + } + } + } + } + + @Bean + open fun mockWebServer(): MockWebServer { + return MOCK_WEB_SERVER + } + + @PreDestroy + open fun shutdown() { + MOCK_WEB_SERVER.shutdown() + } + } + + @Test + fun `opaque token when using custom introspection URI and credentials then custom used`() { + this.spring.register(CustomIntrospectionUriAndCredentialsConfig::class.java).autowire() + + CustomIntrospectionUriAndCredentialsConfig.MOCK_WEB_SERVER.enqueue(MockResponse()) + + this.client.get() + .uri("/") + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + .exchange() + + val recordedRequest = CustomIntrospectionUriAndCredentialsConfig.MOCK_WEB_SERVER.takeRequest() + assertThat(recordedRequest.path).isEqualTo("/introspection-uri") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomIntrospectionUriAndCredentialsConfig { + companion object { + var MOCK_WEB_SERVER: MockWebServer = MockWebServer() + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspectionUri = mockWebServer().url("/introspection-uri").toString() + introspectionClientCredentials("client", "secret") + } + } + } + } + + @Bean + open fun mockWebServer(): MockWebServer { + return MOCK_WEB_SERVER + } + + @PreDestroy + open fun shutdown() { + MOCK_WEB_SERVER.shutdown() + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerReferrerPolicyDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerReferrerPolicyDslTests.kt new file mode 100644 index 00000000000..47ac3271654 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerReferrerPolicyDslTests.kt @@ -0,0 +1,99 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +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.config.test.SpringTestRule +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.ReferrerPolicyServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerReferrerPolicyDsl] + * + * @author Eleftheria Stein + */ +class ServerReferrerPolicyDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when referrer policy configured then referrer policy header in response`() { + this.spring.register(ReferrerPolicyConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("Referrer-Policy", ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER.policy) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class ReferrerPolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + referrerPolicy { } + } + } + } + } + + @Test + fun `request when custom policy configured then custom policy in response header`() { + this.spring.register(CustomPolicyConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals("Referrer-Policy", ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.SAME_ORIGIN.policy) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class CustomPolicyConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + referrerPolicy { + policy = ReferrerPolicyServerHttpHeadersWriter.ReferrerPolicy.SAME_ORIGIN + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDslTests.kt new file mode 100644 index 00000000000..338d10ebab6 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerRequestCacheDslTests.kt @@ -0,0 +1,104 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +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.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.savedrequest.ServerRequestCache +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux +import reactor.core.publisher.Mono + +/** + * Tests for [ServerRequestCacheDsl] + * + * @author Eleftheria Stein + */ +class ServerRequestCacheDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `GET when request cache enabled then redirected to cached page`() { + this.spring.register(RequestCacheConfig::class.java, UserDetailsConfig::class.java).autowire() + `when`(RequestCacheConfig.REQUEST_CACHE.removeMatchingRequest(any())).thenReturn(Mono.empty()) + + this.client.get() + .uri("/test") + .exchange() + + verify(RequestCacheConfig.REQUEST_CACHE).saveRequest(any()) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class RequestCacheConfig { + companion object { + var REQUEST_CACHE: ServerRequestCache = Mockito.mock(ServerRequestCache::class.java) + } + + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { } + requestCache { + requestCache = REQUEST_CACHE + } + } + } + } + + @Configuration + open class UserDetailsConfig { + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerX509DslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerX509DslTests.kt new file mode 100644 index 00000000000..fa46c666da2 --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerX509DslTests.kt @@ -0,0 +1,237 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.io.ClassPathResource +import org.springframework.http.client.reactive.ClientHttpConnector +import org.springframework.http.server.reactive.ServerHttpRequestDecorator +import org.springframework.http.server.reactive.SslInfo +import org.springframework.lang.Nullable +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.config.test.SpringTestRule +import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager +import org.springframework.test.web.reactive.server.MockServerConfigurer +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.reactive.server.WebTestClientConfigurer +import org.springframework.test.web.reactive.server.expectBody +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.server.ServerWebExchange +import org.springframework.web.server.ServerWebExchangeDecorator +import org.springframework.web.server.WebFilter +import org.springframework.web.server.WebFilterChain +import org.springframework.web.server.adapter.WebHttpHandlerBuilder +import reactor.core.publisher.Mono +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate + +/** + * Tests for [ServerX509Dsl] + * + * @author Eleftheria Stein + */ +class ServerX509DslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `x509 when configured with defaults then user authenticated with expected username`() { + this.spring + .register(X509DefaultConfig::class.java, UserDetailsConfig::class.java, UsernameController::class.java) + .autowire() + val certificate = loadCert("rod.cer") + + this.client + .mutateWith(mockX509(certificate)) + .get() + .uri("/username") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("rod") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class X509DefaultConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + x509 { } + } + } + } + + @Test + fun `x509 when principal extractor customized then custom principal extractor used`() { + this.spring + .register(PrincipalExtractorConfig::class.java, UserDetailsConfig::class.java, UsernameController::class.java) + .autowire() + val certificate = loadCert("rodatexampledotcom.cer") + + this.client + .mutateWith(mockX509(certificate)) + .get() + .uri("/username") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("rod") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class PrincipalExtractorConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + val customPrincipalExtractor = SubjectDnX509PrincipalExtractor() + customPrincipalExtractor.setSubjectDnRegex("CN=(.*?)@example.com(?:,|$)") + return http { + x509 { + principalExtractor = customPrincipalExtractor + } + } + } + } + + @Test + fun `x509 when authentication manager customized then custom authentication manager used`() { + this.spring + .register(AuthenticationManagerConfig::class.java, UsernameController::class.java) + .autowire() + val certificate = loadCert("rod.cer") + + this.client + .mutateWith(mockX509(certificate)) + .get() + .uri("/username") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("rod") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class AuthenticationManagerConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + x509 { + authenticationManager = ReactivePreAuthenticatedAuthenticationManager(userDetailsService()) + } + } + } + + fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } + + @RestController + class UsernameController { + @GetMapping("/username") + fun principal(@AuthenticationPrincipal user: User?): String { + return user!!.username + } + } + + @Configuration + open class UserDetailsConfig { + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("rod") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } + } + + private fun mockX509(certificate: X509Certificate): X509Mutator { + return X509Mutator(certificate) + } + + private class X509Mutator internal constructor(private var certificate: X509Certificate) : WebTestClientConfigurer, MockServerConfigurer { + + override fun afterConfigurerAdded(builder: WebTestClient.Builder, + @Nullable httpHandlerBuilder: WebHttpHandlerBuilder?, + @Nullable connector: ClientHttpConnector?) { + val filter = SetSslInfoWebFilter(certificate) + httpHandlerBuilder!!.filters { filters: MutableList -> filters.add(0, filter) } + } + } + + private class SetSslInfoWebFilter(var certificate: X509Certificate) : WebFilter { + + override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono { + return chain.filter(decorate(exchange)) + } + + private fun decorate(exchange: ServerWebExchange): ServerWebExchange { + val decorated: ServerHttpRequestDecorator = object : ServerHttpRequestDecorator(exchange.request) { + override fun getSslInfo(): SslInfo { + val sslInfo = mock(SslInfo::class.java) + `when`(sslInfo.sessionId).thenReturn("sessionId") + `when`(sslInfo.peerCertificates).thenReturn(arrayOf(certificate)) + return sslInfo + } + } + return object : ServerWebExchangeDecorator(exchange) { + override fun getRequest(): org.springframework.http.server.reactive.ServerHttpRequest { + return decorated + } + } + } + } + + private fun loadCert(location: String): T { + ClassPathResource(location).inputStream.use { inputStream -> + val certFactory = CertificateFactory.getInstance("X.509") + return certFactory.generateCertificate(inputStream) as T + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDslTests.kt new file mode 100644 index 00000000000..87a3bdd993a --- /dev/null +++ b/config/src/test/kotlin/org/springframework/security/config/web/server/ServerXssProtectionDslTests.kt @@ -0,0 +1,98 @@ +/* + * 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.config.web.server + +import org.junit.Rule +import org.junit.Test +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.config.test.SpringTestRule +import org.springframework.security.web.server.SecurityWebFilterChain +import org.springframework.security.web.server.header.XXssProtectionServerHttpHeadersWriter +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.reactive.config.EnableWebFlux + +/** + * Tests for [ServerXssProtectionDsl] + * + * @author Eleftheria Stein + */ +class ServerXssProtectionDslTests { + @Rule + @JvmField + val spring = SpringTestRule() + + private lateinit var client: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + this.client = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `request when xss protection configured then xss header in response`() { + this.spring.register(XssConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().valueEquals(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION, "1 ; mode=block") + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class XssConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + xssProtection { } + } + } + } + } + + @Test + fun `request when xss protection disabled then no xss header in response`() { + this.spring.register(XssDisabledConfig::class.java).autowire() + + this.client.get() + .uri("/") + .exchange() + .expectHeader().doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION) + } + + @EnableWebFluxSecurity + @EnableWebFlux + open class XssDisabledConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + headers { + xssProtection { + disable() + } + } + } + } + } +} diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt index c4508ba8b34..72c5ff57be9 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/AuthorizeRequestsDslTests.kt @@ -20,6 +20,8 @@ import org.junit.Rule import org.junit.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod 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 @@ -27,10 +29,13 @@ import org.springframework.security.config.test.SpringTestRule import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic import org.springframework.security.web.util.matcher.RegexRequestMatcher import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post +import org.springframework.test.web.servlet.put import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.bind.annotation.GetMapping @@ -38,6 +43,8 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.servlet.config.annotation.EnableWebMvc +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer /** * Tests for [AuthorizeRequestsDsl] @@ -72,12 +79,29 @@ class AuthorizeRequestsDslTests { } } + @Test + fun `request when allowed by regex matcher with http method then responds based on method`() { + this.spring.register(AuthorizeRequestsByRegexConfig::class.java).autowire() + + this.mockMvc.post("/onlyPostPermitted") { with(csrf()) } + .andExpect { + status { isOk } + } + + this.mockMvc.get("/onlyPostPermitted") + .andExpect { + status { isForbidden } + } + } + @EnableWebSecurity open class AuthorizeRequestsByRegexConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http { authorizeRequests { authorize(RegexRequestMatcher("/path", null), permitAll) + authorize(RegexRequestMatcher("/onlyPostPermitted", "POST"), permitAll) + authorize(RegexRequestMatcher("/onlyPostPermitted", "GET"), denyAll) authorize(RegexRequestMatcher(".*", null), authenticated) } } @@ -88,6 +112,10 @@ class AuthorizeRequestsDslTests { @RequestMapping("/path") fun path() { } + + @RequestMapping("/onlyPostPermitted") + fun onlyPostPermitted() { + } } } @@ -103,7 +131,7 @@ class AuthorizeRequestsDslTests { @Test fun `request when allowed by mvc then responds with OK`() { - this.spring.register(AuthorizeRequestsByMvcConfig::class.java).autowire() + this.spring.register(AuthorizeRequestsByMvcConfig::class.java, LegacyMvcMatchingConfig::class.java).autowire() this.mockMvc.get("/path") .andExpect { @@ -141,6 +169,13 @@ class AuthorizeRequestsDslTests { } } + @Configuration + open class LegacyMvcMatchingConfig : WebMvcConfigurer { + override fun configurePathMatch(configurer: PathMatchConfigurer) { + configurer.setUseSuffixPatternMatch(true) + } + } + @Test fun `request when secured by mvc path variables then responds based on path variable value`() { this.spring.register(MvcMatcherPathVariablesConfig::class.java).autowire() @@ -271,4 +306,91 @@ class AuthorizeRequestsDslTests { } } } + + @EnableWebSecurity + @EnableWebMvc + open class AuthorizeRequestsByMvcConfigWithHttpMethod : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(HttpMethod.GET, "/path", permitAll) + authorize(HttpMethod.PUT, "/path", denyAll) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } + + @Test + fun `request when secured by mvc with http method then responds based on http method`() { + this.spring.register(AuthorizeRequestsByMvcConfigWithHttpMethod::class.java).autowire() + + this.mockMvc.get("/path") + .andExpect { + status { isOk } + } + + this.mockMvc.put("/path") { with(csrf()) } + .andExpect { + status { isForbidden } + } + } + + @EnableWebSecurity + @EnableWebMvc + open class MvcMatcherServletPathHttpMethodConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(HttpMethod.GET, "/path", "/spring", denyAll) + authorize(HttpMethod.PUT, "/path", "/spring", denyAll) + } + } + } + + @RestController + internal class PathController { + @RequestMapping("/path") + fun path() { + } + } + } + + + + @Test + fun `request when secured by mvc with servlet path and http method then responds based on path and method`() { + this.spring.register(MvcMatcherServletPathConfig::class.java).autowire() + + this.mockMvc.perform(MockMvcRequestBuilders.get("/spring/path") + .with { request -> + request.apply { + servletPath = "/spring" + } + }) + .andExpect(status().isForbidden) + + this.mockMvc.perform(MockMvcRequestBuilders.put("/spring/path") + .with { request -> + request.apply { + servletPath = "/spring" + csrf() + } + }) + .andExpect(status().isForbidden) + + this.mockMvc.perform(MockMvcRequestBuilders.get("/other/path") + .with { request -> + request.apply { + servletPath = "/other" + } + }) + .andExpect(status().isOk) + } } 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 7824a7fafeb..b535e36b357 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 @@ -91,4 +91,31 @@ class HeadersDslTests { } } } + + @Test + fun `request when headers disabled then no security headers are in the response`() { + this.spring.register(HeadersDisabledConfig::class.java).autowire() + + this.mockMvc.get("/") + .andExpect { + header { doesNotExist(ContentTypeOptionsServerHttpHeadersWriter.X_CONTENT_OPTIONS) } + header { doesNotExist(XFrameOptionsServerHttpHeadersWriter.X_FRAME_OPTIONS) } + header { doesNotExist(StrictTransportSecurityServerHttpHeadersWriter.STRICT_TRANSPORT_SECURITY) } + header { doesNotExist(HttpHeaders.CACHE_CONTROL) } + header { doesNotExist(HttpHeaders.EXPIRES) } + header { doesNotExist(HttpHeaders.PRAGMA) } + header { doesNotExist(XXssProtectionServerHttpHeadersWriter.X_XSS_PROTECTION) } + } + } + + @EnableWebSecurity + open class HeadersDisabledConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + headers { + disable() + } + } + } + } } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt index 7dd3de75212..f603dff3a5b 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/HttpSecurityDslTests.kt @@ -225,7 +225,7 @@ class HttpSecurityDslTests { val filters: List = filterChain.getFilters("/") assertThat(filters).hasSize(1) - assertThat(filters[0]).isExactlyInstanceOf(CustomFilterConfig.CustomFilter::class.java) + assertThat(filters[0]).isExactlyInstanceOf(CustomFilter::class.java) } @EnableWebSecurity @@ -236,7 +236,124 @@ class HttpSecurityDslTests { addFilterAt(CustomFilter(), UsernamePasswordAuthenticationFilter::class.java) } } + } + + @Test + fun `HTTP security when custom filter configured with reified variant then custom filter added to filter chain`() { + this.spring.register(CustomFilterConfigReified::class.java).autowire() + + val filterChain = spring.context.getBean(FilterChainProxy::class.java) + val filters: List = filterChain.getFilters("/") + + assertThat(filters).hasSize(1) + assertThat(filters[0]).isExactlyInstanceOf(CustomFilter::class.java) + } + + @EnableWebSecurity + @EnableWebMvc + open class CustomFilterConfigReified : WebSecurityConfigurerAdapter(true) { + override fun configure(http: HttpSecurity) { + http { + addFilterAt(CustomFilter()) + } + } + } + + @Test + fun `HTTP security when custom filter configured then custom filter added after specific filter to filter chain`() { + this.spring.register(CustomFilterAfterConfig::class.java).autowire() + + val filterChain = spring.context.getBean(FilterChainProxy::class.java) + val filters: List> = filterChain.getFilters("/").map { it.javaClass } + + assertThat(filters).containsSubsequence( + UsernamePasswordAuthenticationFilter::class.java, + CustomFilter::class.java + ) + } + + @EnableWebSecurity + @EnableWebMvc + open class CustomFilterAfterConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + addFilterAfter(CustomFilter(), UsernamePasswordAuthenticationFilter::class.java) + formLogin {} + } + } + } + + @Test + fun `HTTP security when custom filter configured with reified variant then custom filter added after specific filter to filter chain`() { + this.spring.register(CustomFilterAfterConfigReified::class.java).autowire() + + val filterChain = spring.context.getBean(FilterChainProxy::class.java) + val filterClasses: List> = filterChain.getFilters("/").map { it.javaClass } + + assertThat(filterClasses).containsSubsequence( + UsernamePasswordAuthenticationFilter::class.java, + CustomFilter::class.java + ) + } + + @EnableWebSecurity + @EnableWebMvc + open class CustomFilterAfterConfigReified : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + addFilterAfter(CustomFilter()) + formLogin { } + } + } + } + + @Test + fun `HTTP security when custom filter configured then custom filter added before specific filter to filter chain`() { + this.spring.register(CustomFilterBeforeConfig::class.java).autowire() + + val filterChain = spring.context.getBean(FilterChainProxy::class.java) + val filters: List> = filterChain.getFilters("/").map { it.javaClass } + + assertThat(filters).containsSubsequence( + CustomFilter::class.java, + UsernamePasswordAuthenticationFilter::class.java + ) + } + + @EnableWebSecurity + @EnableWebMvc + open class CustomFilterBeforeConfig : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + addFilterBefore(CustomFilter(), UsernamePasswordAuthenticationFilter::class.java) + formLogin {} + } + } + } + + @Test + fun `HTTP security when custom filter configured with reified variant then custom filter added before specific filter to filter chain`() { + this.spring.register(CustomFilterBeforeConfigReified::class.java).autowire() + + val filterChain = spring.context.getBean(FilterChainProxy::class.java) + val filterClasses: List> = filterChain.getFilters("/").map { it.javaClass } - class CustomFilter : UsernamePasswordAuthenticationFilter() + assertThat(filterClasses).containsSubsequence( + CustomFilter::class.java, + UsernamePasswordAuthenticationFilter::class.java + ) } + + @EnableWebSecurity + @EnableWebMvc + open class CustomFilterBeforeConfigReified : WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + addFilterBefore(CustomFilter()) + formLogin { } + } + } + } + + class CustomFilter : UsernamePasswordAuthenticationFilter() } diff --git a/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDslTest.kt b/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDslTests.kt similarity index 99% rename from config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDslTest.kt rename to config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDslTests.kt index 8624d240095..e8709817174 100644 --- a/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDslTest.kt +++ b/config/src/test/kotlin/org/springframework/security/config/web/servlet/session/SessionFixationDslTests.kt @@ -40,7 +40,7 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders * * @author Eleftheria Stein */ -class SessionFixationDslTest { +class SessionFixationDslTests { @Rule @JvmField var spring = SpringTestRule() diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml index 6e8e79b504d..9edc7c8efa2 100644 --- a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchers.xml @@ -32,7 +32,9 @@ - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml index ad9f5d9ad33..c8a4cc42e99 100644 --- a/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml +++ b/config/src/test/resources/org/springframework/security/config/http/InterceptUrlConfigTests-MvcMatchersServletPath.xml @@ -32,7 +32,9 @@ - + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestRejectedHandler.xml b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestRejectedHandler.xml new file mode 100644 index 00000000000..be62e9a47c5 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/MiscHttpConfigTests-RequestRejectedHandler.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml new file mode 100644 index 00000000000..bf596623672 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml new file mode 100644 index 00000000000..dd0afc93510 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-AuthorizedClientArgumentResolver.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java index 8958f6089cb..26bdbf154a2 100644 --- a/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java +++ b/core/src/main/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -19,7 +19,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.*; import org.aopalliance.intercept.MethodInvocation; @@ -87,10 +89,10 @@ protected MethodSecurityExpressionOperations createSecurityExpressionRoot( } /** - * Filters the {@code filterTarget} object (which must be either a collection, array, + * Filters the {@code filterTarget} object (which must be either a collection, array, map * or stream), by evaluating the supplied expression. *

- * If a {@code Collection} is used, the original instance will be modified to contain + * If a {@code Collection} or {@code Map} is used, the original instance will be modified to contain * the elements for which the permission expression evaluates to {@code true}. For an * array, a new array instance will be returned. */ @@ -173,6 +175,32 @@ public Object filter(Object filterTarget, Expression filterExpression, return filtered; } + if (filterTarget instanceof Map) { + final Map map = (Map) filterTarget; + final Map retainMap = new LinkedHashMap(map.size()); + + if (debug) { + logger.debug("Filtering map with " + map.size() + " elements"); + } + + for (Map.Entry filterObject : map.entrySet()) { + rootObject.setFilterObject(filterObject); + + if (ExpressionUtils.evaluateAsBoolean(filterExpression, ctx)) { + retainMap.put(filterObject.getKey(), filterObject.getValue()); + } + } + + if (debug) { + logger.debug("Retaining elements: " + retainMap); + } + + map.clear(); + map.putAll(retainMap); + + return filterTarget; + } + if (filterTarget instanceof Stream) { final Stream original = (Stream) filterTarget; @@ -184,7 +212,7 @@ public Object filter(Object filterTarget, Expression filterExpression, } throw new IllegalArgumentException( - "Filter target must be a collection, array, or stream type, but was " + "Filter target must be a collection, array, map or stream type, but was " + filterTarget); } diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java index d32f7e6e20c..cbc6e3b71bf 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java @@ -55,7 +55,7 @@ public abstract class AbstractUserDetailsReactiveAuthenticationManager implement private ReactiveUserDetailsPasswordService userDetailsPasswordService; - private Scheduler scheduler = Schedulers.newParallel("password-encoder", Schedulers.DEFAULT_POOL_SIZE, true); + private Scheduler scheduler = Schedulers.boundedElastic(); private UserDetailsChecker preAuthenticationChecks = user -> { if (!user.isAccountNonLocked()) { diff --git a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java index 418e4850812..959309c95e8 100644 --- a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java @@ -30,6 +30,7 @@ import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Iterates an {@link Authentication} request through a list of @@ -145,7 +146,7 @@ private void checkState() { throw new IllegalArgumentException( "A parent AuthenticationManager or a list " + "of AuthenticationProviders is required"); - } else if (providers.contains(null)) { + } else if (CollectionUtils.contains(providers.iterator(), null)) { throw new IllegalArgumentException( "providers list cannot contain null values"); } @@ -237,7 +238,7 @@ public Authentication authenticate(Authentication authentication) ((CredentialsContainer) result).eraseCredentials(); } - // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent + // If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); @@ -254,7 +255,7 @@ public Authentication authenticate(Authentication authentication) "No AuthenticationProvider found for {0}")); } - // If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent + // If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent // This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it if (parentException == null) { prepareException(lastException, authentication); 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 f10fefa8fc4..011ee95b1a7 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java @@ -90,7 +90,7 @@ public static void enableDefaultTyping(ObjectMapper mapper) { if (mapper != null) { TypeResolverBuilder typeBuilder = mapper.getDeserializationConfig().getDefaultTyper(null); if (typeBuilder == null) { - mapper.setDefaultTyping(createWhitelistedDefaultTyping()); + mapper.setDefaultTyping(createAllowlistedDefaultTyping()); } } } @@ -148,11 +148,11 @@ private static void addToModulesList(ClassLoader loader, List modules, S } /** - * Creates a TypeResolverBuilder that performs whitelisting. - * @return a TypeResolverBuilder that performs whitelisting. + * Creates a TypeResolverBuilder that restricts allowed types. + * @return a TypeResolverBuilder that restricts allowed types. */ - private static TypeResolverBuilder createWhitelistedDefaultTyping() { - TypeResolverBuilder result = new WhitelistTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL); + private static TypeResolverBuilder createAllowlistedDefaultTyping() { + TypeResolverBuilder result = new AllowlistTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL); result = result.init(JsonTypeInfo.Id.CLASS, null); result = result.inclusion(JsonTypeInfo.As.PROPERTY); return result; @@ -164,9 +164,9 @@ private static TypeResolverBuilder createWhitelis * and overrides the {@code TypeIdResolver} * @author Rob Winch */ - static class WhitelistTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder { + static class AllowlistTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder { - WhitelistTypeResolverBuilder(ObjectMapper.DefaultTyping defaultTyping) { + AllowlistTypeResolverBuilder(ObjectMapper.DefaultTyping defaultTyping) { super( defaultTyping, //we do explicit validation in the TypeIdResolver @@ -182,17 +182,17 @@ protected TypeIdResolver idResolver(MapperConfig config, PolymorphicTypeValidator subtypeValidator, Collection subtypes, boolean forSer, boolean forDeser) { TypeIdResolver result = super.idResolver(config, baseType, subtypeValidator, subtypes, forSer, forDeser); - return new WhitelistTypeIdResolver(result); + return new AllowlistTypeIdResolver(result); } } /** * A {@link TypeIdResolver} that delegates to an existing implementation and throws an IllegalStateException if the - * class being looked up is not whitelisted, does not provide an explicit mixin, and is not annotated with Jackson + * class being looked up is not in the allowlist, does not provide an explicit mixin, and is not annotated with Jackson * mappings. See https://github.com/spring-projects/spring-security/issues/4370 */ - static class WhitelistTypeIdResolver implements TypeIdResolver { - private static final Set WHITELIST_CLASS_NAMES = Collections.unmodifiableSet(new HashSet(Arrays.asList( + static class AllowlistTypeIdResolver implements TypeIdResolver { + private static final Set ALLOWLIST_CLASS_NAMES = Collections.unmodifiableSet(new HashSet(Arrays.asList( "java.util.ArrayList", "java.util.Collections$EmptyList", "java.util.Collections$EmptyMap", @@ -209,7 +209,7 @@ static class WhitelistTypeIdResolver implements TypeIdResolver { private final TypeIdResolver delegate; - WhitelistTypeIdResolver(TypeIdResolver delegate) { + AllowlistTypeIdResolver(TypeIdResolver delegate) { this.delegate = delegate; } @@ -238,7 +238,7 @@ public JavaType typeFromId(DatabindContext context, String id) throws IOExceptio DeserializationConfig config = (DeserializationConfig) context.getConfig(); JavaType result = delegate.typeFromId(context, id); String className = result.getRawClass().getName(); - if (isWhitelisted(className)) { + if (isInAllowlist(className)) { return result; } boolean isExplicitMixin = config.findMixInClassFor(result.getRawClass()) != null; @@ -249,14 +249,14 @@ public JavaType typeFromId(DatabindContext context, String id) throws IOExceptio if (jacksonAnnotation != null) { return result; } - throw new IllegalArgumentException("The class with " + id + " and name of " + className + " is not whitelisted. " + + throw new IllegalArgumentException("The class with " + id + " and name of " + className + " is not in the allowlist. " + "If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. " + "If the serialization is only done by a trusted source, you can also enable default typing. " + "See https://github.com/spring-projects/spring-security/issues/4370 for details"); } - private boolean isWhitelisted(String id) { - return WHITELIST_CLASS_NAMES.contains(id); + private boolean isInAllowlist(String id) { + return ALLOWLIST_CLASS_NAMES.contains(id); } @Override diff --git a/core/src/main/resources/org/springframework/security/messages.properties b/core/src/main/resources/org/springframework/security/messages.properties index 665a742002e..a84f259753f 100644 --- a/core/src/main/resources/org/springframework/security/messages.properties +++ b/core/src/main/resources/org/springframework/security/messages.properties @@ -31,6 +31,7 @@ DigestAuthenticationFilter.usernameNotFound=Username {0} not found JdbcDaoImpl.noAuthority=User {0} has no GrantedAuthority JdbcDaoImpl.notFound=User {0} not found LdapAuthenticationProvider.badCredentials=Bad credentials +LdapAuthenticationProvider.badLdapConnection=Connection to LDAP server failed LdapAuthenticationProvider.credentialsExpired=User credentials have expired LdapAuthenticationProvider.disabled=User is disabled LdapAuthenticationProvider.expired=User account has expired diff --git a/core/src/main/resources/org/springframework/security/messages_zh_TW.properties b/core/src/main/resources/org/springframework/security/messages_zh_TW.properties index 9050abcc696..7f8f871f1cf 100644 --- a/core/src/main/resources/org/springframework/security/messages_zh_TW.properties +++ b/core/src/main/resources/org/springframework/security/messages_zh_TW.properties @@ -31,6 +31,7 @@ DigestAuthenticationFilter.usernameNotFound=\u627E\u4E0D\u5230\u4F7F\u7528\u8005 JdbcDaoImpl.noAuthority=\u4F7F\u7528\u8005 {0} \u6C92\u6709 GrantedAuthority JdbcDaoImpl.notFound=\u627E\u4E0D\u5230\u4F7F\u7528\u8005 {0} LdapAuthenticationProvider.badCredentials=\u6191\u8B49\u932F\u8AA4 +LdapAuthenticationProvider.badLdapConnection=\u7121\u6CD5\u9023\u7DDA\u5230 LDAP \u4F3A\u670D\u5668 LdapAuthenticationProvider.credentialsExpired=\u4F7F\u7528\u8005\u6191\u8B49\u5DF2\u904E\u671F LdapAuthenticationProvider.disabled=\u4F7F\u7528\u8005\u5DF2\u88AB\u505C\u7528 LdapAuthenticationProvider.expired=\u4F7F\u7528\u8005\u5E33\u865F\u5DF2\u904E\u671F diff --git a/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java b/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java index 4fca5a18976..85dd94acd48 100644 --- a/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java +++ b/core/src/test/java/org/springframework/security/access/expression/method/DefaultMethodSecurityExpressionHandlerTests.java @@ -15,7 +15,9 @@ */ package org.springframework.security.access.expression.method; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -78,6 +80,72 @@ public void createEvaluationContextCustomTrustResolver() { verify(trustResolver).isAnonymous(authentication); } + @Test + @SuppressWarnings("unchecked") + public void filterByKeyWhenUsingMapThenFiltersMap() { + final Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key2", "value2"); + map.put("key3", "value3"); + + Expression expression = handler.getExpressionParser().parseExpression("filterObject.key eq 'key2'"); + + EvaluationContext context = handler.createEvaluationContext(authentication, + methodInvocation); + + Object filtered = handler.filter(map, expression, context); + + assertThat(filtered == map); + Map result = ((Map) filtered); + assertThat(result.size() == 1); + assertThat(result).containsKey("key2"); + assertThat(result).containsValue("value2"); + } + + @Test + @SuppressWarnings("unchecked") + public void filterByValueWhenUsingMapThenFiltersMap() { + final Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key2", "value2"); + map.put("key3", "value3"); + + Expression expression = handler.getExpressionParser().parseExpression("filterObject.value eq 'value3'"); + + EvaluationContext context = handler.createEvaluationContext(authentication, + methodInvocation); + + Object filtered = handler.filter(map, expression, context); + + assertThat(filtered == map); + Map result = ((Map) filtered); + assertThat(result.size() == 1); + assertThat(result).containsKey("key3"); + assertThat(result).containsValue("value3"); + } + + @Test + @SuppressWarnings("unchecked") + public void filterByKeyAndValueWhenUsingMapThenFiltersMap() { + final Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key2", "value2"); + map.put("key3", "value3"); + + Expression expression = handler.getExpressionParser().parseExpression("(filterObject.key eq 'key1') or (filterObject.value eq 'value2')"); + + EvaluationContext context = handler.createEvaluationContext(authentication, + methodInvocation); + + Object filtered = handler.filter(map, expression, context); + + assertThat(filtered == map); + Map result = ((Map) filtered); + assertThat(result.size() == 2); + assertThat(result).containsKeys("key1", "key2"); + assertThat(result).containsValues("value1", "value2"); + } + @Test @SuppressWarnings("unchecked") public void filterWhenUsingStreamThenFiltersStream() { diff --git a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java index 8b9249b8181..a28c1c4e414 100644 --- a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java @@ -16,19 +16,27 @@ package org.springframework.security.authentication; -import org.junit.Test; -import org.springframework.context.MessageSource; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import org.junit.Test; + +import org.springframework.context.MessageSource; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; /** * Tests {@link ProviderManager}. @@ -102,6 +110,21 @@ public void testStartupFailsIfProvidersNotSetAsVarargs() { new ProviderManager((AuthenticationProvider) null); } + @Test(expected = IllegalArgumentException.class) + public void testStartupFailsIfProvidersContainNullElement() { + new ProviderManager(Arrays.asList(mock(AuthenticationProvider.class), null)); + } + + // gh-8689 + @Test + public void constructorWhenUsingListOfThenNoException() { + List providers = spy(ArrayList.class); + // List.of(null) in JDK 9 throws a NullPointerException + when(providers.contains(eq(null))).thenThrow(NullPointerException.class); + providers.add(mock(AuthenticationProvider.class)); + new ProviderManager(providers); + } + @Test public void detailsAreNotSetOnAuthenticationTokenIfAlreadySetByProvider() { Object requestDetails = "(Request Details)"; diff --git a/core/src/test/java/org/springframework/security/jackson2/SecurityJackson2ModulesTests.java b/core/src/test/java/org/springframework/security/jackson2/SecurityJackson2ModulesTests.java index da7aac86f0e..e28aaf0ffd8 100644 --- a/core/src/test/java/org/springframework/security/jackson2/SecurityJackson2ModulesTests.java +++ b/core/src/test/java/org/springframework/security/jackson2/SecurityJackson2ModulesTests.java @@ -44,20 +44,20 @@ public void setup() { } @Test - public void readValueWhenNotWhitelistedOrMappedThenThrowsException() { - String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelisted\",\"property\":\"bar\"}"; + public void readValueWhenNotAllowedOrMappedThenThrowsException() { + String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotAllowlisted\",\"property\":\"bar\"}"; assertThatThrownBy(() -> { mapper.readValue(content, Object.class); } - ).hasStackTraceContaining("whitelisted"); + ).hasStackTraceContaining("allowlist"); } @Test public void readValueWhenExplicitDefaultTypingAfterSecuritySetupThenReadsAsSpecificType() throws Exception { mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); - String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelisted\",\"property\":\"bar\"}"; + String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotAllowlisted\",\"property\":\"bar\"}"; - assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotWhitelisted.class); + assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotAllowlisted.class); } @Test @@ -65,29 +65,29 @@ public void readValueWhenExplicitDefaultTypingBeforeSecuritySetupThenReadsAsSpec mapper = new ObjectMapper(); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); SecurityJackson2Modules.enableDefaultTyping(mapper); - String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelisted\",\"property\":\"bar\"}"; + String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotAllowlisted\",\"property\":\"bar\"}"; - assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotWhitelisted.class); + assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotAllowlisted.class); } @Test public void readValueWhenAnnotatedThenReadsAsSpecificType() throws Exception { - String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelistedButAnnotated\",\"property\":\"bar\"}"; + String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotAllowlistedButAnnotated\",\"property\":\"bar\"}"; - assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotWhitelistedButAnnotated.class); + assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotAllowlistedButAnnotated.class); } @Test public void readValueWhenMixinProvidedThenReadsAsSpecificType() throws Exception { - mapper.addMixIn(NotWhitelisted.class, NotWhitelistedMixin.class); - String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotWhitelisted\",\"property\":\"bar\"}"; + mapper.addMixIn(NotAllowlisted.class, NotAllowlistedMixin.class); + String content = "{\"@class\":\"org.springframework.security.jackson2.SecurityJackson2ModulesTests$NotAllowlisted\",\"property\":\"bar\"}"; - assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotWhitelisted.class); + assertThat(mapper.readValue(content, Object.class)).isInstanceOf(NotAllowlisted.class); } @Test public void readValueWhenHashMapThenReadsAsSpecificType() throws Exception { - mapper.addMixIn(NotWhitelisted.class, NotWhitelistedMixin.class); + mapper.addMixIn(NotAllowlisted.class, NotAllowlistedMixin.class); String content = "{\"@class\":\"java.util.HashMap\"}"; assertThat(mapper.readValue(content, Object.class)).isInstanceOf(HashMap.class); @@ -99,7 +99,7 @@ public void readValueWhenHashMapThenReadsAsSpecificType() throws Exception { public @interface NotJacksonAnnotation {} @NotJacksonAnnotation - static class NotWhitelisted { + static class NotAllowlisted { private String property = "bar"; public String getProperty() { @@ -111,7 +111,7 @@ public void setProperty(String property) { } @JsonIgnoreType(false) - static class NotWhitelistedButAnnotated { + static class NotAllowlistedButAnnotated { private String property = "bar"; public String getProperty() { @@ -126,7 +126,7 @@ public void setProperty(String property) { @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE) @JsonIgnoreProperties(ignoreUnknown = true) - abstract class NotWhitelistedMixin { + abstract class NotAllowlistedMixin { } } diff --git a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java index c59246320d6..dd787a9ea42 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java @@ -99,6 +99,10 @@ public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom r } public String encode(CharSequence rawPassword) { + if (rawPassword == null) { + throw new IllegalArgumentException("rawPassword cannot be null"); + } + String salt; if (random != null) { salt = BCrypt.gensalt(version.getVersion(), strength, random); @@ -109,6 +113,10 @@ public String encode(CharSequence rawPassword) { } public boolean matches(CharSequence rawPassword, String encodedPassword) { + if (rawPassword == null) { + throw new IllegalArgumentException("rawPassword cannot be null"); + } + if (encodedPassword == null || encodedPassword.length() == 0) { logger.warn("Empty encoded password"); return false; diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java index 2d0008e0a13..95c7b4067fe 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java +++ b/crypto/src/main/java/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java @@ -36,7 +36,7 @@ import org.springframework.security.crypto.keygen.KeyGenerators; /** - * Encryptor that uses 256-bit AES encryption. + * Encryptor that uses AES encryption. * * @author Keith Donald * @author Dave Syer @@ -99,9 +99,19 @@ public AesBytesEncryptor(String password, CharSequence salt, public AesBytesEncryptor(String password, CharSequence salt, BytesKeyGenerator ivGenerator, CipherAlgorithm alg) { - PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), Hex.decode(salt), - 1024, 256); - SecretKey secretKey = newSecretKey("PBKDF2WithHmacSHA1", keySpec); + this(newSecretKey("PBKDF2WithHmacSHA1", new PBEKeySpec(password.toCharArray(), Hex.decode(salt), + 1024, 256)), ivGenerator, alg); + } + + /** + * Constructs an encryptor that uses AES encryption. + * + * @param secretKey the secret (symmetric) key + * @param ivGenerator the generator used to generate the initialization vector. If null, + * then a default algorithm will be used based on the provided {@link CipherAlgorithm} + * @param alg the {@link CipherAlgorithm} to be used + */ + public AesBytesEncryptor(SecretKey secretKey, BytesKeyGenerator ivGenerator, CipherAlgorithm alg) { this.secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES"); this.alg = alg; this.encryptor = alg.createCipher(); diff --git a/crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java b/crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java index 7e0c5e8295e..7ebfb5a356d 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java +++ b/crypto/src/main/java/org/springframework/security/crypto/encrypt/Encryptors.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-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. @@ -32,16 +32,13 @@ public class Encryptors { * (Password-Based Key Derivation Function #2). Salts the password to prevent * dictionary attacks against the key. The provided salt is expected to be * hex-encoded; it should be random and at least 8 bytes in length. Also applies a - * random 16 byte initialization vector to ensure each encrypted message will be + * random 16-byte initialization vector to ensure each encrypted message will be * unique. Requires Java 6. * * @param password the password used to generate the encryptor's secret key; should * not be shared * @param salt a hex-encoded, random, site-global salt value to use to generate the * key - * - * @see #standard(CharSequence, CharSequence) which uses the slightly weaker CBC mode - * (instead of GCM) */ public static BytesEncryptor stronger(CharSequence password, CharSequence salt) { return new AesBytesEncryptor(password.toString(), salt, @@ -53,13 +50,21 @@ public static BytesEncryptor stronger(CharSequence password, CharSequence salt) * Derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation * Function #2). Salts the password to prevent dictionary attacks against the key. The * provided salt is expected to be hex-encoded; it should be random and at least 8 - * bytes in length. Also applies a random 16 byte initialization vector to ensure each + * bytes in length. Also applies a random 16-byte initialization vector to ensure each * encrypted message will be unique. Requires Java 6. + * NOTE: This mode is not + * authenticated + * and does not provide any guarantees about the authenticity of the data. + * For a more secure alternative, users should prefer + * {@link #stronger(CharSequence, CharSequence)}. * * @param password the password used to generate the encryptor's secret key; should * not be shared * @param salt a hex-encoded, random, site-global salt value to use to generate the * key + * + * @see #stronger(CharSequence, CharSequence), which uses the significatly more secure + * GCM (instead of CBC) */ public static BytesEncryptor standard(CharSequence password, CharSequence salt) { return new AesBytesEncryptor(password.toString(), salt, @@ -100,7 +105,10 @@ public static TextEncryptor text(CharSequence password, CharSequence salt) { * not be shared * @param salt a hex-encoded, random, site-global salt value to use to generate the * secret key + * @deprecated This encryptor is not secure. Instead, look to your data store for a + * mechanism to query encrypted data. */ + @Deprecated public static TextEncryptor queryableText(CharSequence password, CharSequence salt) { return new HexEncodingTextEncryptor(new AesBytesEncryptor(password.toString(), salt)); diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java index a0110e86124..7fe06a979de 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/Md4PasswordEncoder.java @@ -83,8 +83,6 @@ public class Md4PasswordEncoder implements PasswordEncoder { private StringKeyGenerator saltGenerator = new Base64StringKeyGenerator(); private boolean encodeHashAsBase64; - private Digester digester; - public void setEncodeHashAsBase64(boolean encodeHashAsBase64) { this.encodeHashAsBase64 = encodeHashAsBase64; diff --git a/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java b/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java index 907eb7c718e..3c28e167419 100644 --- a/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java +++ b/crypto/src/main/java/org/springframework/security/crypto/password/NoOpPasswordEncoder.java @@ -26,7 +26,8 @@ * @deprecated This PasswordEncoder is not secure. Instead use an * adaptive one way function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or * SCryptPasswordEncoder. Even better use {@link DelegatingPasswordEncoder} which supports - * password upgrades. + * password upgrades. There are no plans to remove this support. It is deprecated to indicate that + * this is a legacy implementation and using it is considered insecure. */ @Deprecated public final class NoOpPasswordEncoder implements PasswordEncoder { diff --git a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java index 28ac723bce6..1ae357f0193 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoderTests.java @@ -200,4 +200,16 @@ public void upgradeFromNonBCrypt() { encoder.upgradeEncoding("not-a-bcrypt-password"); } + @Test(expected = IllegalArgumentException.class) + public void encodeNullRawPassword() { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + encoder.encode(null); + } + + @Test(expected = IllegalArgumentException.class) + public void matchNullRawPassword() { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + encoder.matches(null, "does-not-matter"); + } + } diff --git a/crypto/src/test/java/org/springframework/security/crypto/encrypt/AesBytesEncryptorTests.java b/crypto/src/test/java/org/springframework/security/crypto/encrypt/AesBytesEncryptorTests.java index f16928eb478..ce95884e9db 100644 --- a/crypto/src/test/java/org/springframework/security/crypto/encrypt/AesBytesEncryptorTests.java +++ b/crypto/src/test/java/org/springframework/security/crypto/encrypt/AesBytesEncryptorTests.java @@ -22,10 +22,15 @@ import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.PBEKeySpec; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.security.crypto.encrypt.AesBytesEncryptor.CipherAlgorithm.GCM; +import static org.springframework.security.crypto.encrypt.CipherUtils.newSecretKey; +import static org.springframework.security.crypto.password.Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1; /** * Tests for {@link AesBytesEncryptor} @@ -69,6 +74,23 @@ public void roundtripWhenUsingDefaultCipherThenEncryptsAndDecrypts() { public void roundtripWhenUsingGcmThenEncryptsAndDecrypts() { CryptoAssumptions.assumeGCMJCE(); AesBytesEncryptor encryptor = new AesBytesEncryptor(this.password, this.hexSalt, this.generator, GCM); + + byte[] encryption = encryptor.encrypt(this.secret.getBytes()); + assertThat(new String(Hex.encode(encryption))) + .isEqualTo("4b0febebd439db7ca77153cb254520c3e4d61ae38207b4e42b820d311dc3d4e0e2f37ed5ee"); + + byte[] decryption = encryptor.decrypt(encryption); + assertThat(new String(decryption)).isEqualTo(this.secret); + } + + @Test + public void roundtripWhenUsingSecretKeyThenEncryptsAndDecrypts() { + CryptoAssumptions.assumeGCMJCE(); + PBEKeySpec keySpec = new PBEKeySpec(this.password.toCharArray(), Hex.decode(this.hexSalt), + 1024, 256); + SecretKey secretKey = newSecretKey(PBKDF2WithHmacSHA1.name(), keySpec); + AesBytesEncryptor encryptor = new AesBytesEncryptor(secretKey, this.generator, GCM); + byte[] encryption = encryptor.encrypt(this.secret.getBytes()); assertThat(new String(Hex.encode(encryption))) .isEqualTo("4b0febebd439db7ca77153cb254520c3e4d61ae38207b4e42b820d311dc3d4e0e2f37ed5ee"); diff --git a/docs/articles/src/docbook/codebase-structure.xml b/docs/articles/src/docbook/codebase-structure.xml index bd2e06999c7..5d55ba1db5b 100644 --- a/docs/articles/src/docbook/codebase-structure.xml +++ b/docs/articles/src/docbook/codebase-structure.xml @@ -94,7 +94,7 @@ spring-security-core - Core authentication and access-contol classes and interfaces. + Core authentication and access-control classes and interfaces. Remoting support and basic provisioning APIs. Required by any application which uses Spring Security. Supports standalone applications, remote clients, method @@ -146,7 +146,7 @@ spring-security-openid OpenID web authentication support. If you need to authenticate users against an external OpenID - server. + server. (Deprecated) org.springframework.security.openid @@ -174,7 +174,7 @@ The core package and sub packages contain the basic classes and interfaces which are used throughout the framework and the other two main packages within the core jar are authentication and - access. The access package containst + access. The access package contains access-control/authorization code such as the AccessDecisionManager and related voter-based implementations, the interception and method security infrastructure, annotation diff --git a/docs/guides/src/docs/asciidoc/form-javaconfig.asc b/docs/guides/src/docs/asciidoc/form-javaconfig.asc index 845185f6330..c8281e9b861 100644 --- a/docs/guides/src/docs/asciidoc/form-javaconfig.asc +++ b/docs/guides/src/docs/asciidoc/form-javaconfig.asc @@ -199,7 +199,7 @@ Our existing configuration means that all we need to do is create a *login.html* IMPORTANT: Do not display details about why authentication failed. For example, we do not want to display that the user does not exist as this will tell an attacker that they should try a different username. -TIP: We use Thymeleaf to automatically add the CSRF token to our form. If we were not using Thymleaf or Spring MVCs taglib we could also manually add the CSRF token using ``. +TIP: We use Thymeleaf to automatically add the CSRF token to our form. If we were not using Thymeleaf or Spring MVCs taglib we could also manually add the CSRF token using ``. Start up the server and try visiting http://localhost:8080/sample/ to see the updates to our configuration. We now see our login page, but it does not look very pretty. The issue is that we have not granted access to the css files. diff --git a/docs/guides/src/docs/asciidoc/helloworld-boot.asc b/docs/guides/src/docs/asciidoc/helloworld-boot.asc index 7cec963d941..cc75981d58e 100644 --- a/docs/guides/src/docs/asciidoc/helloworld-boot.asc +++ b/docs/guides/src/docs/asciidoc/helloworld-boot.asc @@ -66,7 +66,7 @@ in order to utilize the _sec:authentication_ and _sec:authorize_ attributes. <3> Displays the authorities of the currently authenticated principal. <4> The logout form. -TIP: Thymeleaf will automatically add the CSRF token to our logout form. If we were not using Thymleaf or Spring MVCs taglib we could also manually add the CSRF token using ``. +TIP: Thymeleaf will automatically add the CSRF token to our logout form. If we were not using Thymeleaf or Spring MVCs taglib we could also manually add the CSRF token using ``. ==== Update the _secured_ page diff --git a/docs/manual/src/docs/asciidoc/_includes/about/authentication/password-storage.adoc b/docs/manual/src/docs/asciidoc/_includes/about/authentication/password-storage.adoc index 018fa0dc160..7770eb6b5a1 100644 --- a/docs/manual/src/docs/asciidoc/_includes/about/authentication/password-storage.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/about/authentication/password-storage.adoc @@ -68,18 +68,26 @@ You can easily construct an instance of `DelegatingPasswordEncoder` using `Pass .Create Default DelegatingPasswordEncoder ==== -[source,java] +.Java +[source,java,role="primary"] ---- PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder() +---- ==== Alternatively, you may create your own custom instance. For example: .Create Custom DelegatingPasswordEncoder ==== -[source,java] +.Java +[source,java,role="primary"] ---- String idForEncode = "bcrypt"; Map encoders = new HashMap<>(); @@ -92,6 +100,20 @@ encoders.put("sha256", new StandardPasswordEncoder()); PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders); ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val idForEncode = "bcrypt" +val encoders: MutableMap = mutableMapOf() +encoders[idForEncode] = BCryptPasswordEncoder() +encoders["noop"] = NoOpPasswordEncoder.getInstance() +encoders["pbkdf2"] = Pbkdf2PasswordEncoder() +encoders["scrypt"] = SCryptPasswordEncoder() +encoders["sha256"] = StandardPasswordEncoder() + +val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders) +---- ==== [[authentication-password-storage-dpe-format]] @@ -180,7 +202,8 @@ There are convenience mechanisms to make this easier, but this is still not inte .withDefaultPasswordEncoder Example ==== -[source,java,attrs="-attributes"] +.Java +[source,java,role="primary",attrs="-attributes"] ---- User user = User.withDefaultPasswordEncoder() .username("user") @@ -190,13 +213,26 @@ User user = User.withDefaultPasswordEncoder() System.out.println(user.getPassword()); // {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG ---- + +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] +---- +val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("user") + .build() +println(user.password) +// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG +---- ==== If you are creating multiple users, you can also reuse the builder. .withDefaultPasswordEncoder Reusing the Builder ==== -[source,java] +.Java +[source,java,role="primary"] ---- UserBuilder users = User.withDefaultPasswordEncoder(); User user = users @@ -210,6 +246,22 @@ User admin = users .roles("USER","ADMIN") .build(); ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val users = User.withDefaultPasswordEncoder() +val user = users + .username("user") + .password("password") + .roles("USER") + .build() +val admin = users + .username("admin") + .password("password") + .roles("USER", "ADMIN") + .build() +---- ==== This does hash the password that is stored, but the passwords are still exposed in memory and in the compiled source code. @@ -272,8 +324,13 @@ https://docs.spring.io/spring-security/site/docs/5.0.x/api/org/springframework/s The `BCryptPasswordEncoder` implementation uses the widely supported https://en.wikipedia.org/wiki/Bcrypt[bcrypt] algorithm to hash the passwords. In order to make it more resistent to password cracking, bcrypt is deliberately slow. Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system. +The default implementation of `BCryptPasswordEncoder` uses strength 10 as mentioned in the Javadoc of https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.html[BCryptPasswordEncoder]. You are encouraged to +tune and test the strength parameter on your own system so that it takes roughly 1 second to verify a password. -[source,java] +.BCryptPasswordEncoder +==== +.Java +[source,java,role="primary"] ---- // Create an encoder with strength 16 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16); @@ -281,6 +338,16 @@ String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result)); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +// Create an encoder with strength 16 +val encoder = BCryptPasswordEncoder(16) +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +---- +==== + [[authentication-password-storage-argon2]] == Argon2PasswordEncoder @@ -288,9 +355,12 @@ The `Argon2PasswordEncoder` implementation uses the https://en.wikipedia.org/wik Argon2 is the winner of the https://en.wikipedia.org/wiki/Password_Hashing_Competition[Password Hashing Competition]. In order to defeat password cracking on custom hardware, Argon2 is a deliberately slow algorithm that requires large amounts of memory. Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system. -The current implementation if the `Argon2PasswordEncoder` requires BouncyCastle. +The current implementation of the `Argon2PasswordEncoder` requires BouncyCastle. -[source,java] +.Argon2PasswordEncoder +==== +.Java +[source,java,role="primary"] ---- // Create an encoder with all the defaults Argon2PasswordEncoder encoder = new Argon2PasswordEncoder(); @@ -298,6 +368,16 @@ String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result)); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +// Create an encoder with all the defaults +val encoder = Argon2PasswordEncoder() +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +---- +==== + [[authentication-password-storage-pbkdf2]] == Pbkdf2PasswordEncoder @@ -306,7 +386,10 @@ In order to defeat password cracking PBKDF2 is a deliberately slow algorithm. Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system. This algorithm is a good choice when FIPS certification is required. -[source,java] +.Pbkdf2PasswordEncoder +==== +.Java +[source,java,role="primary"] ---- // Create an encoder with all the defaults Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(); @@ -314,6 +397,16 @@ String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result)); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +// Create an encoder with all the defaults +val encoder = Pbkdf2PasswordEncoder() +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +---- +==== + [[authentication-password-storage-scrypt]] == SCryptPasswordEncoder @@ -321,7 +414,10 @@ The `SCryptPasswordEncoder` implementation uses https://en.wikipedia.org/wiki/Sc In order to defeat password cracking on custom hardware scrypt is a deliberately slow algorithm that requires large amounts of memory. Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system. -[source,java] +.SCryptPasswordEncoder +==== +.Java +[source,java,role="primary"] ---- // Create an encoder with all the defaults SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); @@ -329,6 +425,16 @@ String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result)); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +// Create an encoder with all the defaults +val encoder = SCryptPasswordEncoder() +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +---- +==== + [[authentication-password-storage-other]] == Other PasswordEncoders diff --git a/docs/manual/src/docs/asciidoc/_includes/about/exploits/csrf.adoc b/docs/manual/src/docs/asciidoc/_includes/about/exploits/csrf.adoc index 647aaa6a8e4..1b73157804f 100644 --- a/docs/manual/src/docs/asciidoc/_includes/about/exploits/csrf.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/about/exploits/csrf.adoc @@ -378,7 +378,7 @@ The first option is to include the actual CSRF token in the body of the request. By placing the CSRF token in the body, the body will be read before authorization is performed. This means that anyone can place temporary files on your server. However, only authorized users will be able to submit a File that is processed by your application. -In general, this is the recommended approach because the temporary file uplaod should have a negligible impact on most servers. +In general, this is the recommended approach because the temporary file upload should have a negligible impact on most servers. [[csrf-considerations-multipart-url]] ==== Include CSRF Token in URL diff --git a/docs/manual/src/docs/asciidoc/_includes/about/exploits/headers.adoc b/docs/manual/src/docs/asciidoc/_includes/about/exploits/headers.adoc index 32abe9a31ad..f11a5c1364e 100644 --- a/docs/manual/src/docs/asciidoc/_includes/about/exploits/headers.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/about/exploits/headers.adoc @@ -72,7 +72,7 @@ Expires: 0 ==== In order to be secure by default, Spring Security adds these headers by default. -However, if your application provides it's own cache control headers Spring Security will back out of the way. +However, if your application provides its own cache control headers Spring Security will back out of the way. This allows for applications to ensure that static resources like CSS and JavaScript can be cached. @@ -119,7 +119,7 @@ Refer to the relevant sections to see how to customize the defaults for both < { + val csrfToken: Mono? = exchange.getAttribute(CsrfToken::class.java.name) + return csrfToken!!.doOnSuccess { token -> + exchange.attributes[CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME] = token + } + } +} +---- ==== Fortunately, Thymeleaf provides <> that works without any additional work. @@ -130,7 +176,7 @@ Next we will discuss various ways of including the CSRF token in a form as a hid Spring Security's CSRF support provides integration with Spring's https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/result/view/RequestDataValueProcessor.html[RequestDataValueProcessor] via its https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.html[CsrfRequestDataValueProcessor]. In order for `CsrfRequestDataValueProcessor` to work, the `Mono` must be subscribed to and the `CsrfToken` must be <> that matches https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.html#DEFAULT_CSRF_ATTR_NAME[DEFAULT_CSRF_ATTR_NAME]. -Fortunately, Thymleaf https://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#integration-with-requestdatavalueprocessor[provides support] to take care of all the boilerplate for you by integrating with `RequestDataValueProcessor` to ensure that forms that have an unsafe HTTP method (i.e. post) will automatically include the actual CSRF token. +Fortunately, Thymeleaf https://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#integration-with-requestdatavalueprocessor[provides support] to take care of all the boilerplate for you by integrating with `RequestDataValueProcessor` to ensure that forms that have an unsafe HTTP method (i.e. post) will automatically include the actual CSRF token. [[webflux-csrf-include-form-attr]] ===== CsrfToken Request Attribute @@ -253,7 +299,8 @@ For example, the following Java Configuration will perform logout with the URL ` .Log out with HTTP GET ==== -[source,java] +.Java +[source,java,role="primary"] ---- @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -262,7 +309,20 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) .logout(logout -> logout.requiresLogout(new PathPatternParserServerWebExchangeMatcher("/logout"))) return http.build(); } +---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + logout { + requiresLogout = PathPatternParserServerWebExchangeMatcher("/logout") + } + } +} ---- ==== @@ -301,7 +361,8 @@ In a WebFlux application, this can be configured with the following configuratio .Enable obtaining CSRF token from multipart/form-data ==== -[source,java] +.Java +[source,java,role="primary"] ---- @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { @@ -310,7 +371,20 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) .csrf(csrf -> csrf.tokenFromMultipartDataEnabled(true)) return http.build(); } +---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + csrf { + tokenFromMultipartDataEnabled = true + } + } +} ---- ==== diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/login.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/login.adoc index fbc1be05c84..18478b91901 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/login.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/login.adoc @@ -160,3 +160,21 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { return http.build(); } ---- + +You may register a `GrantedAuthoritiesMapper` `@Bean` to have it automatically applied to the default configuration, as shown in the following example: + +[source,java] +---- +@Bean +public GrantedAuthoritiesMapper userAuthoritiesMapper() { + ... +} + +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .oauth2Login(withDefaults()); + return http.build(); +} +---- diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc index 5e45a744271..16413cd94f8 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/oauth2/resource-server.adoc @@ -14,7 +14,8 @@ This authorization server can be consulted by resource servers to authorize requ A complete working example for {gh-samples-url}/boot/oauth2resourceserver-webflux[*JWTs*] is available in the {gh-samples-url}[Spring Security repository]. ==== -== Dependencies +[[webflux-oauth2resourceserver-jwt-minimaldependencies]] +== Minimal Dependencies for JWT Most Resource Server support is collected into `spring-security-oauth2-resource-server`. However, the support for decoding and verifying JWTs is in `spring-security-oauth2-jose`, meaning that both are necessary in order to have a working resource server that supports JWT-encoded Bearer Tokens. @@ -549,6 +550,12 @@ ReactiveJwtDecoder jwtDecoder() { return jwtDecoder; } ---- +[[webflux-oauth2resourceserver-opaque-minimaldependencies]] +=== Minimal Dependencies for Introspection +As described in <> most of Resource Server support is collected in `spring-security-oauth2-resource-server`. +However unless a custom <> is provided, the Resource Server will fallback to ReactiveOpaqueTokenIntrospector. +Meaning that both `spring-security-oauth2-resource-server` and `oauth2-oidc-sdk` are necessary in order to have a working minimal Resource Server that supports opaque Bearer Tokens. +Please refer to `spring-security-oauth2-resource-server` in order to determin the correct version for `oauth2-oidc-sdk`. [[webflux-oauth2resourceserver-opaque-minimalconfiguration]] === Minimal Configuration for Introspection @@ -1075,7 +1082,30 @@ In this case, you construct `JwtIssuerReactiveAuthenticationManagerResolver` wit This approach allows us to add and remove elements from the repository (shown as a `Map` in the snippet) at runtime. NOTE: It would be unsafe to simply take any issuer and construct an `ReactiveAuthenticationManager` from it. -The issuer should be one that the code can verify from a trusted source like a whitelist. +The issuer should be one that the code can verify from a trusted source like an allowed list of issuers. + +[[webflux-oauth2resourceserver-bearertoken-resolver]] +== Bearer Token Resolution + +By default, Resource Server looks for a bearer token in the `Authorization` header. +This, however, can be customized. + +For example, you may have a need to read the bearer token from a custom header. +To achieve this, you can wire an instance of `ServerBearerTokenAuthenticationConverter` into the DSL, as you can see in the following example: + +.Custom Bearer Token Header +==== +.Java +[source,java,role="primary"] +---- +ServerBearerTokenAuthenticationConverter converter = new ServerBearerTokenAuthenticationConverter(); +converter.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION); +http + .oauth2ResourceServer(oauth2 -> oauth2 + .bearerTokenConverter(converter) + ); +---- +==== == Bearer Token Propagation diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/test.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/test.adoc index 48901da4f8e..4a9e3a9e390 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/test.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/test.adoc @@ -218,7 +218,7 @@ assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SC Spring Security does the necessary work to make sure that the `OidcUser` instance is available for <>. -Further, it also links that `OidcUser` to a simple instance of `OAuth2AuthorizedClient` that it deposits into an `WebSessionOAuth2ServerAuthorizedClientRepository`. +Further, it also links that `OidcUser` to a simple instance of `OAuth2AuthorizedClient` that it deposits into a mock `ServerOAuth2AuthorizedClientRepository`. This can be handy if your tests <>.. [[webflux-testing-oidc-login-authorities]] @@ -339,7 +339,7 @@ assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SC Spring Security does the necessary work to make sure that the `OAuth2User` instance is available for <>. -Further, it also links that `OAuth2User` to a simple instance of `OAuth2AuthorizedClient` that it deposits in an `WebSessionOAuth2ServerAuthorizedClientRepository`. +Further, it also links that `OAuth2User` to a simple instance of `OAuth2AuthorizedClient` that it deposits in a mock `ServerOAuth2AuthorizedClientRepository`. This can be handy if your tests <>. [[webflux-testing-oauth2-login-authorities]] @@ -431,7 +431,7 @@ public Mono foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2Author ---- Simulating this handshake with the authorization server could be cumbersome. -Instead, you can use `SecurityMockServerConfigurers#oauth2Client` to add a `OAuth2AuthorizedClient` into an `WebSessionOAuth2ServerAuthorizedClientRepository`: +Instead, you can use `SecurityMockServerConfigurers#oauth2Client` to add a `OAuth2AuthorizedClient` into a mock `ServerOAuth2AuthorizedClientRepository`: [source,java] ---- @@ -440,19 +440,6 @@ client .get().uri("/endpoint").exchange(); ---- -If your application isn't already using an `WebSessionOAuth2ServerAuthorizedClientRepository`, then you can supply one as a `@TestConfiguration`: - -[source,java] ----- -@TestConfiguration -static class AuthorizedClientConfig { - @Bean - OAuth2ServerAuthorizedClientRepository authorizedClientRepository() { - return new WebSessionOAuth2ServerAuthorizedClientRepository(); - } -} ----- - What this will do is create an `OAuth2AuthorizedClient` that has a simple `ClientRegistration`, `OAuth2AccessToken`, and resource owner name. Specifically, it will include a `ClientRegistration` with a client id of "test-client" and client secret of "test-secret": @@ -478,8 +465,7 @@ assertThat(authorizedClient.getAccessToken().getScopes()).hasSize(1); assertThat(authorizedClient.getAccessToken().getScopes()).containsExactly("read"); ---- -Spring Security does the necessary work to make sure that the `OAuth2AuthorizedClient` instance is available in the associated `HttpSession`. -That means that it can be retrieved from an `WebSessionOAuth2ServerAuthorizedClientRepository`. +The client can then be retrieved as normal using `@RegisteredOAuth2AuthorizedClient` in a controller method. [[webflux-testing-oauth2-client-scopes]] ==== Configuring Scopes diff --git a/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc b/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc index f39c8163687..da909d6385b 100644 --- a/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/reactive/webflux.adoc @@ -13,7 +13,10 @@ You can find a few sample applications that demonstrate the code below: You can find a minimal WebFlux Security configuration below: -[source,java] +.Minimal WebFlux Security Configuration +==== +.Java +[source,java,role="primary"] ----- @EnableWebFluxSecurity @@ -31,13 +34,35 @@ public class HelloWebfluxSecurityConfig { } ----- +.Kotlin +[source,kotlin,role="secondary"] +----- +@EnableWebFluxSecurity +class HelloWebfluxSecurityConfig { + + @Bean + fun userDetailsService(): ReactiveUserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("user") + .roles("USER") + .build() + return MapReactiveUserDetailsService(userDetails) + } +} +----- +==== + This configuration provides form and http basic authentication, sets up authorization to require an authenticated user for accessing any page, sets up a default log in page and a default log out page, sets up security related HTTP headers, CSRF protection, and more. == Explicit WebFlux Security Configuration You can find an explicit version of the minimal WebFlux Security configuration below: -[source,java] +.Explicit WebFlux Security Configuration +==== +.Java +[source,java,role="primary"] ----- @Configuration @EnableWebFluxSecurity @@ -66,5 +91,36 @@ public class HelloWebfluxSecurityConfig { } ----- +.Kotlin +[source,kotlin,role="secondary"] +----- +@Configuration +@EnableWebFluxSecurity +class HelloWebfluxSecurityConfig { + + @Bean + fun userDetailsService(): ReactiveUserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("user") + .roles("USER") + .build() + return MapReactiveUserDetailsService(userDetails) + } + + @Bean + fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { } + httpBasic { } + } + } +} +----- +==== + This configuration explicitly sets up all the same things as our minimal configuration. From here you can easily make the changes to the defaults. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/dependencies.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/dependencies.adoc deleted file mode 100644 index 40bbd8d130a..00000000000 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/dependencies.adoc +++ /dev/null @@ -1,261 +0,0 @@ - - -[[appendix-dependencies]] -== Spring Security Dependencies -This appendix provides a reference of the modules in Spring Security and the additional dependencies that they require in order to function in a running application. -We don't include dependencies that are only used when building or testing Spring Security itself. -Nor do we include transitive dependencies which are required by external dependencies. - -The version of Spring required is listed on the project website, so the specific versions are omitted for Spring dependencies below. -Note that some of the dependencies listed as "optional" below may still be required for other non-security functionality in a Spring application. -Also dependencies listed as "optional" may not actually be marked as such in the project's Maven POM files if they are used in most applications. -They are "optional" only in the sense that you don't need them unless you are using the specified functionality. - -Where a module depends on another Spring Security module, the non-optional dependencies of the module it depends on are also assumed to be required and are not listed separately. - - -=== spring-security-core - -The core module must be included in any project using Spring Security. - -.Core Dependencies -|=== -| Dependency | Version | Description - -| ehcache -| 1.6.2 -| Required if the Ehcache-based user cache implementation is used (optional). - -| spring-aop -| -| Method security is based on Spring AOP - -| spring-beans -| -| Required for Spring configuration - -| spring-expression -| -| Required for expression-based method security (optional) - -| spring-jdbc -| -| Required if using a database to store user data (optional). - -| spring-tx -| -| Required if using a database to store user data (optional). - -| aspectjrt -| 1.6.10 -| Required if using AspectJ support (optional). - -| jsr250-api -| 1.0 -| Required if you are using JSR-250 method-security annotations (optional). -|=== - -=== spring-security-remoting -This module is typically required in web applications which use the Servlet API. - -.Remoting Dependencies -|=== -| Dependency | Version | Description - -| spring-security-core -| -| - -| spring-web -| -| Required for clients which use HTTP remoting support. -|=== - -=== spring-security-web -This module is typically required in web applications which use the Servlet API. - -.Web Dependencies -|=== -| Dependency | Version | Description - -| spring-security-core -| -| - -| spring-web -| -| Spring web support classes are used extensively. - -| spring-jdbc -| -| Required for JDBC-based persistent remember-me token repository (optional). - -| spring-tx -| -| Required by remember-me persistent token repository implementations (optional). -|=== - -=== spring-security-ldap -This module is only required if you are using LDAP authentication. - -.LDAP Dependencies -|=== -| Dependency | Version | Description - -| spring-security-core -| -| - -| spring-ldap-core -| 1.3.0 -| LDAP support is based on Spring LDAP. - -| spring-tx -| -| Data exception classes are required. - -| apache-ds footnote:[The modules `apacheds-core`, `apacheds-core-entry`, `apacheds-protocol-shared`, `apacheds-protocol-ldap` and `apacheds-server-jndi` are required. -] -| 1.5.5 -| Required if you are using an embedded LDAP server (optional). - -| shared-ldap -| 0.9.15 -| Required if you are using an embedded LDAP server (optional). - -| ldapsdk -| 4.1 -| Mozilla LdapSDK. -Used for decoding LDAP password policy controls if you are using password-policy functionality with OpenLDAP, for example. -|=== - - -=== spring-security-config -This module is required if you are using Spring Security namespace configuration. - -.Config Dependencies -|=== -| Dependency | Version | Description - -| spring-security-core -| -| - -| spring-security-web -| -| Required if you are using any web-related namespace configuration (optional). - -| spring-security-ldap -| -| Required if you are using the LDAP namespace options (optional). - -| spring-security-openid -| -| Required if you are using OpenID authentication (optional). - -| aspectjweaver -| 1.6.10 -| Required if using the protect-pointcut namespace syntax (optional). -|=== - - -=== spring-security-acl -The ACL module. - -.ACL Dependencies -|=== -| Dependency | Version | Description - -| spring-security-core -| -| - -| ehcache -| 1.6.2 -| Required if the Ehcache-based ACL cache implementation is used (optional if you are using your own implementation). - -| spring-jdbc -| -| Required if you are using the default JDBC-based AclService (optional if you implement your own). - -| spring-tx -| -| Required if you are using the default JDBC-based AclService (optional if you implement your own). -|=== - -=== spring-security-cas -The CAS module provides integration with JA-SIG CAS. - -.CAS Dependencies -|=== -| Dependency | Version | Description - -| spring-security-core -| -| - -| spring-security-web -| -| - -| cas-client-core -| 3.1.12 -| The JA-SIG CAS Client. -This is the basis of the Spring Security integration. - -| ehcache -| 1.6.2 -| Required if you are using the Ehcache-based ticket cache (optional). -|=== - -=== spring-security-openid -The OpenID module. - -.OpenID Dependencies -|=== -| Dependency | Version | Description - -| spring-security-core -| -| - -| spring-security-web -| -| - -| openid4java-nodeps -| 0.9.6 -| Spring Security's OpenID integration uses OpenID4Java. - -| httpclient -| 4.1.1 -| openid4java-nodeps depends on HttpClient 4. - -| guice -| 2.0 -| openid4java-nodeps depends on Guice 2. -|=== - -=== spring-security-taglibs -Provides Spring Security's JSP tag implementations. - -.Taglib Dependencies -|=== -| Dependency | Version | Description - -| spring-security-core -| -| - -| spring-security-web -| -| - -| spring-security-acl -| -| Required if you are using the `accesscontrollist` tag or `hasPermission()` expressions with ACLs (optional). - -| spring-expression -| -| Required if you are using SPEL expressions in your tag access constraints. -|=== diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/faq.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/faq.adoc index 01c1c23f7df..cb690f54a5a 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/faq.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/faq.adoc @@ -20,7 +20,7 @@ Spring Security provides you with a very flexible framework for your authentication and authorization requirements, but there are many other considerations for building a secure application that are outside its scope. Web applications are vulnerable to all kinds of attacks which you should be familiar with, preferably before you start development so you can design and code with them in mind from the beginning. -Check out thehttp://www.owasp.org/[OWASP web site] for information on the major issues facing web application developers and the countermeasures you can use against them. +Check out the https://www.owasp.org/[OWASP web site] for information on the major issues facing web application developers and the countermeasures you can use against them. [[appendix-faq-web-xml]] @@ -77,9 +77,9 @@ It should also be compatible with applications using Spring 2.5.x. ==== I'm new to Spring Security and I need to build an application that supports CAS single sign-on over HTTPS, while allowing Basic authentication locally for certain URLs, authenticating against multiple back end user information sources (LDAP and JDBC). I've copied some configuration files I found but it doesn't work. What could be wrong? -Or subsititute an alternative complex scenario... +Or substitute an alternative complex scenario... -Realistically, you need an understanding of the technolgies you are intending to use before you can successfully build applications with them. +Realistically, you need an understanding of the technologies you are intending to use before you can successfully build applications with them. Security is complicated. Setting up a simple configuration using a login form and some hard-coded users using Spring Security's namespace is reasonably straightforward. Moving to using a backed JDBC database is also easy enough. @@ -131,7 +131,7 @@ If you are using hashed passwords, make sure the value stored in your database i [[appendix-faq-login-loop]] ==== My application goes into an "endless loop" when I try to login, what's going on? -A common user problem with infinite loop and redirecting to the login page is caused by accidently configuring the login page as a "secured" resource. +A common user problem with infinite loop and redirecting to the login page is caused by accidentally configuring the login page as a "secured" resource. Make sure your configuration allows anonymous access to the login page, either by excluding it from the security filter chain or marking it as requiring ROLE_ANONYMOUS. If your AccessDecisionManager includes an AuthenticatedVoter, you can use the attribute "IS_AUTHENTICATED_ANONYMOUSLY". This is automatically available if you are using the standard namespace configuration setup. @@ -378,7 +378,7 @@ For third-party jars the situation isn't always quite so obvious. A good starting point is to copy those from one of the pre-built sample applications WEB-INF/lib directories. For a basic application, you can start with the tutorial sample. If you want to use LDAP, with an embedded test server, then use the LDAP sample as a starting point. -The reference manual also includeshttp://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#appendix-dependencies[an appendix] listing the first-level dependencies for each Spring Security module with some information on whether they are optional and what they are required for. +The reference manual also includes https://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#appendix-dependencies[an appendix] listing the first-level dependencies for each Spring Security module with some information on whether they are optional and what they are required for. If you are building your project with maven, then adding the appropriate Spring Security modules as dependencies to your pom.xml will automatically pull in the core jars that the framework requires. Any which are marked as "optional" in the Spring Security POM files will have to be added to your own pom.xml file if you need them. @@ -387,7 +387,7 @@ Any which are marked as "optional" in the Spring Security POM files will have to [[appendix-faq-apacheds-deps]] ==== What dependencies are needed to run an embedded ApacheDS LDAP server? -If you are using Maven, you need to add the folowing to your pom dependencies: +If you are using Maven, you need to add the following to your pom dependencies: [source] ---- diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/index.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/index.adoc index f0a169ecfbc..7ab1808a9ca 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/index.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/index.adoc @@ -5,6 +5,4 @@ include::database-schema.adoc[] include::namespace.adoc[] -include::dependencies.adoc[] - include::faq.adoc[] diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc index 6ab07162948..02754f1780e 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc @@ -504,43 +504,12 @@ Default false. ** `DENY` The page cannot be displayed in a frame, regardless of the site attempting to do so. This is the default when frame-options-policy is specified. ** `SAMEORIGIN` The page can only be displayed in a frame on the same origin as the page itself -** `ALLOW-FROM origin` The page can only be displayed in a frame on the specified origin. + In other words, if you specify DENY, not only will attempts to load the page in a frame fail when loaded from other sites, attempts to do so will fail when loaded from the same site. On the other hand, if you specify SAMEORIGIN, you can still use the page in a frame as long as the site including it in a frame it is the same as the one serving the page. -[[nsa-frame-options-strategy]] -* **strategy** -Select the `AllowFromStrategy` to use when using the ALLOW-FROM policy. - -** `static` Use a single static ALLOW-FROM value. -The value can be set through the <> attribute. -** `regexp` Use a regelur expression to validate incoming requests and if they are allowed. -The regular expression can be set through the <> attribute. -The request parameter used to retrieve the value to validate can be specified using the <>. -** `whitelist` A comma-seperated list containing the allowed domains. -The comma-seperated list can be set through the <> attribute. -The request parameter used to retrieve the value to validate can be specified using the <>. - - - - -[[nsa-frame-options-ref]] -* **ref** -Instead of using one of the predefined strategies it is also possible to use a custom `AllowFromStrategy`. -The reference to this bean can be specified through this ref attribute. - - -[[nsa-frame-options-value]] -* **value** -The value to use when ALLOW-FROM is used a <>. - - -[[nsa-frame-options-from-parameter]] -* **from-parameter** -Specify the name of the request parameter to use when using regexp or whitelist for the ALLOW-FROM strategy. [[nsa-frame-options-parents]] diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/delegating-filter-proxy.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/delegating-filter-proxy.adoc index d75a5c677dd..d7c3d45ed3b 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/delegating-filter-proxy.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/delegating-filter-proxy.adoc @@ -16,7 +16,8 @@ The pseudo code of `DelegatingFilterProxy` can be seen below. .`DelegatingFilterProxy` Pseudo Code ==== -[source,java,subs="+quotes,+macros"] +.Java +[source,java,role="primary",subs="+quotes,+macros"] ---- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { // Lazily get Filter that was registered as a Spring Bean @@ -26,6 +27,18 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha delegate.doFilter(request, response); } ---- + +.Kotlin +[source,kotlin,role="secondary",subs="+quotes,+macros"] +---- +fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + // Lazily get Filter that was registered as a Spring Bean + // For the example in <> `delegate` is an instance of __Bean Filter~0~__ + val delegate: Filter = getFilterBean(someBeanName) + // delegate work to the Spring Bean + delegate.doFilter(request, response) +} +---- ==== Another benefit of `DelegatingFilterProxy` is that it allows delaying looking `Filter` bean instances. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/filters.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/filters.adoc index 212d5ec1ad6..4b63aa4deb9 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/filters.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/filters.adoc @@ -21,7 +21,8 @@ The power of the `Filter` comes from the `FilterChain` that is passed into it. .`FilterChain` Usage Example ==== -[source,java] +.Java +[source,java,role="primary"] ---- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { // do something before the rest of the application @@ -29,6 +30,16 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha // do something after the rest of the application } ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + // do something before the rest of the application + chain.doFilter(request, response) // invoke the rest of the application + // do something after the rest of the application +} +---- ==== Since a `Filter` only impacts downstream ``Filter``s and the `Servlet`, the order each `Filter` is invoked is extremely important. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/security-filter-chain.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/security-filter-chain.adoc index 4af4f0e89c3..508e9e3d218 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/security-filter-chain.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/architecture/security-filter-chain.adoc @@ -23,7 +23,7 @@ In a Servlet container, ``Filter``s are invoked based upon the URL alone. However, `FilterChainProxy` can determine invocation based upon anything in the `HttpServletRequest` by leveraging the `RequestMatcher` interface. In fact, `FilterChainProxy` can be used to determine which `SecurityFilterChain` should be used. -This allows providing a totally separate configuration for different _slices_ if your application. +This allows providing a totally separate configuration for different _slices_ of your application. .Multiple SecurityFilterChain [[servlet-multi-securityfilterchain-figure]] diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/architecture/provider-manager.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/architecture/provider-manager.adoc index 1075eed6c1f..4ed8cb4615a 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/architecture/provider-manager.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/architecture/provider-manager.adoc @@ -6,7 +6,7 @@ `ProviderManager` delegates to a `List` of <>. // FIXME: link to AuthenticationProvider Each `AuthenticationProvider` has an opportunity to indicate that authentication should be successful, fail, or indicate it cannot make a decision and allow a downstream `AuthenticationProvider` to decide. -If none of the configured ``AuthenticationProvider``s can authenticate, then authentication will fail with a `ProviderNotFoundException` which is a special `AuthenticationException` that indicates the `ProviderManager` was not configured support the type of `Authentication` that was passed into it. +If none of the configured ``AuthenticationProvider``s can authenticate, then authentication will fail with a `ProviderNotFoundException` which is a special `AuthenticationException` that indicates the `ProviderManager` was not configured to support the type of `Authentication` that was passed into it. image::{figures}/providermanager.png[] diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/architecture/security-context-holder.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/architecture/security-context-holder.adoc index 4c1516a1af9..db2f4c6d4cc 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/architecture/security-context-holder.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/architecture/security-context-holder.adoc @@ -16,7 +16,8 @@ The simplest way to indicate a user is authenticated is to set the `SecurityCont .Setting `SecurityContextHolder` ==== -[source,java] +.Java +[source,java,role="primary"] ---- SecurityContext context = SecurityContextHolder.createEmptyContext(); // <1> Authentication authentication = @@ -25,6 +26,16 @@ context.setAuthentication(authentication); SecurityContextHolder.setContext(context); // <3> ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val context: SecurityContext = SecurityContextHolder.createEmptyContext() // <1> +val authentication: Authentication = TestingAuthenticationToken("username", "password", "ROLE_USER") // <2> +context.authentication = authentication + +SecurityContextHolder.setContext(context) // <3> +---- ==== <1> We start by creating an empty `SecurityContext`. @@ -40,7 +51,8 @@ If you wish to obtain information about the authenticated principal, you can do .Access Currently Authenticated User ==== -[source,java] +.Java +[source,java,role="primary"] ---- SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); @@ -48,6 +60,16 @@ String username = authentication.getName(); Object principal = authentication.getPrincipal(); Collection authorities = authentication.getAuthorities(); ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val context = SecurityContextHolder.getContext() +val authentication = context.authentication +val username = authentication.name +val principal = authentication.principal +val authorities = authentication.authorities +---- ==== // FIXME: add links to HttpServletRequest.getRemoteUser() and @CurrentSecurityContext @AuthenticationPrincipal diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/cas.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/cas.adoc index c78abd0cafe..4f8afa13981 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/cas.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/cas.adoc @@ -235,7 +235,7 @@ With the configuration above, the flow of logout would be: * The logout success page, `/cas-logout.jsp`, should instruct the user to click a link pointing to `/logout/cas` in order to logout out of all applications. * When the user clicks the link, the user is redirected to the CAS single logout URL (https://localhost:9443/cas/logout). * On the CAS Server side, the CAS single logout URL then submits single logout requests to all the CAS Services. -On the CAS Service side, JASIG's `SingleSignOutFilter` processes the logout request by invaliditing the original session. +On the CAS Service side, JASIG's `SingleSignOutFilter` processes the logout request by invalidating the original session. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/logout.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/logout.adoc index cff12b34cf4..c824ffe0adb 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/logout.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/logout.adoc @@ -2,7 +2,7 @@ == Handling Logouts [[logout-java-configuration]] -=== Logout Java Configuration +=== Logout Java/Kotlin Configuration When using the `{security-api-url}org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.html[WebSecurityConfigurerAdapter]`, logout capabilities are automatically applied. The default is that accessing the URL `/logout` will log the user out by: @@ -14,7 +14,10 @@ The default is that accessing the URL `/logout` will log the user out by: Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements: -[source,java] +.Logout Configuration +==== +.Java +[source,java,role="primary"] ---- protected void configure(HttpSecurity http) throws Exception { http @@ -30,6 +33,24 @@ protected void configure(HttpSecurity http) throws Exception { } ---- +.Kotlin +[source,kotlin,role="secondary"] +----- +override fun configure(http: HttpSecurity) { + http { + logout { + logoutUrl = "/my/logout" // <1> + logoutSuccessUrl = "/my/index" // <2> + logoutSuccessHandler = customLogoutSuccessHandler // <3> + invalidateHttpSession = true // <4> + addLogoutHandler(logoutHandler) // <5> + deleteCookies(cookieNamesToClear) // <6> + } + } +} +----- +==== + <1> Provides logout support. This is automatically applied when using `WebSecurityConfigurerAdapter`. <2> The URL that triggers log out to occur (default is `/logout`). diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/openid.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/openid.adoc index e3d14137d3c..01362127ffa 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/openid.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/openid.adoc @@ -1,5 +1,9 @@ [[servlet-openid]] == OpenID Support + +[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. + The namespace supports https://openid.net/[OpenID] login either instead of, or in addition to normal form-based login, with a simple change: [source,xml] diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/preauth.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/preauth.adoc index e68229393c0..c50de6379fb 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/preauth.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/preauth.adoc @@ -29,13 +29,25 @@ We just provide an outline here so you should consult the Javadoc and source whe This class will check the current contents of the security context and, if empty, it will attempt to extract user information from the HTTP request and submit it to the `AuthenticationManager`. Subclasses override the following methods to obtain this information: -[source,java] +.Override AbstractPreAuthenticatedProcessingFilter +==== +.Java +[source,java,role="primary"] ---- protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request); protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +protected abstract fun getPreAuthenticatedPrincipal(request: HttpServletRequest): Any? + +protected abstract fun getPreAuthenticatedCredentials(request: HttpServletRequest): Any? +---- +==== + After calling these, the filter will create a `PreAuthenticatedAuthenticationToken` containing the returned data and submit it for authentication. By "authentication" here, we really just mean further processing to perhaps load the user's authorities, but the standard Spring Security authentication architecture is followed. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/basic.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/basic.adoc index 849fc278b04..aa2ff02f297 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/basic.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/basic.adoc @@ -23,13 +23,14 @@ The `RequestCache` is typically a `NullRequestCache` that does not save the requ When a client receives the WWW-Authenticate header it knows it should retry with a username and password. Below is the flow for the username and password being processed. +[[servlet-authentication-basicauthenticationfilter]] .Authenticating Username and Password image::{figures}/basicauthenticationfilter.png[] The figure builds off our <> diagram. -image:{icondir}/number_1.png[] When the user submits their username and password, the `UsernamePasswordAuthenticationFilter` creates a `UsernamePasswordAuthenticationToken` which is a type of <> by extracting the username and password from the `HttpServletRequest`. +image:{icondir}/number_1.png[] When the user submits their username and password, the `BasicAuthenticationFilter` creates a `UsernamePasswordAuthenticationToken` which is a type of <> by extracting the username and password from the `HttpServletRequest`. image:{icondir}/number_2.png[] Next, the `UsernamePasswordAuthenticationToken` is passed into the `AuthenticationManager` to be authenticated. The details of what `AuthenticationManager` look like depend on how the <>. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/digest.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/digest.adoc index 90c38bca2d2..603db7d93d3 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/digest.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/digest.adoc @@ -76,7 +76,7 @@ protected void configure(HttpSecurity http) throws Exception { diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/in-memory.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/in-memory.adoc index faccb03a7e9..a364cee0e1a 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/in-memory.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/in-memory.adoc @@ -65,7 +65,7 @@ The samples above store the passwords in a secure format, but leave a lot to be In the sample below we leverage <> to ensure that the password stored in memory is protected. -However, it does not protect the password against obtaining the password by decompiling the source code. +However, it does not protect against obtaining the password by decompiling the source code. For this reason, `User.withDefaultPasswordEncoder` should only be used for "getting started" and is not intended for production. .InMemoryUserDetailsManager with User.withDefaultPasswordEncoder @@ -82,7 +82,7 @@ public UserDetailsService users() { .password("password") .roles("USER") .build(); - UserDetails user = users + UserDetails admin = users .username("admin") .password("password") .roles("USER", "ADMIN") diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/jdbc.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/jdbc.adoc index ddbdc8a4dea..23198edc99a 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/jdbc.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authentication/unpwd/jdbc.adoc @@ -35,7 +35,7 @@ The default schema is also exposed as a classpath resource named `org/springfram ---- create table users( username varchar_ignorecase(50) not null primary key, - password varchar_ignorecase(50) not null, + password varchar_ignorecase(500) not null, enabled boolean not null ); @@ -169,7 +169,8 @@ UserDetailsManager users(DataSource dataSource) { .roles("USER", "ADMIN") .build(); JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource); - users.createUser() + users.createUser(user); + users.createUser(admin); } ---- diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/acls.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/acls.adoc index 41eb2763570..f87e83d4ee0 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/acls.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/acls.adoc @@ -72,7 +72,7 @@ Columns include the ID, a foreign key to the ACL_CLASS table, a unique identifie We have a single row for every domain object instance we're storing ACL permissions for. * Finally, ACL_ENTRY stores the individual permissions assigned to each recipient. -Columns include a foreign key to the ACL_OBJECT_IDENTITY, the recipient (ie a foreign key to ACL_SID), whether we'll be auditing or not, and the integer bit mask that represents the actual permission being granted or denied. +Columns include a foreign key to the ACL_OBJECT_IDENTITY, the recipient (i.e. a foreign key to ACL_SID), whether we'll be auditing or not, and the integer bit mask that represents the actual permission being granted or denied. We have a single row for every recipient that receives a permission to work with a domain object. @@ -113,7 +113,7 @@ The default implementation is called `ObjectIdentityImpl`. * `AclService`: Retrieves the `Acl` applicable for a given `ObjectIdentity`. In the included implementation (`JdbcAclService`), retrieval operations are delegated to a `LookupStrategy`. -The `LookupStrategy` provides a highly optimized strategy for retrieving ACL information, using batched retrievals `(BasicLookupStrategy`) and supporting custom implementations that leverage materialized views, hierarchical queries and similar performance-centric, non-ANSI SQL capabilities. +The `LookupStrategy` provides a highly optimized strategy for retrieving ACL information, using batched retrievals (`BasicLookupStrategy`) and supporting custom implementations that leverage materialized views, hierarchical queries and similar performance-centric, non-ANSI SQL capabilities. * `MutableAclService`: Allows a modified `Acl` to be presented for persistence. It is not essential to use this interface if you do not wish. @@ -141,7 +141,7 @@ You'll also need to populate the database with the four ACL-specific tables list Once you've created the required schema and instantiated `JdbcMutableAclService`, you'll next need to ensure your domain model supports interoperability with the Spring Security ACL package. Hopefully `ObjectIdentityImpl` will prove sufficient, as it provides a large number of ways in which it can be used. Most people will have domain objects that contain a `public Serializable getId()` method. -If the return type is long, or compatible with long (eg an int), you will find you need not give further consideration to `ObjectIdentity` issues. +If the return type is long, or compatible with long (e.g. an int), you will find you need not give further consideration to `ObjectIdentity` issues. Many parts of the ACL module rely on long identifiers. If you're not using long (or an int, byte etc), there is a very good chance you'll need to reimplement a number of classes. We do not intend to support non-long identifiers in Spring Security's ACL module, as longs are already compatible with all database sequences, the most common identifier data type, and are of sufficient length to accommodate all common usage scenarios. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/architecture.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/architecture.adoc index 5d2f31d6c93..15de8dbf70d 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/architecture.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/architecture.adoc @@ -42,7 +42,7 @@ A pre-invocation decision on whether the invocation is allowed to proceed is mad [[authz-access-decision-manager]] === The AccessDecisionManager The `AccessDecisionManager` is called by the `AbstractSecurityInterceptor` and is responsible for making final access control decisions. -the `AccessDecisionManager` interface contains three methods: +The `AccessDecisionManager` interface contains three methods: [source,java] ---- diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/expression-based.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/expression-based.adoc index 9645a371306..580f57f8efb 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/expression-based.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/expression-based.adoc @@ -103,7 +103,7 @@ For example: Here we have defined that the "admin" area of an application (defined by the URL pattern) should only be available to users who have the granted authority "admin" and whose IP address matches a local subnet. We've already seen the built-in `hasRole` expression in the previous section. The expression `hasIpAddress` is an additional built-in expression which is specific to web security. -It is defined by the `WebSecurityExpressionRoot` class, an instance of which is used as the expression root object when evaluation web-access expressions. +It is defined by the `WebSecurityExpressionRoot` class, an instance of which is used as the expression root object when evaluating web-access expressions. This object also directly exposed the `HttpServletRequest` object under the name `request` so you can invoke the request directly in an expression. If expressions are being used, a `WebExpressionVoter` will be added to the `AccessDecisionManager` which is used by the namespace. So if you aren't using the namespace and want to use expressions, you will have to add one of these to your configuration. @@ -125,7 +125,20 @@ public class WebSecurity { You could refer to the method using: -[source,xml] +.Refer to method +==== +.Java +[source,java,role="primary"] +---- +http + .authorizeRequests(authorize -> authorize + .antMatchers("/user/**").access("@webSecurity.check(authentication,request)") + ... + ) +---- + +.XML +[source,xml,role="secondary"] ---- ---- -or in Java configuration - - -[source,java] +.Kotlin +[source,kotlin,role="secondary"] ---- -http - .authorizeRequests(authorize -> authorize - .antMatchers("/user/**").access("@webSecurity.check(authentication,request)") - ... - ) +http { + authorizeRequests { + authorize("/user/**", "@webSecurity.check(authentication,request)") + } +} ---- +==== [[el-access-web-path-variables]] ==== Path Variables in Web Security Expressions @@ -166,7 +178,20 @@ public class WebSecurity { You could refer to the method using: -[source,xml,attrs="-attributes"] +.Path Variables +==== +.Java +[source,java,role="primary",attrs="-attributes"] +---- +http + .authorizeRequests(authorize -> authorize + .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)") + ... + ); +---- + +.XML +[source,xml,role="secondary",attrs="-attributes"] ---- ---- -or in Java configuration - -[source,java,attrs="-attributes"] +.Kotlin +[source,kotlin,role="secondary",attrs="-attributes"] ---- -http - .authorizeRequests(authorize -> authorize - .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)") - ... - ); +http { + authorizeRequests { + authorize("/user/{userId}/**", "@webSecurity.checkUserId(authentication,#userId)") + } +} ---- +==== -In both configurations URLs that match would pass in the path variable (and convert it) into checkUserId method. +In this configuration URLs that match would pass in the path variable (and convert it) into checkUserId method. For example, if the URL were `/user/123/resource`, then the id passed in would be `123`. === Method Security Expressions @@ -207,7 +232,7 @@ Their use is enabled through the `global-method-security` namespace element: ===== Access Control using @PreAuthorize and @PostAuthorize The most obviously useful annotation is `@PreAuthorize` which decides whether a method can actually be invoked or not. -For example (from the"Contacts" sample application) +For example (from the "Contacts" sample application) [source,java] ---- @@ -226,7 +251,7 @@ public void deletePermission(Contact contact, Sid recipient, Permission permissi ---- Here we're actually using a method argument as part of the expression to decide whether the current user has the "admin"permission for the given contact. -The built-in `hasPermission()` expression is linked into the Spring Security ACL module through the application context, as we'll<>. +The built-in `hasPermission()` expression is linked into the Spring Security ACL module through the application context, as we'll <>. You can access any of the method arguments by name as expression variables. There are a number of ways in which Spring Security can resolve the method arguments. @@ -251,7 +276,7 @@ public void doSomething(@P("c") Contact contact); + -Behind the scenes this use implemented using `AnnotationParameterNameDiscoverer` which can be customized to support the value attribute of any specified annotation. +Behind the scenes this is implemented using `AnnotationParameterNameDiscoverer` which can be customized to support the value attribute of any specified annotation. * If Spring Data's `@Param` annotation is present on at least one parameter for the method, the value will be used. This is useful for interfaces compiled with a JDK prior to JDK 8 which do not contain any information about the parameter names. @@ -271,7 +296,7 @@ Contact findContactByName(@Param("n") String name); + -Behind the scenes this use implemented using `AnnotationParameterNameDiscoverer` which can be customized to support the value attribute of any specified annotation. +Behind the scenes this is implemented using `AnnotationParameterNameDiscoverer` which can be customized to support the value attribute of any specified annotation. * If JDK 8 was used to compile the source with the -parameters argument and Spring 4+ is being used, then the standard JDK reflection API is used to discover the parameter names. This works on both classes and interfaces. @@ -304,7 +329,7 @@ To access the return value from a method, use the built-in name `returnObject` i -- ===== Filtering using @PreFilter and @PostFilter -As you may already be aware, Spring Security supports filtering of collections and arrays and this can now be achieved using expressions. +Spring Security supports filtering of collections, arrays, maps and streams using expressions. This is most commonly performed on the return value of a method. For example: @@ -315,8 +340,10 @@ For example: public List getAll(); ---- -When using the `@PostFilter` annotation, Spring Security iterates through the returned collection and removes any elements for which the supplied expression is false. +When using the `@PostFilter` annotation, Spring Security iterates through the returned collection or map and removes any elements for which the supplied expression is false. +For an array, a new array instance will be returned containing filtered elements. The name `filterObject` refers to the current object in the collection. +In case when a map is used it will refer to the current `Map.Entry` object which allows one to use `filterObject.key` or `filterObject.value` in the expresion. You can also filter before the method call, using `@PreFilter`, though this is a less common requirement. The syntax is just the same, but if there is more than one argument which is a collection type then you have to select one by name using the `filterTarget` property of this annotation. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/secure-objects.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/secure-objects.adoc index 3e9140801a5..7370a4202ce 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/secure-objects.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/authorization/secure-objects.adoc @@ -16,7 +16,7 @@ The interceptor uses a `MethodSecurityMetadataSource` instance to obtain the con Other implementations will be used to handle annotation-based configuration. ==== Explicit MethodSecurityInterceptor Configuration -You can of course configure a `MethodSecurityIterceptor` directly in your application context for use with one of Spring AOP's proxying mechanisms: +You can of course configure a `MethodSecurityInterceptor` directly in your application context for use with one of Spring AOP's proxying mechanisms: [source,xml] ---- @@ -140,4 +140,4 @@ A bean declaration which achieves this is shown below: That's it! -Now you can create your beans from anywhere within your application, using whatever means you think fit (eg `new Person();`) and they will have the security interceptor applied. +Now you can create your beans from anywhere within your application, using whatever means you think fit (e.g. `new Person();`) and they will have the security interceptor applied. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/crypto/index.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/crypto/index.adoc index 753c46c4b15..137b3e8b694 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/crypto/index.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/crypto/index.adoc @@ -17,14 +17,26 @@ Encryptors are thread-safe. [[spring-security-crypto-encryption-bytes]] === BytesEncryptor -Use the Encryptors.standard factory method to construct a "standard" BytesEncryptor: +Use the `Encryptors.stronger` factory method to construct a BytesEncryptor: -[source,java] +.BytesEncryptor +==== +.Java +[source,java,role="primary"] +---- +Encryptors.stronger("password", "salt"); +---- + +.Kotlin +[source,kotlin,role="secondary"] ---- -Encryptors.standard("password", "salt"); +Encryptors.stronger("password", "salt") ---- +==== -The "standard" encryption method is 256-bit AES using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2). +The "stronger" encryption method creates an encryptor using 256 bit AES encryption with +Galois Counter Mode (GCM). +It derives the secret key using PKCS #5's PBKDF2 (Password-Based Key Derivation Function #2). This method requires Java 6. The password used to generate the SecretKey should be kept in a secure place and not be shared. The salt is used to prevent dictionary attacks against the key in the event your encrypted data is compromised. @@ -33,31 +45,65 @@ A 16-byte random initialization vector is also applied so each encrypted message The provided salt should be in hex-encoded String form, be random, and be at least 8 bytes in length. Such a salt may be generated using a KeyGenerator: -[source,java] +.Generating a key +==== +.Java +[source,java,role="primary"] ---- String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val salt = KeyGenerators.string().generateKey() // generates a random 8-byte salt that is then hex-encoded +---- +==== + +Users may also use the `standard` encryption method, which is 256-bit AES in Cipher Block Chaining (CBC) Mode. +This mode is not https://en.wikipedia.org/wiki/Authenticated_encryption[authenticated] and does not provide any +guarantees about the authenticity of the data. +For a more secure alternative, users should prefer `Encryptors.stronger`. + [[spring-security-crypto-encryption-text]] === TextEncryptor Use the Encryptors.text factory method to construct a standard TextEncryptor: -[source,java] +.TextEncryptor +==== +.Java +[source,java,role="primary"] ---- - Encryptors.text("password", "salt"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +Encryptors.text("password", "salt") +---- +==== + A TextEncryptor uses a standard BytesEncryptor to encrypt text data. Encrypted results are returned as hex-encoded strings for easy storage on the filesystem or in the database. Use the Encryptors.queryableText factory method to construct a "queryable" TextEncryptor: -[source,java] +.Queryable TextEncryptor +==== +.Java +[source,java,role="primary"] ---- Encryptors.queryableText("password", "salt"); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +Encryptors.queryableText("password", "salt") +---- +==== + The difference between a queryable TextEncryptor and a standard TextEncryptor has to do with initialization vector (iv) handling. The iv used in a queryable TextEncryptor#encrypt operation is shared, or constant, and is not randomly generated. This means the same text encrypted multiple times will always produce the same encryption result. @@ -74,35 +120,76 @@ KeyGenerators are thread-safe. === BytesKeyGenerator Use the KeyGenerators.secureRandom factory methods to generate a BytesKeyGenerator backed by a SecureRandom instance: -[source,java] +.BytesKeyGenerator +==== +.Java +[source,java,role="primary"] ---- BytesKeyGenerator generator = KeyGenerators.secureRandom(); byte[] key = generator.generateKey(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val generator = KeyGenerators.secureRandom() +val key = generator.generateKey() +---- +==== + The default key length is 8 bytes. There is also a KeyGenerators.secureRandom variant that provides control over the key length: -[source,java] +.KeyGenerators.secureRandom +==== +.Java +[source,java,role="primary"] ---- KeyGenerators.secureRandom(16); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +KeyGenerators.secureRandom(16) +---- +==== + Use the KeyGenerators.shared factory method to construct a BytesKeyGenerator that always returns the same key on every invocation: -[source,java] +.KeyGenerators.shared +==== +.Java +[source,java,role="primary"] ---- KeyGenerators.shared(16); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +KeyGenerators.shared(16) +---- +==== + === StringKeyGenerator Use the KeyGenerators.string factory method to construct a 8-byte, SecureRandom KeyGenerator that hex-encodes each key as a String: -[source,java] +.StringKeyGenerator +==== +.Java +[source,java,role="primary"] ---- KeyGenerators.string(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +KeyGenerators.string() +---- +==== + [[spring-security-crypto-passwordencoders]] == Password Encoding The password package of the spring-security-crypto module provides support for encoding passwords. @@ -128,7 +215,10 @@ The higher the value, the more work has to be done to calculate the hash. The default value is 10. You can change this value in your deployed system without affecting existing passwords, as the value is also stored in the encoded hash. -[source,java] +.BCryptPasswordEncoder +==== +.Java +[source,java,role="primary"] ---- // Create an encoder with strength 16 @@ -137,15 +227,38 @@ String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result)); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- + +// Create an encoder with strength 16 +val encoder = BCryptPasswordEncoder(16) +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +---- +==== + The `Pbkdf2PasswordEncoder` implementation uses PBKDF2 algorithm to hash the passwords. In order to defeat password cracking PBKDF2 is a deliberately slow algorithm and should be tuned to take about .5 seconds to verify a password on your system. -[source,java] +.Pbkdf2PasswordEncoder +==== +.Java +[source,java,role="primary"] ---- - // Create an encoder with all the defaults Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(); String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result)); ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +// Create an encoder with all the defaults +val encoder = Pbkdf2PasswordEncoder() +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +---- +==== diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/csrf.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/csrf.adoc index a7bcbc3a529..54f8b6c1ed0 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/csrf.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/exploits/csrf.adoc @@ -155,7 +155,7 @@ Next we will discuss various ways of including the CSRF token in a form as a hid ===== Automatic CSRF Token Inclusion Spring Security's CSRF support provides integration with Spring's https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/support/RequestDataValueProcessor.html[RequestDataValueProcessor] via its https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/servlet/support/csrf/CsrfRequestDataValueProcessor.html[CsrfRequestDataValueProcessor]. -This means that if you leverage https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-view-jsp-formtaglib[Spring’s form tag library], https://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#integration-with-requestdatavalueprocessor[Thymleaf], or any other view technology that integrates with `RequestDataValueProcessor`, then forms that have an unsafe HTTP method (i.e. post) will automatically include the actual CSRF token. +This means that if you leverage https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-view-jsp-formtaglib[Spring’s form tag library], https://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#integration-with-requestdatavalueprocessor[Thymeleaf], or any other view technology that integrates with `RequestDataValueProcessor`, then forms that have an unsafe HTTP method (i.e. post) will automatically include the actual CSRF token. [[servlet-csrf-include-form-tag]] ===== csrfInput Tag diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/jackson.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/jackson.adoc index 247fc8e6b86..9e6418a1757 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/jackson.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/jackson.adoc @@ -1,10 +1,10 @@ [[jackson]] == Jackson Support -Spring Security has added Jackson Support for persisting Spring Security related classes. +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)` as https://wiki.fasterxml.com/JacksonFeatureModules[Jackson Modules]. +To use it, register the `SecurityJackson2Modules.getModules(ClassLoader)` with `ObjectMapper` (https://github.com/FasterXML/jackson-databind[jackson-databind]): [source,java] ---- @@ -18,3 +18,13 @@ 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/manual/src/docs/asciidoc/_includes/servlet/integrations/jsp-taglibs.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/jsp-taglibs.adoc index f4a0a3ef3c8..3f316832582 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/jsp-taglibs.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/jsp-taglibs.adoc @@ -30,7 +30,7 @@ This content will only be visible to users who have the "supervisor" authority i ---- -When used in conjuction with Spring Security's PermissionEvaluator, the tag can also be used to check permissions. +When used in conjunction with Spring Security's PermissionEvaluator, the tag can also be used to check permissions. For example: [source,xml] diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/localization.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/localization.adoc index 6b415e23a7a..0f8f9b402ee 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/localization.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/localization.adoc @@ -5,7 +5,7 @@ If your application is designed for English-speaking users, you don't need to do 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 deployers (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. +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. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/mvc.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/mvc.adoc index 6b1f36558e6..7b7b8fcce02 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/mvc.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/mvc.adoc @@ -221,7 +221,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; @RequestMapping("/messages/inbox") public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) { - // .. find messags for this user and return them ... + // .. find messages for this user and return them ... } ---- @@ -356,7 +356,7 @@ Will output HTML that is similar to the following: Spring Security provides `CsrfTokenArgumentResolver` which can automatically resolve the current `CsrfToken` for Spring MVC arguments. By using <> you will automatically have this added to your Spring MVC configuration. -If you use XML based configuraiton, you must add this yourself. +If you use XML based configuration, you must add this yourself. Once `CsrfTokenArgumentResolver` is properly configured, you can expose the `CsrfToken` to your static HTML based application. diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/servlet-api.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/servlet-api.adoc index 6c75fd305a5..9e957f39a08 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/servlet-api.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/servlet-api.adoc @@ -96,7 +96,7 @@ Typically this would involve a redirect to the welcome page. [[servletapi-start-runnable]] ==== AsyncContext.start(Runnable) -The https://docs.oracle.com/javaee/6/api/javax/servlet/AsyncContext.html#start%28java.lang.Runnable%29[AsynchContext.start(Runnable)] method that ensures your credentials will be propagated to the new Thread. +The https://docs.oracle.com/javaee/6/api/javax/servlet/AsyncContext.html#start%28java.lang.Runnable%29[AsyncContext.start(Runnable)] method that ensures your credentials will be propagated to the new Thread. Using Spring Security's concurrency support, Spring Security overrides the AsyncContext.start(Runnable) to ensure that the current SecurityContext is used when processing the Runnable. For example, the following would output the current user's Authentication: @@ -182,10 +182,10 @@ new Thread("AsyncThread") { ---- The issue is that this Thread is not known to Spring Security, so the SecurityContext is not propagated to it. -This means when we commit the HttpServletResponse there is no SecuriytContext. +This means when we commit the HttpServletResponse there is no SecurityContext. When Spring Security automatically saved the SecurityContext on committing the HttpServletResponse it would lose our logged in user. -Since version 3.2, Spring Security is smart enough to no longer automatically save the SecurityContext on commiting the HttpServletResponse as soon as HttpServletRequest.startAsync() is invoked. +Since version 3.2, Spring Security is smart enough to no longer automatically save the SecurityContext on committing the HttpServletResponse as soon as HttpServletRequest.startAsync() is invoked. [[servletapi-31]] === Servlet 3.1+ Integration diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/websocket.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/websocket.adoc index beb39fa04f2..fcd99840d90 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/websocket.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/integrations/websocket.adoc @@ -4,7 +4,7 @@ Spring Security 4 added support for securing https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html[Spring's WebSocket support]. This section describes how to use Spring Security's WebSocket support. -NOTE: You can find a complete working sample of WebSocket security at https://github.com/spring-projects/spring-session/tree/master/samples/boot/websocket. +NOTE: You can find a complete working sample of WebSocket security at https://github.com/spring-projects/spring-session/tree/master/spring-session-samples/spring-session-sample-boot-websocket. .Direct JSR-356 Support **** diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/java-configuration/index.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/java-configuration/index.adoc index 8792c034320..8b762451229 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/java-configuration/index.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/java-configuration/index.adoc @@ -284,7 +284,7 @@ The code is invoked in the following order: * Code in `MyCustomDsl`s init method is invoked * Code in `MyCustomDsl`s configure method is invoked -If you want, you can have `WebSecurityConfiguerAdapter` add `MyCustomDsl` by default by using `SpringFactories`. +If you want, you can have `WebSecurityConfigurerAdapter` add `MyCustomDsl` by default by using `SpringFactories`. For example, you would create a resource on the classpath named `META-INF/spring.factories` with the following contents: .META-INF/spring.factories diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/kotlin-configuration/index.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/kotlin-configuration/index.adoc index ad02b592fec..2e38e0ec35a 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/kotlin-configuration/index.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/kotlin-configuration/index.adoc @@ -4,7 +4,7 @@ Spring Security Kotlin Configuration support has been available since Spring Security 5.3. It enables users to easily configure Spring Security using a native Kotlin DSL. -NOTE: Spring Security provides https://github.com/spring-projects/spring-security/tree/master/samples/boot/kotlin[a sample applications] which demonstrates the use of Spring Security Kotlin Configuration. +NOTE: Spring Security provides https://github.com/spring-projects/spring-security/tree/master/samples/boot/kotlin[a sample application] which demonstrates the use of Spring Security Kotlin Configuration. [[kotlin-config-httpsecurity]] == HttpSecurity diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-client.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-client.adoc index d122839a896..c4ae5ac8ceb 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-client.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-client.adoc @@ -68,6 +68,27 @@ 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 <>: + +.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: @@ -145,12 +166,13 @@ public final class ClientRegistration { private String tokenUri; <10> private UserInfoEndpoint userInfoEndpoint; private String jwkSetUri; <11> - private Map configurationMetadata; <12> + private String issuerUri; <12> + private Map configurationMetadata; <13> public class UserInfoEndpoint { - private String uri; <13> - private AuthenticationMethod authenticationMethod; <14> - private String userNameAttributeName; <15> + private String uri; <14> + private AuthenticationMethod authenticationMethod; <15> + private String userNameAttributeName; <16> } } @@ -172,12 +194,13 @@ The name may be used in certain scenarios, such as when displaying the name of t <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> `configurationMetadata`: The https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig[OpenID Provider Configuration Information]. +<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. -<13> `(userInfoEndpoint)uri`: The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user. -<14> `(userInfoEndpoint)authenticationMethod`: The authentication method used when sending the access token to the UserInfo Endpoint. +<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*. -<15> `userNameAttributeName`: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user. +<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]. @@ -294,6 +317,8 @@ 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. @@ -327,6 +352,10 @@ public OAuth2AuthorizedClientManager 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()`. @@ -375,6 +404,36 @@ private Function> contextAttributesM } ---- +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: + +[source,java] +---- +@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; +} +---- + [[oauth2Client-auth-grant-support]] === Authorization Grant Support @@ -484,7 +543,7 @@ One of those extended parameters is the `prompt` parameter. [NOTE] OPTIONAL. Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are: none, login, consent, select_account -The following example shows how to implement an `OAuth2AuthorizationRequestResolver` that customizes the Authorization Request for `oauth2Login()`, by including the request parameter `prompt=consent`. +The following example shows how to configure the `DefaultOAuth2AuthorizationRequestResolver` with a `Consumer` that customizes the Authorization Request for `oauth2Login()`, by including the request parameter `prompt=consent`. [source,java] ---- @@ -503,72 +562,32 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { .oauth2Login(oauth2 -> oauth2 .authorizationEndpoint(authorization -> authorization .authorizationRequestResolver( - new CustomAuthorizationRequestResolver( - this.clientRegistrationRepository) <1> + authorizationRequestResolver(this.clientRegistrationRepository) ) ) ); } -} - -public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { - private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver; - public CustomAuthorizationRequestResolver( + private OAuth2AuthorizationRequestResolver authorizationRequestResolver( ClientRegistrationRepository clientRegistrationRepository) { - this.defaultAuthorizationRequestResolver = + DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( clientRegistrationRepository, "/oauth2/authorization"); - } - - @Override - public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { - OAuth2AuthorizationRequest authorizationRequest = - this.defaultAuthorizationRequestResolver.resolve(request); <2> - - return authorizationRequest != null ? <3> - customAuthorizationRequest(authorizationRequest) : - null; - } + authorizationRequestResolver.setAuthorizationRequestCustomizer( + authorizationRequestCustomizer()); - @Override - public OAuth2AuthorizationRequest resolve( - HttpServletRequest request, String clientRegistrationId) { - - OAuth2AuthorizationRequest authorizationRequest = - this.defaultAuthorizationRequestResolver.resolve( - request, clientRegistrationId); <2> - - return authorizationRequest != null ? <3> - customAuthorizationRequest(authorizationRequest) : - null; + return authorizationRequestResolver; } - private OAuth2AuthorizationRequest customAuthorizationRequest( - OAuth2AuthorizationRequest authorizationRequest) { - - Map additionalParameters = - new LinkedHashMap<>(authorizationRequest.getAdditionalParameters()); - additionalParameters.put("prompt", "consent"); <4> - - return OAuth2AuthorizationRequest.from(authorizationRequest) <5> - .additionalParameters(additionalParameters) <6> - .build(); + private Consumer authorizationRequestCustomizer() { + return customizer -> customizer + .additionalParameters(params -> params.put("prompt", "consent")); } } ---- -<1> Configure the custom `OAuth2AuthorizationRequestResolver` -<2> Attempt to resolve the `OAuth2AuthorizationRequest` using the `DefaultOAuth2AuthorizationRequestResolver` -<3> If an `OAuth2AuthorizationRequest` was resolved than return a customized version else return `null` -<4> Add custom parameters to the existing `OAuth2AuthorizationRequest.additionalParameters` -<5> Create a copy of the default `OAuth2AuthorizationRequest` which returns an `OAuth2AuthorizationRequest.Builder` for further modifications -<6> Override the default `additionalParameters` -[TIP] -`OAuth2AuthorizationRequest.Builder.build()` constructs the `OAuth2AuthorizationRequest.authorizationRequestUri`, which represents the complete Authorization Request URI including all query parameters using the `application/x-www-form-urlencoded` format. - -For the simple use case, where the additional request parameter is always the same for a specific provider, it can be added directly in the `authorization-uri`. +For the simple use case, where the additional request parameter is always the same for a specific provider, it may be added directly in the `authorization-uri` property. For example, if the value for the request parameter `prompt` is always `consent` for the provider `okta`, than simply configure as follows: @@ -584,24 +603,19 @@ spring: ---- The preceding example shows the common use case of adding a custom parameter on top of the standard parameters. -Alternatively, if your requirements are more advanced, than you can take full control in building the Authorization Request URI by simply overriding the `OAuth2AuthorizationRequest.authorizationRequestUri` property. +Alternatively, if your requirements are more advanced, you can take full control in building the Authorization Request URI by simply overriding the `OAuth2AuthorizationRequest.authorizationRequestUri` property. + +[TIP] +`OAuth2AuthorizationRequest.Builder.build()` constructs the `OAuth2AuthorizationRequest.authorizationRequestUri`, which represents the Authorization Request URI including all query parameters using the `application/x-www-form-urlencoded` format. -The following example shows a variation of the `customAuthorizationRequest()` method from the preceding example, and instead overrides the `OAuth2AuthorizationRequest.authorizationRequestUri` property. +The following example shows a variation of `authorizationRequestCustomizer()` from the preceding example, and instead overrides the `OAuth2AuthorizationRequest.authorizationRequestUri` property. [source,java] ---- -private OAuth2AuthorizationRequest customAuthorizationRequest( - OAuth2AuthorizationRequest authorizationRequest) { - - String customAuthorizationRequestUri = UriComponentsBuilder - .fromUriString(authorizationRequest.getAuthorizationRequestUri()) - .queryParam("prompt", "consent") - .build(true) - .toUriString(); - - return OAuth2AuthorizationRequest.from(authorizationRequest) - .authorizationRequestUri(customAuthorizationRequestUri) - .build(); +private Consumer authorizationRequestCustomizer() { + return customizer -> customizer + .authorizationRequestUri(uriBuilder -> uriBuilder + .queryParam("prompt", "consent").build()); } ---- @@ -655,8 +669,17 @@ class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { } } ---- -==== +.Xml +[source,xml,role="secondary"] +---- + + + + + +---- +==== ===== Requesting an Access Token @@ -739,6 +762,16 @@ class OAuth2ClientSecurityConfig : WebSecurityConfigurerAdapter() { } } ---- + +.Xml +[source,xml,role="secondary"] +---- + + + + + +---- ==== diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-login.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-login.adoc index 6badf5a7966..fbaff1ee2b2 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-login.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-login.adoc @@ -131,14 +131,16 @@ The following table outlines the mapping of the Spring Boot 2.x OAuth Client pro |`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]_.userNameAttribute` +|`spring.security.oauth2.client.provider._[providerId]_.user-name-attribute` |`providerDetails.userInfoEndpoint.userNameAttributeName` |=== diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc index ae338e60e07..f3844ed84ae 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-resourceserver.adoc @@ -1,5 +1,7 @@ [[oauth2resourceserver]] == OAuth 2.0 Resource Server +:figures: images/servlet/oauth2 +:icondir: images/icons Spring Security supports protecting endpoints using two forms of OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]: @@ -9,12 +11,55 @@ Spring Security supports protecting endpoints using two forms of OAuth 2.0 https This is handy in circumstances where an application has delegated its authority management to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity). This authorization server can be consulted by resource servers to authorize requests. +This section provides details on how Spring Security provides support for OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]. + [NOTE] ==== Working samples for both {gh-samples-url}/boot/oauth2resourceserver[JWTs] and {gh-samples-url}/boot/oauth2resourceserver-opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security repository]. ==== -=== Dependencies +Let's take a look at how Bearer Token Authentication works within Spring Security. +First, we see that, like <>, the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client. + +.Sending WWW-Authenticate Header +image::{figures}/bearerauthenticationentrypoint.png[] + +The figure above builds off our <> diagram. + +image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized. + +image:{icondir}/number_2.png[] Spring Security's <> indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. + +image:{icondir}/number_3.png[] Since the user is not authenticated, <> initiates __Start Authentication__. +The configured <> is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`] which sends a WWW-Authenticate header. +The `RequestCache` is typically a `NullRequestCache` that does not save the request since the client is capable of replaying the requests it originally requested. + +When a client receives the `WWW-Authenticate: Bearer` header, it knows it should retry with a bearer token. +Below is the flow for the bearer token being processed. + +[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]] +.Authenticating Bearer Token +image::{figures}/bearertokenauthenticationfilter.png[] + +The figure builds off our <> diagram. + +image:{icondir}/number_1.png[] When the user submits their bearer token, the `BearerTokenAuthenticationFilter` creates a `BearerTokenAuthenticationToken` which is a type of <> by extracting the token from the `HttpServletRequest`. + +image:{icondir}/number_2.png[] Next, the `HttpServletRequest` is passed to the `AuthenticationManagerResolver`, which selects the `AuthenticationManager`. The `BearerTokenAuthenticationToken` is passed into the `AuthenticationManager` to be authenticated. +The details of what `AuthenticationManager` looks like depends on whether you're configured for <> or <>. + +image:{icondir}/number_3.png[] If authentication fails, then __Failure__ + +* The <> is cleared out. +* The `AuthenticationEntryPoint` is invoked to trigger the WWW-Authenticate header to be sent again. + +image:{icondir}/number_4.png[] If authentication is successful, then __Success__. + +* The <> is set on the <>. +* The `BearerTokenAuthenticationFilter` invokes `FilterChain.doFilter(request,response)` to continue with the rest of the application logic. + +[[oauth2resourceserver-jwt-minimaldependencies]] +=== Minimal Dependencies for JWT Most Resource Server support is collected into `spring-security-oauth2-resource-server`. However, the support for decoding and verifying JWTs is in `spring-security-oauth2-jose`, meaning that both are necessary in order to have a working resource server that supports JWT-encoded Bearer Tokens. @@ -77,20 +122,46 @@ So long as this scheme is indicated, Resource Server will attempt to process the Given a well-formed JWT, Resource Server will: -1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWTs header -2. Validate the JWTs `exp` and `nbf` timestamps and the JWTs `iss` claim, and +1. Validate its signature against a public key obtained from the `jwks_url` endpoint during startup and matched against the JWT +2. Validate the JWT's `exp` and `nbf` timestamps and the JWT's `iss` claim, and 3. Map each scope to an authority with the prefix `SCOPE_`. [NOTE] -As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate the JWT tokens. +As the authorization server makes available new keys, Spring Security will automatically rotate the keys used to validate JWTs. The resulting `Authentication#getPrincipal`, by default, is a Spring Security `Jwt` object, and `Authentication#getName` maps to the JWT's `sub` property, if one is present. From here, consider jumping to: -<> +* <> +* <> +* <> + +[[oauth2resourceserver-jwt-architecture]] +=== How JWT Authentication Works + +Next, let's see the architectural components that Spring Security uses to support https://tools.ietf.org/html/rfc7519[JWT] Authentication in servlet-based applications, like the one we just saw. -<> +{security-api-url}org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationProvider.html[`JwtAuthenticationProvider`] is an <> implementation that leverages a <> and <> to authenticate a JWT. + +Let's take a look at how `JwtAuthenticationProvider` works within Spring Security. +The figure explains details of how the <> in figures from <> works. + +.`JwtAuthenticationProvider` Usage +image::{figures}/jwtauthenticationprovider.png[] + +image:{icondir}/number_1.png[] The authentication `Filter` from <> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by <>. + +image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an <> of type `JwtAuthenticationProvider`. + +[[oauth2resourceserver-jwt-architecture-jwtdecoder]] +image:{icondir}/number_3.png[] `JwtAuthenticationProvider` decodes, verifies, and validates the `Jwt` using a <>. + +[[oauth2resourceserver-jwt-architecture-jwtauthenticationconverter]] +image:{icondir}/number_4.png[] `JwtAuthenticationProvider` then uses the <> to convert the `Jwt` into a `Collection` of granted authorities. + +image:{icondir}/number_5.png[] When authentication is successful, the <> that is returned is of type `JwtAuthenticationToken` and has a principal that is the `Jwt` returned by the configured `JwtDecoder`. +Ultimately, the returned `JwtAuthenticationToken` will be set on the <> by the authentication `Filter`. [[oauth2resourceserver-jwt-jwkseturi]] === Specifying the Authorization Server JWK Set Uri Directly @@ -206,8 +277,8 @@ The above requires the scope of `message:read` for any URL that starts with `/me Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration. -For example, the second `@Bean` Spring Boot creates is a `JwtDecoder`, which decodes `String` tokens into validated instances of `Jwt`: - +[[oauth2resourceserver-jwt-decoder]] +For example, the second `@Bean` Spring Boot creates is a `JwtDecoder`, which <>: .JWT Decoder ==== @@ -323,7 +394,7 @@ Using `jwkSetUri()` takes precedence over any configuration property. [[oauth2resourceserver-jwt-decoder-dsl]] ==== Using `decoder()` -More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of `JwtDecoder`: +More powerful than `jwkSetUri()` is `decoder()`, which will completely replace any Boot auto configuration of <>: .JWT Decoder Configuration ==== @@ -383,7 +454,7 @@ This is handy when deeper configuration, like <> `@Bean` has the same effect as `decoder()`: [source,java] ---- @@ -629,65 +700,30 @@ However, there are a number of circumstances where this default is insufficient. For example, some authorization servers don't use the `scope` attribute, but instead have their own custom attribute. Or, at other times, the resource server may need to adapt the attribute or a composition of attributes into internalized authorities. -To this end, the DSL exposes `jwtAuthenticationConverter()`: +To this end, Spring Security ships with `JwtAuthenticationConverter`, which is responsible for <>. +By default, Spring Security will wire the `JwtAuthenticationProvider` with a default instance of `JwtAuthenticationConverter`. + +As part of configuring a `JwtAuthenticationConverter`, you can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities. -.Authorities Extractor Configuration +Let's say that that your authorization server communicates authorities in a custom claim called `authorities`. +In that case, you can configure the claim that <> should inspect, like so: + +.Authorities Claim Configuration ==== .Java [source,java,role="primary"] ---- -@EnableWebSecurity -public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter { - protected void configure(HttpSecurity http) { - http - .authorizeRequests(authorize -> authorize - .anyRequest().authenticated() - ) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(jwt -> jwt - .jwtAuthenticationConverter(grantedAuthoritiesExtractor()) - ) - ); - } -} - -Converter grantedAuthoritiesExtractor() { - JwtAuthenticationConverter jwtAuthenticationConverter = - new JwtAuthenticationConverter(); - - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter - (new GrantedAuthoritiesExtractor()); +@Bean +public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); + JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); return jwtAuthenticationConverter; } ---- -.Kotlin -[source,kotlin,role="secondary"] ----- -@EnableWebSecurity -class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { - override fun configure(http: HttpSecurity) { - http { - authorizeRequests { - authorize(anyRequest, authenticated) - } - oauth2ResourceServer { - jwt { - jwtAuthenticationConverter = grantedAuthoritiesExtractor() - } - } - } - } - - private fun grantedAuthoritiesExtractor(): JwtAuthenticationConverter { - val jwtAuthenticationConverter = JwtAuthenticationConverter() - jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(GrantedAuthoritiesExtractor()) - return jwtAuthenticationConverter - } -} ----- - .Xml [source,xml,role="secondary"] ---- @@ -696,40 +732,66 @@ class DirectlyConfiguredJwkSetUri : WebSecurityConfigurerAdapter() { + jwt-authentication-converter-ref="jwtAuthenticationConverter"/> - - - - + + + + + ---- ==== -which is responsible for converting a `Jwt` into an `Authentication`. -As part of its configuration, we can supply a subsidiary converter to go from `Jwt` to a `Collection` of granted authorities. +You can also configure the authority prefix to be different as well. +Instead of prefixing each authority with `SCOPE_`, you can change it to `ROLE_` like so: -That final converter might be something like `GrantedAuthoritiesExtractor` below: +.Authorities Prefix Configuration +==== +.Java +[source,java,role="primary"] +---- +@Bean +public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); -[source,java] + JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter); + return jwtAuthenticationConverter; +} ---- -static class GrantedAuthoritiesExtractor - implements Converter> { - public Collection convert(Jwt jwt) { - Collection authorities = (Collection) - jwt.getClaims().getOrDefault("mycustomclaim", Collections.emptyList()); +.Xml +[source,xml,role="secondary"] +---- + + + + + + + - return authorities.stream() - .map(Object::toString) - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toList()); - } -} + + + + + + + ---- +==== + +Or, you can remove the prefix altogether by calling `JwtGrantedAuthoritiesConverter#setAuthorityPrefix("")`. For more flexibility, the DSL supports entirely replacing the converter with any class that implements `Converter`: @@ -740,6 +802,23 @@ static class CustomAuthenticationConverter implements Converter authorize + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(new CustomAuthenticationConverter()) + ) + ); + } +} ---- [[oauth2resourceserver-jwt-validation]] @@ -815,7 +894,7 @@ OAuth2TokenValidator audienceValidator() { } ---- -Then, to add into a resource server, it's a matter of specifying the `JwtDecoder` instance: +Then, to add into a resource server, it's a matter of specifying the <> instance: [source,java] ---- @@ -944,8 +1023,8 @@ To adjust the way in which Resource Server connects to the authorization server, @Bean public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { RestOperations rest = builder - .setConnectionTimeout(60000) - .setReadTimeout(60000) + .setConnectTimeout(Duration.ofSeconds(60)) + .setReadTimeout(Duration.ofSeconds(60)) .build(); NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build(); @@ -953,6 +1032,34 @@ public JwtDecoder jwtDecoder(RestTemplateBuilder builder) { } ``` +Also by default, Resource Server caches in-memory the authorization server's JWK set for 5 minutes, which you may want to adjust. +Further, it doesn't take into account more sophisticated caching patterns like eviction or using a shared cache. + +To adjust the way in which Resource Server caches the JWK set, `NimbusJwtDecoder` accepts an instance of `Cache`: + +```java +@Bean +public JwtDecoder jwtDecoder(CacheManager cacheManager) { + return NimbusJwtDecoder.withJwtSetUri(jwkSetUri) + .cache(cacheManager.getCache("jwks")) + .build(); +} +``` + +When given a `Cache`, Resource Server will use the JWK Set Uri as the key and the JWK Set JSON as the value. + +NOTE: Spring isn't a cache provider, so you'll need to make sure to include the appropriate dependencies, like `spring-boot-starter-cache` and your favorite caching provider. + +NOTE: Whether it's socket or cache timeouts, you may instead want to work with Nimbus directly. +To do so, remember that `NimbusJwtDecoder` ships with a constructor that takes Nimbus's `JWTProcessor`. + +[[oauth2resourceserver-opaque-minimaldependencies]] +=== Minimal Dependencies for Introspection +As described in <> most of Resource Server support is collected in `spring-security-oauth2-resource-server`. +However unless a custom <> is provided, the Resource Server will fallback to NimbusOpaqueTokenIntrospector. +Meaning that both `spring-security-oauth2-resource-server` and `oauth2-oidc-sdk` are necessary in order to have a working minimal Resource Server that supports opaque Bearer Tokens. +Please refer to `spring-security-oauth2-resource-server` in order to determin the correct version for `oauth2-oidc-sdk`. + [[oauth2resourceserver-opaque-minimalconfiguration]] === Minimal Configuration for Introspection @@ -1015,10 +1122,33 @@ The resulting `Authentication#getPrincipal`, by default, is a Spring Security `{ From here, you may want to jump to: +* <> * <> * <> * <> +[[oauth2resourceserver-opaque-architecture]] +=== How Opaque Token Authentication Works + +Next, let's see the architectural components that Spring Security uses to support https://tools.ietf.org/html/rfc7662[opaque token] Authentication in servlet-based applications, like the one we just saw. + +{security-api-url}org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProvider.html[`OpaqueTokenAuthenticationProvider`] is an <> implementation that leverages a <> to authenticate an opaque token. + +Let's take a look at how `OpaqueTokenAuthenticationProvider` works within Spring Security. +The figure explains details of how the <> in figures from <> works. + +.`OpaqueTokenAuthenticationProvider` Usage +image::{figures}/opaquetokenauthenticationprovider.png[] + +image:{icondir}/number_1.png[] The authentication `Filter` from <> passes a `BearerTokenAuthenticationToken` to the `AuthenticationManager` which is implemented by <>. + +image:{icondir}/number_2.png[] The `ProviderManager` is configured to use an <> of type `OpaqueTokenAuthenticationProvider`. + +[[oauth2resourceserver-opaque-architecture-introspector]] +image:{icondir}/number_3.png[] `OpaqueTokenAuthenticationProvider` introspects the opaque token and adds granted authorities using an <>. +When authentication is successful, the <> that is returned is of type `BearerTokenAuthentication` and has a principal that is the `OAuth2AuthenticatedPrincipal` returned by the configured <>. +Ultimately, the returned `BearerTokenAuthentication` will be set on the <> by the authentication `Filter`. + [[oauth2resourceserver-opaque-attributes]] === Looking Up Attributes Post-Authentication @@ -1147,7 +1277,8 @@ The above requires the scope of `message:read` for any URL that starts with `/me Methods on the `oauth2ResourceServer` DSL will also override or replace auto configuration. -For example, the second `@Bean` Spring Boot creates is an `OpaqueTokenIntrospector`, which decodes `String` tokens into validated instances of `OAuth2AuthenticatedPrincipal`: +[[oauth2resourceserver-opaque-introspector]] +For example, the second `@Bean` Spring Boot creates is an `OpaqueTokenIntrospector`, <>: [source,java] ---- @@ -1157,11 +1288,11 @@ public OpaqueTokenIntrospector introspector() { } ---- -If the application doesn't expose a `OpaqueTokenIntrospector` bean, then Spring Boot will expose the above default one. +If the application doesn't expose a <> bean, then Spring Boot will expose the above default one. And its configuration can be overridden using `introspectionUri()` and `introspectionClientCredentials()` or replaced using `introspector()`. -Or, if you're not using Spring Boot at all, then both of these components - the filter chain and a `OpaqueTokenIntrospector` can be specified in XML. +Or, if you're not using Spring Boot at all, then both of these components - the filter chain and a <> can be specified in XML. The filter chain is specified like so: @@ -1179,7 +1310,7 @@ The filter chain is specified like so: ---- ==== -And the `OpaqueTokenIntrospector` like so: +And the <> like so: .Opaque Token Introspector ==== @@ -1260,7 +1391,7 @@ Using `introspectionUri()` takes precedence over any configuration property. [[oauth2resourceserver-opaque-introspector-dsl]] ==== Using `introspector()` -More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of `OpaqueTokenIntrospector`: +More powerful than `introspectionUri()` is `introspector()`, which will completely replace any Boot auto configuration of <>: .Introspector Configuration ==== @@ -1320,7 +1451,7 @@ This is handy when deeper configuration, like <> `@Bean` has the same effect as `introspector()`: [source,java] ---- @@ -1397,7 +1528,7 @@ For example, if the introspection response were: Then Resource Server would generate an `Authentication` with two authorities, one for `message:read` and the other for `message:write`. -This can, of course, be customized using a custom `OpaqueTokenIntrospector` that takes a look at the attribute set and converts in its own way: +This can, of course, be customized using a custom <> that takes a look at the attribute set and converts in its own way: [source,java] ---- @@ -1442,11 +1573,11 @@ To adjust the way in which Resource Server connects to the authorization server, ```java @Bean -public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder) { +public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder, OAuth2ResourceServerProperties properties) { RestOperations rest = builder - .basicAuthentication(clientId, clientSecret) - .setConnectionTimeout(60000) - .setReadTimeout(60000) + .basicAuthentication(properties.getOpaquetoken().getClientId(), properties.getOpaquetoken().getClientSecret()) + .setConnectionTimeout(Duration.ofSeconds(60)) + .setReadTimeout(Duration.ofSeconds(60)) .build(); return new NimbusOpaqueTokenIntrospector(introspectionUri, rest); @@ -1481,7 +1612,7 @@ Any attributes in the corresponding `OAuth2AuthenticatedPrincipal` would be what But, let's say that, oddly enough, the introspection endpoint only returns whether or not the token is active. Now what? -In this case, you can create a custom `OpaqueTokenIntrospector` that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes: +In this case, you can create a custom <> that still hits the endpoint, but then updates the returned principal to have the JWTs claims as the attributes: [source,java] ---- @@ -1526,7 +1657,7 @@ Generally speaking, a Resource Server doesn't care about the underlying user, bu That said, at times it can be valuable to tie the authorization statement back to a user. -If an application is also using `spring-security-oauth2-client`, having set up the appropriate `ClientRegistrationRepository`, then this is quite simple with a custom `OpaqueTokenIntrospector`. +If an application is also using `spring-security-oauth2-client`, having set up the appropriate `ClientRegistrationRepository`, then this is quite simple with a custom <>. This implementation below does three things: * Delegates to the introspection endpoint, to affirm the token's validity @@ -1575,7 +1706,7 @@ public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector } ---- -Either way, having created your `OpaqueTokenIntrospector`, you should publish it as a `@Bean` to override the defaults: +Either way, having created your <>, you should publish it as a `@Bean` to override the defaults: [source,java] ---- @@ -1602,8 +1733,7 @@ AuthenticationManagerResolver tokenAuthenticationManagerReso OpaqueTokenAuthenticationProvider opaqueToken = opaqueToken(); return request -> { - String token = bearerToken.resolve(request); - if (isAJwt(token)) { + if (useJwt(request)) { return jwt::authenticate; } else { return opaqueToken::authenticate; @@ -1612,6 +1742,8 @@ AuthenticationManagerResolver tokenAuthenticationManagerReso } ---- +NOTE: The implementation of `useJwt(HttpServletRequest)` will likely depend on custom request material like the path. + And then specify this `AuthenticationManagerResolver` in the DSL: .Authentication Manager Resolver @@ -1725,13 +1857,13 @@ In this case, you construct `JwtIssuerAuthenticationManagerResolver` with a stra This approach allows us to add and remove elements from the repository (shown as a `Map` in the snippet) at runtime. NOTE: It would be unsafe to simply take any issuer and construct an `AuthenticationManager` from it. -The issuer should be one that the code can verify from a trusted source like a whitelist. +The issuer should be one that the code can verify from a trusted source like a list of allowed issuers. ===== Parsing the Claim Only Once -You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the `JwtDecoder` later on in the request. +You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the <> later on in the request. -This extra parsing can be alleviated by configuring the `JwtDecoder` directly with a `JWTClaimSetAwareJWSKeySelector` from Nimbus: +This extra parsing can be alleviated by configuring the <> directly with a `JWTClaimSetAwareJWSKeySelector` from Nimbus: [source,java] ---- @@ -1775,7 +1907,7 @@ public class TenantJWSKeySelector ---- <1> A hypothetical source for tenant information <2> A cache for `JWKKeySelector`s, keyed by tenant identifier -<3> Looking up the tenant is more secure than simply calculating the JWK Set endpoint on the fly - the lookup acts as a tenant whitelist +<3> Looking up the tenant is more secure than simply calculating the JWK Set endpoint on the fly - the lookup acts as a list of allowed tenants <4> Create a `JWSKeySelector` via the types of keys that come back from the JWK Set endpoint - the lazy lookup here means that you don't need to configure all tenants at startup The above key selector is a composition of many key selectors. @@ -1833,7 +1965,7 @@ public class TenantJwtIssuerValidator implements OAuth2TokenValidator { } ---- -Now that we have a tenant-aware processor and a tenant-aware validator, we can proceed with creating our `JwtDecoder`: +Now that we have a tenant-aware processor and a tenant-aware validator, we can proceed with creating our <>: [source,java] ---- @@ -1858,22 +1990,24 @@ However, if you resolve it by a claim in the bearer token, read on to learn abou === Bearer Token Resolution By default, Resource Server looks for a bearer token in the `Authorization` header. -This, however, can be customized in a couple of ways. +This, however, can be customized in a handful of ways. ==== Reading the Bearer Token from a Custom Header For example, you may have a need to read the bearer token from a custom header. -To achieve this, you can wire a `HeaderBearerTokenResolver` instance into the DSL, as you can see in the following example: +To achieve this, you can expose a `DefaultBearerTokenResolver` as a bean, or wire an instance into the DSL, as you can see in the following example: .Custom Bearer Token Header ==== .Java [source,java,role="primary"] ---- -http - .oauth2ResourceServer(oauth2 -> oauth2 - .bearerTokenResolver(new HeaderBearerTokenResolver("x-goog-iap-jwt-assertion")) - ); +@Bean +BearerTokenResolver bearerTokenResolver() { + DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); + bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION); + return bearerTokenResolver; +} ---- .Xml @@ -1884,12 +2018,14 @@ http - + class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver"> + ---- ==== +Or, in circumstances where a provider is using both a custom header and value, you can use `HeaderBearerTokenResolver` instead. + ==== Reading the Bearer Token from a Form Parameter Or, you may wish to read the token from a form parameter, which you can do by configuring the `DefaultBearerTokenResolver`, as you can see below: @@ -1923,7 +2059,7 @@ http === Bearer Token Propagation -Now that you're in possession of a bearer token, it might be handy to pass that to downstream services. +Now that you're resource server has validated the token, it might be handy to pass it to downstream services. This is quite simple with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.html[ServletBearerExchangeFilterFunction]`, which you can see in the following example: [source,java] @@ -1967,12 +2103,12 @@ this.rest.get() In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain. [NOTE] -Unlike the https://docs.spring.io/spring-security/site/docs/current-SNAPSHOT/api/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.html[OAuth 2.0 Client filter function], this filter function makes no attempt to renew the token, should it be expired. +Unlike the {security-api-url}org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.html[OAuth 2.0 Client filter function], this filter function makes no attempt to renew the token, should it be expired. To obtain this level of support, please use the OAuth 2.0 Client filter. ==== `RestTemplate` support -There is no dedicated support for `RestTemplate` at the moment, but you can achieve propagation quite simply with your own interceptor: +There is no `RestTemplate` equivalent for `ServletBearerExchangeFilterFunction` at the moment, but you can propagate the request's bearer token quite simply with your own interceptor: [source,java] ---- @@ -1997,6 +2133,11 @@ RestTemplate rest() { } ---- + +[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 <>. + [[oauth2resourceserver-bearertoken-failure]] === Bearer Token Failure diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc index 1af5c801019..b0112af45df 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc @@ -139,7 +139,6 @@ For example: [[servlet-saml2-rpr-relyingparty]] ===== Relying Party - * `registrationId` - (required) a unique identifer for this configuration mapping. This identifier may be used in URI paths, so care should be taken that no URI encoding is required. * `localEntityIdTemplate` - (optional) A URI pattern that creates an entity ID for this application based on the incoming request. The default is @@ -150,15 +149,11 @@ http://localhost:8080/saml2/service-provider-metadata/my-test-configuration ``` There is no requirement that this configuration option is a pattern, it can be a fixed URI value. -* `remoteIdpEntityId` - (required) the entity ID of the Identity Provider. Always a fixed URI value or string, -no patterns allowed. * `assertionConsumerServiceUrlTemplate` - (optional) A URI pattern that denotes the assertion consumer service URI to be sent with any `AuthNRequest` from the SP to the IDP during the SP initiated flow. While this can be a pattern the actual URI must resolve to the ACS endpoint on the SP. The default value is `+{baseUrl}/login/saml2/sso/{registrationId}+` and maps directly to the https://github.com/spring-projects/spring-security/blob/5.2.0.RELEASE/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java#L42[`Saml2WebSsoAuthenticationFilter`] endpoint -* `idpWebSsoUrl` - (required) a fixed URI value for the IDP Single Sign On endpoint where -the SP sends the `AuthNRequest` messages. * `credentials` - A list of credentials, private keys and x509 certificates, used for message signing, verification, encryption and decryption. This list can contain redundant credentials to allow for easy rotation of credentials. @@ -170,6 +165,12 @@ Encryption is always done using the first `ENCRYPTION` key in the list. ** [2] - PrivateKey/X509Certificate{SIGNING,DECRYPTION} - The SP's first signing and decryption credential. ** [3] - PrivateKey/X509Certificate{SIGNING,DECRYPTION} - The SP's second decryption credential. Signing is always done using the first `SIGNING` key in the list. +* `ProviderDetails#entityId` - (required) the entity ID of the Identity Provider. Always a fixed URI value or string, +no patterns allowed. +* `ProviderDetails#webSsoUrl` - (required) a fixed URI value for the IDP Single Sign On endpoint where +the SP sends the `AuthNRequest` messages. +* `ProviderDetails#signAuthNRequest` - A boolean indicating whether or not to sign the `AuthNRequest` with the SP's private key, defaults to `true` +* `ProviderDetails#binding` - A `Saml2MessageBinding` indicating what kind of binding to use for the `AuthNRequest`, whether that be `REDIRECT` or `POST`, defaults to `REDIRECT` When an incoming message is received, signatures are always required, the system will first attempt to validate the signature using the certificate at index [0] and only move to the second @@ -216,16 +217,68 @@ credentials must be shared with the Identity Provider [[servlet-saml2-sp-initiated]] ==== Authentication Requests - SP Initiated Flow -To initiate an authentication from the web application, a simple redirect to +To initiate an authentication from the web application, you can redirect to: `+{baseUrl}/saml2/authenticate/{registrationId}+` -The endpoint will generate an `AuthNRequest` by invoking the `createAuthenticationRequest` method on a -configurable factory. Just expose the `Saml2AuthenticationRequestFactory` as a bean in your configuration. +This endpoint will generate an `AuthNRequest` either as a Redirect or POST depending on your `RelyingPartyRegistration`. + +[[servlet-saml2-sp-initiated-factory]] +==== Customizing the AuthNRequest + +To adjust the `AuthNRequest`, you can publish an instance of `Saml2AuthenticationRequestFactory`. + +For example, if you wanted to configure the `AuthNRequest` to request the IDP to send the SAML `Assertion` by REDIRECT, you could do: + [source,java] ---- -public interface Saml2AuthenticationRequestFactory { - String createAuthenticationRequest(Saml2AuthenticationRequest request); +@Bean +public Saml2AuthenticationRequestFactory authenticationRequestFactory() { + OpenSamlAuthenticationRequestFactory authenticationRequestFactory = + new OpenSamlAuthenticationRequestFactory(); + authenticationRequestFactory.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"); + return authenticationRequestFactory; +} +---- + +[[servlet-saml2-sp-initiated-factory-delegate]] +==== Delegating to an AuthenticationRequestFactory + +Or, in circumstances where you need more control over what is sent as parameters to the `AuthenticationRequestFactory`, you can use delegation: + +[source,java] +---- +@Component +public class IssuerSaml2AuthenticationRequestFactory implements Saml2AuthenticationRequestFactory { + private OpenSamlAuthenticationRequestFactory delegate = new OpenSamlAuthenticationRequestFactory(); + + @Override + public String createAuthenticationRequest(Saml2AuthenticationRequest request) { + return this.delegate.createAuthenticationRequest(request); + } + + @Override + public Saml2PostAuthenticationRequest createPostAuthenticationRequest + (Saml2AuthenticationRequestContext context) { + + String issuer = // ... calculate issuer + + Saml2AuthenticationRequestContext customIssuer = Saml2AuthenticationRequestContext.builder() + .assertionConsumerServiceUrl(context.getAssertionConsumerServiceUrl()) + .issuer(issuer) + .relayState(context.getRelayState()) + .relyingPartyRegistration(context.getRelyingPartyRegistration()) + .build(); + + return this.delegate.createPostAuthenticationRequest(customIssuer); + } + + @Override + public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest + (Saml2AuthenticationRequestContext context) { + + throw new UnsupportedOperationException("unsupported"); + } } ---- diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/test/mockmvc.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/test/mockmvc.adoc index f26caf3b1f3..2a4d4970b89 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/test/mockmvc.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/test/mockmvc.adoc @@ -309,7 +309,7 @@ assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SC Spring Security does the necessary work to make sure that the `OidcUser` instance is available for <>. -Further, it also links that `OidcUser` to a simple instance of `OAuth2AuthorizedClient` that it deposits into an `HttpSessionOAuth2AuthorizedClientRepository`. +Further, it also links that `OidcUser` to a simple instance of `OAuth2AuthorizedClient` that it deposits into an mock `OAuth2AuthorizedClientRepository`. This can be handy if your tests <>.. [[testing-oidc-login-authorities]] @@ -432,7 +432,7 @@ assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SC Spring Security does the necessary work to make sure that the `OAuth2User` instance is available for <>. -Further, it also links that `OAuth2User` to a simple instance of `OAuth2AuthorizedClient` that it deposits in an `HttpSessionOAuth2AuthorizedClientRepository`. +Further, it also links that `OAuth2User` to a simple instance of `OAuth2AuthorizedClient` that it deposits in a mock `OAuth2AuthorizedClientRepository`. This can be handy if your tests <>. [[testing-oauth2-login-authorities]] @@ -528,7 +528,7 @@ public String foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2AuthorizedCl ---- Simulating this handshake with the authorization server could be cumbersome. -Instead, you can use `SecurityMockMvcRequestPostProcessor#oauth2Client` to add a `OAuth2AuthorizedClient` into an `HttpSessionOAuth2AuthorizedClientRepository`: +Instead, you can use `SecurityMockMvcRequestPostProcessor#oauth2Client` to add a `OAuth2AuthorizedClient` into a mock `OAuth2AuthorizedClientRepository`: [source,java] ---- @@ -536,19 +536,6 @@ mvc .perform(get("/endpoint").with(oauth2Client("my-app"))); ---- -If your application isn't already using an `HttpSessionOAuth2AuthorizedClientRepository`, then you can supply one as a `@TestConfiguration`: - -[source,java] ----- -@TestConfiguration -static class AuthorizedClientConfig { - @Bean - OAuth2AuthorizedClientRepository authorizedClientRepository() { - return new HttpSessionOAuth2AuthorizedClientRepository(); - } -} ----- - What this will do is create an `OAuth2AuthorizedClient` that has a simple `ClientRegistration`, `OAuth2AccessToken`, and resource owner name. Specifically, it will include a `ClientRegistration` with a client id of "test-client" and client secret of "test-secret": @@ -574,8 +561,7 @@ assertThat(authorizedClient.getAccessToken().getScopes()).hasSize(1); assertThat(authorizedClient.getAccessToken().getScopes()).containsExactly("read"); ---- -Spring Security does the necessary work to make sure that the `OAuth2AuthorizedClient` instance is available in the associated `HttpSession`. -That means that it can be retrieved from an `HttpSessionOAuth2AuthorizedClientRepository`. +The client can then be retrieved as normal using `@RegisteredOAuth2AuthorizedClient` in a controller method. [[testing-oauth2-client-scopes]] ===== Configuring Scopes diff --git a/docs/manual/src/docs/asciidoc/images/servlet/oauth2/beareraccessdeniedhandler.odg b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/beareraccessdeniedhandler.odg new file mode 100644 index 00000000000..e35bce8c378 Binary files /dev/null and b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/beareraccessdeniedhandler.odg differ diff --git a/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearerauthenticationentrypoint.odg b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearerauthenticationentrypoint.odg new file mode 100644 index 00000000000..b4b50fb6e5a Binary files /dev/null and b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearerauthenticationentrypoint.odg differ diff --git a/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearerauthenticationentrypoint.png b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearerauthenticationentrypoint.png new file mode 100644 index 00000000000..a022951a6bc Binary files /dev/null and b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearerauthenticationentrypoint.png differ diff --git a/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearertokenauthenticationfilter.odg b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearertokenauthenticationfilter.odg new file mode 100644 index 00000000000..5dc20b4ba4c Binary files /dev/null and b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearertokenauthenticationfilter.odg differ diff --git a/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearertokenauthenticationfilter.png b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearertokenauthenticationfilter.png new file mode 100644 index 00000000000..7ea79213ec3 Binary files /dev/null and b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/bearertokenauthenticationfilter.png differ diff --git a/docs/manual/src/docs/asciidoc/images/servlet/oauth2/jwtauthenticationprovider.odg b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/jwtauthenticationprovider.odg new file mode 100644 index 00000000000..c79b7c33115 Binary files /dev/null and b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/jwtauthenticationprovider.odg differ diff --git a/docs/manual/src/docs/asciidoc/images/servlet/oauth2/jwtauthenticationprovider.png b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/jwtauthenticationprovider.png new file mode 100644 index 00000000000..8f0f1450857 Binary files /dev/null and b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/jwtauthenticationprovider.png differ diff --git a/docs/manual/src/docs/asciidoc/images/servlet/oauth2/opaquetokenauthenticationprovider.odg b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/opaquetokenauthenticationprovider.odg new file mode 100644 index 00000000000..3f47370f148 Binary files /dev/null and b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/opaquetokenauthenticationprovider.odg differ diff --git a/docs/manual/src/docs/asciidoc/images/servlet/oauth2/opaquetokenauthenticationprovider.png b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/opaquetokenauthenticationprovider.png new file mode 100644 index 00000000000..6b1c0f7c36e Binary files /dev/null and b/docs/manual/src/docs/asciidoc/images/servlet/oauth2/opaquetokenauthenticationprovider.png differ diff --git a/etc/nohttp/whitelist.lines b/etc/nohttp/allowlist.lines similarity index 100% rename from etc/nohttp/whitelist.lines rename to etc/nohttp/allowlist.lines diff --git a/gradle.properties b/gradle.properties index da09dbcb93a..8030f9eddc1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ aspectjVersion=1.9.3 -gaeVersion=1.9.78 -springBootVersion=2.2.5.RELEASE -version=5.4.0.BUILD-SNAPSHOT -kotlinVersion=1.3.70 +gaeVersion=1.9.80 +springBootVersion=2.4.0-M1 +version=5.4.0-SNAPSHOT +kotlinVersion=1.3.72 org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError org.gradle.parallel=true org.gradle.caching=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a2bf1313b8a..a4f0001d203 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-6.2.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/itest/ldap/embedded-ldap-apacheds-default/src/integration-test/resources/applicationContext-security.xml b/itest/ldap/embedded-ldap-apacheds-default/src/integration-test/resources/applicationContext-security.xml index da547d0b041..b1b80079c5b 100644 --- a/itest/ldap/embedded-ldap-apacheds-default/src/integration-test/resources/applicationContext-security.xml +++ b/itest/ldap/embedded-ldap-apacheds-default/src/integration-test/resources/applicationContext-security.xml @@ -4,6 +4,6 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd"> - + diff --git a/itest/ldap/embedded-ldap-mode-apacheds/src/integration-test/resources/applicationContext-security.xml b/itest/ldap/embedded-ldap-mode-apacheds/src/integration-test/resources/applicationContext-security.xml index f980c6ca98a..8e3f4b4380c 100644 --- a/itest/ldap/embedded-ldap-mode-apacheds/src/integration-test/resources/applicationContext-security.xml +++ b/itest/ldap/embedded-ldap-mode-apacheds/src/integration-test/resources/applicationContext-security.xml @@ -4,6 +4,6 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd"> - + diff --git a/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/applicationContext-security.xml b/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/applicationContext-security.xml index 6254829b430..9ab9bf623f4 100644 --- a/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/applicationContext-security.xml +++ b/itest/ldap/embedded-ldap-mode-unboundid/src/integration-test/resources/applicationContext-security.xml @@ -4,6 +4,6 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd"> - + diff --git a/itest/ldap/embedded-ldap-none/src/integration-test/resources/applicationContext-security.xml b/itest/ldap/embedded-ldap-none/src/integration-test/resources/applicationContext-security.xml index da547d0b041..b1b80079c5b 100644 --- a/itest/ldap/embedded-ldap-none/src/integration-test/resources/applicationContext-security.xml +++ b/itest/ldap/embedded-ldap-none/src/integration-test/resources/applicationContext-security.xml @@ -4,6 +4,6 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd"> - + diff --git a/itest/ldap/embedded-ldap-unboundid-default/src/integration-test/resources/applicationContext-security.xml b/itest/ldap/embedded-ldap-unboundid-default/src/integration-test/resources/applicationContext-security.xml index da547d0b041..b1b80079c5b 100644 --- a/itest/ldap/embedded-ldap-unboundid-default/src/integration-test/resources/applicationContext-security.xml +++ b/itest/ldap/embedded-ldap-unboundid-default/src/integration-test/resources/applicationContext-security.xml @@ -4,6 +4,6 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd"> - + diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/ApacheDsContainerConfig.java b/ldap/src/integration-test/java/org/springframework/security/ldap/ApacheDsContainerConfig.java index 29e68a48354..ec6cd0fc7ad 100644 --- a/ldap/src/integration-test/java/org/springframework/security/ldap/ApacheDsContainerConfig.java +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/ApacheDsContainerConfig.java @@ -34,13 +34,14 @@ public class ApacheDsContainerConfig { ApacheDSContainer ldapContainer() throws Exception { this.container = new ApacheDSContainer("dc=springframework,dc=org", "classpath:test-server.ldif"); + this.container.setPort(0); return this.container; } @Bean - ContextSource contextSource() throws Exception { + ContextSource contextSource(ApacheDSContainer ldapContainer) throws Exception { return new DefaultSpringSecurityContextSource("ldap://127.0.0.1:" - + ldapContainer().getPort() + "/dc=springframework,dc=org"); + + ldapContainer.getLocalPort() + "/dc=springframework,dc=org"); } @PreDestroy diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/server/ApacheDSContainerTests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/server/ApacheDSContainerTests.java index 552410af8f2..add74ddf451 100644 --- a/ldap/src/integration-test/java/org/springframework/security/ldap/server/ApacheDSContainerTests.java +++ b/ldap/src/integration-test/java/org/springframework/security/ldap/server/ApacheDSContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * 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. @@ -39,6 +39,7 @@ * @author Luke Taylor * @author Rob Winch * @author Gunnar Hillert + * @author Evgeniy Cheban * @since 3.0 */ public class ApacheDSContainerTests { @@ -212,4 +213,20 @@ private List getDefaultPorts(int count) throws IOException { } } } + + @Test + public void afterPropertiesSetWhenPortIsZeroThenRandomPortIsSelected() throws Exception { + ApacheDSContainer server = new ApacheDSContainer("dc=springframework,dc=org", + "classpath:test-server.ldif"); + server.setPort(0); + try { + server.afterPropertiesSet(); + + assertThat(server.getPort()).isEqualTo(0); + assertThat(server.getLocalPort()).isNotEqualTo(0); + } + finally { + server.destroy(); + } + } } diff --git a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java index 91a41a0f29b..afcb2fa87e2 100644 --- a/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java +++ b/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java @@ -16,6 +16,7 @@ package org.springframework.security.ldap.authentication.ad; import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.ldap.CommunicationException; import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.DistinguishedName; import org.springframework.ldap.core.support.DefaultDirObjectFactory; @@ -24,6 +25,7 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -142,12 +144,15 @@ protected DirContextOperations doAuthentication( UsernamePasswordAuthenticationToken auth) { String username = auth.getName(); String password = (String) auth.getCredentials(); - - DirContext ctx = bindAsUser(username, password); + DirContext ctx = null; try { + ctx = bindAsUser(username, password); return searchForUser(ctx, username); } + catch (CommunicationException e) { + throw badLdapConnection(e); + } catch (NamingException e) { logger.error("Failed to locate directory entry for authenticated user: " + username, e); @@ -210,8 +215,7 @@ private DirContext bindAsUser(String username, String password) { || (e instanceof OperationNotSupportedException)) { handleBindException(bindPrincipal, e); throw badCredentials(e); - } - else { + } else { throw LdapUtils.convertLdapException(e); } } @@ -313,6 +317,12 @@ private BadCredentialsException badCredentials(Throwable cause) { return (BadCredentialsException) badCredentials().initCause(cause); } + private InternalAuthenticationServiceException badLdapConnection(Throwable cause) { + return new InternalAuthenticationServiceException(messages.getMessage( + "LdapAuthenticationProvider.badLdapConnection", + "Connection to LDAP server failed."), cause); + } + private DirContextOperations searchForUser(DirContext context, String username) throws NamingException { SearchControls searchControls = new SearchControls(); @@ -327,6 +337,9 @@ private DirContextOperations searchForUser(DirContext context, String username) searchControls, searchRoot, searchFilter, new Object[] { bindPrincipal, username }); } + catch (CommunicationException ldapCommunicationException) { + throw badLdapConnection(ldapCommunicationException); + } catch (IncorrectResultSizeDataAccessException incorrectResults) { // Search should never return multiple results if properly configured - just // rethrow 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 9aad1749755..f02dd51edc8 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-2016 the original author or authors. + * 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. @@ -15,6 +15,7 @@ */ package org.springframework.security.ldap.server; +import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -39,6 +40,7 @@ import org.apache.directory.server.protocol.shared.transport.TcpTransport; import org.apache.directory.shared.ldap.exception.LdapNameNotFoundException; import org.apache.directory.shared.ldap.name.LdapDN; +import org.apache.mina.transport.socket.SocketAcceptor; import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; @@ -69,6 +71,7 @@ * @author Luke Taylor * @author Rob Winch * @author Gunnar Hillert + * @author Evgeniy Cheban * @deprecated Use {@link UnboundIdContainer} instead because ApacheDS 1.x is no longer * supported with no GA version to replace it. */ @@ -80,6 +83,7 @@ public class ApacheDSContainer implements InitializingBean, DisposableBean, Life final DefaultDirectoryService service; LdapServer server; + private TcpTransport transport; private ApplicationContext ctxt; private File workingDir; @@ -88,6 +92,7 @@ public class ApacheDSContainer implements InitializingBean, DisposableBean, Life private final JdbmPartition partition; private final String root; private int port = 53389; + private int localPort; private boolean ldapOverSslEnabled; private File keyStoreFile; @@ -143,7 +148,7 @@ public void afterPropertiesSet() throws Exception { server.setDirectoryService(service); // AbstractLdapIntegrationTests assume IPv4, so we specify the same here - TcpTransport transport = new TcpTransport(port); + this.transport = new TcpTransport(port); if (ldapOverSslEnabled) { transport.setEnableSSL(true); server.setKeystoreFile(this.keyStoreFile.getAbsolutePath()); @@ -190,6 +195,15 @@ public int getPort() { return this.port; } + /** + * Returns the port that is resolved by {@link TcpTransport}. + * + * @return the port that is resolved by {@link TcpTransport} + */ + public int getLocalPort() { + return this.localPort; + } + /** * If set to {@code true} will enable LDAP over SSL (LDAPs). If set to {@code true} * {@link ApacheDSContainer#setCertificatePassord(String)} must be set as well. @@ -262,6 +276,10 @@ public void start() { logger.error("Lookup failed", e); } + SocketAcceptor socketAcceptor = this.server.getSocketAcceptor(this.transport); + InetSocketAddress localAddress = socketAcceptor.getLocalAddress(); + this.localPort = localAddress.getPort(); + running = true; try { diff --git a/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java b/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java index 934b507d056..27226bd6310 100644 --- a/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java +++ b/ldap/src/test/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProviderTests.java @@ -15,6 +15,18 @@ */ package org.springframework.security.ldap.authentication.ad; +import java.util.Collections; +import java.util.Hashtable; +import javax.naming.AuthenticationException; +import javax.naming.CommunicationException; +import javax.naming.Name; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; + import org.apache.directory.shared.ldap.util.EmptyEnumeration; import org.hamcrest.BaseMatcher; import org.hamcrest.CoreMatchers; @@ -25,6 +37,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; + import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DistinguishedName; @@ -32,25 +45,18 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.CredentialsExpiredException; import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import javax.naming.AuthenticationException; -import javax.naming.CommunicationException; -import javax.naming.Name; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.DirContext; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; - -import java.util.Hashtable; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.ContextFactory; /** @@ -58,6 +64,9 @@ * @author Rob Winch */ public class ActiveDirectoryLdapAuthenticationProviderTests { + public static final String EXISTING_LDAP_PROVIDER = "ldap://192.168.1.200/"; + public static final String NON_EXISTING_LDAP_PROVIDER = "ldap://192.168.1.201/"; + @Rule public ExpectedException thrown = ExpectedException.none(); @@ -378,16 +387,31 @@ public void errorWithNoSubcodeIsHandledCleanly() { } @Test(expected = org.springframework.ldap.CommunicationException.class) - public void nonAuthenticationExceptionIsConvertedToSpringLdapException() { - provider.contextFactory = createContextFactoryThrowing(new CommunicationException( - msg)); - provider.authenticate(joe); + public void nonAuthenticationExceptionIsConvertedToSpringLdapException() throws Throwable { + try { + provider.contextFactory = createContextFactoryThrowing(new CommunicationException( + msg)); + provider.authenticate(joe); + } catch (InternalAuthenticationServiceException e) { + // Since GH-8418 ldap communication exception is wrapped into InternalAuthenticationServiceException. + // This test is about the wrapped exception, so we throw it. + throw e.getCause(); + } + } + + @Test(expected = org.springframework.security.authentication.InternalAuthenticationServiceException.class ) + public void connectionExceptionIsWrappedInInternalException() throws Exception { + ActiveDirectoryLdapAuthenticationProvider noneReachableProvider = new ActiveDirectoryLdapAuthenticationProvider( + "mydomain.eu", NON_EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain"); + noneReachableProvider.setContextEnvironmentProperties( + Collections.singletonMap("com.sun.jndi.ldap.connect.timeout", "5")); + noneReachableProvider.doAuthentication(joe); } @Test public void rootDnProvidedSeparatelyFromDomainAlsoWorks() throws Exception { ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider( - "mydomain.eu", "ldap://192.168.1.200/", "dc=ad,dc=eu,dc=mydomain"); + "mydomain.eu", EXISTING_LDAP_PROVIDER, "dc=ad,dc=eu,dc=mydomain"); checkAuthentication("dc=ad,dc=eu,dc=mydomain", provider); } @@ -413,8 +437,11 @@ public void contextEnvironmentPropertiesUsed() { provider.authenticate(joe); fail("CommunicationException was expected with a root cause of ClassNotFoundException"); } - catch (org.springframework.ldap.CommunicationException expected) { - assertThat(expected.getRootCause()).isInstanceOf(ClassNotFoundException.class); + catch (InternalAuthenticationServiceException expected) { + assertThat(expected.getCause()).isInstanceOf(org.springframework.ldap.CommunicationException.class); + org.springframework.ldap.CommunicationException cause = + (org.springframework.ldap.CommunicationException) expected.getCause(); + assertThat(cause.getRootCause()).isInstanceOf(ClassNotFoundException.class); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceOAuth2AuthorizedClientManager.java index 168551ba168..179e4f40a8f 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceOAuth2AuthorizedClientManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceOAuth2AuthorizedClientManager.java @@ -72,9 +72,13 @@ * @see OAuth2AuthorizationFailureHandler */ public final class AuthorizedClientServiceOAuth2AuthorizedClientManager implements OAuth2AuthorizedClientManager { + private static final OAuth2AuthorizedClientProvider DEFAULT_AUTHORIZED_CLIENT_PROVIDER = + OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); private final ClientRegistrationRepository clientRegistrationRepository; private final OAuth2AuthorizedClientService authorizedClientService; - private OAuth2AuthorizedClientProvider authorizedClientProvider = context -> null; + private OAuth2AuthorizedClientProvider authorizedClientProvider; private Function> contextAttributesMapper; private OAuth2AuthorizationSuccessHandler authorizationSuccessHandler; private OAuth2AuthorizationFailureHandler authorizationFailureHandler; @@ -91,6 +95,7 @@ public AuthorizedClientServiceOAuth2AuthorizedClientManager(ClientRegistrationRe Assert.notNull(authorizedClientService, "authorizedClientService cannot be null"); this.clientRegistrationRepository = clientRegistrationRepository; this.authorizedClientService = authorizedClientService; + this.authorizedClientProvider = DEFAULT_AUTHORIZED_CLIENT_PROVIDER; this.contextAttributesMapper = new DefaultContextAttributesMapper(); this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> authorizedClientService.saveAuthorizedClient(authorizedClient, principal); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.java index 70393299b8f..3a446ec2da8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.java @@ -69,9 +69,13 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager { + private static final ReactiveOAuth2AuthorizedClientProvider DEFAULT_AUTHORIZED_CLIENT_PROVIDER = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); private final ReactiveClientRegistrationRepository clientRegistrationRepository; private final ReactiveOAuth2AuthorizedClientService authorizedClientService; - private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty(); + private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = DEFAULT_AUTHORIZED_CLIENT_PROVIDER; private Function>> contextAttributesMapper = new DefaultContextAttributesMapper(); private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler; private ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java index a4ac9071233..cd3da393acd 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientService.java @@ -16,6 +16,7 @@ package org.springframework.security.oauth2.client; import org.springframework.dao.DataRetrievalFailureException; +import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.PreparedStatementSetter; @@ -52,6 +53,7 @@ * and therefore MUST be defined in the database schema. * * @author Joe Grandja + * @author Stav Shamir * @since 5.3 * @see OAuth2AuthorizedClientService * @see OAuth2AuthorizedClient @@ -77,6 +79,11 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; private static final String REMOVE_AUTHORIZED_CLIENT_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + PK_FILTER; + private static final String UPDATE_AUTHORIZED_CLIENT_SQL = "UPDATE " + TABLE_NAME + + " SET access_token_type = ?, access_token_value = ?, access_token_issued_at = ?," + + " access_token_expires_at = ?, access_token_scopes = ?," + + " refresh_token_value = ?, refresh_token_issued_at = ?" + + " WHERE " + PK_FILTER; protected final JdbcOperations jdbcOperations; protected RowMapper authorizedClientRowMapper; protected Function> authorizedClientParametersMapper; @@ -120,6 +127,35 @@ public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authen Assert.notNull(authorizedClient, "authorizedClient cannot be null"); Assert.notNull(principal, "principal cannot be null"); + boolean existsAuthorizedClient = null != this.loadAuthorizedClient( + authorizedClient.getClientRegistration().getRegistrationId(), principal.getName()); + + if (existsAuthorizedClient) { + updateAuthorizedClient(authorizedClient, principal); + } else { + try { + insertAuthorizedClient(authorizedClient, principal); + } catch (DuplicateKeyException e) { + updateAuthorizedClient(authorizedClient, principal); + } + } + } + + private void updateAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) { + List parameters = this.authorizedClientParametersMapper.apply( + new OAuth2AuthorizedClientHolder(authorizedClient, principal)); + + SqlParameterValue clientRegistrationIdParameter = parameters.remove(0); + SqlParameterValue principalNameParameter = parameters.remove(0); + parameters.add(clientRegistrationIdParameter); + parameters.add(principalNameParameter); + + PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray()); + + this.jdbcOperations.update(UPDATE_AUTHORIZED_CLIENT_SQL, pss); + } + + private void insertAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) { List parameters = this.authorizedClientParametersMapper.apply( new OAuth2AuthorizedClientHolder(authorizedClient, principal)); PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray()); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index 938fd9c2155..4ad62b09ce6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -20,7 +20,11 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.util.Assert; /** @@ -40,6 +44,7 @@ * @see Section 4.1.4 Access Token Response */ public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider { + private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; private final OAuth2AccessTokenResponseClient accessTokenResponseClient; /** @@ -59,8 +64,18 @@ public Authentication authenticate(Authentication authentication) throws Authent OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken) authentication; - OAuth2AuthorizationExchangeValidator.validate( - authorizationCodeAuthentication.getAuthorizationExchange()); + OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication + .getAuthorizationExchange().getAuthorizationResponse(); + if (authorizationResponse.statusError()) { + throw new OAuth2AuthorizationException(authorizationResponse.getError()); + } + + OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication + .getAuthorizationExchange().getAuthorizationRequest(); + if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); + throw new OAuth2AuthorizationException(oauth2Error); + } OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse( @@ -73,7 +88,8 @@ public Authentication authenticate(Authentication authentication) throws Authent authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange(), accessTokenResponse.getAccessToken(), - accessTokenResponse.getRefreshToken()); + accessTokenResponse.getRefreshToken(), + accessTokenResponse.getAdditionalParameters()); authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); return authenticationResult; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java index 4ecdd3b85cc..28e4d7bdfe9 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeReactiveAuthenticationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -22,9 +22,13 @@ import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; import reactor.core.publisher.Mono; @@ -55,8 +59,8 @@ * @see Section 4.1.3 Access Token Request * @see Section 4.1.4 Access Token Response */ -public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements - ReactiveAuthenticationManager { +public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements ReactiveAuthenticationManager { + private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; private final ReactiveOAuth2AccessTokenResponseClient accessTokenResponseClient; public OAuth2AuthorizationCodeReactiveAuthenticationManager( @@ -70,7 +74,16 @@ public Mono authenticate(Authentication authentication) { return Mono.defer(() -> { OAuth2AuthorizationCodeAuthenticationToken token = (OAuth2AuthorizationCodeAuthenticationToken) authentication; - OAuth2AuthorizationExchangeValidator.validate(token.getAuthorizationExchange()); + OAuth2AuthorizationResponse authorizationResponse = token.getAuthorizationExchange().getAuthorizationResponse(); + if (authorizationResponse.statusError()) { + return Mono.error(new OAuth2AuthorizationException(authorizationResponse.getError())); + } + + OAuth2AuthorizationRequest authorizationRequest = token.getAuthorizationExchange().getAuthorizationRequest(); + if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { + OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); + return Mono.error(new OAuth2AuthorizationException(oauth2Error)); + } OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest( token.getClientRegistration(), diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationExchangeValidator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationExchangeValidator.java deleted file mode 100644 index a240e0521a8..00000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationExchangeValidator.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2002-2019 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.oauth2.client.authentication; - -import org.springframework.security.oauth2.core.OAuth2AuthorizationException; -import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; - -/** - * A validator for an "exchange" of an OAuth 2.0 Authorization Request and Response. - * - * @author Joe Grandja - * @since 5.1 - * @see OAuth2AuthorizationExchange - */ -final class OAuth2AuthorizationExchangeValidator { - private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter"; - - static void validate(OAuth2AuthorizationExchange authorizationExchange) { - OAuth2AuthorizationRequest authorizationRequest = authorizationExchange.getAuthorizationRequest(); - OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse(); - - if (authorizationResponse.statusError()) { - throw new OAuth2AuthorizationException(authorizationResponse.getError()); - } - - if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { - OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); - throw new OAuth2AuthorizationException(oauth2Error); - } - } -} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java index 02d4b5e7b1f..8d3b70234d8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -28,7 +28,6 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2Error; -import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; @@ -60,7 +59,7 @@ * @see Section 4.1.4 Access Token Response */ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider { - private final OAuth2AccessTokenResponseClient accessTokenResponseClient; + private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider; private final OAuth2UserService userService; private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); @@ -74,59 +73,54 @@ public OAuth2LoginAuthenticationProvider( OAuth2AccessTokenResponseClient accessTokenResponseClient, OAuth2UserService userService) { - Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null"); Assert.notNull(userService, "userService cannot be null"); - this.accessTokenResponseClient = accessTokenResponseClient; + this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient); this.userService = userService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - OAuth2LoginAuthenticationToken authorizationCodeAuthentication = + OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication; // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // scope // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. - if (authorizationCodeAuthentication.getAuthorizationExchange() + if (loginAuthenticationToken.getAuthorizationExchange() .getAuthorizationRequest().getScopes().contains("openid")) { // This is an OpenID Connect Authentication Request so return null // and let OidcAuthorizationCodeAuthenticationProvider handle it instead return null; } - OAuth2AccessTokenResponse accessTokenResponse; + OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken; try { - OAuth2AuthorizationExchangeValidator.validate( - authorizationCodeAuthentication.getAuthorizationExchange()); - - accessTokenResponse = this.accessTokenResponseClient.getTokenResponse( - new OAuth2AuthorizationCodeGrantRequest( - authorizationCodeAuthentication.getClientRegistration(), - authorizationCodeAuthentication.getAuthorizationExchange())); - + authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider + .authenticate(new OAuth2AuthorizationCodeAuthenticationToken( + loginAuthenticationToken.getClientRegistration(), + loginAuthenticationToken.getAuthorizationExchange())); } catch (OAuth2AuthorizationException ex) { OAuth2Error oauth2Error = ex.getError(); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } - OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); - Map additionalParameters = accessTokenResponse.getAdditionalParameters(); + OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken(); + Map additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters(); OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest( - authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters)); + loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters)); Collection mappedAuthorities = this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities()); OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken( - authorizationCodeAuthentication.getClientRegistration(), - authorizationCodeAuthentication.getAuthorizationExchange(), + loginAuthenticationToken.getClientRegistration(), + loginAuthenticationToken.getAuthorizationExchange(), oauth2User, mappedAuthorities, accessToken, - accessTokenResponse.getRefreshToken()); - authenticationResult.setDetails(authorizationCodeAuthentication.getDetails()); + authorizationCodeAuthenticationToken.getRefreshToken()); + authenticationResult.setDetails(loginAuthenticationToken.getDetails()); return authenticationResult; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java index 30c597d9558..9fb1820ff51 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -95,6 +95,18 @@ public Mono authenticate(Authentication authentication) { }); } + /** + * Sets the {@link GrantedAuthoritiesMapper} used for mapping {@link OAuth2User#getAuthorities()} + * to a new set of authorities which will be associated to the {@link OAuth2LoginAuthenticationToken}. + * + * @since 5.4 + * @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the user's authorities + */ + public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { + Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null"); + this.authoritiesMapper = authoritiesMapper; + } + private Mono onSuccess(OAuth2AuthorizationCodeAuthenticationToken authentication) { OAuth2AccessToken accessToken = authentication.getAccessToken(); Map additionalParameters = authentication.getAdditionalParameters(); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java index 86895f328de..f1e52113b5a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/ClientRegistrationDeserializer.java @@ -78,6 +78,7 @@ public ClientRegistration deserialize(JsonParser parser, DeserializationContext findObjectNode(userInfoEndpointNode, "authenticationMethod"))) .userNameAttributeName(findStringValue(userInfoEndpointNode, "userNameAttributeName")) .jwkSetUri(findStringValue(providerDetailsNode, "jwkSetUri")) + .issuerUri(findStringValue(providerDetailsNode, "issuerUri")) .providerConfigurationMetadata(findValue(providerDetailsNode, "configurationMetadata", MAP_TYPE_REFERENCE, mapper)) .build(); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java index 9852ff14795..a413dca1459 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -121,13 +121,14 @@ public Mono authenticate(Authentication authentication) { .getAuthorizationExchange().getAuthorizationResponse(); if (authorizationResponse.statusError()) { - throw new OAuth2AuthenticationException( - authorizationResponse.getError(), authorizationResponse.getError().toString()); + return Mono.error(new OAuth2AuthenticationException( + authorizationResponse.getError(), authorizationResponse.getError().toString())); } if (!authorizationResponse.getState().equals(authorizationRequest.getState())) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE); - throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); + return Mono.error(new OAuth2AuthenticationException( + oauth2Error, oauth2Error.toString())); } OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest( @@ -139,7 +140,7 @@ public Mono authenticate(Authentication authentication) { .onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException(e.getError(), e.getError().toString())) .onErrorMap(JwtException.class, e -> { OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, e.getMessage(), null); - throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), e); + return new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), e); }); }); } @@ -156,6 +157,18 @@ public final void setJwtDecoderFactory(ReactiveJwtDecoderFactory authenticationResult(OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication, OAuth2AccessTokenResponse accessTokenResponse) { OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken(); ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration(); @@ -166,7 +179,7 @@ private Mono authenticationResult(OAuth2Authoriz INVALID_ID_TOKEN_ERROR_CODE, "Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(), null); - throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()); + return Mono.error(new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString())); } return createOidcToken(clientRegistration, accessTokenResponse) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java index 7ceaee3a70a..03ae739a7b8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidator.java @@ -33,6 +33,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** * An {@link OAuth2TokenValidator} responsible for @@ -68,7 +69,11 @@ public OAuth2TokenValidatorResult validate(Jwt idToken) { // 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) // MUST exactly match the value of the iss (issuer) Claim. - // TODO Depends on gh-4413 + String metadataIssuer = this.clientRegistration.getProviderDetails().getIssuerUri(); + + if (metadataIssuer != null && !Objects.equals(metadataIssuer, idToken.getIssuer().toExternalForm())) { + invalidClaims.put(IdTokenClaimNames.ISS, idToken.getIssuer()); + } // 3. The Client MUST validate that the aud (audience) Claim contains its client_id value // registered at the Issuer identified by the iss (issuer) Claim as an audience. diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java index e2185714562..2051fdcf2d4 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -163,6 +163,7 @@ public class ProviderDetails implements Serializable { private String tokenUri; private UserInfoEndpoint userInfoEndpoint = new UserInfoEndpoint(); private String jwkSetUri; + private String issuerUri; private Map configurationMetadata = Collections.emptyMap(); private ProviderDetails() { @@ -204,6 +205,17 @@ public String getJwkSetUri() { return this.jwkSetUri; } + /** + * Returns the issuer identifier uri for the OpenID Connect 1.0 provider + * or the OAuth 2.0 Authorization Server. + * + * @since 5.4 + * @return the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server + */ + public String getIssuerUri() { + return this.issuerUri; + } + /** * Returns a {@code Map} of the metadata describing the provider's configuration. * @@ -296,6 +308,7 @@ public static class Builder implements Serializable { private AuthenticationMethod userInfoAuthenticationMethod = AuthenticationMethod.HEADER; private String userNameAttributeName; private String jwkSetUri; + private String issuerUri; private Map configurationMetadata = Collections.emptyMap(); private String clientName; @@ -317,6 +330,7 @@ private Builder(ClientRegistration clientRegistration) { this.userInfoAuthenticationMethod = clientRegistration.providerDetails.userInfoEndpoint.authenticationMethod; this.userNameAttributeName = clientRegistration.providerDetails.userInfoEndpoint.userNameAttributeName; this.jwkSetUri = clientRegistration.providerDetails.jwkSetUri; + this.issuerUri = clientRegistration.providerDetails.issuerUri; Map configurationMetadata = clientRegistration.providerDetails.configurationMetadata; if (configurationMetadata != EMPTY_MAP) { this.configurationMetadata = new HashMap<>(configurationMetadata); @@ -486,6 +500,19 @@ public Builder jwkSetUri(String jwkSetUri) { return this; } + /** + * Sets the issuer identifier uri for the OpenID Connect 1.0 provider + * or the OAuth 2.0 Authorization Server. + * + * @since 5.4 + * @param issuerUri the issuer identifier uri for the OpenID Connect 1.0 provider or the OAuth 2.0 Authorization Server + * @return the {@link Builder} + */ + public Builder issuerUri(String issuerUri) { + this.issuerUri = issuerUri; + return this; + } + /** * Sets the metadata describing the provider's configuration. * @@ -554,6 +581,7 @@ private ClientRegistration create() { providerDetails.userInfoEndpoint.authenticationMethod = this.userInfoAuthenticationMethod; providerDetails.userInfoEndpoint.userNameAttributeName = this.userNameAttributeName; providerDetails.jwkSetUri = this.jwkSetUri; + providerDetails.issuerUri = this.issuerUri; providerDetails.configurationMetadata = Collections.unmodifiableMap(this.configurationMetadata); clientRegistration.providerDetails = providerDetails; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java index 57d10aaadca..857b150db09 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/registration/ClientRegistrations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -146,9 +146,12 @@ private static Supplier oidc(URI issuer) { RequestEntity request = RequestEntity.get(uri).build(); Map configuration = rest.exchange(request, typeReference).getBody(); OIDCProviderMetadata metadata = parse(configuration, OIDCProviderMetadata::parse); - return withProviderConfiguration(metadata, issuer.toASCIIString()) - .jwkSetUri(metadata.getJWKSetURI().toASCIIString()) - .userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString()); + ClientRegistration.Builder builder = withProviderConfiguration(metadata, issuer.toASCIIString()) + .jwkSetUri(metadata.getJWKSetURI().toASCIIString()); + if (metadata.getUserInfoEndpointURI() != null) { + builder.userInfoUri(metadata.getUserInfoEndpointURI().toASCIIString()); + } + return builder; }; } @@ -245,6 +248,7 @@ private static ClientRegistration.Builder withProviderConfiguration(Authorizatio .authorizationUri(metadata.getAuthorizationEndpointURI().toASCIIString()) .providerConfigurationMetadata(configurationMetadata) .tokenUri(metadata.getTokenEndpointURI().toASCIIString()) + .issuerUri(issuer) .clientName(issuer); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java index a97db4a26be..8060660f2c8 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolver.java @@ -87,6 +87,9 @@ public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository cl @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { String registrationId = this.resolveRegistrationId(request); + if (registrationId == null) { + return null; + } String redirectUriAction = getAction(request, "login"); return resolve(request, registrationId, redirectUriAction); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java index 2da4ab8af59..708db52020a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizedClientManager.java @@ -25,6 +25,7 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.RemoveAuthorizedClientOAuth2AuthorizationFailureHandler; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -83,9 +84,16 @@ * @see OAuth2AuthorizationFailureHandler */ public final class DefaultOAuth2AuthorizedClientManager implements OAuth2AuthorizedClientManager { + private static final OAuth2AuthorizedClientProvider DEFAULT_AUTHORIZED_CLIENT_PROVIDER = + OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); private final ClientRegistrationRepository clientRegistrationRepository; private final OAuth2AuthorizedClientRepository authorizedClientRepository; - private OAuth2AuthorizedClientProvider authorizedClientProvider = context -> null; + private OAuth2AuthorizedClientProvider authorizedClientProvider; private Function> contextAttributesMapper; private OAuth2AuthorizationSuccessHandler authorizationSuccessHandler; private OAuth2AuthorizationFailureHandler authorizationFailureHandler; @@ -102,6 +110,7 @@ public DefaultOAuth2AuthorizedClientManager(ClientRegistrationRepository clientR Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); this.clientRegistrationRepository = clientRegistrationRepository; this.authorizedClientRepository = authorizedClientRepository; + this.authorizedClientProvider = DEFAULT_AUTHORIZED_CLIENT_PROVIDER; this.contextAttributesMapper = new DefaultContextAttributesMapper(); this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultReactiveOAuth2AuthorizedClientManager.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultReactiveOAuth2AuthorizedClientManager.java index 31ba1a2b174..a29a4c7e33c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultReactiveOAuth2AuthorizedClientManager.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/DefaultReactiveOAuth2AuthorizedClientManager.java @@ -23,6 +23,7 @@ import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizationSuccessHandler; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; @@ -78,6 +79,13 @@ * @see ReactiveOAuth2AuthorizationFailureHandler */ public final class DefaultReactiveOAuth2AuthorizedClientManager implements ReactiveOAuth2AuthorizedClientManager { + private static final ReactiveOAuth2AuthorizedClientProvider DEFAULT_AUTHORIZED_CLIENT_PROVIDER = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); private static final Mono currentServerWebExchangeMono = Mono.subscriberContext() .filter(c -> c.hasKey(ServerWebExchange.class)) @@ -85,7 +93,7 @@ public final class DefaultReactiveOAuth2AuthorizedClientManager implements React private final ReactiveClientRegistrationRepository clientRegistrationRepository; private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository; - private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = context -> Mono.empty(); + private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = DEFAULT_AUTHORIZED_CLIENT_PROVIDER; private Function>> contextAttributesMapper = new DefaultContextAttributesMapper(); private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler; private ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java index 4f8aaefbafa..8d2f157c457 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java @@ -83,6 +83,7 @@ * * * @author Joe Grandja + * @author Parikshit Dutta * @since 5.1 * @see OAuth2AuthorizationCodeAuthenticationToken * @see OAuth2AuthorizationCodeAuthenticationProvider @@ -104,7 +105,7 @@ public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter { new HttpSessionOAuth2AuthorizationRequestRepository(); private final AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); - private final RequestCache requestCache = new HttpSessionRequestCache(); + private RequestCache requestCache = new HttpSessionRequestCache(); /** * Constructs an {@code OAuth2AuthorizationCodeGrantFilter} using the provided parameters. @@ -134,6 +135,18 @@ public final void setAuthorizationRequestRepository(AuthorizationRequestReposito this.authorizationRequestRepository = authorizationRequestRepository; } + /** + * Sets the {@link RequestCache} used for loading a previously saved request (if available) + * and replaying it after completing the processing of the OAuth 2.0 Authorization Response. + * + * @since 5.4 + * @param requestCache the cache used for loading a previously saved request (if available) + */ + public final void setRequestCache(RequestCache requestCache) { + Assert.notNull(requestCache, "requestCache cannot be null"); + this.requestCache = requestCache; + } + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index f0385fb885a..8241f1bce19 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -93,25 +93,9 @@ public OAuth2AuthorizedClientArgumentResolver(ClientRegistrationRepository clien OAuth2AuthorizedClientRepository authorizedClientRepository) { Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); - this.authorizedClientManager = createDefaultAuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository); - this.defaultAuthorizedClientManager = true; - } - - private static OAuth2AuthorizedClientManager createDefaultAuthorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( + this.authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; + this.defaultAuthorizedClientManager = true; } @Override diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java index 99dff8c2710..23232bd30a5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.java @@ -208,14 +208,6 @@ private static ReactiveOAuth2AuthorizedClientManager createDefaultAuthorizedClie ServerOAuth2AuthorizedClientRepository authorizedClientRepository, ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) { - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - // gh-7544 if (authorizedClientRepository instanceof UnAuthenticatedServerOAuth2AuthorizedClientRepository) { UnAuthenticatedReactiveOAuth2AuthorizedClientManager unauthenticatedAuthorizedClientManager = @@ -223,13 +215,19 @@ private static ReactiveOAuth2AuthorizedClientManager createDefaultAuthorizedClie clientRegistrationRepository, (UnAuthenticatedServerOAuth2AuthorizedClientRepository) authorizedClientRepository, authorizationFailureHandler); - unauthenticatedAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + unauthenticatedAuthorizedClientManager.setAuthorizedClientProvider( + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build()); return unauthenticatedAuthorizedClientManager; } - DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); authorizedClientManager.setAuthorizationFailureHandler(authorizationFailureHandler); return authorizedClientManager; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index 610adf2c2b3..8694121c8e2 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java @@ -216,32 +216,15 @@ public ServletOAuth2AuthorizedClientExchangeFilterFunction( authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, principal, (HttpServletRequest) attributes.get(HttpServletRequest.class.getName()), (HttpServletResponse) attributes.get(HttpServletResponse.class.getName()))); - this.authorizedClientManager = createDefaultAuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository, authorizationFailureHandler); + DefaultOAuth2AuthorizedClientManager defaultAuthorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + defaultAuthorizedClientManager.setAuthorizationFailureHandler(authorizationFailureHandler); + this.authorizedClientManager = defaultAuthorizedClientManager; this.defaultAuthorizedClientManager = true; this.clientResponseHandler = new AuthorizationFailureForwarder(authorizationFailureHandler); } - private static OAuth2AuthorizedClientManager createDefaultAuthorizedClientManager( - ClientRegistrationRepository clientRegistrationRepository, - OAuth2AuthorizedClientRepository authorizedClientRepository, - OAuth2AuthorizationFailureHandler authorizationFailureHandler) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = - OAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - authorizedClientManager.setAuthorizationFailureHandler(authorizationFailureHandler); - - return authorizedClientManager; - } - /** * Sets the {@link OAuth2AccessTokenResponseClient} used for getting an {@link OAuth2AuthorizedClient} for the client_credentials grant. * diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java index 5f784a51c1b..798fafc30cd 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/result/method/annotation/OAuth2AuthorizedClientArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -25,8 +25,6 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; -import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; @@ -87,24 +85,8 @@ public OAuth2AuthorizedClientArgumentResolver(ReactiveClientRegistrationReposito ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null"); - this.authorizedClientManager = createDefaultAuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository); - } - - private static ReactiveOAuth2AuthorizedClientManager createDefaultAuthorizedClientManager( - ReactiveClientRegistrationRepository clientRegistrationRepository, ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { - - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = - ReactiveOAuth2AuthorizedClientProviderBuilder.builder() - .authorizationCode() - .refreshToken() - .clientCredentials() - .password() - .build(); - DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager( + this.authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientRepository); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - - return authorizedClientManager; } @Override diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java index 7ef55667cbb..a8d10bff0e1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilter.java @@ -27,6 +27,8 @@ import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; @@ -35,6 +37,8 @@ import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.savedrequest.ServerRequestCache; +import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.util.Assert; import org.springframework.web.server.ServerWebExchange; @@ -80,6 +84,7 @@ * * @author Rob Winch * @author Joe Grandja + * @author Parikshit Dutta * @since 5.1 * @see OAuth2AuthorizationCodeAuthenticationToken * @see org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeReactiveAuthenticationManager @@ -111,6 +116,8 @@ public class OAuth2AuthorizationCodeGrantWebFilter implements WebFilter { private ServerWebExchangeMatcher requiresAuthenticationMatcher; + private ServerRequestCache requestCache = new WebSessionServerRequestCache(); + private AnonymousAuthenticationToken anonymousToken = new AnonymousAuthenticationToken("key", "anonymous", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); @@ -129,7 +136,10 @@ public OAuth2AuthorizationCodeGrantWebFilter( authenticationConverter.setAuthorizationRequestRepository(this.authorizationRequestRepository); this.authenticationConverter = authenticationConverter; this.defaultAuthenticationConverter = true; - this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler(); + RedirectServerAuthenticationSuccessHandler authenticationSuccessHandler = + new RedirectServerAuthenticationSuccessHandler(); + authenticationSuccessHandler.setRequestCache(this.requestCache); + this.authenticationSuccessHandler = authenticationSuccessHandler; this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception); } @@ -144,7 +154,10 @@ public OAuth2AuthorizationCodeGrantWebFilter( this.authorizedClientRepository = authorizedClientRepository; this.requiresAuthenticationMatcher = this::matchesAuthorizationResponse; this.authenticationConverter = authenticationConverter; - this.authenticationSuccessHandler = new RedirectServerAuthenticationSuccessHandler(); + RedirectServerAuthenticationSuccessHandler authenticationSuccessHandler = + new RedirectServerAuthenticationSuccessHandler(); + authenticationSuccessHandler.setRequestCache(this.requestCache); + this.authenticationSuccessHandler = authenticationSuccessHandler; this.authenticationFailureHandler = (webFilterExchange, exception) -> Mono.error(exception); } @@ -169,19 +182,42 @@ private void updateDefaultAuthenticationConverter() { } } + /** + * Sets the {@link ServerRequestCache} used for loading a previously saved request (if available) + * and replaying it after completing the processing of the OAuth 2.0 Authorization Response. + * + * @since 5.4 + * @param requestCache the cache used for loading a previously saved request (if available) + */ + public final void setRequestCache(ServerRequestCache requestCache) { + Assert.notNull(requestCache, "requestCache cannot be null"); + this.requestCache = requestCache; + updateDefaultAuthenticationSuccessHandler(); + } + + private void updateDefaultAuthenticationSuccessHandler() { + ((RedirectServerAuthenticationSuccessHandler) this.authenticationSuccessHandler).setRequestCache(this.requestCache); + } + @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { return this.requiresAuthenticationMatcher.matches(exchange) .filter(ServerWebExchangeMatcher.MatchResult::isMatch) - .flatMap(matchResult -> this.authenticationConverter.convert(exchange)) + .flatMap(matchResult -> + this.authenticationConverter.convert(exchange) + .onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException( + e.getError(), e.getError().toString()))) .switchIfEmpty(chain.filter(exchange).then(Mono.empty())) - .flatMap(token -> authenticate(exchange, chain, token)); + .flatMap(token -> authenticate(exchange, chain, token)) + .onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler + .onAuthenticationFailure(new WebFilterExchange(exchange, chain), e)); } - private Mono authenticate(ServerWebExchange exchange, - WebFilterChain chain, Authentication token) { + private Mono authenticate(ServerWebExchange exchange, WebFilterChain chain, Authentication token) { WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain); return this.authenticationManager.authenticate(token) + .onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException( + e.getError(), e.getError().toString())) .switchIfEmpty(Mono.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass())))) .flatMap(authentication -> onAuthenticationSuccess(authentication, webFilterExchange)) .onErrorResume(AuthenticationException.class, e -> this.authenticationFailureHandler diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/ServerOAuth2AuthorizationCodeAuthenticationTokenConverter.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/ServerOAuth2AuthorizationCodeAuthenticationTokenConverter.java index 2e9bd68c173..6ff9d34fd1e 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/ServerOAuth2AuthorizationCodeAuthenticationTokenConverter.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/server/ServerOAuth2AuthorizationCodeAuthenticationTokenConverter.java @@ -18,7 +18,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken; -import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -33,7 +32,7 @@ import reactor.core.publisher.Mono; /** - * Converts from a {@link ServerWebExchange} to an {@link OAuth2LoginAuthenticationToken} that can be authenticated. The + * Converts from a {@link ServerWebExchange} to an {@link OAuth2AuthorizationCodeAuthenticationToken} that can be authenticated. The * converter does not validate any errors it only performs a conversion. * @author Rob Winch * @since 5.1 diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientServiceTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientServiceTests.java index 40b2957e2a8..78fe9c30fb4 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientServiceTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/JdbcOAuth2AuthorizedClientServiceTests.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Test; import org.springframework.dao.DataRetrievalFailureException; -import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; @@ -47,12 +46,14 @@ import java.sql.Timestamp; import java.sql.Types; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.within; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; @@ -64,6 +65,7 @@ * Tests for {@link JdbcOAuth2AuthorizedClientService}. * * @author Joe Grandja + * @author Stav Shamir */ public class JdbcOAuth2AuthorizedClientServiceTests { private static final String OAUTH2_CLIENT_SCHEMA_SQL_RESOURCE = "org/springframework/security/oauth2/client/oauth2-client-schema.sql"; @@ -154,11 +156,11 @@ public void loadAuthorizedClientWhenExistsThenReturnAuthorizedClient() { assertThat(authorizedClient.getPrincipalName()).isEqualTo(expected.getPrincipalName()); assertThat(authorizedClient.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType()); assertThat(authorizedClient.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue()); - assertThat(authorizedClient.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt()); - assertThat(authorizedClient.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt()); + assertThat(authorizedClient.getAccessToken().getIssuedAt()).isCloseTo(expected.getAccessToken().getIssuedAt(), within(1, ChronoUnit.MILLIS)); + assertThat(authorizedClient.getAccessToken().getExpiresAt()).isCloseTo(expected.getAccessToken().getExpiresAt(), within(1, ChronoUnit.MILLIS)); assertThat(authorizedClient.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes()); assertThat(authorizedClient.getRefreshToken().getTokenValue()).isEqualTo(expected.getRefreshToken().getTokenValue()); - assertThat(authorizedClient.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt()); + assertThat(authorizedClient.getRefreshToken().getIssuedAt()).isCloseTo(expected.getRefreshToken().getIssuedAt(), within(1, ChronoUnit.MILLIS)); } @Test @@ -209,11 +211,11 @@ public void saveAuthorizedClientWhenSaveThenLoadReturnsSaved() { assertThat(authorizedClient.getPrincipalName()).isEqualTo(expected.getPrincipalName()); assertThat(authorizedClient.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType()); assertThat(authorizedClient.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue()); - assertThat(authorizedClient.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt()); - assertThat(authorizedClient.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt()); + assertThat(authorizedClient.getAccessToken().getIssuedAt()).isCloseTo(expected.getAccessToken().getIssuedAt(), within(1, ChronoUnit.MILLIS)); + assertThat(authorizedClient.getAccessToken().getExpiresAt()).isCloseTo(expected.getAccessToken().getExpiresAt(), within(1, ChronoUnit.MILLIS)); assertThat(authorizedClient.getAccessToken().getScopes()).isEqualTo(expected.getAccessToken().getScopes()); assertThat(authorizedClient.getRefreshToken().getTokenValue()).isEqualTo(expected.getRefreshToken().getTokenValue()); - assertThat(authorizedClient.getRefreshToken().getIssuedAt()).isEqualTo(expected.getRefreshToken().getIssuedAt()); + assertThat(authorizedClient.getRefreshToken().getIssuedAt()).isCloseTo(expected.getRefreshToken().getIssuedAt(), within(1, ChronoUnit.MILLIS)); // Test save/load of NOT NULL attributes only principal = createPrincipal(); @@ -229,21 +231,37 @@ public void saveAuthorizedClientWhenSaveThenLoadReturnsSaved() { assertThat(authorizedClient.getPrincipalName()).isEqualTo(expected.getPrincipalName()); assertThat(authorizedClient.getAccessToken().getTokenType()).isEqualTo(expected.getAccessToken().getTokenType()); assertThat(authorizedClient.getAccessToken().getTokenValue()).isEqualTo(expected.getAccessToken().getTokenValue()); - assertThat(authorizedClient.getAccessToken().getIssuedAt()).isEqualTo(expected.getAccessToken().getIssuedAt()); - assertThat(authorizedClient.getAccessToken().getExpiresAt()).isEqualTo(expected.getAccessToken().getExpiresAt()); + assertThat(authorizedClient.getAccessToken().getIssuedAt()).isCloseTo(expected.getAccessToken().getIssuedAt(), within(1, ChronoUnit.MILLIS)); + assertThat(authorizedClient.getAccessToken().getExpiresAt()).isCloseTo(expected.getAccessToken().getExpiresAt(), within(1, ChronoUnit.MILLIS)); assertThat(authorizedClient.getAccessToken().getScopes()).isEmpty(); assertThat(authorizedClient.getRefreshToken()).isNull(); } @Test - public void saveAuthorizedClientWhenSaveDuplicateThenThrowDuplicateKeyException() { + public void saveAuthorizedClientWhenSaveClientWithExistingPrimaryKeyThenUpdate() { + // Given a saved authorized client Authentication principal = createPrincipal(); OAuth2AuthorizedClient authorizedClient = createAuthorizedClient(principal, this.clientRegistration); - this.authorizedClientService.saveAuthorizedClient(authorizedClient, principal); - assertThatThrownBy(() -> this.authorizedClientService.saveAuthorizedClient(authorizedClient, principal)) - .isInstanceOf(DuplicateKeyException.class); + // When a client with the same principal and registration id is saved + OAuth2AuthorizedClient updatedClient = createAuthorizedClient(principal, this.clientRegistration); + this.authorizedClientService.saveAuthorizedClient(updatedClient, principal); + + // Then the saved client is updated + OAuth2AuthorizedClient savedClient = this.authorizedClientService.loadAuthorizedClient( + this.clientRegistration.getRegistrationId(), principal.getName()); + + assertThat(savedClient).isNotNull(); + assertThat(savedClient.getClientRegistration()).isEqualTo(updatedClient.getClientRegistration()); + assertThat(savedClient.getPrincipalName()).isEqualTo(updatedClient.getPrincipalName()); + assertThat(savedClient.getAccessToken().getTokenType()).isEqualTo(updatedClient.getAccessToken().getTokenType()); + assertThat(savedClient.getAccessToken().getTokenValue()).isEqualTo(updatedClient.getAccessToken().getTokenValue()); + assertThat(savedClient.getAccessToken().getIssuedAt()).isCloseTo(updatedClient.getAccessToken().getIssuedAt(), within(1, ChronoUnit.MILLIS)); + assertThat(savedClient.getAccessToken().getExpiresAt()).isCloseTo(updatedClient.getAccessToken().getExpiresAt(), within(1, ChronoUnit.MILLIS)); + assertThat(savedClient.getAccessToken().getScopes()).isEqualTo(updatedClient.getAccessToken().getScopes()); + assertThat(savedClient.getRefreshToken().getTokenValue()).isEqualTo(updatedClient.getRefreshToken().getTokenValue()); + assertThat(savedClient.getRefreshToken().getIssuedAt()).isCloseTo(updatedClient.getRefreshToken().getIssuedAt(), within(1, ChronoUnit.MILLIS)); } @Test diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java index 2ba5e369277..41ebe4a1e6e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.security.oauth2.client.authentication; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.junit.Before; import org.junit.Test; @@ -119,4 +121,26 @@ public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessTo assertThat(authenticationResult.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); assertThat(authenticationResult.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken()); } + + // gh-5368 + @Test + public void authenticateWhenAuthorizationSuccessResponseThenAdditionalParametersIncluded() { + Map additionalParameters = new HashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters) + .build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + OAuth2AuthorizationExchange authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest, + success().build()); + + OAuth2AuthorizationCodeAuthenticationToken authentication = (OAuth2AuthorizationCodeAuthenticationToken) this.authenticationProvider + .authenticate( + new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, authorizationExchange)); + + assertThat(authentication.getAdditionalParameters()) + .containsAllEntriesOf(accessTokenResponse.getAdditionalParameters()); + } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java index ee636c3f794..197bf53d867 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginReactiveAuthenticationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -20,10 +20,13 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.Before; @@ -33,8 +36,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient; @@ -96,6 +102,12 @@ public void constructorWhenNullUserServiceThenIllegalArgumentException() { .isInstanceOf(IllegalArgumentException.class); } + @Test + public void setAuthoritiesMapperWhenAuthoritiesMapperIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.manager.setAuthoritiesMapper(null)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void authenticateWhenNoSubscriptionThenDoesNothing() { // we didn't do anything because it should cause a ClassCastException (as verified below) @@ -178,6 +190,24 @@ public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToU .containsAllEntriesOf(accessTokenResponse.getAdditionalParameters()); } + @Test + public void authenticateWhenAuthoritiesMapperSetThenReturnMappedAuthorities() { + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + DefaultOAuth2User user = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), Collections.singletonMap("user", "rob"), "user"); + when(this.userService.loadUser(any())).thenReturn(Mono.just(user)); + List mappedAuthorities = AuthorityUtils.createAuthorityList("ROLE_OAUTH_USER"); + GrantedAuthoritiesMapper authoritiesMapper = mock(GrantedAuthoritiesMapper.class); + when(authoritiesMapper.mapAuthorities(anyCollection())).thenAnswer((Answer>) invocation -> mappedAuthorities); + manager.setAuthoritiesMapper(authoritiesMapper); + + OAuth2LoginAuthenticationToken result = (OAuth2LoginAuthenticationToken) this.manager.authenticate(loginToken()).block(); + + assertThat(result.getAuthorities()).isEqualTo(mappedAuthorities); + } + private OAuth2AuthorizationCodeAuthenticationToken loginToken() { ClientRegistration clientRegistration = this.registration.build(); OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixinTests.java index 21ca65fb874..2630efeabc3 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixinTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizationRequestMixinTests.java @@ -27,7 +27,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; @@ -71,8 +70,8 @@ public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception this.authorizationRequestBuilder .scopes(null) .state(null) - .additionalParameters(Collections.emptyMap()) - .attributes(Collections.emptyMap()) + .additionalParameters(Map::clear) + .attributes(Map::clear) .build(); String expectedJson = asJson(authorizationRequest); String json = this.mapper.writeValueAsString(authorizationRequest); @@ -119,8 +118,8 @@ public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Excep this.authorizationRequestBuilder .scopes(null) .state(null) - .additionalParameters(Collections.emptyMap()) - .attributes(Collections.emptyMap()) + .additionalParameters(Map::clear) + .attributes(Map::clear) .build(); String json = asJson(expectedAuthorizationRequest); OAuth2AuthorizationRequest authorizationRequest = this.mapper.readValue(json, OAuth2AuthorizationRequest.class); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java index 93dfd2dd083..d17675faad7 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson2/OAuth2AuthorizedClientMixinTests.java @@ -86,6 +86,7 @@ public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception .userInfoUri(null) .userNameAttributeName(null) .jwkSetUri(null) + .issuerUri(null) .build(); OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient( clientRegistration, this.principalName, TestOAuth2AccessTokens.noScopes()); @@ -139,6 +140,8 @@ public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { .isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()); assertThat(clientRegistration.getProviderDetails().getJwkSetUri()) .isEqualTo(expectedClientRegistration.getProviderDetails().getJwkSetUri()); + assertThat(clientRegistration.getProviderDetails().getIssuerUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getIssuerUri()); assertThat(clientRegistration.getProviderDetails().getConfigurationMetadata()) .containsExactlyEntriesOf(clientRegistration.getProviderDetails().getConfigurationMetadata()); assertThat(clientRegistration.getClientName()) @@ -174,6 +177,7 @@ public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Excep .userInfoUri(null) .userNameAttributeName(null) .jwkSetUri(null) + .issuerUri(null) .build(); OAuth2AccessToken expectedAccessToken = TestOAuth2AccessTokens.noScopes(); OAuth2AuthorizedClient expectedAuthorizedClient = new OAuth2AuthorizedClient( @@ -203,6 +207,7 @@ public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Excep .isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()); assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).isNull(); assertThat(clientRegistration.getProviderDetails().getJwkSetUri()).isNull(); + assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNull(); assertThat(clientRegistration.getProviderDetails().getConfigurationMetadata()).isEmpty(); assertThat(clientRegistration.getClientName()) .isEqualTo(clientRegistration.getRegistrationId()); @@ -276,6 +281,7 @@ private static String asJson(ClientRegistration clientRegistration) { " \"userNameAttributeName\": " + (userInfoEndpoint.getUserNameAttributeName() != null ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + "\n" + " },\n" + " \"jwkSetUri\": " + (providerDetails.getJwkSetUri() != null ? "\"" + providerDetails.getJwkSetUri() + "\"" : null) + ",\n" + + " \"issuerUri\": " + (providerDetails.getIssuerUri() != null ? "\"" + providerDetails.getIssuerUri() + "\"" : null) + ",\n" + " \"configurationMetadata\": {\n" + " " + configurationMetadata + "\n" + " }\n" + diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java index 5e19ab85695..fff93cdc286 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -21,6 +21,7 @@ import java.util.Base64; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.Before; @@ -29,6 +30,10 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import reactor.core.publisher.Mono; import org.springframework.security.authentication.TestingAuthenticationToken; @@ -63,6 +68,8 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager.createHash; import static org.springframework.security.oauth2.jwt.TestJwts.jwt; @@ -123,6 +130,12 @@ public void setJwtDecoderFactoryWhenNullThenIllegalArgumentException() { .isInstanceOf(IllegalArgumentException.class); } + @Test + public void setAuthoritiesMapperWhenAuthoritiesMapperIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.manager.setAuthoritiesMapper(null)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void authenticateWhenNoSubscriptionThenDoesNothing() { // we didn't do anything because it should cause a ClassCastException (as verified below) @@ -316,6 +329,42 @@ public void authenticateWhenTokenSuccessResponseThenAdditionalParametersAddedToU .containsAllEntriesOf(accessTokenResponse.getAdditionalParameters()); } + @Test + public void authenticateWhenAuthoritiesMapperSetThenReturnMappedAuthorities() { + ClientRegistration clientRegistration = this.registration.build(); + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("foo") + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .additionalParameters(Collections.singletonMap(OidcParameterNames.ID_TOKEN, this.idToken.getTokenValue())) + .build(); + + OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = loginToken(); + + Map claims = new HashMap<>(); + claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + claims.put(IdTokenClaimNames.SUB, "rob"); + claims.put(IdTokenClaimNames.AUD, Collections.singletonList(clientRegistration.getClientId())); + claims.put(IdTokenClaimNames.NONCE, this.nonceHash); + Jwt idToken = jwt().claims(c -> c.putAll(claims)).build(); + + + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(Mono.just(accessTokenResponse)); + DefaultOidcUser user = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), this.idToken); + ArgumentCaptor userRequestArgCaptor = ArgumentCaptor.forClass(OidcUserRequest.class); + when(this.userService.loadUser(userRequestArgCaptor.capture())).thenReturn(Mono.just(user)); + + List mappedAuthorities = AuthorityUtils.createAuthorityList("ROLE_OIDC_USER"); + GrantedAuthoritiesMapper authoritiesMapper = mock(GrantedAuthoritiesMapper.class); + when(authoritiesMapper.mapAuthorities(anyCollection())).thenAnswer( + (Answer>) invocation -> mappedAuthorities); + when(this.jwtDecoder.decode(any())).thenReturn(Mono.just(idToken)); + this.manager.setJwtDecoderFactory(c -> this.jwtDecoder); + this.manager.setAuthoritiesMapper(authoritiesMapper); + + Authentication result = this.manager.authenticate(authorizationCodeAuthentication).block(); + + assertThat(result.getAuthorities()).isEqualTo(mappedAuthorities); + } + private OAuth2AuthorizationCodeAuthenticationToken loginToken() { ClientRegistration clientRegistration = this.registration.build(); Map attributes = new HashMap<>(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidatorTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidatorTests.java index f357cc38c85..098fe8d1f29 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidatorTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenValidatorTests.java @@ -51,7 +51,7 @@ public class OidcIdTokenValidatorTests { @Before public void setup() { this.headers.put("alg", JwsAlgorithms.RS256); - this.claims.put(IdTokenClaimNames.ISS, "https://issuer.example.com"); + this.claims.put(IdTokenClaimNames.ISS, "https://example.com"); this.claims.put(IdTokenClaimNames.SUB, "rob"); this.claims.put(IdTokenClaimNames.AUD, Collections.singletonList("client-id")); } @@ -92,6 +92,31 @@ public void validateWhenIssuerNullThenHasErrors() { .allMatch(msg -> msg.contains(IdTokenClaimNames.ISS)); } + @Test + public void validateWhenMetadataIssuerMismatchThenHasErrors() { + /* + * When the issuer is set in the provider metadata, and it does not match the issuer in the ID Token, + * the validation must fail + */ + this.registration = this.registration.issuerUri("https://somethingelse.com"); + + assertThat(this.validateIdToken()) + .hasSize(1) + .extracting(OAuth2Error::getDescription) + .allMatch(msg -> msg.contains(IdTokenClaimNames.ISS)); + } + + @Test + public void validateWhenMetadataIssuerMatchThenNoErrors() { + /* + * When the issuer is set in the provider metadata, and it does match the issuer in the ID Token, + * the validation must succeed + */ + this.registration = this.registration.issuerUri("https://example.com"); + + assertThat(this.validateIdToken()).isEmpty(); + } + @Test public void validateWhenSubNullThenHasErrors() { this.claims.remove(IdTokenClaimNames.SUB); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java index 2a2adb07fef..1547706f2a8 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -48,6 +48,7 @@ public class ClientRegistrationTests { private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/authorization"; private static final String TOKEN_URI = "https://provider.com/oauth2/token"; private static final String JWK_SET_URI = "https://provider.com/oauth2/keys"; + private static final String ISSUER_URI = "https://provider.com"; private static final String CLIENT_NAME = "Client 1"; private static final Map PROVIDER_CONFIGURATION_METADATA = Collections.unmodifiableMap(createProviderConfigurationMetadata()); @@ -89,6 +90,7 @@ public void buildWhenAuthorizationCodeGrantAllAttributesProvidedThenAllAttribute .tokenUri(TOKEN_URI) .userInfoAuthenticationMethod(AuthenticationMethod.FORM) .jwkSetUri(JWK_SET_URI) + .issuerUri(ISSUER_URI) .providerConfigurationMetadata(PROVIDER_CONFIGURATION_METADATA) .clientName(CLIENT_NAME) .build(); @@ -104,6 +106,7 @@ public void buildWhenAuthorizationCodeGrantAllAttributesProvidedThenAllAttribute assertThat(registration.getProviderDetails().getTokenUri()).isEqualTo(TOKEN_URI); assertThat(registration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()).isEqualTo(AuthenticationMethod.FORM); assertThat(registration.getProviderDetails().getJwkSetUri()).isEqualTo(JWK_SET_URI); + assertThat(registration.getProviderDetails().getIssuerUri()).isEqualTo(ISSUER_URI); assertThat(registration.getProviderDetails().getConfigurationMetadata()).isEqualTo(PROVIDER_CONFIGURATION_METADATA); assertThat(registration.getClientName()).isEqualTo(CLIENT_NAME); } @@ -743,6 +746,7 @@ public void buildWhenClientRegistrationProvidedThenEachPropertyMatches() { .isEqualTo(updatedUserInfoEndpoint.getUserNameAttributeName()); assertThat(providerDetails.getJwkSetUri()).isEqualTo(updatedProviderDetails.getJwkSetUri()); + assertThat(providerDetails.getIssuerUri()).isEqualTo(updatedProviderDetails.getIssuerUri()); assertThat(providerDetails.getConfigurationMetadata()) .isEqualTo(updatedProviderDetails.getConfigurationMetadata()); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java index 3897870e702..03677717b18 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -162,6 +162,7 @@ private void assertIssuerMetadata(ClientRegistration registration, assertThat(provider.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth"); assertThat(provider.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token"); assertThat(provider.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs"); + assertThat(provider.getIssuerUri()).isEqualTo(this.issuer); assertThat(provider.getConfigurationMetadata()).containsKeys("authorization_endpoint", "claims_supported", "code_challenge_methods_supported", "id_token_signing_alg_values_supported", "issuer", "jwks_uri", "response_types_supported", "revocation_endpoint", "scopes_supported", "subject_types_supported", @@ -195,6 +196,14 @@ public void issuerWhenOAuth2ResponseMissingJwksUriThenThenSuccess() throws Excep assertThat(provider.getJwkSetUri()).isNull(); } + // gh-8187 + @Test + public void issuerWhenResponseMissingUserInfoUriThenSuccess() throws Exception { + this.response.remove("userinfo_endpoint"); + ClientRegistration registration = registration("").build(); + assertThat(registration.getProviderDetails().getUserInfoEndpoint().getUri()).isNull(); + } + @Test public void issuerWhenContainsTrailingSlashThenSuccess() throws Exception { assertThat(registration("")).isNotNull(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java index 7cf750e9df1..fe0391af818 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/TestClientRegistrations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -33,6 +33,7 @@ public static ClientRegistration.Builder clientRegistration() { .authorizationUri("https://example.com/login/oauth/authorize") .tokenUri("https://example.com/login/oauth/access_token") .jwkSetUri("https://example.com/oauth2/jwk") + .issuerUri("https://example.com") .userInfoUri("https://api.example.com/user") .userNameAttributeName("id") .clientName("Client Name") diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java index 45cf3897ef8..36a11b94d90 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/DefaultOAuth2AuthorizationRequestResolverTests.java @@ -15,8 +15,12 @@ */ package org.springframework.security.oauth2.client.web; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @@ -99,6 +103,25 @@ public void resolveWhenNotAuthorizationRequestThenDoesNotResolve() { assertThat(authorizationRequest).isNull(); } + // gh-8650 + @Test + public void resolveWhenNotAuthorizationRequestThenRequestBodyNotConsumed() throws IOException { + String requestUri = "/path"; + MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); + request.setContent("foo".getBytes(StandardCharsets.UTF_8)); + request.setCharacterEncoding(StandardCharsets.UTF_8.name()); + HttpServletRequest spyRequest = Mockito.spy(request); + + this.resolver.resolve(spyRequest); + + Mockito.verify(spyRequest, Mockito.never()).getReader(); + Mockito.verify(spyRequest, Mockito.never()).getInputStream(); + Mockito.verify(spyRequest, Mockito.never()).getParameter(Mockito.anyString()); + Mockito.verify(spyRequest, Mockito.never()).getParameterMap(); + Mockito.verify(spyRequest, Mockito.never()).getParameterNames(); + Mockito.verify(spyRequest, Mockito.never()).getParameterValues(Mockito.anyString()); + } + @Test public void resolveWhenAuthorizationRequestWithInvalidClientThenThrowIllegalArgumentException() { ClientRegistration clientRegistration = this.registration1; @@ -437,6 +460,7 @@ public void resolveWhenAuthorizationRequestCustomizerRemovesNonceThenQueryExclud OAuth2AuthorizationRequest authorizationRequest = this.resolver.resolve(request); assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(OidcParameterNames.NONCE); assertThat(authorizationRequest.getAttributes()).doesNotContainKey(OidcParameterNames.NONCE); + assertThat(authorizationRequest.getAttributes()).containsKey(OAuth2ParameterNames.REGISTRATION_ID); assertThat(authorizationRequest.getAuthorizationRequestUri()) .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java index 39b3011f03b..d1dba0d8997 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java @@ -72,6 +72,7 @@ * Tests for {@link OAuth2AuthorizationCodeGrantFilter}. * * @author Joe Grandja + * @author Parikshit Dutta */ public class OAuth2AuthorizationCodeGrantFilterTests { private ClientRegistration registration1; @@ -130,6 +131,12 @@ public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryI .isInstanceOf(IllegalArgumentException.class); } + @Test + public void setRequestCacheWhenRequestCacheIsNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.filter.setRequestCache(null)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void doFilterWhenNotAuthorizationResponseThenNotProcessed() throws Exception { String requestUri = "/path"; @@ -326,6 +333,28 @@ public void doFilterWhenAuthorizationSucceedsAndHasSavedRequestThenRedirectToSav assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/saved-request"); } + @Test + public void doFilterWhenAuthorizationSucceedsAndRequestCacheConfiguredThenRequestCacheUsed() throws Exception { + MockHttpServletRequest authorizationRequest = createAuthorizationRequest("/callback/client-1"); + MockHttpServletRequest authorizationResponse = createAuthorizationResponse(authorizationRequest); + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChain filterChain = mock(FilterChain.class); + this.setUpAuthorizationRequest(authorizationRequest, response, this.registration1); + this.setUpAuthenticationResult(this.registration1); + + RequestCache requestCache = spy(HttpSessionRequestCache.class); + this.filter.setRequestCache(requestCache); + + authorizationRequest.setRequestURI("/saved-request"); + requestCache.saveRequest(authorizationRequest, response); + + this.filter.doFilter(authorizationResponse, response, filterChain); + + verify(requestCache).getRequest(any(HttpServletRequest.class), any(HttpServletResponse.class)); + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/saved-request"); + } + @Test public void doFilterWhenAuthorizationSucceedsAndAnonymousAccessThenAuthorizedClientSavedToHttpSession() throws Exception { AnonymousAuthenticationToken anonymousPrincipal = diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java index 77479bcf8e8..958799b0146 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/DefaultServerOAuth2AuthorizationRequestResolverTests.java @@ -29,6 +29,7 @@ import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; @@ -162,6 +163,7 @@ public void resolveWhenAuthorizationRequestCustomizerRemovesNonceThenQueryExclud assertThat(authorizationRequest.getAdditionalParameters()).doesNotContainKey(OidcParameterNames.NONCE); assertThat(authorizationRequest.getAttributes()).doesNotContainKey(OidcParameterNames.NONCE); + assertThat(authorizationRequest.getAttributes()).containsKey(OAuth2ParameterNames.REGISTRATION_ID); assertThat(authorizationRequest.getAuthorizationRequestUri()) .matches("https://example.com/login/oauth/authorize\\?" + "response_type=code&client_id=client-id&" + diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilterTests.java index 3c2a4bcc2d6..1efd84389ee 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilterTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/OAuth2AuthorizationCodeGrantWebFilterTests.java @@ -29,19 +29,28 @@ import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.server.savedrequest.ServerRequestCache; import org.springframework.util.CollectionUtils; +import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.handler.DefaultWebFilterChain; import reactor.core.publisher.Mono; +import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -50,6 +59,7 @@ /** * @author Rob Winch + * @author Parikshit Dutta * @since 5.1 */ @RunWith(MockitoJUnitRunner.class) @@ -99,6 +109,12 @@ public void constructorWhenAuthorizedClientRepositoryNullThenIllegalArgumentExce .isInstanceOf(IllegalArgumentException.class); } + @Test + public void setRequestCacheWhenRequestCacheIsNullThenThrowIllegalArgumentException() { + assertThatCode(() -> this.filter.setRequestCache(null)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void filterWhenNotMatchThenAuthenticationManagerNotCalled() { MockServerWebExchange exchange = MockServerWebExchange @@ -233,6 +249,99 @@ public void filterWhenAuthorizationRequestRedirectUriParametersNotMatchThenNotPr verifyNoInteractions(this.authenticationManager); } + @Test + public void filterWhenAuthorizationSucceedsAndRequestCacheConfiguredThenRequestCacheUsed() { + ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build(); + when(this.clientRegistrationRepository.findByRegistrationId(any())) + .thenReturn(Mono.just(clientRegistration)); + when(this.authorizedClientRepository.saveAuthorizedClient(any(), any(), any())) + .thenReturn(Mono.empty()); + when(this.authenticationManager.authenticate(any())) + .thenReturn(Mono.just(TestOAuth2AuthorizationCodeAuthenticationTokens.authenticated())); + + MockServerHttpRequest authorizationRequest = createAuthorizationRequest("/authorization/callback"); + OAuth2AuthorizationRequest oauth2AuthorizationRequest = + createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration); + when(this.authorizationRequestRepository.loadAuthorizationRequest(any())) + .thenReturn(Mono.just(oauth2AuthorizationRequest)); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any())) + .thenReturn(Mono.just(oauth2AuthorizationRequest)); + + MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest); + MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse); + DefaultWebFilterChain chain = new DefaultWebFilterChain( + e -> e.getResponse().setComplete(), Collections.emptyList()); + + ServerRequestCache requestCache = mock(ServerRequestCache.class); + when(requestCache.getRedirectUri(any(ServerWebExchange.class))).thenReturn(Mono.just(URI.create("/saved-request"))); + + this.filter.setRequestCache(requestCache); + + this.filter.filter(exchange, chain).block(); + + verify(requestCache).getRedirectUri(exchange); + assertThat(exchange.getResponse().getHeaders().getLocation().toString()).isEqualTo("/saved-request"); + } + + // gh-8609 + @Test + public void filterWhenAuthenticationConverterThrowsOAuth2AuthorizationExceptionThenMappedToOAuth2AuthenticationException() { + ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build(); + when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(Mono.empty()); + + MockServerHttpRequest authorizationRequest = + createAuthorizationRequest("/authorization/callback"); + OAuth2AuthorizationRequest oauth2AuthorizationRequest = + createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration); + when(this.authorizationRequestRepository.loadAuthorizationRequest(any())) + .thenReturn(Mono.just(oauth2AuthorizationRequest)); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any())) + .thenReturn(Mono.just(oauth2AuthorizationRequest)); + + MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest); + MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse); + DefaultWebFilterChain chain = new DefaultWebFilterChain( + e -> e.getResponse().setComplete(), Collections.emptyList()); + + assertThatThrownBy(() -> this.filter.filter(exchange, chain).block()) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo("client_registration_not_found"); + verifyNoInteractions(this.authenticationManager); + } + + // gh-8609 + @Test + public void filterWhenAuthenticationManagerThrowsOAuth2AuthorizationExceptionThenMappedToOAuth2AuthenticationException() { + ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build(); + when(this.clientRegistrationRepository.findByRegistrationId(any())) + .thenReturn(Mono.just(clientRegistration)); + + MockServerHttpRequest authorizationRequest = + createAuthorizationRequest("/authorization/callback"); + OAuth2AuthorizationRequest oauth2AuthorizationRequest = + createOAuth2AuthorizationRequest(authorizationRequest, clientRegistration); + when(this.authorizationRequestRepository.loadAuthorizationRequest(any())) + .thenReturn(Mono.just(oauth2AuthorizationRequest)); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any())) + .thenReturn(Mono.just(oauth2AuthorizationRequest)); + + when(this.authenticationManager.authenticate(any())) + .thenReturn(Mono.error(new OAuth2AuthorizationException(new OAuth2Error("authorization_error")))); + + MockServerHttpRequest authorizationResponse = createAuthorizationResponse(authorizationRequest); + MockServerWebExchange exchange = MockServerWebExchange.from(authorizationResponse); + DefaultWebFilterChain chain = new DefaultWebFilterChain( + e -> e.getResponse().setComplete(), Collections.emptyList()); + + assertThatThrownBy(() -> this.filter.filter(exchange, chain).block()) + .isInstanceOf(OAuth2AuthenticationException.class) + .extracting(ex -> ((OAuth2AuthenticationException) ex).getError()) + .extracting("errorCode") + .isEqualTo("authorization_error"); + } + private static OAuth2AuthorizationRequest createOAuth2AuthorizationRequest( MockServerHttpRequest authorizationRequest, ClientRegistration registration) { Map attributes = new HashMap<>(); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/ServerOAuth2AuthorizationCodeAuthenticationTokenConverterTest.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/ServerOAuth2AuthorizationCodeAuthenticationTokenConverterTest.java index 83aa315cd5a..95b19f7014e 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/ServerOAuth2AuthorizationCodeAuthenticationTokenConverterTest.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/server/ServerOAuth2AuthorizationCodeAuthenticationTokenConverterTest.java @@ -35,6 +35,7 @@ import reactor.core.publisher.Mono; import java.util.Collections; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -96,7 +97,7 @@ public void applyWhenAuthorizationRequestEmptyThenOAuth2AuthorizationException() @Test public void applyWhenAttributesMissingThenOAuth2AuthorizationException() { - this.authorizationRequest.attributes(Collections.emptyMap()); + this.authorizationRequest.attributes(Map::clear); when(this.authorizationRequestRepository.removeAuthorizationRequest(any())).thenReturn(Mono.just(this.authorizationRequest.build())); assertThatThrownBy(() -> applyConverter()) diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java index 55488926cb1..5bd0df56967 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -82,8 +82,9 @@ public static Builder withToken(String tokenValue) { } /** - * Returns a new {@link Builder}, initialized with the provided response - * @param response the response to intialize the builder with + * Returns a new {@link Builder}, initialized with the provided response. + * + * @param response the response to initialize the builder with * @return the {@link Builder} */ public static Builder withResponse(OAuth2AccessTokenResponse response) { @@ -96,20 +97,19 @@ public static Builder withResponse(OAuth2AccessTokenResponse response) { public static class Builder { private String tokenValue; private OAuth2AccessToken.TokenType tokenType; + private Instant issuedAt; + private Instant expiresAt; private long expiresIn; private Set scopes; private String refreshToken; private Map additionalParameters; - private Instant issuedAt; - private Instant expiresAt; - private Builder(OAuth2AccessTokenResponse response) { OAuth2AccessToken accessToken = response.getAccessToken(); this.tokenValue = accessToken.getTokenValue(); this.tokenType = accessToken.getTokenType(); - this.expiresAt = accessToken.getExpiresAt(); this.issuedAt = accessToken.getIssuedAt(); + this.expiresAt = accessToken.getExpiresAt(); this.scopes = accessToken.getScopes(); this.refreshToken = response.getRefreshToken() == null ? null : response.getRefreshToken().getTokenValue(); @@ -139,6 +139,7 @@ public Builder tokenType(OAuth2AccessToken.TokenType tokenType) { */ public Builder expiresIn(long expiresIn) { this.expiresIn = expiresIn; + this.expiresAt = null; return this; } @@ -182,7 +183,6 @@ public Builder additionalParameters(Map additionalParameters) { */ public OAuth2AccessTokenResponse build() { Instant issuedAt = getIssuedAt(); - Instant expiresAt = getExpiresAt(); OAuth2AccessTokenResponse accessTokenResponse = new OAuth2AccessTokenResponse(); diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java index 28937c61c45..9323ce2a537 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequest.java @@ -229,9 +229,9 @@ public static class Builder { private String redirectUri; private Set scopes; private String state; - private Consumer> additionalParametersConsumer = params -> {}; + private Map additionalParameters = new LinkedHashMap<>(); private Consumer> parametersConsumer = params -> {}; - private Consumer> attributesConsumer = attrs -> {}; + private Map attributes = new LinkedHashMap<>(); private String authorizationRequestUri; private Function authorizationRequestUriFunction = builder -> builder.build(); private final DefaultUriBuilderFactory uriBuilderFactory; @@ -325,8 +325,8 @@ public Builder state(String state) { * @return the {@link Builder} */ public Builder additionalParameters(Map additionalParameters) { - if (additionalParameters != null) { - return additionalParameters(params -> params.putAll(additionalParameters)); + if (!CollectionUtils.isEmpty(additionalParameters)) { + this.additionalParameters.putAll(additionalParameters); } return this; } @@ -340,7 +340,7 @@ public Builder additionalParameters(Map additionalParameters) { */ public Builder additionalParameters(Consumer> additionalParametersConsumer) { if (additionalParametersConsumer != null) { - this.additionalParametersConsumer = additionalParametersConsumer; + additionalParametersConsumer.accept(this.additionalParameters); } return this; } @@ -367,8 +367,8 @@ public Builder parameters(Consumer> parametersConsumer) { * @return the {@link Builder} */ public Builder attributes(Map attributes) { - if (attributes != null) { - return attributes(attrs -> attrs.putAll(attributes)); + if (!CollectionUtils.isEmpty(attributes)) { + this.attributes.putAll(attributes); } return this; } @@ -382,7 +382,7 @@ public Builder attributes(Map attributes) { */ public Builder attributes(Consumer> attributesConsumer) { if (attributesConsumer != null) { - this.attributesConsumer = attributesConsumer; + attributesConsumer.accept(this.attributes); } return this; } @@ -439,12 +439,8 @@ public OAuth2AuthorizationRequest build() { authorizationRequest.scopes = Collections.unmodifiableSet( CollectionUtils.isEmpty(this.scopes) ? Collections.emptySet() : new LinkedHashSet<>(this.scopes)); - Map additionalParameters = new LinkedHashMap<>(); - this.additionalParametersConsumer.accept(additionalParameters); - authorizationRequest.additionalParameters = Collections.unmodifiableMap(additionalParameters); - Map attributes = new LinkedHashMap<>(); - this.attributesConsumer.accept(attributes); - authorizationRequest.attributes = Collections.unmodifiableMap(attributes); + authorizationRequest.additionalParameters = Collections.unmodifiableMap(this.additionalParameters); + authorizationRequest.attributes = Collections.unmodifiableMap(this.attributes); authorizationRequest.authorizationRequestUri = StringUtils.hasText(this.authorizationRequestUri) ? this.authorizationRequestUri : this.buildAuthorizationRequestUri(); @@ -457,7 +453,7 @@ private String buildAuthorizationRequestUri() { this.parametersConsumer.accept(parameters); MultiValueMap queryParams = new LinkedMultiValueMap<>(); parameters.forEach((k, v) -> queryParams.set( - encodeQueryParam(k), encodeQueryParam(v.toString()))); // Encoded + encodeQueryParam(k), encodeQueryParam(String.valueOf(v)))); // Encoded UriBuilder uriBuilder = this.uriBuilderFactory.uriString(this.authorizationUri) .queryParams(queryParams); return this.authorizationRequestUriFunction.apply(uriBuilder).toString(); @@ -477,9 +473,7 @@ private Map getParameters() { if (this.redirectUri != null) { parameters.put(OAuth2ParameterNames.REDIRECT_URI, this.redirectUri); } - Map additionalParameters = new LinkedHashMap<>(); - this.additionalParametersConsumer.accept(additionalParameters); - additionalParameters.forEach((k, v) -> parameters.put(k, v.toString())); + parameters.putAll(this.additionalParameters); return parameters; } diff --git a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverter.java b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverter.java index 1e60804d80b..a9901c97d91 100644 --- a/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverter.java +++ b/oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -34,6 +34,7 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; /** * A {@link HttpMessageConverter} for an {@link OAuth2Error OAuth 2.0 Error}. @@ -46,8 +47,8 @@ public class OAuth2ErrorHttpMessageConverter extends AbstractHttpMessageConverter { private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - private static final ParameterizedTypeReference> PARAMETERIZED_RESPONSE_TYPE = - new ParameterizedTypeReference>() {}; + private static final ParameterizedTypeReference> PARAMETERIZED_RESPONSE_TYPE = + new ParameterizedTypeReference>() {}; private GenericHttpMessageConverter jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter(); @@ -69,10 +70,16 @@ protected OAuth2Error readInternal(Class clazz, HttpInput throws HttpMessageNotReadableException { try { + // gh-8157 + // Parse parameter values as Object in order to handle potential JSON Object and then convert values to String @SuppressWarnings("unchecked") - Map errorParameters = (Map) this.jsonMessageConverter.read( + Map errorParameters = (Map) this.jsonMessageConverter.read( PARAMETERIZED_RESPONSE_TYPE.getType(), null, inputMessage); - return this.errorConverter.convert(errorParameters); + return this.errorConverter.convert( + errorParameters.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> String.valueOf(entry.getValue())))); } catch (Exception ex) { throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Error: " + ex.getMessage(), ex, inputMessage); diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java index fa83124ac6e..8f2d3aa1cbc 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AccessTokenResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -153,4 +153,20 @@ public void buildWhenResponseAndRefreshNullThenRefreshNull() { assertThat(withResponse.getRefreshToken()).isNull(); } + + @Test + public void buildWhenResponseAndExpiresInThenExpiresAtEqualToIssuedAtPlusExpiresIn() { + OAuth2AccessTokenResponse tokenResponse = OAuth2AccessTokenResponse + .withToken(TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .build(); + + long expiresIn = 30; + OAuth2AccessTokenResponse withResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse) + .expiresIn(expiresIn) + .build(); + + assertThat(withResponse.getAccessToken().getExpiresAt()).isEqualTo( + withResponse.getAccessToken().getIssuedAt().plusSeconds(expiresIn)); + } } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java index 7952db6a54b..8f0745d4f29 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/endpoint/OAuth2AuthorizationRequestTests.java @@ -121,7 +121,7 @@ public void buildWhenStateIsNullThenDoesNotThrowAnyException() { } @Test - public void buildWhenAdditionalParametersIsNullThenDoesNotThrowAnyException() { + public void buildWhenAdditionalParametersEmptyThenDoesNotThrowAnyException() { assertThatCode(() -> OAuth2AuthorizationRequest.authorizationCode() .authorizationUri(AUTHORIZATION_URI) @@ -129,7 +129,7 @@ public void buildWhenAdditionalParametersIsNullThenDoesNotThrowAnyException() { .redirectUri(REDIRECT_URI) .scopes(SCOPES) .state(STATE) - .additionalParameters((Map) null) + .additionalParameters(Map::clear) .build()) .doesNotThrowAnyException(); } diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverterTests.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverterTests.java index a57b1df1b83..11211aad561 100644 --- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverterTests.java +++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/http/converter/OAuth2ErrorHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -78,6 +78,25 @@ public void readInternalWhenErrorResponseThenReadOAuth2Error() throws Exception assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2"); } + // gh-8157 + @Test + public void readInternalWhenErrorResponseWithObjectThenReadOAuth2Error() throws Exception { + String errorResponse = "{\n" + + " \"error\": \"unauthorized_client\",\n" + + " \"error_description\": \"The client is not authorized\",\n" + + " \"error_codes\": [65001],\n" + + " \"error_uri\": \"https://tools.ietf.org/html/rfc6749#section-5.2\"\n" + + "}\n"; + + MockClientHttpResponse response = new MockClientHttpResponse( + errorResponse.getBytes(), HttpStatus.BAD_REQUEST); + + OAuth2Error oauth2Error = this.messageConverter.readInternal(OAuth2Error.class, response); + assertThat(oauth2Error.getErrorCode()).isEqualTo("unauthorized_client"); + assertThat(oauth2Error.getDescription()).isEqualTo("The client is not authorized"); + assertThat(oauth2Error.getUri()).isEqualTo("https://tools.ietf.org/html/rfc6749#section-5.2"); + } + @Test public void readInternalWhenConversionFailsThenThrowHttpMessageNotReadableException() { Converter errorConverter = mock(Converter.class); diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWSAlgorithmMapJWSKeySelector.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWSAlgorithmMapJWSKeySelector.java deleted file mode 100644 index 2947e90ff50..00000000000 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JWSAlgorithmMapJWSKeySelector.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2002-2019 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.oauth2.jwt; - -import java.security.Key; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.KeySourceException; -import com.nimbusds.jose.proc.JWSKeySelector; -import com.nimbusds.jose.proc.SecurityContext; - -/** - * Class for delegating to a Nimbus JWSKeySelector by the given JWSAlgorithm - * - * @author Josh Cummings - */ -class JWSAlgorithmMapJWSKeySelector implements JWSKeySelector { - private Map> jwsKeySelectors; - - JWSAlgorithmMapJWSKeySelector(Map> jwsKeySelectors) { - this.jwsKeySelectors = jwsKeySelectors; - } - - @Override - public List selectJWSKeys(JWSHeader header, C context) throws KeySourceException { - JWSKeySelector keySelector = this.jwsKeySelectors.get(header.getAlgorithm()); - if (keySelector == null) { - throw new IllegalArgumentException("Unsupported algorithm of " + header.getAlgorithm()); - } - return keySelector.selectJWSKeys(header, context); - } - - public Set getExpectedJWSAlgorithms() { - return this.jwsKeySelectors.keySet(); - } -} diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java index 68c2318a1ba..03993b32e68 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java @@ -23,7 +23,6 @@ import java.text.ParseException; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; @@ -34,6 +33,8 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.RemoteKeySourceException; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.JWKSetCache; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.jwk.source.RemoteJWKSet; import com.nimbusds.jose.proc.JWSKeySelector; @@ -49,6 +50,7 @@ import com.nimbusds.jwt.proc.DefaultJWTProcessor; import com.nimbusds.jwt.proc.JWTProcessor; +import org.springframework.cache.Cache; import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -68,6 +70,7 @@ * * @author Josh Cummings * @author Joe Grandja + * @author Mykyta Bezverkhyi * @since 5.2 */ public final class NimbusJwtDecoder implements JwtDecoder { @@ -215,10 +218,13 @@ public static final class JwkSetUriJwtDecoderBuilder { private String jwkSetUri; private Set signatureAlgorithms = new HashSet<>(); private RestOperations restOperations = new RestTemplate(); + private Cache cache; + private Consumer> jwtProcessorCustomizer; private JwkSetUriJwtDecoderBuilder(String jwkSetUri) { Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty"); this.jwkSetUri = jwkSetUri; + this.jwtProcessorCustomizer = (processor) -> {}; } /** @@ -264,31 +270,66 @@ public JwkSetUriJwtDecoderBuilder restOperations(RestOperations restOperations) return this; } + /** + * Use the given {@link Cache} to store + * JWK Set. + * + * @param cache the {@link Cache} to be used to store JWK Set + * @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations + * @since 5.4 + */ + public JwkSetUriJwtDecoderBuilder cache(Cache cache) { + Assert.notNull(cache, "cache cannot be null"); + this.cache = cache; + return this; + } + + /** + * Use the given {@link Consumer} to customize the {@link JWTProcessor ConfigurableJWTProcessor} before + * passing it to the build {@link NimbusJwtDecoder}. + * + * @param jwtProcessorCustomizer the callback used to alter the processor + * @return a {@link JwkSetUriJwtDecoderBuilder} for further configurations + * @since 5.4 + */ + public JwkSetUriJwtDecoderBuilder jwtProcessorCustomizer(Consumer> jwtProcessorCustomizer) { + Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null"); + this.jwtProcessorCustomizer = jwtProcessorCustomizer; + return this; + } + JWSKeySelector jwsKeySelector(JWKSource jwkSource) { if (this.signatureAlgorithms.isEmpty()) { return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource); - } else if (this.signatureAlgorithms.size() == 1) { - JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(this.signatureAlgorithms.iterator().next().getName()); - return new JWSVerificationKeySelector<>(jwsAlgorithm, jwkSource); } else { - Map> jwsKeySelectors = new HashMap<>(); + Set jwsAlgorithms = new HashSet<>(); for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) { - JWSAlgorithm jwsAlg = JWSAlgorithm.parse(signatureAlgorithm.getName()); - jwsKeySelectors.put(jwsAlg, new JWSVerificationKeySelector<>(jwsAlg, jwkSource)); + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName()); + jwsAlgorithms.add(jwsAlgorithm); } - return new JWSAlgorithmMapJWSKeySelector<>(jwsKeySelectors); + return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource); } } + JWKSource jwkSource(ResourceRetriever jwkSetRetriever) { + if (this.cache == null) { + return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever); + } + ResourceRetriever cachingJwkSetRetriever = new CachingResourceRetriever(this.cache, jwkSetRetriever); + return new RemoteJWKSet<>(toURL(this.jwkSetUri), cachingJwkSetRetriever, new NoOpJwkSetCache()); + } + JWTProcessor processor() { ResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever(this.restOperations); - JWKSource jwkSource = new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever); + JWKSource jwkSource = jwkSource(jwkSetRetriever); ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); jwtProcessor.setJWSKeySelector(jwsKeySelector(jwkSource)); // Spring Security validates the claim set independent from Nimbus jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); + this.jwtProcessorCustomizer.accept(jwtProcessor); + return jwtProcessor; } @@ -309,6 +350,51 @@ private static URL toURL(String url) { } } + private static class NoOpJwkSetCache implements JWKSetCache { + @Override + public void put(JWKSet jwkSet) { + } + + @Override + public JWKSet get() { + return null; + } + + @Override + public boolean requiresRefresh() { + return true; + } + } + + private static class CachingResourceRetriever implements ResourceRetriever { + private final Cache cache; + private final ResourceRetriever resourceRetriever; + + CachingResourceRetriever(Cache cache, ResourceRetriever resourceRetriever) { + this.cache = cache; + this.resourceRetriever = resourceRetriever; + } + + @Override + public Resource retrieveResource(URL url) throws IOException { + String jwkSet; + try { + jwkSet = this.cache.get(url.toString(), + () -> this.resourceRetriever.retrieveResource(url).getContent()); + } catch (Cache.ValueRetrievalException ex) { + Throwable thrownByValueLoader = ex.getCause(); + if (thrownByValueLoader instanceof IOException) { + throw (IOException) thrownByValueLoader; + } + throw new IOException(thrownByValueLoader); + } catch (Exception ex) { + throw new IOException(ex); + } + + return new Resource(jwkSet, "UTF-8"); + } + } + private static class RestOperationsResourceRetriever implements ResourceRetriever { private static final MediaType APPLICATION_JWK_SET_JSON = new MediaType("application", "jwk-set+json"); private final RestOperations restOperations; @@ -346,11 +432,13 @@ public Resource retrieveResource(URL url) throws IOException { public static final class PublicKeyJwtDecoderBuilder { private JWSAlgorithm jwsAlgorithm; private RSAPublicKey key; + private Consumer> jwtProcessorCustomizer; private PublicKeyJwtDecoderBuilder(RSAPublicKey key) { Assert.notNull(key, "key cannot be null"); this.jwsAlgorithm = JWSAlgorithm.RS256; this.key = key; + this.jwtProcessorCustomizer = (processor) -> {}; } /** @@ -369,6 +457,20 @@ public PublicKeyJwtDecoderBuilder signatureAlgorithm(SignatureAlgorithm signatur return this; } + /** + * Use the given {@link Consumer} to customize the {@link JWTProcessor ConfigurableJWTProcessor} before + * passing it to the build {@link NimbusJwtDecoder}. + * + * @param jwtProcessorCustomizer the callback used to alter the processor + * @return a {@link PublicKeyJwtDecoderBuilder} for further configurations + * @since 5.4 + */ + public PublicKeyJwtDecoderBuilder jwtProcessorCustomizer(Consumer> jwtProcessorCustomizer) { + Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null"); + this.jwtProcessorCustomizer = jwtProcessorCustomizer; + return this; + } + JWTProcessor processor() { if (!JWSAlgorithm.Family.RSA.contains(this.jwsAlgorithm)) { throw new IllegalStateException("The provided key is of type RSA; " + @@ -384,6 +486,8 @@ JWTProcessor processor() { // Spring Security validates the claim set independent from Nimbus jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); + this.jwtProcessorCustomizer.accept(jwtProcessor); + return jwtProcessor; } @@ -403,10 +507,12 @@ public NimbusJwtDecoder build() { public static final class SecretKeyJwtDecoderBuilder { private final SecretKey secretKey; private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256; + private Consumer> jwtProcessorCustomizer; private SecretKeyJwtDecoderBuilder(SecretKey secretKey) { Assert.notNull(secretKey, "secretKey cannot be null"); this.secretKey = secretKey; + this.jwtProcessorCustomizer = (processor) -> {}; } /** @@ -426,6 +532,20 @@ public SecretKeyJwtDecoderBuilder macAlgorithm(MacAlgorithm macAlgorithm) { return this; } + /** + * Use the given {@link Consumer} to customize the {@link JWTProcessor ConfigurableJWTProcessor} before + * passing it to the build {@link NimbusJwtDecoder}. + * + * @param jwtProcessorCustomizer the callback used to alter the processor + * @return a {@link SecretKeyJwtDecoderBuilder} for further configurations + * @since 5.4 + */ + public SecretKeyJwtDecoderBuilder jwtProcessorCustomizer(Consumer> jwtProcessorCustomizer) { + Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null"); + this.jwtProcessorCustomizer = jwtProcessorCustomizer; + return this; + } + /** * Build the configured {@link NimbusJwtDecoder}. * @@ -444,6 +564,8 @@ JWTProcessor processor() { // Spring Security validates the claim set independent from Nimbus jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); + this.jwtProcessorCustomizer.accept(jwtProcessor); + return jwtProcessor; } } diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java index c80bbb4a3a3..934a8297e98 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java @@ -17,7 +17,6 @@ import java.security.interfaces.RSAPublicKey; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; @@ -45,6 +44,7 @@ import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.PlainJWT; import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import com.nimbusds.jwt.proc.JWTProcessor; import reactor.core.publisher.Flux; @@ -246,10 +246,12 @@ public static final class JwkSetUriReactiveJwtDecoderBuilder { private final String jwkSetUri; private Set signatureAlgorithms = new HashSet<>(); private WebClient webClient = WebClient.create(); + private Consumer> jwtProcessorCustomizer; private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) { Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty"); this.jwkSetUri = jwkSetUri; + this.jwtProcessorCustomizer = (processor) -> {}; } /** @@ -295,6 +297,20 @@ public JwkSetUriReactiveJwtDecoderBuilder webClient(WebClient webClient) { return this; } + /** + * Use the given {@link Consumer} to customize the {@link JWTProcessor ConfigurableJWTProcessor} before + * passing it to the build {@link NimbusReactiveJwtDecoder}. + * + * @param jwtProcessorCustomizer the callback used to alter the processor + * @return a {@link JwkSetUriReactiveJwtDecoderBuilder} for further configurations + * @since 5.4 + */ + public JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer(Consumer> jwtProcessorCustomizer) { + Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null"); + this.jwtProcessorCustomizer = jwtProcessorCustomizer; + return this; + } + /** * Build the configured {@link NimbusReactiveJwtDecoder}. * @@ -307,16 +323,13 @@ public NimbusReactiveJwtDecoder build() { JWSKeySelector jwsKeySelector(JWKSource jwkSource) { if (this.signatureAlgorithms.isEmpty()) { return new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, jwkSource); - } else if (this.signatureAlgorithms.size() == 1) { - JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(this.signatureAlgorithms.iterator().next().getName()); - return new JWSVerificationKeySelector<>(jwsAlgorithm, jwkSource); } else { - Map> jwsKeySelectors = new HashMap<>(); + Set jwsAlgorithms = new HashSet<>(); for (SignatureAlgorithm signatureAlgorithm : this.signatureAlgorithms) { - JWSAlgorithm jwsAlg = JWSAlgorithm.parse(signatureAlgorithm.getName()); - jwsKeySelectors.put(jwsAlg, new JWSVerificationKeySelector<>(jwsAlg, jwkSource)); + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(signatureAlgorithm.getName()); + jwsAlgorithms.add(jwsAlgorithm); } - return new JWSAlgorithmMapJWSKeySelector<>(jwsKeySelectors); + return new JWSVerificationKeySelector<>(jwsAlgorithms, jwkSource); } } @@ -327,10 +340,12 @@ Converter> processor() { jwtProcessor.setJWSKeySelector(jwsKeySelector); jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {}); + this.jwtProcessorCustomizer.accept(jwtProcessor); + ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri); source.setWebClient(this.webClient); - Set expectedJwsAlgorithms = getExpectedJwsAlgorithms(jwsKeySelector); + Function expectedJwsAlgorithms = getExpectedJwsAlgorithms(jwsKeySelector); return jwt -> { JWKSelector selector = createSelector(expectedJwsAlgorithms, jwt.getHeader()); return source.get(selector) @@ -339,22 +354,20 @@ Converter> processor() { }; } - private Set getExpectedJwsAlgorithms(JWSKeySelector jwsKeySelector) { + private Function getExpectedJwsAlgorithms(JWSKeySelector jwsKeySelector) { if (jwsKeySelector instanceof JWSVerificationKeySelector) { - return Collections.singleton(((JWSVerificationKeySelector) jwsKeySelector).getExpectedJWSAlgorithm()); - } - if (jwsKeySelector instanceof JWSAlgorithmMapJWSKeySelector) { - return ((JWSAlgorithmMapJWSKeySelector) jwsKeySelector).getExpectedJWSAlgorithms(); + return ((JWSVerificationKeySelector) jwsKeySelector)::isAllowed; } throw new IllegalArgumentException("Unsupported key selector type " + jwsKeySelector.getClass()); } - private JWKSelector createSelector(Set expectedJwsAlgorithms, Header header) { - if (!expectedJwsAlgorithms.contains(header.getAlgorithm())) { + private JWKSelector createSelector(Function expectedJwsAlgorithms, Header header) { + JWSHeader jwsHeader = (JWSHeader) header; + if (!expectedJwsAlgorithms.apply(jwsHeader.getAlgorithm())) { throw new BadJwtException("Unsupported algorithm of " + header.getAlgorithm()); } - return new JWKSelector(JWKMatcher.forJWSHeader((JWSHeader) header)); + return new JWKSelector(JWKMatcher.forJWSHeader(jwsHeader)); } } @@ -366,11 +379,13 @@ private JWKSelector createSelector(Set expectedJwsAlgorithms, Head public static final class PublicKeyReactiveJwtDecoderBuilder { private final RSAPublicKey key; private JWSAlgorithm jwsAlgorithm; + private Consumer> jwtProcessorCustomizer; private PublicKeyReactiveJwtDecoderBuilder(RSAPublicKey key) { Assert.notNull(key, "key cannot be null"); this.key = key; this.jwsAlgorithm = JWSAlgorithm.RS256; + this.jwtProcessorCustomizer = (processor) -> {}; } /** @@ -388,6 +403,20 @@ public PublicKeyReactiveJwtDecoderBuilder signatureAlgorithm(SignatureAlgorithm return this; } + /** + * Use the given {@link Consumer} to customize the {@link JWTProcessor ConfigurableJWTProcessor} before + * passing it to the build {@link NimbusReactiveJwtDecoder}. + * + * @param jwtProcessorCustomizer the callback used to alter the processor + * @return a {@link PublicKeyReactiveJwtDecoderBuilder} for further configurations + * @since 5.4 + */ + public PublicKeyReactiveJwtDecoderBuilder jwtProcessorCustomizer(Consumer> jwtProcessorCustomizer) { + Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null"); + this.jwtProcessorCustomizer = jwtProcessorCustomizer; + return this; + } + /** * Build the configured {@link NimbusReactiveJwtDecoder}. * @@ -412,6 +441,8 @@ Converter> processor() { // Spring Security validates the claim set independent from Nimbus jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); + this.jwtProcessorCustomizer.accept(jwtProcessor); + return jwt -> Mono.just(createClaimsSet(jwtProcessor, jwt, null)); } } @@ -424,10 +455,12 @@ Converter> processor() { public static final class SecretKeyReactiveJwtDecoderBuilder { private final SecretKey secretKey; private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.HS256; + private Consumer> jwtProcessorCustomizer; private SecretKeyReactiveJwtDecoderBuilder(SecretKey secretKey) { Assert.notNull(secretKey, "secretKey cannot be null"); this.secretKey = secretKey; + this.jwtProcessorCustomizer = (processor) -> {}; } /** @@ -447,6 +480,20 @@ public SecretKeyReactiveJwtDecoderBuilder macAlgorithm(MacAlgorithm macAlgorithm return this; } + /** + * Use the given {@link Consumer} to customize the {@link JWTProcessor ConfigurableJWTProcessor} before + * passing it to the build {@link NimbusReactiveJwtDecoder}. + * + * @param jwtProcessorCustomizer the callback used to alter the processor + * @return a {@link SecretKeyReactiveJwtDecoderBuilder} for further configurations + * @since 5.4 + */ + public SecretKeyReactiveJwtDecoderBuilder jwtProcessorCustomizer(Consumer> jwtProcessorCustomizer) { + Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null"); + this.jwtProcessorCustomizer = jwtProcessorCustomizer; + return this; + } + /** * Build the configured {@link NimbusReactiveJwtDecoder}. * @@ -465,6 +512,8 @@ Converter> processor() { // Spring Security validates the claim set independent from Nimbus jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); + this.jwtProcessorCustomizer.accept(jwtProcessor); + return jwt -> Mono.just(createClaimsSet(jwtProcessor, jwt, null)); } } @@ -477,10 +526,12 @@ Converter> processor() { public static final class JwkSourceReactiveJwtDecoderBuilder { private final Function> jwkSource; private JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256; + private Consumer> jwtProcessorCustomizer; private JwkSourceReactiveJwtDecoderBuilder(Function> jwkSource) { Assert.notNull(jwkSource, "jwkSource cannot be null"); this.jwkSource = jwkSource; + this.jwtProcessorCustomizer = (processor) -> {}; } /** @@ -496,6 +547,20 @@ public JwkSourceReactiveJwtDecoderBuilder jwsAlgorithm(JwsAlgorithm jwsAlgorithm return this; } + /** + * Use the given {@link Consumer} to customize the {@link JWTProcessor ConfigurableJWTProcessor} before + * passing it to the build {@link NimbusReactiveJwtDecoder}. + * + * @param jwtProcessorCustomizer the callback used to alter the processor + * @return a {@link JwkSourceReactiveJwtDecoderBuilder} for further configurations + * @since 5.4 + */ + public JwkSourceReactiveJwtDecoderBuilder jwtProcessorCustomizer(Consumer> jwtProcessorCustomizer) { + Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null"); + this.jwtProcessorCustomizer = jwtProcessorCustomizer; + return this; + } + /** * Build the configured {@link NimbusReactiveJwtDecoder}. * @@ -513,6 +578,8 @@ Converter> processor() { jwtProcessor.setJWSKeySelector(jwsKeySelector); jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {}); + this.jwtProcessorCustomizer.accept(jwtProcessor); + return jwt -> { if (jwt instanceof SignedJWT) { return this.jwkSource.apply((SignedJWT) jwt) diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java index 27ab00ff357..a6f5b5e1367 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java @@ -32,8 +32,10 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; import javax.crypto.SecretKey; +import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; @@ -41,6 +43,7 @@ import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; import com.nimbusds.jose.proc.JWSKeySelector; import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; @@ -55,6 +58,8 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.springframework.cache.Cache; +import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -66,6 +71,7 @@ import org.springframework.security.oauth2.jose.TestKeys; import org.springframework.security.oauth2.jose.jws.MacAlgorithm; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; import static org.assertj.core.api.Assertions.assertThat; @@ -75,6 +81,8 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri; import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withPublicKey; @@ -85,6 +93,7 @@ * * @author Josh Cummings * @author Joe Grandja + * @author Mykyta Bezverkhyi */ public class NimbusJwtDecoderTests { private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}"; @@ -247,6 +256,21 @@ public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsJwtException() throws } } + @Test + public void decodeWhenJwkEndpointIsUnresponsiveAndCacheIsConfiguredThenReturnsJwtException() throws Exception { + try ( MockWebServer server = new MockWebServer() ) { + Cache cache = new ConcurrentMapCache("test-jwk-set-cache"); + String jwkSetUri = server.url("/.well-known/jwks.json").toString(); + NimbusJwtDecoder jwtDecoder = withJwkSetUri(jwkSetUri).cache(cache).build(); + + server.shutdown(); + assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtException.class) + .isNotInstanceOf(BadJwtException.class) + .hasMessageContaining("An error occurred while attempting to decode the Jwt"); + } + } + @Test public void withJwkSetUriWhenNullOrEmptyThenThrowsException() { Assertions.assertThatCode(() -> withJwkSetUri(null)).isInstanceOf(IllegalArgumentException.class); @@ -264,6 +288,12 @@ public void restOperationsWhenNullThenThrowsException() { Assertions.assertThatCode(() -> builder.restOperations(null)).isInstanceOf(IllegalArgumentException.class); } + @Test + public void cacheWhenNullThenThrowsException() { + NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = withJwkSetUri(JWK_SET_URI); + Assertions.assertThatCode(() -> builder.cache(null)).isInstanceOf(IllegalArgumentException.class); + } + @Test public void withPublicKeyWhenNullThenThrowsException() { assertThatThrownBy(() -> withPublicKey(null)) @@ -318,6 +348,30 @@ public void decodeWhenSignatureMismatchesAlgorithmThenThrowsException() throws E .isInstanceOf(BadJwtException.class); } + // gh-8730 + @Test + public void withPublicKeyWhenUsingCustomTypeHeaderThenSuccessfullyDecodes() throws Exception { + RSAPublicKey publicKey = TestKeys.DEFAULT_PUBLIC_KEY; + RSAPrivateKey privateKey = TestKeys.DEFAULT_PRIVATE_KEY; + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).type(new JOSEObjectType("JWS")).build(); + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .expirationTime(Date.from(Instant.now().plusSeconds(60))) + .build(); + SignedJWT signedJwt = signedJwt(privateKey, header, claimsSet); + NimbusJwtDecoder decoder = withPublicKey(publicKey) + .signatureAlgorithm(SignatureAlgorithm.RS256) + .jwtProcessorCustomizer(p -> p.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWS")))) + .build(); + assertThat(decoder.decode(signedJwt.serialize()).containsClaim(JwtClaimNames.EXP)).isNotNull(); + } + + @Test + public void withPublicKeyWhenJwtProcessorCustomizerNullThenThrowsIllegalArgumentException() { + assertThatThrownBy(() -> withPublicKey(key()).jwtProcessorCustomizer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwtProcessorCustomizer cannot be null"); + } + @Test public void withSecretKeyWhenNullThenThrowsIllegalArgumentException() { assertThatThrownBy(() -> withSecretKey(null)) @@ -379,6 +433,30 @@ public void decodeWhenUsingSecertKeyWithKidThenStillUsesKey() throws Exception { .isEqualTo("test-subject"); } + // gh-8730 + @Test + public void withSecretKeyWhenUsingCustomTypeHeaderThenSuccessfullyDecodes() throws Exception { + SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY; + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.HS256).type(new JOSEObjectType("JWS")).build(); + JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() + .expirationTime(Date.from(Instant.now().plusSeconds(60))) + .build(); + SignedJWT signedJwt = signedJwt(secretKey, header, claimsSet); + NimbusJwtDecoder decoder = withSecretKey(secretKey) + .macAlgorithm(MacAlgorithm.HS256) + .jwtProcessorCustomizer(p -> p.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWS")))) + .build(); + assertThat(decoder.decode(signedJwt.serialize()).containsClaim(JwtClaimNames.EXP)).isNotNull(); + } + + @Test + public void withSecretKeyWhenJwtProcessorCustomizerNullThenThrowsIllegalArgumentException() { + SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY; + assertThatThrownBy(() -> withSecretKey(secretKey).jwtProcessorCustomizer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwtProcessorCustomizer cannot be null"); + } + @Test public void jwsKeySelectorWhenNoAlgorithmThenReturnsRS256Selector() { JWKSource jwkSource = mock(JWKSource.class); @@ -387,8 +465,8 @@ public void jwsKeySelectorWhenNoAlgorithmThenReturnsRS256Selector() { assertThat(jwsKeySelector instanceof JWSVerificationKeySelector); JWSVerificationKeySelector jwsVerificationKeySelector = (JWSVerificationKeySelector) jwsKeySelector; - assertThat(jwsVerificationKeySelector.getExpectedJWSAlgorithm()) - .isEqualTo(JWSAlgorithm.RS256); + assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS256)) + .isTrue(); } @Test @@ -400,8 +478,8 @@ public void jwsKeySelectorWhenOneAlgorithmThenReturnsSingleSelector() { assertThat(jwsKeySelector instanceof JWSVerificationKeySelector); JWSVerificationKeySelector jwsVerificationKeySelector = (JWSVerificationKeySelector) jwsKeySelector; - assertThat(jwsVerificationKeySelector.getExpectedJWSAlgorithm()) - .isEqualTo(JWSAlgorithm.RS512); + assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS512)) + .isTrue(); } @Test @@ -412,11 +490,13 @@ public void jwsKeySelectorWhenMultipleAlgorithmThenReturnsCompositeSelector() { .jwsAlgorithm(SignatureAlgorithm.RS256) .jwsAlgorithm(SignatureAlgorithm.RS512) .jwsKeySelector(jwkSource); - assertThat(jwsKeySelector instanceof JWSAlgorithmMapJWSKeySelector); - JWSAlgorithmMapJWSKeySelector jwsAlgorithmMapKeySelector = - (JWSAlgorithmMapJWSKeySelector) jwsKeySelector; - assertThat(jwsAlgorithmMapKeySelector.getExpectedJWSAlgorithms()) - .containsExactlyInAnyOrder(JWSAlgorithm.RS256, JWSAlgorithm.RS512); + assertThat(jwsKeySelector instanceof JWSVerificationKeySelector); + JWSVerificationKeySelector jwsAlgorithmMapKeySelector = + (JWSVerificationKeySelector) jwsKeySelector; + assertThat(jwsAlgorithmMapKeySelector.isAllowed(JWSAlgorithm.RS256)) + .isTrue(); + assertThat(jwsAlgorithmMapKeySelector.isAllowed(JWSAlgorithm.RS512)) + .isTrue(); } // gh-7290 @@ -425,7 +505,7 @@ public void decodeWhenJwkSetRequestedThenAcceptHeaderJsonAndJwkSetJson() { RestOperations restOperations = mock(RestOperations.class); when(restOperations.exchange(any(RequestEntity.class), eq(String.class))) .thenReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK)); - JWTProcessor processor = withJwkSetUri("https://issuer/.well-known/jwks.json") + JWTProcessor processor = withJwkSetUri(JWK_SET_URI) .restOperations(restOperations) .processor(); NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(processor); @@ -436,6 +516,86 @@ public void decodeWhenJwkSetRequestedThenAcceptHeaderJsonAndJwkSetJson() { assertThat(acceptHeader).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); } + @Test + public void decodeWhenCacheThenStoreRetrievedJwkSetToCache() { + // given + Cache cache = new ConcurrentMapCache("test-jwk-set-cache"); + RestOperations restOperations = mock(RestOperations.class); + when(restOperations.exchange(any(RequestEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK)); + NimbusJwtDecoder jwtDecoder = withJwkSetUri(JWK_SET_URI) + .restOperations(restOperations) + .cache(cache) + .build(); + // when + jwtDecoder.decode(SIGNED_JWT); + // then + assertThat(cache.get(JWK_SET_URI, String.class)).isEqualTo(JWK_SET); + ArgumentCaptor requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class); + verify(restOperations).exchange(requestEntityCaptor.capture(), eq(String.class)); + verifyNoMoreInteractions(restOperations); + List acceptHeader = requestEntityCaptor.getValue().getHeaders().getAccept(); + assertThat(acceptHeader).contains(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON); + } + + @Test + public void decodeWhenCacheThenRetrieveFromCache() { + // given + RestOperations restOperations = mock(RestOperations.class); + Cache cache = mock(Cache.class); + when(cache.get(eq(JWK_SET_URI), any(Callable.class))).thenReturn(JWK_SET); + NimbusJwtDecoder jwtDecoder = withJwkSetUri(JWK_SET_URI) + .cache(cache) + .restOperations(restOperations) + .build(); + // when + jwtDecoder.decode(SIGNED_JWT); + // then + verify(cache).get(eq(JWK_SET_URI), any(Callable.class)); + verifyNoMoreInteractions(cache); + verifyNoInteractions(restOperations); + } + + @Test + public void decodeWhenCacheIsConfiguredAndValueLoaderErrorsThenThrowsJwtException() { + // given + Cache cache = new ConcurrentMapCache("test-jwk-set-cache"); + RestOperations restOperations = mock(RestOperations.class); + when(restOperations.exchange(any(RequestEntity.class), eq(String.class))) + .thenThrow(new RestClientException("Cannot retrieve JWK Set")); + NimbusJwtDecoder jwtDecoder = withJwkSetUri(JWK_SET_URI) + .restOperations(restOperations) + .cache(cache) + .build(); + // then + assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)) + .isInstanceOf(JwtException.class) + .isNotInstanceOf(BadJwtException.class) + .hasMessageContaining("An error occurred while attempting to decode the Jwt"); + } + + // gh-8730 + @Test + public void withJwkSetUriWhenUsingCustomTypeHeaderThenRefuseOmittedType() throws Exception { + RestOperations restOperations = mock(RestOperations.class); + when(restOperations.exchange(any(RequestEntity.class), eq(String.class))) + .thenReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK)); + NimbusJwtDecoder jwtDecoder = withJwkSetUri(JWK_SET_URI) + .restOperations(restOperations) + .jwtProcessorCustomizer(p -> p.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWS")))) + .build(); + assertThatCode(() -> jwtDecoder.decode(SIGNED_JWT)) + .isInstanceOf(BadJwtException.class) + .hasMessageContaining("An error occurred while attempting to decode the Jwt: Required JOSE header \"typ\" (type) parameter is missing"); + } + + @Test + public void withJwkSetUriWhenJwtProcessorCustomizerNullThenThrowsIllegalArgumentException() { + assertThatThrownBy(() -> withJwkSetUri(JWK_SET_URI).jwtProcessorCustomizer(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwtProcessorCustomizer cannot be null"); + } + private RSAPublicKey key() throws InvalidKeySpecException { byte[] decoded = Base64.getDecoder().decode(VERIFY_KEY.getBytes()); EncodedKeySpec spec = new X509EncodedKeySpec(decoded); @@ -466,7 +626,7 @@ private static JWTProcessor withSigning(String jwkResponse) { RestOperations restOperations = mock(RestOperations.class); when(restOperations.exchange(any(RequestEntity.class), eq(String.class))) .thenReturn(new ResponseEntity<>(jwkResponse, HttpStatus.OK)); - return withJwkSetUri("https://issuer/.well-known/jwks.json") + return withJwkSetUri(JWK_SET_URI) .restOperations(restOperations) .processor(); } diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java index 74a9d8f671c..d701b7b6f30 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java @@ -31,12 +31,14 @@ import java.util.Map; import javax.crypto.SecretKey; +import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; import com.nimbusds.jose.proc.JWKSecurityContext; import com.nimbusds.jose.proc.JWSKeySelector; import com.nimbusds.jose.proc.JWSVerificationKeySelector; @@ -44,6 +46,7 @@ import com.nimbusds.jwt.SignedJWT; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; @@ -269,6 +272,13 @@ public void jwsAlgorithmWhenNullThenThrowsException() { assertThatCode(() -> builder.jwsAlgorithm(null)).isInstanceOf(IllegalArgumentException.class); } + @Test + public void withJwkSetUriWhenJwtProcessorCustomizerNullThenThrowsIllegalArgumentException() { + assertThatCode(() -> withJwkSetUri(jwkSetUri).jwtProcessorCustomizer(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwtProcessorCustomizer cannot be null"); + } + @Test public void restOperationsWhenNullThenThrowsException() { NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder builder = withJwkSetUri(this.jwkSetUri); @@ -286,6 +296,19 @@ public void decodeWhenSignedThenOk() { verify(webClient).get(); } + // gh-8730 + @Test + public void withJwkSetUriWhenUsingCustomTypeHeaderThenRefuseOmittedType() { + WebClient webClient = mockJwkSetResponse(this.jwkSet); + NimbusReactiveJwtDecoder decoder = withJwkSetUri(this.jwkSetUri) + .webClient(webClient) + .jwtProcessorCustomizer(p -> p.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWS")))) + .build(); + assertThatCode(() -> decoder.decode(messageReadToken).block()) + .isInstanceOf(BadJwtException.class) + .hasRootCauseMessage("Required JOSE header \"typ\" (type) parameter is missing"); + } + @Test public void withPublicKeyWhenNullThenThrowsException() { assertThatThrownBy(() -> withPublicKey(null)) @@ -300,6 +323,13 @@ public void buildWhenSignatureAlgorithmMismatchesKeyTypeThenThrowsException() { .isInstanceOf(IllegalStateException.class); } + @Test + public void buildWhenJwtProcessorCustomizerNullThenThrowsIllegalArgumentException() { + assertThatCode(() -> withPublicKey(key()).jwtProcessorCustomizer(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwtProcessorCustomizer cannot be null"); + } + @Test public void decodeWhenUsingPublicKeyThenSuccessfullyDecodes() throws Exception { NimbusReactiveJwtDecoder decoder = withPublicKey(key()).build(); @@ -325,12 +355,31 @@ public void decodeWhenSignatureMismatchesAlgorithmThenThrowsException() throws E .isInstanceOf(BadJwtException.class); } + // gh-8730 + @Test + public void withPublicKeyWhenUsingCustomTypeHeaderThenRefuseOmittedType() throws Exception { + NimbusReactiveJwtDecoder decoder = withPublicKey(key()) + .jwtProcessorCustomizer(p -> p.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWS")))) + .build(); + + AssertionsForClassTypes.assertThatCode(() -> decoder.decode(this.rsa256).block()) + .isInstanceOf(BadJwtException.class) + .hasRootCauseMessage("Required JOSE header \"typ\" (type) parameter is missing"); + } + @Test public void withJwkSourceWhenNullThenThrowsException() { assertThatCode(() -> withJwkSource(null)) .isInstanceOf(IllegalArgumentException.class); } + @Test + public void withJwkSourceWhenJwtProcessorCustomizerNullThenThrowsIllegalArgumentException() { + assertThatCode(() -> withJwkSource(jwt -> Flux.empty()).jwtProcessorCustomizer(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwtProcessorCustomizer cannot be null"); + } + @Test public void decodeWhenCustomJwkSourceResolutionThenDecodes() { NimbusReactiveJwtDecoder decoder = @@ -342,6 +391,18 @@ public void decodeWhenCustomJwkSourceResolutionThenDecodes() { .isNotNull(); } + // gh-8730 + @Test + public void withJwkSourceWhenUsingCustomTypeHeaderThenRefuseOmittedType() { + NimbusReactiveJwtDecoder decoder = withJwkSource(jwt -> Flux.empty()) + .jwtProcessorCustomizer(p -> p.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWS")))) + .build(); + + assertThatCode(() -> decoder.decode(this.messageReadToken).block()) + .isInstanceOf(BadJwtException.class) + .hasRootCauseMessage("Required JOSE header \"typ\" (type) parameter is missing"); + } + @Test public void withSecretKeyWhenSecretKeyNullThenThrowsIllegalArgumentException() { assertThatThrownBy(() -> withSecretKey(null)) @@ -349,6 +410,14 @@ public void withSecretKeyWhenSecretKeyNullThenThrowsIllegalArgumentException() { .hasMessage("secretKey cannot be null"); } + @Test + public void withSecretKeyWhenJwtProcessorCustomizerNullThenThrowsIllegalArgumentException() { + SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY; + assertThatThrownBy(() -> withSecretKey(secretKey).jwtProcessorCustomizer(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("jwtProcessorCustomizer cannot be null"); + } + @Test public void withSecretKeyWhenMacAlgorithmNullThenThrowsIllegalArgumentException() { SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY; @@ -372,6 +441,18 @@ public void decodeWhenSecretKeyThenSuccess() throws Exception { assertThat(jwt.getSubject()).isEqualTo("test-subject"); } + // gh-8730 + @Test + public void withSecretKeyWhenUsingCustomTypeHeaderThenRefuseOmittedType() { + SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY; + NimbusReactiveJwtDecoder decoder = withSecretKey(secretKey) + .jwtProcessorCustomizer(p -> p.setJWSTypeVerifier(new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("JWS")))) + .build(); + assertThatCode(() -> decoder.decode(messageReadToken).block()) + .isInstanceOf(BadJwtException.class) + .hasRootCauseMessage("Required JOSE header \"typ\" (type) parameter is missing"); + } + @Test public void decodeWhenSecretKeyAndAlgorithmMismatchThenThrowsJwtException() throws Exception { SecretKey secretKey = TestKeys.DEFAULT_SECRET_KEY; @@ -395,8 +476,8 @@ public void jwsKeySelectorWhenNoAlgorithmThenReturnsRS256Selector() { assertThat(jwsKeySelector instanceof JWSVerificationKeySelector); JWSVerificationKeySelector jwsVerificationKeySelector = (JWSVerificationKeySelector) jwsKeySelector; - assertThat(jwsVerificationKeySelector.getExpectedJWSAlgorithm()) - .isEqualTo(JWSAlgorithm.RS256); + assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS256)) + .isTrue(); } @Test @@ -408,8 +489,8 @@ public void jwsKeySelectorWhenOneAlgorithmThenReturnsSingleSelector() { assertThat(jwsKeySelector instanceof JWSVerificationKeySelector); JWSVerificationKeySelector jwsVerificationKeySelector = (JWSVerificationKeySelector) jwsKeySelector; - assertThat(jwsVerificationKeySelector.getExpectedJWSAlgorithm()) - .isEqualTo(JWSAlgorithm.RS512); + assertThat(jwsVerificationKeySelector.isAllowed(JWSAlgorithm.RS512)) + .isTrue(); } @Test @@ -420,11 +501,13 @@ public void jwsKeySelectorWhenMultipleAlgorithmThenReturnsCompositeSelector() { .jwsAlgorithm(SignatureAlgorithm.RS256) .jwsAlgorithm(SignatureAlgorithm.RS512) .jwsKeySelector(jwkSource); - assertThat(jwsKeySelector instanceof JWSAlgorithmMapJWSKeySelector); - JWSAlgorithmMapJWSKeySelector jwsAlgorithmMapKeySelector = - (JWSAlgorithmMapJWSKeySelector) jwsKeySelector; - assertThat(jwsAlgorithmMapKeySelector.getExpectedJWSAlgorithms()) - .containsExactlyInAnyOrder(JWSAlgorithm.RS256, JWSAlgorithm.RS512); + assertThat(jwsKeySelector instanceof JWSVerificationKeySelector); + JWSVerificationKeySelector jwsAlgorithmMapKeySelector = + (JWSVerificationKeySelector) jwsKeySelector; + assertThat(jwsAlgorithmMapKeySelector.isAllowed(JWSAlgorithm.RS256)) + .isTrue(); + assertThat(jwsAlgorithmMapKeySelector.isAllowed(JWSAlgorithm.RS512)) + .isTrue(); } private SignedJWT signedJwt(SecretKey secretKey, MacAlgorithm jwsAlgorithm, JWTClaimsSet claimsSet) throws Exception { diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java index 22fea53fb0a..4b8171e8cb5 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * 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. @@ -22,21 +22,30 @@ import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.util.Assert; /** * @author Rob Winch * @author Josh Cummings + * @author Evgeniy Cheban * @since 5.1 */ public class JwtAuthenticationConverter implements Converter { private Converter> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + private String principalClaimName; + @Override public final AbstractAuthenticationToken convert(Jwt jwt) { Collection authorities = extractAuthorities(jwt); - return new JwtAuthenticationToken(jwt, authorities); + if (this.principalClaimName == null) { + return new JwtAuthenticationToken(jwt, authorities); + } + + String name = jwt.getClaim(this.principalClaimName); + return new JwtAuthenticationToken(jwt, authorities, name); } /** @@ -65,4 +74,16 @@ public void setJwtGrantedAuthoritiesConverter(Converter convert(Jwt jwt) { * @since 5.2 */ public void setAuthorityPrefix(String authorityPrefix) { - Assert.hasText(authorityPrefix, "authorityPrefix cannot be empty"); + Assert.notNull(authorityPrefix, "authorityPrefix cannot be null"); this.authorityPrefix = authorityPrefix; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java index 3f132fbe2c6..97cc3d3fe9f 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtIssuerAuthenticationManagerResolver.java @@ -45,7 +45,7 @@ * * To use, this class must be able to determine whether or not the `iss` claim is trusted. Recall that * anyone can stand up an authorization server and issue valid tokens to a resource server. The simplest way - * to achieve this is to supply a whitelist of trusted issuers in the constructor. + * to achieve this is to supply a list of trusted issuers in the constructor. * * This class derives the Issuer from the `iss` claim found in the {@link HttpServletRequest}'s * Bearer Token. @@ -60,7 +60,7 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat /** * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided parameters * - * @param trustedIssuers a whitelist of trusted issuers + * @param trustedIssuers a list of trusted issuers */ public JwtIssuerAuthenticationManagerResolver(String... trustedIssuers) { this(Arrays.asList(trustedIssuers)); @@ -69,7 +69,7 @@ public JwtIssuerAuthenticationManagerResolver(String... trustedIssuers) { /** * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided parameters * - * @param trustedIssuers a whitelist of trusted issuers + * @param trustedIssuers a list of trusted issuers */ public JwtIssuerAuthenticationManagerResolver(Collection trustedIssuers) { Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); @@ -82,7 +82,7 @@ public JwtIssuerAuthenticationManagerResolver(Collection trustedIssuers) * Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided parameters * * Note that the {@link AuthenticationManagerResolver} provided in this constructor will need to - * verify that the issuer is trusted. This should be done via a whitelist. + * verify that the issuer is trusted. This should be done via an allowlist. * * One way to achieve this is with a {@link Map} where the keys are the known issuers: *
@@ -93,7 +93,7 @@ public JwtIssuerAuthenticationManagerResolver(Collection trustedIssuers)
 	 *     	(authenticationManagers::get);
 	 * 
* - * The keys in the {@link Map} are the whitelist. + * The keys in the {@link Map} are the allowed issuers. * * @param issuerAuthenticationManagerResolver a strategy for resolving the {@link AuthenticationManager} by the issuer */ 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 c2f415378b7..0328d5ae3b8 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 @@ -48,7 +48,7 @@ * * To use, this class must be able to determine whether or not the `iss` claim is trusted. Recall that * anyone can stand up an authorization server and issue valid tokens to a resource server. The simplest way - * to achieve this is to supply a whitelist of trusted issuers in the constructor. + * to achieve this is to supply a list of trusted issuers in the constructor. * * This class derives the Issuer from the `iss` claim found in the {@link ServerWebExchange}'s * Bearer Token. @@ -66,7 +66,7 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver /** * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the provided parameters * - * @param trustedIssuers a whitelist of trusted issuers + * @param trustedIssuers a list of trusted issuers */ public JwtIssuerReactiveAuthenticationManagerResolver(String... trustedIssuers) { this(Arrays.asList(trustedIssuers)); @@ -75,7 +75,7 @@ public JwtIssuerReactiveAuthenticationManagerResolver(String... trustedIssuers) /** * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the provided parameters * - * @param trustedIssuers a whitelist of trusted issuers + * @param trustedIssuers a collection of trusted issuers */ public JwtIssuerReactiveAuthenticationManagerResolver(Collection trustedIssuers) { Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); @@ -87,7 +87,7 @@ public JwtIssuerReactiveAuthenticationManagerResolver(Collection trusted * Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the provided parameters * * Note that the {@link ReactiveAuthenticationManagerResolver} provided in this constructor will need to - * verify that the issuer is trusted. This should be done via a whitelist. + * verify that the issuer is trusted. This should be done via an allowed list of issuers. * * One way to achieve this is with a {@link Map} where the keys are the known issuers: *
@@ -98,7 +98,7 @@ public JwtIssuerReactiveAuthenticationManagerResolver(Collection trusted
 	 *     	(issuer -> Mono.justOrEmpty(authenticationManagers.get(issuer));
 	 * 
* - * The keys in the {@link Map} are the whitelist. + * The keys in the {@link Map} are the trusted issuers. * * @param issuerAuthenticationManagerResolver a strategy for resolving the {@link ReactiveAuthenticationManager} * by the issuer diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/NimbusOpaqueTokenIntrospector.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/NimbusOpaqueTokenIntrospector.java index 1f0821cff78..440ba8b9c4b 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/NimbusOpaqueTokenIntrospector.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/NimbusOpaqueTokenIntrospector.java @@ -39,7 +39,6 @@ import org.springframework.http.client.support.BasicAuthenticationInterceptor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -232,7 +231,7 @@ private OAuth2AuthenticatedPrincipal convertClaimsSet(TokenIntrospectionSuccessR } } - return new DefaultOAuth2AuthenticatedPrincipal(claims, authorities); + return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities); } private URL issuer(String uri) { diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/NimbusReactiveOpaqueTokenIntrospector.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/NimbusReactiveOpaqueTokenIntrospector.java index 2aa31b792c8..05c1e9c4c37 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/NimbusReactiveOpaqueTokenIntrospector.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/NimbusReactiveOpaqueTokenIntrospector.java @@ -37,7 +37,6 @@ import org.springframework.http.MediaType; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.util.Assert; import org.springframework.web.reactive.function.BodyInserters; @@ -193,7 +192,7 @@ private OAuth2AuthenticatedPrincipal convertClaimsSet(TokenIntrospectionSuccessR } } - return new DefaultOAuth2AuthenticatedPrincipal(claims, authorities); + return new OAuth2IntrospectionAuthenticatedPrincipal(claims, authorities); } private URL issuer(String uri) { diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionAuthenticatedPrincipal.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionAuthenticatedPrincipal.java new file mode 100644 index 00000000000..0f4f9254407 --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionAuthenticatedPrincipal.java @@ -0,0 +1,108 @@ +/* + * 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.oauth2.server.resource.introspection; + +import static org.springframework.security.core.authority.AuthorityUtils.NO_AUTHORITIES; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; +import org.springframework.util.Assert; + +/** + * A domain object that wraps the attributes of OAuth 2.0 Token Introspection. + * + * @author David Kovac + * @since 5.4 + * @see Introspection Response + */ +public final class OAuth2IntrospectionAuthenticatedPrincipal implements OAuth2AuthenticatedPrincipal, + OAuth2IntrospectionClaimAccessor, Serializable { + private final Map attributes; + private final Collection authorities; + private final String name; + + /** + * Constructs an {@code OAuth2IntrospectionAuthenticatedPrincipal} using the provided parameters. + * + * @param attributes the attributes of the OAuth 2.0 Token Introspection + * @param authorities the authorities of the OAuth 2.0 Token Introspection + */ + public OAuth2IntrospectionAuthenticatedPrincipal(Map attributes, + Collection authorities) { + + this(null, attributes, authorities); + } + + /** + * Constructs an {@code OAuth2IntrospectionAuthenticatedPrincipal} using the provided parameters. + * + * @param name the name attached to the OAuth 2.0 Token Introspection + * @param attributes the attributes of the OAuth 2.0 Token Introspection + * @param authorities the authorities of the OAuth 2.0 Token Introspection + */ + public OAuth2IntrospectionAuthenticatedPrincipal(String name, Map attributes, + Collection authorities) { + + Assert.notEmpty(attributes, "attributes cannot be empty"); + this.attributes = Collections.unmodifiableMap(attributes); + this.authorities = authorities == null ? + NO_AUTHORITIES : Collections.unmodifiableCollection(authorities); + this.name = name == null ? getSubject() : name; + } + + /** + * Gets the attributes of the OAuth 2.0 Token Introspection in map form. + * + * @return a {@link Map} of the attribute's objects keyed by the attribute's names + */ + @Override + public Map getAttributes() { + return this.attributes; + } + + /** + * Get the {@link Collection} of {@link GrantedAuthority}s associated + * with this OAuth 2.0 Token Introspection + * + * @return the OAuth 2.0 Token Introspection authorities + */ + @Override + public Collection getAuthorities() { + return this.authorities; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return this.name; + } + + /** + * {@inheritDoc} + */ + @Override + public Map getClaims() { + return getAttributes(); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionClaimAccessor.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionClaimAccessor.java new file mode 100644 index 00000000000..18c3c30e78f --- /dev/null +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionClaimAccessor.java @@ -0,0 +1,144 @@ +/* + * 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.oauth2.server.resource.introspection; + +import java.net.URL; +import java.time.Instant; +import java.util.List; + +import org.springframework.security.oauth2.core.ClaimAccessor; + +/** + * A {@link ClaimAccessor} for the "claims" that may be contained + * in the Introspection Response. + * + * @author David Kovac + * @since 5.4 + * @see ClaimAccessor + * @see OAuth2IntrospectionClaimNames + * @see OAuth2IntrospectionAuthenticatedPrincipal + * @see Introspection Response + */ +public interface OAuth2IntrospectionClaimAccessor extends ClaimAccessor { + /** + * Returns the indicator {@code (active)} whether or not the token is currently active + * + * @return the indicator whether or not the token is currently active + */ + default boolean isActive() { + return Boolean.TRUE.equals(this.getClaimAsBoolean(OAuth2IntrospectionClaimNames.ACTIVE)); + } + + /** + * Returns the scopes {@code (scope)} associated with the token + * + * @return the scopes associated with the token + */ + default String getScope() { + return this.getClaimAsString(OAuth2IntrospectionClaimNames.SCOPE); + } + + /** + * Returns the client identifier {@code (client_id)} for the token + * + * @return the client identifier for the token + */ + default String getClientId() { + return this.getClaimAsString(OAuth2IntrospectionClaimNames.CLIENT_ID); + } + + /** + * Returns a human-readable identifier {@code (username)} for the resource owner that authorized the token + * + * @return a human-readable identifier for the resource owner that authorized the token + */ + default String getUsername() { + return this.getClaimAsString(OAuth2IntrospectionClaimNames.USERNAME); + } + + /** + * Returns the type of the token {@code (token_type)}, for example {@code bearer}. + * + * @return the type of the token, for example {@code bearer}. + */ + default String getTokenType() { + return this.getClaimAsString(OAuth2IntrospectionClaimNames.TOKEN_TYPE); + } + + /** + * Returns a timestamp {@code (exp)} indicating when the token expires + * + * @return a timestamp indicating when the token expires + */ + default Instant getExpiresAt() { + return this.getClaimAsInstant(OAuth2IntrospectionClaimNames.EXPIRES_AT); + } + + /** + * Returns a timestamp {@code (iat)} indicating when the token was issued + * + * @return a timestamp indicating when the token was issued + */ + default Instant getIssuedAt() { + return this.getClaimAsInstant(OAuth2IntrospectionClaimNames.ISSUED_AT); + } + + /** + * Returns a timestamp {@code (nbf)} indicating when the token is not to be used before + * + * @return a timestamp indicating when the token is not to be used before + */ + default Instant getNotBefore() { + return this.getClaimAsInstant(OAuth2IntrospectionClaimNames.NOT_BEFORE); + } + + /** + * Returns usually a machine-readable identifier {@code (sub)} of the resource owner who authorized the token + * + * @return usually a machine-readable identifier of the resource owner who authorized the token + */ + default String getSubject() { + return this.getClaimAsString(OAuth2IntrospectionClaimNames.SUBJECT); + } + + /** + * Returns the intended audience {@code (aud)} for the token + * + * @return the intended audience for the token + */ + default List getAudience() { + return this.getClaimAsStringList(OAuth2IntrospectionClaimNames.AUDIENCE); + } + + /** + * Returns the issuer {@code (iss)} of the token + * + * @return the issuer of the token + */ + default URL getIssuer() { + return this.getClaimAsURL(OAuth2IntrospectionClaimNames.ISSUER); + } + + /** + * Returns the identifier {@code (jti)} for the token + * + * @return the identifier for the token + */ + default String getId() { + return this.getClaimAsString(OAuth2IntrospectionClaimNames.JTI); + } +} diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java index f96cdbcddbd..386b6ebfcf1 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolver.java @@ -45,6 +45,8 @@ public final class DefaultBearerTokenResolver implements BearerTokenResolver { private boolean allowUriQueryParameter = false; + private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION; + /** * {@inheritDoc} */ @@ -85,8 +87,21 @@ public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { this.allowUriQueryParameter = allowUriQueryParameter; } - private static String resolveFromAuthorizationHeader(HttpServletRequest request) { - String authorization = request.getHeader(HttpHeaders.AUTHORIZATION); + /** + * Set this value to configure what header is checked when resolving a Bearer Token. + * This value is defaulted to {@link HttpHeaders#AUTHORIZATION}. + * + * This allows other headers to be used as the Bearer Token source such as {@link HttpHeaders#PROXY_AUTHORIZATION} + * + * @param bearerTokenHeaderName the header to check when retrieving the Bearer Token. + * @since 5.4 + */ + public void setBearerTokenHeaderName(String bearerTokenHeaderName) { + this.bearerTokenHeaderName = bearerTokenHeaderName; + } + + private String resolveFromAuthorizationHeader(HttpServletRequest request) { + String authorization = request.getHeader(this.bearerTokenHeaderName); if (StringUtils.startsWithIgnoreCase(authorization, "bearer")) { Matcher matcher = authorizationPattern.matcher(authorization); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java index f6dda6cab70..eaa579c4d47 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverter.java @@ -50,6 +50,7 @@ public class ServerBearerTokenAuthenticationConverter Pattern.CASE_INSENSITIVE); private boolean allowUriQueryParameter = false; + private String bearerTokenHeaderName = HttpHeaders.AUTHORIZATION; public Mono convert(ServerWebExchange exchange) { return Mono.justOrEmpty(token(exchange.getRequest())) @@ -90,8 +91,21 @@ public void setAllowUriQueryParameter(boolean allowUriQueryParameter) { this.allowUriQueryParameter = allowUriQueryParameter; } - private static String resolveFromAuthorizationHeader(HttpHeaders headers) { - String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION); + /** + * Set this value to configure what header is checked when resolving a Bearer Token. + * This value is defaulted to {@link HttpHeaders#AUTHORIZATION}. + * + * This allows other headers to be used as the Bearer Token source such as {@link HttpHeaders#PROXY_AUTHORIZATION} + * + * @param bearerTokenHeaderName the header to check when retrieving the Bearer Token. + * @since 5.4 + */ + public void setBearerTokenHeaderName(String bearerTokenHeaderName) { + this.bearerTokenHeaderName = bearerTokenHeaderName; + } + + private String resolveFromAuthorizationHeader(HttpHeaders headers) { + String authorization = headers.getFirst(this.bearerTokenHeaderName); if (StringUtils.startsWithIgnoreCase(authorization, "bearer")) { Matcher matcher = authorizationPattern.matcher(authorization); diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/core/TestOAuth2AuthenticatedPrincipals.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/core/TestOAuth2AuthenticatedPrincipals.java index c1b593d800d..92c9437a349 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/core/TestOAuth2AuthenticatedPrincipals.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/core/TestOAuth2AuthenticatedPrincipals.java @@ -28,6 +28,7 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames; /** @@ -56,7 +57,7 @@ public static OAuth2AuthenticatedPrincipal active(Consumer> Collection authorities = Arrays.asList(new SimpleGrantedAuthority("SCOPE_read"), new SimpleGrantedAuthority("SCOPE_write"), new SimpleGrantedAuthority("SCOPE_dolphin")); - return new DefaultOAuth2AuthenticatedPrincipal(attributes, authorities); + return new OAuth2IntrospectionAuthenticatedPrincipal(attributes, authorities); } private static URL url(String url) { diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java index 62fbd20ed3c..6ade846fbc0 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationConverterTests.java @@ -35,6 +35,7 @@ * Tests for {@link JwtAuthenticationConverter} * * @author Josh Cummings + * @author Evgeniy Cheban */ public class JwtAuthenticationConverterTests { JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); @@ -73,4 +74,35 @@ public void convertWithOverriddenGrantedAuthoritiesConverter() { assertThat(authorities).containsExactly( new SimpleGrantedAuthority("blah")); } + + @Test + public void whenSettingNullPrincipalClaimName() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.jwtAuthenticationConverter.setPrincipalClaimName(null)) + .withMessage("principalClaimName cannot be empty"); + } + + @Test + public void whenSettingEmptyPrincipalClaimName() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.jwtAuthenticationConverter.setPrincipalClaimName("")) + .withMessage("principalClaimName cannot be empty"); + } + + @Test + public void whenSettingBlankPrincipalClaimName() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.jwtAuthenticationConverter.setPrincipalClaimName(" ")) + .withMessage("principalClaimName cannot be empty"); + } + + @Test + public void convertWhenPrincipalClaimNameSet() { + this.jwtAuthenticationConverter.setPrincipalClaimName("user_id"); + + Jwt jwt = jwt().claim("user_id", "100").build(); + AbstractAuthenticationToken authentication = this.jwtAuthenticationConverter.convert(jwt); + + assertThat(authentication.getName()).isEqualTo("100"); + } } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java index d8c6d911779..70ecf618e77 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtGrantedAuthoritiesConverterTests.java @@ -37,6 +37,12 @@ */ public class JwtGrantedAuthoritiesConverterTests { + @Test(expected = IllegalArgumentException.class) + public void setAuthorityPrefixWithNullThenException() { + JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix(null); + } + @Test public void convertWhenTokenHasScopeAttributeThenTranslatedToAuthorities() { Jwt jwt = jwt().claim("scope", "message:read message:write").build(); @@ -62,6 +68,19 @@ public void convertWithCustomAuthorityPrefixWhenTokenHasScopeAttributeThenTransl new SimpleGrantedAuthority("ROLE_message:write")); } + @Test + public void convertWithBlankAsCustomAuthorityPrefixWhenTokenHasScopeAttributeThenTranslatedToAuthorities() { + Jwt jwt = jwt().claim("scope", "message:read message:write").build(); + + JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix(""); + Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt); + + assertThat(authorities).containsExactly( + new SimpleGrantedAuthority("message:read"), + new SimpleGrantedAuthority("message:write")); + } + @Test public void convertWhenTokenHasEmptyScopeAttributeThenTranslatedToNoAuthorities() { Jwt jwt = jwt().claim("scope", "").build(); @@ -97,6 +116,19 @@ public void convertWithCustomAuthorityPrefixWhenTokenHasScpAttributeThenTranslat new SimpleGrantedAuthority("ROLE_message:write")); } + @Test + public void convertWithBlankAsCustomAuthorityPrefixWhenTokenHasScpAttributeThenTranslatedToAuthorities() { + Jwt jwt = jwt().claim("scp", "message:read message:write").build(); + + JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix(""); + Collection authorities = jwtGrantedAuthoritiesConverter.convert(jwt); + + assertThat(authorities).containsExactly( + new SimpleGrantedAuthority("message:read"), + new SimpleGrantedAuthority("message:write")); + } + @Test public void convertWhenTokenHasEmptyScpAttributeThenTranslatedToNoAuthorities() { Jwt jwt = jwt().claim("scp", Collections.emptyList()).build(); diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java index c91fbd89038..61496ff77d4 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenAuthenticationProviderTests.java @@ -25,9 +25,9 @@ import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; @@ -63,9 +63,9 @@ public void authenticateWhenActiveTokenThenOk() throws Exception { Authentication result = provider.authenticate(new BearerTokenAuthenticationToken("token")); - assertThat(result.getPrincipal()).isInstanceOf(DefaultOAuth2AuthenticatedPrincipal.class); + assertThat(result.getPrincipal()).isInstanceOf(OAuth2IntrospectionAuthenticatedPrincipal.class); - Map attributes = ((DefaultOAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes(); + Map attributes = ((OAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes(); assertThat(attributes) .isNotNull() .containsEntry(ACTIVE, true) @@ -85,7 +85,7 @@ public void authenticateWhenActiveTokenThenOk() throws Exception { @Test public void authenticateWhenMissingScopeAttributeThenNoAuthorities() { - OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null); + OAuth2AuthenticatedPrincipal principal = new OAuth2IntrospectionAuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null); OpaqueTokenIntrospector introspector = mock(OpaqueTokenIntrospector.class); when(introspector.introspect(any())).thenReturn(principal); OpaqueTokenAuthenticationProvider provider = new OpaqueTokenAuthenticationProvider(introspector); diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java index 03411ede4ec..e7b92f8a457 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/OpaqueTokenReactiveAuthenticationManagerTests.java @@ -27,9 +27,9 @@ import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionClaimNames; import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; @@ -66,9 +66,9 @@ public void authenticateWhenActiveTokenThenOk() throws Exception { Authentication result = provider.authenticate(new BearerTokenAuthenticationToken("token")).block(); - assertThat(result.getPrincipal()).isInstanceOf(DefaultOAuth2AuthenticatedPrincipal.class); + assertThat(result.getPrincipal()).isInstanceOf(OAuth2IntrospectionAuthenticatedPrincipal.class); - Map attributes = ((DefaultOAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes(); + Map attributes = ((OAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes(); assertThat(attributes) .isNotNull() .containsEntry(ACTIVE, true) @@ -88,7 +88,7 @@ public void authenticateWhenActiveTokenThenOk() throws Exception { @Test public void authenticateWhenMissingScopeAttributeThenNoAuthorities() { - OAuth2AuthenticatedPrincipal authority = new DefaultOAuth2AuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null); + OAuth2AuthenticatedPrincipal authority = new OAuth2IntrospectionAuthenticatedPrincipal(Collections.singletonMap("claim", "value"), null); ReactiveOpaqueTokenIntrospector introspector = mock(ReactiveOpaqueTokenIntrospector.class); when(introspector.introspect(any())).thenReturn(Mono.just(authority)); OpaqueTokenReactiveAuthenticationManager provider = @@ -96,9 +96,9 @@ public void authenticateWhenMissingScopeAttributeThenNoAuthorities() { Authentication result = provider.authenticate(new BearerTokenAuthenticationToken("token")).block(); - assertThat(result.getPrincipal()).isInstanceOf(DefaultOAuth2AuthenticatedPrincipal.class); + assertThat(result.getPrincipal()).isInstanceOf(OAuth2IntrospectionAuthenticatedPrincipal.class); - Map attributes = ((DefaultOAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes(); + Map attributes = ((OAuth2AuthenticatedPrincipal) result.getPrincipal()).getAttributes(); assertThat(attributes) .isNotNull() .doesNotContainKey(SCOPE); diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionAuthenticatedPrincipalTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionAuthenticatedPrincipalTests.java new file mode 100644 index 00000000000..83b6f318f4b --- /dev/null +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/OAuth2IntrospectionAuthenticatedPrincipalTests.java @@ -0,0 +1,184 @@ +/* + * 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.oauth2.server.resource.introspection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; + +/** + * Tests for {@link OAuth2IntrospectionAuthenticatedPrincipal} + * + * @author David Kovac + */ +public class OAuth2IntrospectionAuthenticatedPrincipalTests { + private static final String AUTHORITY = "SCOPE_read"; + private static final Collection AUTHORITIES = AuthorityUtils.createAuthorityList(AUTHORITY); + + private static final String SUBJECT = "test-subject"; + + private static final String ACTIVE_CLAIM = "active"; + private static final String CLIENT_ID_CLAIM = "client_id"; + private static final String USERNAME_CLAIM = "username"; + private static final String TOKEN_TYPE_CLAIM = "token_type"; + private static final String EXP_CLAIM = "exp"; + private static final String IAT_CLAIM = "iat"; + private static final String NBF_CLAIM = "nbf"; + private static final String SUB_CLAIM = "sub"; + private static final String AUD_CLAIM = "aud"; + private static final String ISS_CLAIM = "iss"; + private static final String JTI_CLAIM = "jti"; + + private static final boolean ACTIVE_VALUE = true; + private static final String CLIENT_ID_VALUE = "client-id-1"; + private static final String USERNAME_VALUE = "username-1"; + private static final String TOKEN_TYPE_VALUE = "token-type-1"; + private static final long EXP_VALUE = Instant.now().plusSeconds(60).getEpochSecond(); + private static final long IAT_VALUE = Instant.now().getEpochSecond(); + private static final long NBF_VALUE = Instant.now().plusSeconds(5).getEpochSecond(); + private static final String SUB_VALUE = "subject1"; + private static final List AUD_VALUE = Arrays.asList("aud1", "aud2"); + private static final String ISS_VALUE = "https://provider.com"; + private static final String JTI_VALUE = "jwt-id-1"; + + private static final Map CLAIMS; + + static { + CLAIMS = new HashMap<>(); + CLAIMS.put(ACTIVE_CLAIM, ACTIVE_VALUE); + CLAIMS.put(CLIENT_ID_CLAIM, CLIENT_ID_VALUE); + CLAIMS.put(USERNAME_CLAIM, USERNAME_VALUE); + CLAIMS.put(TOKEN_TYPE_CLAIM, TOKEN_TYPE_VALUE); + CLAIMS.put(EXP_CLAIM, EXP_VALUE); + CLAIMS.put(IAT_CLAIM, IAT_VALUE); + CLAIMS.put(NBF_CLAIM, NBF_VALUE); + CLAIMS.put(SUB_CLAIM, SUB_VALUE); + CLAIMS.put(AUD_CLAIM, AUD_VALUE); + CLAIMS.put(ISS_CLAIM, ISS_VALUE); + CLAIMS.put(JTI_CLAIM, JTI_VALUE); + } + + @Test + public void constructorWhenAttributesIsNullOrEmptyThenIllegalArgumentException() { + assertThatCode(() -> new OAuth2IntrospectionAuthenticatedPrincipal(null, AUTHORITIES)) + .isInstanceOf(IllegalArgumentException.class); + + assertThatCode(() -> new OAuth2IntrospectionAuthenticatedPrincipal(Collections.emptyMap(), AUTHORITIES)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void constructorWhenAuthoritiesIsNullOrEmptyThenNoAuthorities() { + Collection authorities = + new OAuth2IntrospectionAuthenticatedPrincipal(CLAIMS, null).getAuthorities(); + assertThat(authorities).isEmpty(); + + authorities = new OAuth2IntrospectionAuthenticatedPrincipal(CLAIMS, + Collections.emptyList()).getAuthorities(); + assertThat(authorities).isEmpty(); + } + + @Test + public void constructorWhenNameIsNullThenFallsbackToSubAttribute() { + OAuth2AuthenticatedPrincipal principal = + new OAuth2IntrospectionAuthenticatedPrincipal(null, CLAIMS, AUTHORITIES); + assertThat(principal.getName()).isEqualTo(CLAIMS.get(SUB_CLAIM)); + } + + @Test + public void constructorWhenAttributesAuthoritiesProvidedThenCreated() { + OAuth2IntrospectionAuthenticatedPrincipal principal = + new OAuth2IntrospectionAuthenticatedPrincipal(CLAIMS, AUTHORITIES); + + assertThat(principal.getName()).isEqualTo(CLAIMS.get(SUB_CLAIM)); + assertThat(principal.getAttributes()).isEqualTo(CLAIMS); + assertThat(principal.getClaims()).isEqualTo(CLAIMS); + assertThat(principal.isActive()).isEqualTo(ACTIVE_VALUE); + assertThat(principal.getClientId()).isEqualTo(CLIENT_ID_VALUE); + assertThat(principal.getUsername()).isEqualTo(USERNAME_VALUE); + assertThat(principal.getTokenType()).isEqualTo(TOKEN_TYPE_VALUE); + assertThat(principal.getExpiresAt().getEpochSecond()).isEqualTo(EXP_VALUE); + assertThat(principal.getIssuedAt().getEpochSecond()).isEqualTo(IAT_VALUE); + assertThat(principal.getNotBefore().getEpochSecond()).isEqualTo(NBF_VALUE); + assertThat(principal.getSubject()).isEqualTo(SUB_VALUE); + assertThat(principal.getAudience()).isEqualTo(AUD_VALUE); + assertThat(principal.getIssuer().toString()).isEqualTo(ISS_VALUE); + assertThat(principal.getId()).isEqualTo(JTI_VALUE); + assertThat(principal.getAuthorities()).hasSize(1); + assertThat(principal.getAuthorities().iterator().next().getAuthority()).isEqualTo(AUTHORITY); + } + + @Test + public void constructorWhenAllParametersProvidedAndValidThenCreated() { + OAuth2IntrospectionAuthenticatedPrincipal principal = + new OAuth2IntrospectionAuthenticatedPrincipal(SUBJECT, CLAIMS, AUTHORITIES); + + assertThat(principal.getName()).isEqualTo(SUBJECT); + assertThat(principal.getAttributes()).isEqualTo(CLAIMS); + assertThat(principal.getClaims()).isEqualTo(CLAIMS); + assertThat(principal.isActive()).isEqualTo(ACTIVE_VALUE); + assertThat(principal.getClientId()).isEqualTo(CLIENT_ID_VALUE); + assertThat(principal.getUsername()).isEqualTo(USERNAME_VALUE); + assertThat(principal.getTokenType()).isEqualTo(TOKEN_TYPE_VALUE); + assertThat(principal.getExpiresAt().getEpochSecond()).isEqualTo(EXP_VALUE); + assertThat(principal.getIssuedAt().getEpochSecond()).isEqualTo(IAT_VALUE); + assertThat(principal.getNotBefore().getEpochSecond()).isEqualTo(NBF_VALUE); + assertThat(principal.getSubject()).isEqualTo(SUB_VALUE); + assertThat(principal.getAudience()).isEqualTo(AUD_VALUE); + assertThat(principal.getIssuer().toString()).isEqualTo(ISS_VALUE); + assertThat(principal.getId()).isEqualTo(JTI_VALUE); + assertThat(principal.getAuthorities()).hasSize(1); + assertThat(principal.getAuthorities().iterator().next().getAuthority()).isEqualTo(AUTHORITY); + } + + @Test + public void getNameWhenInConstructorThenReturns() { + OAuth2AuthenticatedPrincipal principal = + new OAuth2IntrospectionAuthenticatedPrincipal(SUB_VALUE, CLAIMS, AUTHORITIES); + assertThat(principal.getName()).isEqualTo(SUB_VALUE); + } + + @Test + public void getAttributeWhenGivenKeyThenReturnsValue() { + OAuth2AuthenticatedPrincipal principal = + new OAuth2IntrospectionAuthenticatedPrincipal(CLAIMS, AUTHORITIES); + + assertThat((Object) principal.getAttribute(ACTIVE_CLAIM)).isEqualTo(ACTIVE_VALUE); + assertThat((Object) principal.getAttribute(CLIENT_ID_CLAIM)).isEqualTo(CLIENT_ID_VALUE); + assertThat((Object) principal.getAttribute(USERNAME_CLAIM)).isEqualTo(USERNAME_VALUE); + assertThat((Object) principal.getAttribute(TOKEN_TYPE_CLAIM)).isEqualTo(TOKEN_TYPE_VALUE); + assertThat((Object) principal.getAttribute(EXP_CLAIM)).isEqualTo(EXP_VALUE); + assertThat((Object) principal.getAttribute(IAT_CLAIM)).isEqualTo(IAT_VALUE); + assertThat((Object) principal.getAttribute(NBF_CLAIM)).isEqualTo(NBF_VALUE); + assertThat((Object) principal.getAttribute(SUB_CLAIM)).isEqualTo(SUB_VALUE); + assertThat((Object) principal.getAttribute(AUD_CLAIM)).isEqualTo(AUD_VALUE); + assertThat((Object) principal.getAttribute(ISS_CLAIM)).isEqualTo(ISS_VALUE); + assertThat((Object) principal.getAttribute(JTI_CLAIM)).isEqualTo(JTI_VALUE); + } +} diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java index d0487369429..f2015a6b074 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/DefaultBearerTokenResolverTests.java @@ -33,7 +33,7 @@ * @author Vedran Pavic */ public class DefaultBearerTokenResolverTests { - + private static final String CUSTOM_HEADER = "custom-header"; private static final String TEST_TOKEN = "test-token"; private DefaultBearerTokenResolver resolver; @@ -51,6 +51,15 @@ public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() { assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN); } + @Test + public void resolveWhenCustomDefinedHeaderIsValidAndPresentThenTokenIsResolved() { + this.resolver.setBearerTokenHeaderName(CUSTOM_HEADER); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(CUSTOM_HEADER, "Bearer " + TEST_TOKEN); + + assertThat(this.resolver.resolve(request)).isEqualTo(TEST_TOKEN); + } + @Test public void resolveWhenLowercaseHeaderIsPresentThenTokenIsResolved() { MockHttpServletRequest request = new MockHttpServletRequest(); diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java index 1a36a2eef8f..f3e8eefefdd 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/server/ServerBearerTokenAuthenticationConverterTests.java @@ -38,6 +38,7 @@ * @since 5.1 */ public class ServerBearerTokenAuthenticationConverterTests { + private static final String CUSTOM_HEADER = "custom-header"; private static final String TEST_TOKEN = "test-token"; private ServerBearerTokenAuthenticationConverter converter; @@ -56,6 +57,16 @@ public void resolveWhenValidHeaderIsPresentThenTokenIsResolved() { assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN); } + @Test + public void resolveWhenCustomDefinedHeaderIsValidAndPresentThenTokenIsResolved() { + this.converter.setBearerTokenHeaderName(CUSTOM_HEADER); + MockServerHttpRequest.BaseBuilder request = MockServerHttpRequest + .get("/") + .header(CUSTOM_HEADER, "Bearer " + TEST_TOKEN); + + assertThat(convertToToken(request).getToken()).isEqualTo(TEST_TOKEN); + } + // gh-7011 @Test public void resolveWhenValidHeaderIsEmptyStringThenTokenIsResolved() { diff --git a/openid/spring-security-openid.gradle b/openid/spring-security-openid.gradle index a24719a6f6d..8e3820d1513 100644 --- a/openid/spring-security-openid.gradle +++ b/openid/spring-security-openid.gradle @@ -1,3 +1,7 @@ +// 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. + apply plugin: 'io.spring.convention.spring-module' dependencies { diff --git a/openid/src/main/java/org/springframework/security/openid/AuthenticationCancelledException.java b/openid/src/main/java/org/springframework/security/openid/AuthenticationCancelledException.java index 243bde56e98..e5e89d5e38f 100644 --- a/openid/src/main/java/org/springframework/security/openid/AuthenticationCancelledException.java +++ b/openid/src/main/java/org/springframework/security/openid/AuthenticationCancelledException.java @@ -20,6 +20,9 @@ /** * Indicates that OpenID authentication was cancelled * + * @deprecated 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. * @author Robin Bramley, Opsera Ltd */ public class AuthenticationCancelledException extends AuthenticationException { diff --git a/openid/src/main/java/org/springframework/security/openid/AxFetchListFactory.java b/openid/src/main/java/org/springframework/security/openid/AxFetchListFactory.java index 16d1e64f591..3c99c94626c 100644 --- a/openid/src/main/java/org/springframework/security/openid/AxFetchListFactory.java +++ b/openid/src/main/java/org/springframework/security/openid/AxFetchListFactory.java @@ -24,6 +24,9 @@ * This allows the list of attributes for a fetch request to be tailored for different * OpenID providers, since they do not all support the same attributes. * + * @deprecated 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. * @author Luke Taylor * @since 3.1 */ diff --git a/openid/src/main/java/org/springframework/security/openid/NullAxFetchListFactory.java b/openid/src/main/java/org/springframework/security/openid/NullAxFetchListFactory.java index 2a653ee1321..75df033bac3 100644 --- a/openid/src/main/java/org/springframework/security/openid/NullAxFetchListFactory.java +++ b/openid/src/main/java/org/springframework/security/openid/NullAxFetchListFactory.java @@ -19,6 +19,9 @@ import java.util.List; /** + * @deprecated 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. * @author Luke Taylor * @since 3.1 */ diff --git a/openid/src/main/java/org/springframework/security/openid/OpenID4JavaConsumer.java b/openid/src/main/java/org/springframework/security/openid/OpenID4JavaConsumer.java index 2323f99985c..7ab6f476154 100644 --- a/openid/src/main/java/org/springframework/security/openid/OpenID4JavaConsumer.java +++ b/openid/src/main/java/org/springframework/security/openid/OpenID4JavaConsumer.java @@ -41,6 +41,9 @@ import org.springframework.util.StringUtils; /** + * @deprecated 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. * @author Ray Krueger * @author Luke Taylor */ diff --git a/openid/src/main/java/org/springframework/security/openid/OpenIDAttribute.java b/openid/src/main/java/org/springframework/security/openid/OpenIDAttribute.java index 65a9774c8bb..c15d45856db 100644 --- a/openid/src/main/java/org/springframework/security/openid/OpenIDAttribute.java +++ b/openid/src/main/java/org/springframework/security/openid/OpenIDAttribute.java @@ -27,6 +27,9 @@ * should be requested during a fetch request, or to hold values for an attribute which * are returned during the authentication process. * + * @deprecated 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. * @author Luke Taylor * @since 3.0 */ diff --git a/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationFilter.java b/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationFilter.java index 430c7078007..9c3999d704d 100644 --- a/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationFilter.java +++ b/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationFilter.java @@ -59,6 +59,9 @@ * where it should (normally) be processed by an OpenIDAuthenticationProvider in * order to load the authorities for the user. * + * @deprecated 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. * @author Robin Bramley * @author Ray Krueger * @author Luke Taylor diff --git a/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationProvider.java b/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationProvider.java index 6b9f723aacc..e42baa6d1f6 100644 --- a/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationProvider.java +++ b/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationProvider.java @@ -44,6 +44,9 @@ * {@code Authentication} token, so additional properties such as email addresses, * telephone numbers etc can easily be stored. * + * @deprecated 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. * @author Robin Bramley, Opsera Ltd. * @author Luke Taylor */ diff --git a/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationStatus.java b/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationStatus.java index b7a52a0aeae..db2eee832b2 100644 --- a/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationStatus.java +++ b/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationStatus.java @@ -18,6 +18,9 @@ /** * Authentication status codes, based on JanRain status codes * + * @deprecated 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. * @author JanRain Inc. * @author Robin Bramley, Opsera Ltd * @author Luke Taylor diff --git a/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationToken.java b/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationToken.java index dc6f7fc466e..0625e07727c 100644 --- a/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationToken.java +++ b/openid/src/main/java/org/springframework/security/openid/OpenIDAuthenticationToken.java @@ -26,6 +26,9 @@ /** * OpenID Authentication Token * + * @deprecated 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. * @author Robin Bramley */ public class OpenIDAuthenticationToken extends AbstractAuthenticationToken { diff --git a/openid/src/main/java/org/springframework/security/openid/OpenIDConsumer.java b/openid/src/main/java/org/springframework/security/openid/OpenIDConsumer.java index ed57b1256c9..303b143a9af 100644 --- a/openid/src/main/java/org/springframework/security/openid/OpenIDConsumer.java +++ b/openid/src/main/java/org/springframework/security/openid/OpenIDConsumer.java @@ -20,6 +20,9 @@ /** * An interface for OpenID library implementations * + * @deprecated 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. * @author Ray Krueger * @author Robin Bramley, Opsera Ltd */ diff --git a/openid/src/main/java/org/springframework/security/openid/OpenIDConsumerException.java b/openid/src/main/java/org/springframework/security/openid/OpenIDConsumerException.java index e7bb4c3d0df..f184032c155 100644 --- a/openid/src/main/java/org/springframework/security/openid/OpenIDConsumerException.java +++ b/openid/src/main/java/org/springframework/security/openid/OpenIDConsumerException.java @@ -18,6 +18,9 @@ /** * Thrown by an OpenIDConsumer if it cannot process a request * + * @deprecated 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. * @author Robin Bramley, Opsera Ltd */ public class OpenIDConsumerException extends Exception { diff --git a/openid/src/main/java/org/springframework/security/openid/RegexBasedAxFetchListFactory.java b/openid/src/main/java/org/springframework/security/openid/RegexBasedAxFetchListFactory.java index 92834199812..b59481bb389 100644 --- a/openid/src/main/java/org/springframework/security/openid/RegexBasedAxFetchListFactory.java +++ b/openid/src/main/java/org/springframework/security/openid/RegexBasedAxFetchListFactory.java @@ -22,7 +22,9 @@ import java.util.regex.Pattern; /** - * + * @deprecated 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. * @author Luke Taylor * @since 3.1 */ diff --git a/openid/src/main/java/org/springframework/security/openid/package.html b/openid/src/main/java/org/springframework/security/openid/package.html index 80e7f0c0f43..f2417fd6153 100644 --- a/openid/src/main/java/org/springframework/security/openid/package.html +++ b/openid/src/main/java/org/springframework/security/openid/package.html @@ -1,5 +1,9 @@ -Authenticates standard web browser users via OpenID. +

Authenticates standard web browser users via OpenID.

+ +

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.

- \ No newline at end of file + diff --git a/openid/src/test/java/org/springframework/security/openid/MockOpenIDConsumer.java b/openid/src/test/java/org/springframework/security/openid/MockOpenIDConsumer.java index 7f22d587af7..ce8f1a2382c 100644 --- a/openid/src/test/java/org/springframework/security/openid/MockOpenIDConsumer.java +++ b/openid/src/test/java/org/springframework/security/openid/MockOpenIDConsumer.java @@ -15,12 +15,12 @@ */ package org.springframework.security.openid; -import org.springframework.security.openid.OpenIDAuthenticationToken; -import org.springframework.security.openid.OpenIDConsumer; - import javax.servlet.http.HttpServletRequest; /** + * @deprecated 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. * @author Robin Bramley, Opsera Ltd */ public class MockOpenIDConsumer implements OpenIDConsumer { diff --git a/openid/src/test/java/org/springframework/security/openid/OpenID4JavaConsumerTests.java b/openid/src/test/java/org/springframework/security/openid/OpenID4JavaConsumerTests.java index e87ce2fd2f9..971c19a15fa 100644 --- a/openid/src/test/java/org/springframework/security/openid/OpenID4JavaConsumerTests.java +++ b/openid/src/test/java/org/springframework/security/openid/OpenID4JavaConsumerTests.java @@ -40,6 +40,9 @@ import java.util.*; /** + * @deprecated 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. * @author Luke Taylor */ public class OpenID4JavaConsumerTests { diff --git a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationFilterTests.java b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationFilterTests.java index 818698313aa..dbaf2eeb686 100644 --- a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationFilterTests.java +++ b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationFilterTests.java @@ -31,6 +31,11 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +/** + * @deprecated 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. + */ public class OpenIDAuthenticationFilterTests { OpenIDAuthenticationFilter filter; diff --git a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java index 8544cf68d59..902ca9b8feb 100644 --- a/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java +++ b/openid/src/test/java/org/springframework/security/openid/OpenIDAuthenticationProviderTests.java @@ -35,6 +35,9 @@ /** * Tests {@link OpenIDAuthenticationProvider} * + * @deprecated 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. * @author Robin Bramley, Opsera Ltd */ public class OpenIDAuthenticationProviderTests { diff --git a/openid/src/test/resources/logback-test.xml b/openid/src/test/resources/logback-test.xml index 2d51ba4180a..cc1fc42b4ef 100644 --- a/openid/src/test/resources/logback-test.xml +++ b/openid/src/test/resources/logback-test.xml @@ -1,3 +1,7 @@ + + diff --git a/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadSocketAcceptor.java b/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadSocketAcceptor.java index 5e8839c1392..a5a849096b3 100644 --- a/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadSocketAcceptor.java +++ b/rsocket/src/main/java/org/springframework/security/rsocket/core/PayloadSocketAcceptor.java @@ -72,6 +72,7 @@ public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) return intercept(setup, dataMimeType, metadataMimeType) .flatMap(ctx -> this.delegate.accept(setup, sendingSocket) .map(acceptingSocket -> new PayloadInterceptorRSocket(acceptingSocket, this.interceptors, metadataMimeType, dataMimeType, ctx)) + .subscriberContext(ctx) ); } diff --git a/rsocket/src/test/java/org/springframework/security/rsocket/core/CaptureSecurityContextSocketAcceptor.java b/rsocket/src/test/java/org/springframework/security/rsocket/core/CaptureSecurityContextSocketAcceptor.java new file mode 100644 index 00000000000..f434ed4c70c --- /dev/null +++ b/rsocket/src/test/java/org/springframework/security/rsocket/core/CaptureSecurityContextSocketAcceptor.java @@ -0,0 +1,50 @@ +/* + * Copyright 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.rsocket.core; + +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import reactor.core.publisher.Mono; + +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; + +/** + * A {@link SocketAcceptor} that captures the {@link SecurityContext} and then continues with the {@link RSocket} + * @author Rob Winch + */ +class CaptureSecurityContextSocketAcceptor implements SocketAcceptor { + private final RSocket accept; + + private SecurityContext securityContext; + + CaptureSecurityContextSocketAcceptor(RSocket accept) { + this.accept = accept; + } + + @Override + public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { + return ReactiveSecurityContextHolder.getContext() + .doOnNext(securityContext -> this.securityContext = securityContext) + .thenReturn(this.accept); + } + + public SecurityContext getSecurityContext() { + return this.securityContext; + } +} diff --git a/rsocket/src/test/java/org/springframework/security/rsocket/core/PayloadSocketAcceptorTests.java b/rsocket/src/test/java/org/springframework/security/rsocket/core/PayloadSocketAcceptorTests.java index 943fc978b9c..69b7e2356dd 100644 --- a/rsocket/src/test/java/org/springframework/security/rsocket/core/PayloadSocketAcceptorTests.java +++ b/rsocket/src/test/java/org/springframework/security/rsocket/core/PayloadSocketAcceptorTests.java @@ -16,6 +16,10 @@ package org.springframework.security.rsocket.core; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import io.rsocket.ConnectionSetupPayload; import io.rsocket.Payload; import io.rsocket.RSocket; @@ -27,16 +31,16 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + import org.springframework.http.MediaType; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.rsocket.api.PayloadExchange; import org.springframework.security.rsocket.api.PayloadInterceptor; -import org.springframework.security.rsocket.core.PayloadInterceptorRSocket; -import org.springframework.security.rsocket.core.PayloadSocketAcceptor; -import reactor.core.publisher.Mono; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -144,6 +148,27 @@ public void acceptWhenExplicitMimeTypeThenThenOverrideDefault() { assertThat(exchange.getDataMimeType()).isEqualTo(MediaType.APPLICATION_JSON); } + + @Test + // gh-8654 + public void acceptWhenDelegateAcceptRequiresReactiveSecurityContext() { + when(this.setupPayload.metadataMimeType()).thenReturn(MediaType.TEXT_PLAIN_VALUE); + when(this.setupPayload.dataMimeType()).thenReturn(MediaType.APPLICATION_JSON_VALUE); + SecurityContext expectedSecurityContext = new SecurityContextImpl(new TestingAuthenticationToken("user", "password", "ROLE_USER")); + CaptureSecurityContextSocketAcceptor captureSecurityContext = new CaptureSecurityContextSocketAcceptor(this.rSocket); + PayloadInterceptor authenticateInterceptor = (exchange, chain) -> { + Context withSecurityContext = ReactiveSecurityContextHolder.withSecurityContext(Mono.just(expectedSecurityContext)); + return chain.next(exchange) + .subscriberContext(withSecurityContext); + }; + List interceptors = Arrays.asList(authenticateInterceptor); + this.acceptor = new PayloadSocketAcceptor(captureSecurityContext, interceptors); + + this.acceptor.accept(this.setupPayload, this.rSocket).block(); + + assertThat(captureSecurityContext.getSecurityContext()).isEqualTo(expectedSecurityContext); + } + private PayloadExchange captureExchange() { when(this.delegate.accept(any(), any())).thenReturn(Mono.just(this.rSocket)); when(this.interceptor.intercept(any(), any())).thenReturn(Mono.empty()); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java index a45af3e78bf..f5f69e9a397 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -15,12 +15,42 @@ */ package org.springframework.security.saml2.provider.service.authentication; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; + +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.opensaml.saml.common.SignableSAMLObject; -import org.opensaml.saml.common.assertion.AssertionValidationException; +import org.joda.time.DateTime; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.Marshaller; + +import org.opensaml.core.xml.schema.XSAny; +import org.opensaml.core.xml.schema.XSBoolean; +import org.opensaml.core.xml.schema.XSBooleanValue; +import org.opensaml.core.xml.schema.XSDateTime; +import org.opensaml.core.xml.schema.XSInteger; +import org.opensaml.core.xml.schema.XSString; +import org.opensaml.core.xml.schema.XSURI; import org.opensaml.saml.common.assertion.ValidationContext; import org.opensaml.saml.common.assertion.ValidationResult; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.criterion.ProtocolCriterion; +import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; import org.opensaml.saml.saml2.assertion.ConditionValidator; import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator; import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; @@ -29,6 +59,8 @@ import org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator; import org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator; import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedID; import org.opensaml.saml.saml2.core.NameID; @@ -40,16 +72,20 @@ import org.opensaml.security.credential.Credential; import org.opensaml.security.credential.CredentialResolver; import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion; +import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion; import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.security.criteria.UsageCriterion; +import org.opensaml.security.x509.BasicX509Credential; import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; import org.opensaml.xmlsec.encryption.support.DecryptionException; import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; import org.opensaml.xmlsec.keyinfo.impl.StaticKeyInfoCredentialResolver; -import org.opensaml.xmlsec.signature.support.SignatureException; import org.opensaml.xmlsec.signature.support.SignaturePrevalidator; import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; -import org.opensaml.xmlsec.signature.support.SignatureValidator; import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; + import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; @@ -60,30 +96,24 @@ import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.credentials.Saml2X509Credential; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; -import java.security.cert.X509Certificate; -import java.time.Duration; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static java.lang.String.format; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; +import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.CLOCK_SKEW; +import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.COND_VALID_AUDIENCES; +import static org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters.SIGNATURE_REQUIRED; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.DECRYPTION_ERROR; +import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR; +import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_ASSERTION; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_DESTINATION; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_ISSUER; +import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.INVALID_SIGNATURE; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.MALFORMED_RESPONSE_DATA; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.SUBJECT_NOT_FOUND; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.USERNAME_NOT_FOUND; import static org.springframework.util.Assert.notNull; -import static org.springframework.util.StringUtils.hasText; /** * Implementation of {@link AuthenticationProvider} for SAML authentications when receiving a @@ -125,6 +155,20 @@ public final class OpenSamlAuthenticationProvider implements AuthenticationProvi private static Log logger = LogFactory.getLog(OpenSamlAuthenticationProvider.class); + private final List conditions = Collections.singletonList(new AudienceRestrictionConditionValidator()); + private final SubjectConfirmationValidator subjectConfirmationValidator = new BearerSubjectConfirmationValidator() { + @Nonnull + @Override + protected ValidationResult validateAddress(@Nonnull SubjectConfirmation confirmation, + @Nonnull Assertion assertion, @Nonnull ValidationContext context) { + // skipping address validation - gh-7514 + return ValidationResult.VALID; + } + }; + private final List subjects = Collections.singletonList(this.subjectConfirmationValidator); + private final List statements = Collections.emptyList(); + private final SignaturePrevalidator signaturePrevalidator = new SAMLSignatureProfileValidator(); + private final OpenSamlImplementation saml = OpenSamlImplementation.getInstance(); private Converter> authoritiesExtractor = (a -> singletonList(new SimpleGrantedAuthority("ROLE_USER"))); @@ -173,17 +217,18 @@ public void setResponseTimeValidationSkew(Duration responseTimeValidationSkew) { public Authentication authenticate(Authentication authentication) throws AuthenticationException { try { Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication; - Response samlResponse = getSaml2Response(token); - Assertion assertion = validateSaml2Response(token, token.getRecipientUri(), samlResponse); + Response response = parse(token.getSaml2Response()); + List validAssertions = validateResponse(token, response); + Assertion assertion = validAssertions.get(0); String username = getUsername(token, assertion); + Map> attributes = getAssertionAttributes(assertion); return new Saml2Authentication( - new SimpleSaml2AuthenticatedPrincipal(username), token.getSaml2Response(), - this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion)) - ); + new SimpleSaml2AuthenticatedPrincipal(username, attributes), token.getSaml2Response(), + this.authoritiesMapper.mapAuthorities(getAssertionAuthorities(assertion))); } catch (Saml2AuthenticationException e) { throw e; } catch (Exception e) { - throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e); + throw authException(INTERNAL_VALIDATION_ERROR, e.getMessage(), e); } } @@ -199,241 +244,187 @@ private Collection getAssertionAuthorities(Assertion return this.authoritiesExtractor.convert(assertion); } - private String getUsername(Saml2AuthenticationToken token, Assertion assertion) throws Saml2AuthenticationException { - String username = null; - Subject subject = assertion.getSubject(); - if (subject == null) { - throw authException(SUBJECT_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a subject"); - } - if (subject.getNameID() != null) { - username = subject.getNameID().getValue(); - } - else if (subject.getEncryptedID() != null) { - NameID nameId = decrypt(token, subject.getEncryptedID()); - username = nameId.getValue(); - } - if (username == null) { - throw authException(USERNAME_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a user identifier"); + private Response parse(String response) throws Saml2Exception, Saml2AuthenticationException { + try { + Object result = this.saml.resolve(response); + if (result instanceof Response) { + return (Response) result; + } + else { + throw authException(UNKNOWN_RESPONSE_CLASS, "Invalid response class:" + result.getClass().getName()); + } + } catch (Saml2Exception x) { + throw authException(MALFORMED_RESPONSE_DATA, x.getMessage(), x); } - return username; + } - private Assertion validateSaml2Response(Saml2AuthenticationToken token, - String recipient, - Response samlResponse) throws Saml2AuthenticationException { - //optional validation if the response contains a destination - if (hasText(samlResponse.getDestination()) && !recipient.equals(samlResponse.getDestination())) { - throw authException(INVALID_DESTINATION, "Invalid SAML response destination: " + samlResponse.getDestination()); - } + private List validateResponse(Saml2AuthenticationToken token, Response response) + throws Saml2AuthenticationException { - String issuer = samlResponse.getIssuer().getValue(); + List validAssertions = new ArrayList<>(); + String issuer = response.getIssuer().getValue(); if (logger.isDebugEnabled()) { logger.debug("Validating SAML response from " + issuer); } - if (!hasText(issuer) || (!issuer.equals(token.getIdpEntityId()))) { - String message = String.format("Response issuer '%s' doesn't match '%s'", issuer, token.getIdpEntityId()); - throw authException(INVALID_ISSUER, message); - } - Saml2AuthenticationException lastValidationError = null; - boolean responseSigned = hasValidSignature(samlResponse, token); - for (Assertion a : samlResponse.getAssertions()) { - if (logger.isDebugEnabled()) { - logger.debug("Checking plain assertion validity " + a); - } - try { - validateAssertion(recipient, a, token, !responseSigned); - return a; - } catch (Saml2AuthenticationException e) { - lastValidationError = e; - } - } - for (EncryptedAssertion ea : samlResponse.getEncryptedAssertions()) { - if (logger.isDebugEnabled()) { - logger.debug("Checking encrypted assertion validity " + ea); - } - try { - Assertion a = decrypt(token, ea); - validateAssertion(recipient, a, token, !responseSigned); - return a; - } catch (Saml2AuthenticationException e) { - lastValidationError = e; - } - } - if (lastValidationError != null) { - throw lastValidationError; + List assertions = new ArrayList<>(response.getAssertions()); + for (EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) { + Assertion assertion = decrypt(token, encryptedAssertion); + assertions.add(assertion); } - else { + if (assertions.isEmpty()) { throw authException(MALFORMED_RESPONSE_DATA, "No assertions found in response."); } - } - private boolean hasValidSignature(SignableSAMLObject samlObject, Saml2AuthenticationToken token) { - if (!samlObject.isSigned()) { - if (logger.isDebugEnabled()) { - logger.debug("SAML object is not signed, no signatures found"); - } - return false; + if (!isSigned(response, assertions)) { + throw authException(INVALID_SIGNATURE, "Either the response or one of the assertions is unsigned. " + + "Please either sign the response or all of the assertions."); } - List verificationKeys = getVerificationCertificates(token); - if (verificationKeys.isEmpty()) { - return false; - } + SignatureTrustEngine signatureTrustEngine = buildSignatureTrustEngine(token); - for (X509Certificate certificate : verificationKeys) { - Credential credential = getVerificationCredential(certificate); + Map validationExceptions = new HashMap<>(); + if (response.isSigned()) { + SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); try { - SignatureValidator.validate(samlObject.getSignature(), credential); - if (logger.isDebugEnabled()) { - logger.debug("Valid signature found in SAML object:"+samlObject.getClass().getName()); - } - return true; + profileValidator.validate(response.getSignature()); + } catch (Exception e) { + validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + response.getID() + "]", e)); } - catch (SignatureException ignored) { - if (logger.isTraceEnabled()) { - logger.trace("Signature validation failed with cert:"+certificate.toString(), ignored); - } - else if (logger.isDebugEnabled()) { - logger.debug("Signature validation failed with cert:"+certificate.toString()); + + try { + CriteriaSet criteriaSet = new CriteriaSet(); + criteriaSet.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(issuer))); + criteriaSet.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion(SAMLConstants.SAML20P_NS))); + criteriaSet.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); + if (!signatureTrustEngine.validate(response.getSignature(), criteriaSet)) { + validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + response.getID() + "]")); } + } catch (Exception e) { + validationExceptions.put(INVALID_SIGNATURE, authException(INVALID_SIGNATURE, + "Invalid signature for SAML Response [" + response.getID() + "]", e)); } } - return false; - } - private void validateAssertion(String recipient, Assertion a, Saml2AuthenticationToken token, boolean signatureRequired) { - SAML20AssertionValidator validator = getAssertionValidator(token); - Map validationParams = new HashMap<>(); - validationParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false); - validationParams.put( - SAML2AssertionValidationParameters.CLOCK_SKEW, - this.responseTimeValidationSkew.toMillis() - ); - validationParams.put( - SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, - singleton(token.getLocalSpEntityId()) - ); - if (hasText(recipient)) { - validationParams.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, singleton(recipient)); + String destination = response.getDestination(); + if (StringUtils.hasText(destination) && !destination.equals(token.getRecipientUri())) { + String message = "Invalid destination [" + destination + "] for SAML response [" + response.getID() + "]"; + validationExceptions.put(INVALID_DESTINATION, authException(INVALID_DESTINATION, message)); } - if (signatureRequired && !hasValidSignature(a, token)) { - if (logger.isDebugEnabled()) { - logger.debug(format("Assertion [%s] does not a valid signature.", a.getID())); - } - throw authException(Saml2ErrorCodes.INVALID_SIGNATURE, "Assertion doesn't have a valid signature."); + if (!StringUtils.hasText(issuer) || !issuer.equals(token.getIdpEntityId())) { + String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID()); + validationExceptions.put(INVALID_ISSUER, authException(INVALID_ISSUER, message)); } - //ensure that OpenSAML doesn't attempt signature validation, already performed - a.setSignature(null); - //ensure that we don't validate IP addresses as part of our validation gh-7514 - if (a.getSubject() != null) { - for (SubjectConfirmation sc : a.getSubject().getSubjectConfirmations()) { - if (sc.getSubjectConfirmationData() != null) { - sc.getSubjectConfirmationData().setAddress(null); - } - } - } + SAML20AssertionValidator validator = buildSamlAssertionValidator(signatureTrustEngine); + ValidationContext context = buildValidationContext(token, response); - //remainder of assertion validation - ValidationContext vctx = new ValidationContext(validationParams); - try { - ValidationResult result = validator.validate(a, vctx); - boolean valid = result.equals(ValidationResult.VALID); - if (!valid) { - if (logger.isDebugEnabled()) { - logger.debug(format("Failed to validate assertion from %s", token.getIdpEntityId())); - } - throw authException(Saml2ErrorCodes.INVALID_ASSERTION, vctx.getValidationFailureMessage()); - } + if (logger.isDebugEnabled()) { + logger.debug("Validating " + assertions.size() + " assertions"); } - catch (AssertionValidationException e) { - if (logger.isDebugEnabled()) { - logger.debug("Failed to validate assertion:", e); + for (Assertion assertion : assertions) { + if (logger.isTraceEnabled()) { + logger.trace("Validating assertion " + assertion.getID()); + } + try { + validAssertions.add(validateAssertion(assertion, validator, context)); + } catch (Exception e) { + String message = String.format("Invalid assertion [%s] for SAML response [%s]", assertion.getID(), response.getID()); + validationExceptions.put(INVALID_ASSERTION, authException(INVALID_ASSERTION, message, e)); } - throw authException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, e.getMessage(), e); } - } - - private Response getSaml2Response(Saml2AuthenticationToken token) throws Saml2Exception, Saml2AuthenticationException { - try { - Object result = this.saml.resolve(token.getSaml2Response()); - if (result instanceof Response) { - return (Response) result; + if (validationExceptions.isEmpty()) { + if (logger.isDebugEnabled()) { + logger.debug("Successfully validated SAML Response [" + response.getID() + "]"); } - else { - throw authException(UNKNOWN_RESPONSE_CLASS, "Invalid response class:" + result.getClass().getName()); + } else { + if (logger.isTraceEnabled()) { + logger.debug("Found " + validationExceptions.size() + " validation errors in SAML response [" + response.getID() + "]: " + + validationExceptions.values()); + } else if (logger.isDebugEnabled()) { + logger.debug("Found " + validationExceptions.size() + " validation errors in SAML response [" + response.getID() + "]"); } - } catch (Saml2Exception x) { - throw authException(MALFORMED_RESPONSE_DATA, x.getMessage(), x); } - } + if (!validationExceptions.isEmpty()) { + throw validationExceptions.values().iterator().next(); + } + if (validAssertions.isEmpty()) { + throw authException(MALFORMED_RESPONSE_DATA, "No valid assertions found in response."); + } - private Saml2Error validationError(String code, String description) { - return new Saml2Error( - code, - description - ); + return validAssertions; } - private Saml2AuthenticationException authException(String code, String description) throws Saml2AuthenticationException { - return new Saml2AuthenticationException( - validationError(code, description) - ); - } + private boolean isSigned(Response samlResponse, List assertions) { + if (samlResponse.isSigned()) { + return true; + } + for (Assertion assertion : assertions) { + if (!assertion.isSigned()) { + return false; + } + } - private Saml2AuthenticationException authException(String code, String description, Exception cause) throws Saml2AuthenticationException { - return new Saml2AuthenticationException( - validationError(code, description), - cause - ); + return true; } - private SAML20AssertionValidator getAssertionValidator(Saml2AuthenticationToken provider) { - List conditions = Collections.singletonList(new AudienceRestrictionConditionValidator()); - BearerSubjectConfirmationValidator subjectConfirmationValidator = new BearerSubjectConfirmationValidator(); - - List subjects = Collections.singletonList(subjectConfirmationValidator); - List statements = Collections.emptyList(); - + private SignatureTrustEngine buildSignatureTrustEngine(Saml2AuthenticationToken token) { Set credentials = new HashSet<>(); - for (X509Certificate key : getVerificationCertificates(provider)) { - Credential cred = getVerificationCredential(key); + for (X509Certificate key : getVerificationCertificates(token)) { + BasicX509Credential cred = new BasicX509Credential(key); + cred.setUsageType(UsageType.SIGNING); + cred.setEntityId(token.getIdpEntityId()); credentials.add(cred); } CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); - SignatureTrustEngine signatureTrustEngine = new ExplicitKeySignatureTrustEngine( + return new ExplicitKeySignatureTrustEngine( credentialsResolver, DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver() ); - SignaturePrevalidator signaturePrevalidator = new SAMLSignatureProfileValidator(); - return new SAML20AssertionValidator( - conditions, - subjects, - statements, - signatureTrustEngine, - signaturePrevalidator - ); } - private Credential getVerificationCredential(X509Certificate certificate) { - return CredentialSupport.getSimpleCredential(certificate, null); + private ValidationContext buildValidationContext(Saml2AuthenticationToken token, Response response) { + Map validationParams = new HashMap<>(); + validationParams.put(SIGNATURE_REQUIRED, !response.isSigned()); + validationParams.put(CLOCK_SKEW, this.responseTimeValidationSkew.toMillis()); + validationParams.put(COND_VALID_AUDIENCES, singleton(token.getLocalSpEntityId())); + if (StringUtils.hasText(token.getRecipientUri())) { + validationParams.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, singleton(token.getRecipientUri())); + } + return new ValidationContext(validationParams); } - private Decrypter getDecrypter(Saml2X509Credential key) { - Credential credential = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); - KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(credential); - Decrypter decrypter = new Decrypter(null, resolver, this.saml.getEncryptedKeyResolver()); - decrypter.setRootInNewDocument(true); - return decrypter; + private SAML20AssertionValidator buildSamlAssertionValidator(SignatureTrustEngine signatureTrustEngine) { + return new SAML20AssertionValidator( + this.conditions, this.subjects, this.statements, signatureTrustEngine, this.signaturePrevalidator); + } + + private Assertion validateAssertion(Assertion assertion, + SAML20AssertionValidator validator, ValidationContext context) { + + ValidationResult result; + try { + result = validator.validate(assertion, context); + } catch (Exception e) { + throw new Saml2Exception("An error occurred while validation the assertion", e); + } + if (result != ValidationResult.VALID) { + throw new Saml2Exception("An error occurred while validating the assertion: " + + context.getValidationFailureMessage()); + } + return assertion; } private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion assertion) throws Saml2AuthenticationException { + Saml2AuthenticationException last = null; List decryptionCredentials = getDecryptionCredentials(token); if (decryptionCredentials.isEmpty()) { @@ -451,7 +442,58 @@ private Assertion decrypt(Saml2AuthenticationToken token, EncryptedAssertion ass throw last; } - private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) throws Saml2AuthenticationException { + private Decrypter getDecrypter(Saml2X509Credential key) { + Credential credential = CredentialSupport.getSimpleCredential(key.getCertificate(), key.getPrivateKey()); + KeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(credential); + Decrypter decrypter = new Decrypter(null, resolver, this.saml.getEncryptedKeyResolver()); + decrypter.setRootInNewDocument(true); + return decrypter; + } + + private List getDecryptionCredentials(Saml2AuthenticationToken token) { + List result = new LinkedList<>(); + for (Saml2X509Credential c : token.getX509Credentials()) { + if (c.isDecryptionCredential()) { + result.add(c); + } + } + return result; + } + + private List getVerificationCertificates(Saml2AuthenticationToken token) { + List result = new LinkedList<>(); + for (Saml2X509Credential c : token.getX509Credentials()) { + if (c.isSignatureVerficationCredential()) { + result.add(c.getCertificate()); + } + } + return result; + } + + private String getUsername(Saml2AuthenticationToken token, Assertion assertion) + throws Saml2AuthenticationException { + + String username = null; + Subject subject = assertion.getSubject(); + if (subject == null) { + throw authException(SUBJECT_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a subject"); + } + if (subject.getNameID() != null) { + username = subject.getNameID().getValue(); + } + else if (subject.getEncryptedID() != null) { + NameID nameId = decrypt(token, subject.getEncryptedID()); + username = nameId.getValue(); + } + if (username == null) { + throw authException(USERNAME_NOT_FOUND, "Assertion [" + assertion.getID() + "] is missing a user identifier"); + } + return username; + } + + private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) + throws Saml2AuthenticationException { + Saml2AuthenticationException last = null; List decryptionCredentials = getDecryptionCredentials(token); if (decryptionCredentials.isEmpty()) { @@ -469,23 +511,70 @@ private NameID decrypt(Saml2AuthenticationToken token, EncryptedID assertion) th throw last; } - private List getDecryptionCredentials(Saml2AuthenticationToken token) { - List result = new LinkedList<>(); - for (Saml2X509Credential c : token.getX509Credentials()) { - if (c.isDecryptionCredential()) { - result.add(c); + private Map> getAssertionAttributes(Assertion assertion) { + Map> attributeMap = new LinkedHashMap<>(); + for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) { + for (Attribute attribute : attributeStatement.getAttributes()) { + + List attributeValues = new ArrayList<>(); + for (XMLObject xmlObject : attribute.getAttributeValues()) { + Object attributeValue = getXmlObjectValue(xmlObject); + if (attributeValue != null) { + attributeValues.add(attributeValue); + } + } + attributeMap.put(attribute.getName(), attributeValues); + } } - return result; + return attributeMap; } - private List getVerificationCertificates(Saml2AuthenticationToken token) { - List result = new LinkedList<>(); - for (Saml2X509Credential c : token.getX509Credentials()) { - if (c.isSignatureVerficationCredential()) { - result.add(c.getCertificate()); - } + private Object getXmlObjectValue(XMLObject xmlObject) { + if (xmlObject instanceof XSAny) { + return getXSAnyObjectValue((XSAny) xmlObject); } - return result; + if (xmlObject instanceof XSString) { + return ((XSString) xmlObject).getValue(); + } + if (xmlObject instanceof XSInteger) { + return ((XSInteger) xmlObject).getValue(); + } + if (xmlObject instanceof XSURI) { + return ((XSURI) xmlObject).getValue(); + } + if (xmlObject instanceof XSBoolean) { + XSBooleanValue xsBooleanValue = ((XSBoolean) xmlObject).getValue(); + return xsBooleanValue != null ? xsBooleanValue.getValue() : null; + } + if (xmlObject instanceof XSDateTime) { + DateTime dateTime = ((XSDateTime) xmlObject).getValue(); + return dateTime != null ? Instant.ofEpochMilli(dateTime.getMillis()) : null; + } + return null; + } + + private Object getXSAnyObjectValue(XSAny xsAny) { + Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(xsAny); + if (marshaller != null) { + return this.saml.serialize(xsAny); + } + return xsAny.getTextContent(); + } + + private Saml2Error validationError(String code, String description) { + return new Saml2Error(code, description); + } + + private Saml2AuthenticationException authException(String code, String description) + throws Saml2AuthenticationException { + + return new Saml2AuthenticationException(validationError(code, description)); + } + + private Saml2AuthenticationException authException(String code, String description, Exception cause) + throws Saml2AuthenticationException { + + return new Saml2AuthenticationException(validationError(code, description), cause); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java index 3aee2258bbf..973e4bb7b51 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactory.java @@ -16,22 +16,22 @@ package org.springframework.security.saml2.provider.service.authentication; +import java.time.Clock; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.UUID; + import org.joda.time.DateTime; import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.Issuer; + import org.springframework.security.saml2.credentials.Saml2X509Credential; import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest.Builder; import org.springframework.util.Assert; -import java.time.Clock; -import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.UUID; - import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Collections.emptyList; import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlDeflate; import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlEncode; @@ -46,7 +46,9 @@ public class OpenSamlAuthenticationRequestFactory implements Saml2Authentication @Override @Deprecated public String createAuthenticationRequest(Saml2AuthenticationRequest request) { - return createAuthenticationRequest(request, request.getCredentials()); + AuthnRequest authnRequest = createAuthnRequest(request.getIssuer(), + request.getDestination(), request.getAssertionConsumerServiceUrl()); + return this.saml.serialize(authnRequest, request.getCredentials()); } /** @@ -54,11 +56,11 @@ public String createAuthenticationRequest(Saml2AuthenticationRequest request) { */ @Override public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2AuthenticationRequestContext context) { - List signingCredentials = context.getRelyingPartyRegistration().getProviderDetails().isSignAuthNRequest() ? - context.getRelyingPartyRegistration().getSigningCredentials() : - emptyList(); + AuthnRequest authnRequest = createAuthnRequest(context); + String xml = context.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned() ? + this.saml.serialize(authnRequest, context.getRelyingPartyRegistration().getSigningCredentials()) : + this.saml.serialize(authnRequest); - String xml = createAuthenticationRequest(context, signingCredentials); return Saml2PostAuthenticationRequest.withAuthenticationRequestContext(context) .samlRequest(samlEncode(xml.getBytes(UTF_8))) .build(); @@ -69,13 +71,14 @@ public Saml2PostAuthenticationRequest createPostAuthenticationRequest(Saml2Authe */ @Override public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext context) { - String xml = createAuthenticationRequest(context, emptyList()); + AuthnRequest authnRequest = createAuthnRequest(context); + String xml = this.saml.serialize(authnRequest); Builder result = Saml2RedirectAuthenticationRequest.withAuthenticationRequestContext(context); String deflatedAndEncoded = samlEncode(samlDeflate(xml)); result.samlRequest(deflatedAndEncoded) .relayState(context.getRelayState()); - if (context.getRelyingPartyRegistration().getProviderDetails().isSignAuthNRequest()) { + if (context.getRelyingPartyRegistration().getAssertingPartyDetails().getWantAuthnRequestsSigned()) { List signingCredentials = context.getRelyingPartyRegistration().getSigningCredentials(); Map signedParams = this.saml.signQueryParameters( signingCredentials, @@ -91,27 +94,24 @@ public Saml2RedirectAuthenticationRequest createRedirectAuthenticationRequest(Sa return result.build(); } - private String createAuthenticationRequest(Saml2AuthenticationRequestContext request, List credentials) { - return createAuthenticationRequest(Saml2AuthenticationRequest.withAuthenticationRequestContext(request).build(), credentials); + private AuthnRequest createAuthnRequest(Saml2AuthenticationRequestContext context) { + return createAuthnRequest(context.getIssuer(), + context.getDestination(), context.getAssertionConsumerServiceUrl()); } - private String createAuthenticationRequest(Saml2AuthenticationRequest context, List credentials) { - AuthnRequest auth = this.saml.buildSAMLObject(AuthnRequest.class); + private AuthnRequest createAuthnRequest(String issuer, String destination, String assertionConsumerServiceUrl) { + AuthnRequest auth = this.saml.buildSamlObject(AuthnRequest.DEFAULT_ELEMENT_NAME); auth.setID("ARQ" + UUID.randomUUID().toString().substring(1)); auth.setIssueInstant(new DateTime(this.clock.millis())); auth.setForceAuthn(Boolean.FALSE); auth.setIsPassive(Boolean.FALSE); auth.setProtocolBinding(protocolBinding); - Issuer issuer = this.saml.buildSAMLObject(Issuer.class); - issuer.setValue(context.getIssuer()); - auth.setIssuer(issuer); - auth.setDestination(context.getDestination()); - auth.setAssertionConsumerServiceURL(context.getAssertionConsumerServiceUrl()); - return this.saml.toXml( - auth, - credentials, - context.getIssuer() - ); + Issuer iss = this.saml.buildSamlObject(Issuer.DEFAULT_ELEMENT_NAME); + iss.setValue(issuer); + auth.setIssuer(iss); + auth.setDestination(destination); + auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl); + return auth; } /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java index a049fc7d9c3..25dd4388e4a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementation.java @@ -16,6 +16,16 @@ package org.springframework.security.saml2.provider.service.authentication; +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.xml.XMLConstants; +import javax.xml.namespace.QName; + import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.xml.BasicParserPool; import net.shibboleth.utilities.java.support.xml.SerializeSupport; @@ -24,13 +34,14 @@ import org.opensaml.core.config.InitializationException; import org.opensaml.core.config.InitializationService; import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.XMLObjectBuilderFactory; import org.opensaml.core.xml.config.XMLObjectProviderRegistry; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.MarshallerFactory; import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.core.xml.io.UnmarshallerFactory; import org.opensaml.core.xml.io.UnmarshallingException; -import org.opensaml.saml.common.SignableSAMLObject; +import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; import org.opensaml.security.SecurityException; import org.opensaml.security.credential.BasicCredential; @@ -47,28 +58,17 @@ import org.opensaml.xmlsec.signature.support.SignatureConstants; import org.opensaml.xmlsec.signature.support.SignatureException; import org.opensaml.xmlsec.signature.support.SignatureSupport; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + import org.springframework.security.saml2.Saml2Exception; import org.springframework.security.saml2.credentials.Saml2X509Credential; -import org.springframework.security.saml2.provider.service.authentication.Saml2Utils; import org.springframework.util.Assert; import org.springframework.web.util.UriUtils; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import javax.xml.XMLConstants; -import javax.xml.namespace.QName; -import java.io.ByteArrayInputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.Arrays.asList; -import static org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport.getBuilderFactory; import static org.springframework.util.StringUtils.hasText; /** @@ -76,6 +76,8 @@ */ final class OpenSamlImplementation { private static OpenSamlImplementation instance = new OpenSamlImplementation(); + private static XMLObjectBuilderFactory xmlObjectBuilderFactory = + XMLObjectProviderRegistrySupport.getBuilderFactory(); private final BasicParserPool parserPool = new BasicParserPool(); private final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( @@ -167,37 +169,31 @@ EncryptedKeyResolver getEncryptedKeyResolver() { return this.encryptedKeyResolver; } - T buildSAMLObject(final Class clazz) { - try { - QName defaultElementName = (QName) clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); - return (T) getBuilderFactory().getBuilder(defaultElementName).buildObject(defaultElementName); - } - catch (NoSuchFieldException | IllegalAccessException e) { - throw new Saml2Exception("Could not create SAML object", e); - } + T buildSamlObject(QName qName) { + return (T) xmlObjectBuilderFactory.getBuilder(qName).buildObject(qName); } XMLObject resolve(String xml) { return resolve(xml.getBytes(StandardCharsets.UTF_8)); } - String toXml(XMLObject object, List signingCredentials, String localSpEntityId) { - if (object instanceof SignableSAMLObject && null != hasSigningCredential(signingCredentials)) { - signXmlObject( - (SignableSAMLObject) object, - signingCredentials, - localSpEntityId - ); - } + String serialize(XMLObject xmlObject) { final MarshallerFactory marshallerFactory = XMLObjectProviderRegistrySupport.getMarshallerFactory(); try { - Element element = marshallerFactory.getMarshaller(object).marshall(object); + Element element = marshallerFactory.getMarshaller(xmlObject).marshall(xmlObject); return SerializeSupport.nodeToString(element); } catch (MarshallingException e) { throw new Saml2Exception(e); } } + String serialize(AuthnRequest authnRequest, List signingCredentials) { + if (hasSigningCredential(signingCredentials) != null) { + signAuthnRequest(authnRequest, signingCredentials); + } + return serialize(authnRequest); + } + /** * Returns query parameter after creating a Query String signature * All return values are unencoded and will need to be encoded prior to sending @@ -306,15 +302,15 @@ private Credential getSigningCredential(List signingCredent return cred; } - private void signXmlObject(SignableSAMLObject object, List signingCredentials, String entityId) { + private void signAuthnRequest(AuthnRequest authnRequest, List signingCredentials) { SignatureSigningParameters parameters = new SignatureSigningParameters(); - Credential credential = getSigningCredential(signingCredentials, entityId); + Credential credential = getSigningCredential(signingCredentials, authnRequest.getIssuer().getValue()); parameters.setSigningCredential(credential); parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); try { - SignatureSupport.signObject(object, parameters); + SignatureSupport.signObject(authnRequest, parameters); } catch (MarshallingException | SignatureException | SecurityException e) { throw new Saml2Exception(e); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java index 97bc90d65ff..54cb297ffb7 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticatedPrincipal.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -16,7 +16,13 @@ package org.springframework.security.saml2.provider.service.authentication; +import org.springframework.lang.Nullable; import org.springframework.security.core.AuthenticatedPrincipal; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; /** * Saml2 representation of an {@link AuthenticatedPrincipal}. @@ -25,4 +31,40 @@ * @since 5.2.2 */ public interface Saml2AuthenticatedPrincipal extends AuthenticatedPrincipal { + /** + * Get the first value of Saml2 token attribute by name + * + * @param name the name of the attribute + * @param the type of the attribute + * @return the first attribute value or {@code null} otherwise + * @since 5.4 + */ + @Nullable + default A getFirstAttribute(String name) { + List values = getAttribute(name); + return CollectionUtils.firstElement(values); + } + + /** + * Get the Saml2 token attribute by name + * + * @param name the name of the attribute + * @param the type of the attribute + * @return the attribute or {@code null} otherwise + * @since 5.4 + */ + @Nullable + default List getAttribute(String name) { + return (List) getAttributes().get(name); + } + + /** + * Get the Saml2 token attributes + * + * @return the Saml2 token attributes + * @since 5.4 + */ + default Map> getAttributes() { + return Collections.emptyMap(); + } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java index 3e80e69ae88..01343f2b3c0 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestContext.java @@ -29,13 +29,13 @@ * @see Saml2AuthenticationRequestFactory#createRedirectAuthenticationRequest(Saml2AuthenticationRequestContext) * @since 5.3 */ -public final class Saml2AuthenticationRequestContext { +public class Saml2AuthenticationRequestContext { private final RelyingPartyRegistration relyingPartyRegistration; private final String issuer; private final String assertionConsumerServiceUrl; private final String relayState; - private Saml2AuthenticationRequestContext( + protected Saml2AuthenticationRequestContext( RelyingPartyRegistration relyingPartyRegistration, String issuer, String assertionConsumerServiceUrl, @@ -91,7 +91,7 @@ public String getRelayState() { * @return the Destination value */ public String getDestination() { - return this.getRelyingPartyRegistration().getProviderDetails().getWebSsoUrl(); + return this.getRelyingPartyRegistration().getAssertingPartyDetails().getSingleSignOnServiceLocation(); } /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/SimpleSaml2AuthenticatedPrincipal.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/SimpleSaml2AuthenticatedPrincipal.java index 3eb752c46a5..d926d9c5bc9 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/SimpleSaml2AuthenticatedPrincipal.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/SimpleSaml2AuthenticatedPrincipal.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -17,6 +17,8 @@ package org.springframework.security.saml2.provider.service.authentication; import java.io.Serializable; +import java.util.List; +import java.util.Map; /** * Default implementation of a {@link Saml2AuthenticatedPrincipal}. @@ -27,13 +29,20 @@ class SimpleSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPrincipal, Serializable { private final String name; + private final Map> attributes; - SimpleSaml2AuthenticatedPrincipal(String name) { + SimpleSaml2AuthenticatedPrincipal(String name, Map> attributes) { this.name = name; + this.attributes = attributes; } @Override public String getName() { return this.name; } + + @Override + public Map> getAttributes() { + return this.attributes; + } } 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 f96d492ed94..14c29c23d14 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 @@ -16,125 +16,178 @@ package org.springframework.security.saml2.provider.service.registration; -import org.springframework.security.saml2.credentials.Saml2X509Credential; -import org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType; -import org.springframework.util.Assert; - import java.util.Collection; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; -import static java.util.Collections.unmodifiableList; -import static org.springframework.util.Assert.hasText; -import static org.springframework.util.Assert.notEmpty; -import static org.springframework.util.Assert.notNull; +import org.springframework.security.saml2.credentials.Saml2X509Credential; +import org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType; +import org.springframework.util.Assert; /** - * Represents a configured service provider, SP, and a remote identity provider, IDP, pair. - * Each SP/IDP pair is uniquely identified using a registrationId, an arbitrary string. - * A fully configured registration may look like + * Represents a configured relying party (aka Service Provider) and asserting party (aka Identity Provider) pair. + * + *

+ * Each RP/AP pair is uniquely identified using a {@code registrationId}, an arbitrary string. + * + *

+ * A fully configured registration may look like: + * *

- *		//remote IDP entity ID
- *		String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php";
- *		//remote WebSSO Endpoint - Where to Send AuthNRequests to
- *		String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php";
- *		//local registration ID
- *		String registrationId = "simplesamlphp";
- *		//local entity ID - autogenerated based on URL
- *		String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
- *		//local SSO URL - autogenerated, endpoint to receive SAML Response objects
- *		String acsUrlTemplate = "{baseUrl}/login/saml2/sso/{registrationId}";
- *		//local signing (and local decryption key and remote encryption certificate)
- *		Saml2X509Credential signingCredential = getSigningCredential();
- *		//IDP certificate for verification of incoming messages
- *		Saml2X509Credential idpVerificationCertificate = getVerificationCertificate();
- *		RelyingPartyRegistration rp = RelyingPartyRegistration.withRegistrationId(registrationId)
- * 				.providerDetails(config -> config.entityId(idpEntityId));
- * 				.providerDetails(config -> config.webSsoUrl(url));
- * 				.credentials(c -> c.add(signingCredential))
- * 				.credentials(c -> c.add(idpVerificationCertificate))
- * 				.localEntityIdTemplate(localEntityIdTemplate)
- * 				.assertionConsumerServiceUrlTemplate(acsUrlTemplate)
- * 				.build();
+ *	String registrationId = "simplesamlphp";
+ *
+ * 	String relyingPartyEntityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}";
+ *	String assertionConsumerServiceLocation = "{baseUrl}/login/saml2/sso/{registrationId}";
+ *	Saml2X509Credential relyingPartySigningCredential = ...;
+ *
+ *	String assertingPartyEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php";
+ *	String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php";
+ * 	Saml2X509Credential assertingPartyVerificationCredential = ...;
+ *
+ *
+ *	RelyingPartyRegistration rp = RelyingPartyRegistration.withRegistrationId(registrationId)
+ * 			.entityId(relyingPartyEntityId)
+ * 			.assertionConsumerServiceLocation(assertingConsumerServiceLocation)
+ * 		 	.credentials(c -> c.add(relyingPartySigningCredential))
+ * 			.assertingPartyDetails(details -> details
+ * 				.entityId(assertingPartyEntityId));
+ * 				.singleSignOnServiceLocation(singleSignOnServiceLocation))
+ * 				.credentials(c -> c.add(assertingPartyVerificationCredential))
+ * 			.build();
  * 
+ * * @since 5.2 + * @author Filip Hanik + * @author Josh Cummings */ public class RelyingPartyRegistration { private final String registrationId; - private final String assertionConsumerServiceUrlTemplate; - private final List credentials; - private final String localEntityIdTemplate; + private final String entityId; + private final String assertionConsumerServiceLocation; private final ProviderDetails providerDetails; + private final List credentials; private RelyingPartyRegistration( String registrationId, - String assertionConsumerServiceUrlTemplate, + String entityId, + String assertionConsumerServiceLocation, ProviderDetails providerDetails, - List credentials, - String localEntityIdTemplate) { - hasText(registrationId, "registrationId cannot be empty"); - hasText(assertionConsumerServiceUrlTemplate, "assertionConsumerServiceUrlTemplate cannot be empty"); - hasText(localEntityIdTemplate, "localEntityIdTemplate cannot be empty"); - notEmpty(credentials, "credentials cannot be empty"); - notNull(providerDetails, "providerDetails cannot be null"); - hasText(providerDetails.webSsoUrl, "providerDetails.webSsoUrl cannot be empty"); + List credentials) { + + Assert.hasText(registrationId, "registrationId cannot be empty"); + Assert.hasText(entityId, "entityId cannot be empty"); + Assert.hasText(assertionConsumerServiceLocation, "assertionConsumerServiceLocation cannot be empty"); + Assert.notNull(providerDetails, "providerDetails cannot be null"); + Assert.notEmpty(credentials, "credentials cannot be empty"); for (Saml2X509Credential c : credentials) { - notNull(c, "credentials cannot contain null elements"); + Assert.notNull(c, "credentials cannot contain null elements"); } this.registrationId = registrationId; - this.assertionConsumerServiceUrlTemplate = assertionConsumerServiceUrlTemplate; - this.credentials = unmodifiableList(new LinkedList<>(credentials)); + this.entityId = entityId; + this.assertionConsumerServiceLocation = assertionConsumerServiceLocation; this.providerDetails = providerDetails; - this.localEntityIdTemplate = localEntityIdTemplate; + this.credentials = Collections.unmodifiableList(new LinkedList<>(credentials)); + } + + /** + * Get the unique registration id for this RP/AP pair + * + * @return the unique registration id for this RP/AP pair + */ + public String getRegistrationId() { + return this.registrationId; + } + + /** + * Get the relying party's + *
EntityID. + * + *

+ * Equivalent to the value found in the relying party's + * <EntityDescriptor EntityID="..."/> + * + *

+ * This value may contain a number of placeholders, which need to be + * resolved before use. They are {@code baseUrl}, {@code registrationId}, + * {@code baseScheme}, {@code baseHost}, and {@code basePort}. + * + * @return the relying party's EntityID + * @since 5.4 + */ + public String getEntityId() { + return this.entityId; + } + + /** + * Get the AssertionConsumerService Location. + * Equivalent to the value found in <AssertionConsumerService Location="..."/> + * in the relying party's <SPSSODescriptor>. + * + * This value may contain a number of placeholders, which need to be + * resolved before use. They are {@code baseUrl}, {@code registrationId}, + * {@code baseScheme}, {@code baseHost}, and {@code basePort}. + * + * @return the AssertionConsumerService Location + * @since 5.4 + */ + public String getAssertionConsumerServiceLocation() { + return this.assertionConsumerServiceLocation; + } + + /** + * Get the configuration details for the Asserting Party + * + * @return the {@link AssertingPartyDetails} + * @since 5.4 + */ + public AssertingPartyDetails getAssertingPartyDetails() { + return this.providerDetails.assertingPartyDetails; } /** * Returns the entity ID of the IDP, the asserting party. * @return entity ID of the asserting party - * @deprecated use {@link ProviderDetails#getEntityId()} from {@link #getProviderDetails()} + * @deprecated use {@link AssertingPartyDetails#getEntityId} from {@link #getAssertingPartyDetails} */ @Deprecated public String getRemoteIdpEntityId() { return this.providerDetails.getEntityId(); } - /** - * Returns the unique relying party registration ID - * @return registrationId - */ - public String getRegistrationId() { - return this.registrationId; - } - /** * returns the URL template for which ACS URL authentication requests should contain * Possible variables are {@code baseUrl}, {@code registrationId}, * {@code baseScheme}, {@code baseHost}, and {@code basePort}. * @return string containing the ACS URL template, with or without variables present + * @deprecated Use {@link #getAssertionConsumerServiceLocation} instead */ + @Deprecated public String getAssertionConsumerServiceUrlTemplate() { - return this.assertionConsumerServiceUrlTemplate; + return this.assertionConsumerServiceLocation; } /** * Contains the URL for which to send the SAML 2 Authentication Request to initiate * a single sign on flow. * @return a IDP URL that accepts REDIRECT or POST binding for authentication requests - * @deprecated use {@link ProviderDetails#getWebSsoUrl()} from {@link #getProviderDetails()} + * @deprecated use {@link AssertingPartyDetails#getSingleSignOnServiceLocation} from {@link #getAssertingPartyDetails} */ @Deprecated public String getIdpWebSsoUrl() { - return this.getProviderDetails().webSsoUrl; + return this.getAssertingPartyDetails().getSingleSignOnServiceLocation(); } /** * Returns specific configuration around the Identity Provider SSO endpoint * @return the IDP SSO endpoint configuration * @since 5.3 + * @deprecated Use {@link #getAssertingPartyDetails} instead */ + @Deprecated public ProviderDetails getProviderDetails() { return this.providerDetails; } @@ -145,9 +198,11 @@ public ProviderDetails getProviderDetails() { * {@code baseScheme}, {@code baseHost}, and {@code basePort}, for example * {@code {baseUrl}/saml2/service-provider-metadata/{registrationId}} * @return a string containing the entity ID or entity ID template + * @deprecated Use {@link #getEntityId} instead */ + @Deprecated public String getLocalEntityIdTemplate() { - return this.localEntityIdTemplate; + return this.entityId; } /** @@ -223,40 +278,196 @@ public static Builder withRegistrationId(String registrationId) { public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) { Assert.notNull(registration, "registration cannot be null"); return withRegistrationId(registration.getRegistrationId()) - .providerDetails(c -> { - c.webSsoUrl(registration.getProviderDetails().getWebSsoUrl()); - c.binding(registration.getProviderDetails().getBinding()); - c.signAuthNRequest(registration.getProviderDetails().isSignAuthNRequest()); - c.entityId(registration.getProviderDetails().getEntityId()); - }) - .credentials(c -> c.addAll(registration.getCredentials())) - .localEntityIdTemplate(registration.getLocalEntityIdTemplate()) - .assertionConsumerServiceUrlTemplate(registration.getAssertionConsumerServiceUrlTemplate()) - ; + .entityId(registration.getEntityId()) + .assertionConsumerServiceLocation(registration.getAssertionConsumerServiceLocation()) + .assertingPartyDetails(c -> c + .entityId(registration.getAssertingPartyDetails().getEntityId()) + .wantAuthnRequestsSigned(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) + .singleSignOnServiceLocation(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()) + .singleSignOnServiceBinding(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()) + ) + .credentials(c -> c.addAll(registration.getCredentials())); } + /** - * Configuration for IDP SSO endpoint configuration - * @since 5.3 + * The configuration metadata of the Asserting party + * + * @since 5.4 */ - public final static class ProviderDetails { + public final static class AssertingPartyDetails { private final String entityId; - private final String webSsoUrl; - private final boolean signAuthNRequest; - private final Saml2MessageBinding binding; + private final boolean wantAuthnRequestsSigned; + private final String singleSignOnServiceLocation; + private final Saml2MessageBinding singleSignOnServiceBinding; - private ProviderDetails( + private AssertingPartyDetails( String entityId, - String webSsoUrl, - boolean signAuthNRequest, - Saml2MessageBinding binding) { - hasText(entityId, "entityId cannot be null or empty"); - notNull(webSsoUrl, "webSsoUrl cannot be null"); - notNull(binding, "binding cannot be null"); + boolean wantAuthnRequestsSigned, + String singleSignOnServiceLocation, + Saml2MessageBinding singleSignOnServiceBinding) { + + Assert.hasText(entityId, "entityId cannot be null or empty"); + Assert.notNull(singleSignOnServiceLocation, "singleSignOnServiceLocation cannot be null"); + Assert.notNull(singleSignOnServiceBinding, "singleSignOnServiceBinding cannot be null"); this.entityId = entityId; - this.webSsoUrl = webSsoUrl; - this.signAuthNRequest = signAuthNRequest; - this.binding = binding; + this.wantAuthnRequestsSigned = wantAuthnRequestsSigned; + this.singleSignOnServiceLocation = singleSignOnServiceLocation; + this.singleSignOnServiceBinding = singleSignOnServiceBinding; + } + + /** + * Get the asserting party's + * EntityID. + * + *

+ * Equivalent to the value found in the asserting party's + * <EntityDescriptor EntityID="..."/> + * + *

+ * This value may contain a number of placeholders, which need to be + * resolved before use. They are {@code baseUrl}, {@code registrationId}, + * {@code baseScheme}, {@code baseHost}, and {@code basePort}. + * + * @return the asserting party's EntityID + */ + public String getEntityId() { + return this.entityId; + } + + /** + * Get the WantAuthnRequestsSigned setting, indicating the asserting party's preference that + * relying parties should sign the AuthnRequest before sending. + * + * @return the WantAuthnRequestsSigned value + */ + public boolean getWantAuthnRequestsSigned() { + return this.wantAuthnRequestsSigned; + } + + /** + * Get the + * SingleSignOnService + * Location. + * + *

+ * Equivalent to the value found in <SingleSignOnService Location="..."/> + * in the asserting party's <IDPSSODescriptor>. + * + * @return the SingleSignOnService Location + */ + public String getSingleSignOnServiceLocation() { + return this.singleSignOnServiceLocation; + } + + /** + * Get the + * SingleSignOnService + * Binding. + * + *

+ * Equivalent to the value found in <SingleSignOnService Binding="..."/> + * in the asserting party's <IDPSSODescriptor>. + * + * @return the SingleSignOnService Location + */ + public Saml2MessageBinding getSingleSignOnServiceBinding() { + return this.singleSignOnServiceBinding; + } + + public final static class Builder { + private String entityId; + private boolean wantAuthnRequestsSigned = true; + private String singleSignOnServiceLocation; + private Saml2MessageBinding singleSignOnServiceBinding = Saml2MessageBinding.REDIRECT; + + /** + * Set the asserting party's + * EntityID. + * Equivalent to the value found in the asserting party's + * <EntityDescriptor EntityID="..."/> + * + * @param entityId the asserting party's EntityID + * @return the {@link ProviderDetails.Builder} for further configuration + */ + public Builder entityId(String entityId) { + this.entityId = entityId; + return this; + } + + /** + * Set the WantAuthnRequestsSigned setting, indicating the asserting party's preference that + * relying parties should sign the AuthnRequest before sending. + * + * @param wantAuthnRequestsSigned the WantAuthnRequestsSigned setting + * @return the {@link ProviderDetails.Builder} for further configuration + */ + public Builder wantAuthnRequestsSigned(boolean wantAuthnRequestsSigned) { + this.wantAuthnRequestsSigned = wantAuthnRequestsSigned; + return this; + } + + /** + * Set the + * SingleSignOnService + * Location. + * + *

+ * Equivalent to the value found in <SingleSignOnService Location="..."/> + * in the asserting party's <IDPSSODescriptor>. + * + * @param singleSignOnServiceLocation the SingleSignOnService Location + * @return the {@link ProviderDetails.Builder} for further configuration + */ + public Builder singleSignOnServiceLocation(String singleSignOnServiceLocation) { + this.singleSignOnServiceLocation = singleSignOnServiceLocation; + return this; + } + + /** + * Set the + * SingleSignOnService + * Binding. + * + *

+ * Equivalent to the value found in <SingleSignOnService Binding="..."/> + * in the asserting party's <IDPSSODescriptor>. + * + * @param singleSignOnServiceBinding the SingleSignOnService Binding + * @return the {@link ProviderDetails.Builder} for further configuration + */ + public Builder singleSignOnServiceBinding(Saml2MessageBinding singleSignOnServiceBinding) { + this.singleSignOnServiceBinding = singleSignOnServiceBinding; + return this; + } + + /** + * Creates an immutable ProviderDetails object representing the configuration for an Identity Provider, IDP + * @return immutable ProviderDetails object + */ + public AssertingPartyDetails build() { + return new AssertingPartyDetails( + this.entityId, + this.wantAuthnRequestsSigned, + this.singleSignOnServiceLocation, + this.singleSignOnServiceBinding + ); + } + } + } + + /** + * Configuration for IDP SSO endpoint configuration + * @since 5.3 + * @deprecated Use {@link AssertingPartyDetails} instead + */ + @Deprecated + public final static class ProviderDetails { + private final AssertingPartyDetails assertingPartyDetails; + + private ProviderDetails(AssertingPartyDetails assertingPartyDetails) { + Assert.notNull("assertingPartyDetails cannot be null"); + this.assertingPartyDetails = assertingPartyDetails; } /** @@ -264,7 +475,7 @@ private ProviderDetails( * @return the entity ID of the IDP */ public String getEntityId() { - return entityId; + return this.assertingPartyDetails.getEntityId(); } /** @@ -273,7 +484,7 @@ public String getEntityId() { * @return a IDP URL that accepts REDIRECT or POST binding for authentication requests */ public String getWebSsoUrl() { - return webSsoUrl; + return this.assertingPartyDetails.getSingleSignOnServiceLocation(); } /** @@ -281,34 +492,38 @@ public String getWebSsoUrl() { * {@code false} if no signature is required. */ public boolean isSignAuthNRequest() { - return signAuthNRequest; + return this.assertingPartyDetails.getWantAuthnRequestsSigned(); } /** * @return the type of SAML 2 Binding the AuthNRequest should be sent on */ public Saml2MessageBinding getBinding() { - return binding; + return this.assertingPartyDetails.getSingleSignOnServiceBinding(); } /** * Builder for IDP SSO endpoint configuration * @since 5.3 + * @deprecated Use {@link AssertingPartyDetails.Builder} instead */ + @Deprecated public final static class Builder { - private String entityId; - private String webSsoUrl; - private boolean signAuthNRequest = true; - private Saml2MessageBinding binding = Saml2MessageBinding.REDIRECT; + private final AssertingPartyDetails.Builder assertingPartyDetailsBuilder = + new AssertingPartyDetails.Builder(); /** - * Sets the {@code EntityID} for the remote asserting party, the Identity Provider. + * Set the asserting party's + * EntityID. + * Equivalent to the value found in the asserting party's + * <EntityDescriptor EntityID="..."/> * - * @param entityId - the EntityID of the IDP. May be a URL. - * @return this object + * @param entityId the asserting party's EntityID + * @return the {@link Builder} for further configuration + * @since 5.4 */ public Builder entityId(String entityId) { - this.entityId = entityId; + this.assertingPartyDetailsBuilder.entityId(entityId); return this; } @@ -319,7 +534,7 @@ public Builder entityId(String entityId) { * @return this object */ public Builder webSsoUrl(String url) { - this.webSsoUrl = url; + this.assertingPartyDetailsBuilder.singleSignOnServiceLocation(url); return this; } @@ -330,7 +545,7 @@ public Builder webSsoUrl(String url) { * @return this object */ public Builder signAuthNRequest(boolean signAuthNRequest) { - this.signAuthNRequest = signAuthNRequest; + this.assertingPartyDetailsBuilder.wantAuthnRequestsSigned(signAuthNRequest); return this; } @@ -342,7 +557,7 @@ public Builder signAuthNRequest(boolean signAuthNRequest) { * @return this object */ public Builder binding(Saml2MessageBinding binding) { - this.binding = binding; + this.assertingPartyDetailsBuilder.singleSignOnServiceBinding(binding); return this; } @@ -351,22 +566,17 @@ public Builder binding(Saml2MessageBinding binding) { * @return immutable ProviderDetails object */ public ProviderDetails build() { - return new ProviderDetails( - this.entityId, - this.webSsoUrl, - this.signAuthNRequest, - this.binding - ); + return new ProviderDetails(this.assertingPartyDetailsBuilder.build()); } } } public final static class Builder { private String registrationId; - private String assertionConsumerServiceUrlTemplate; - private List credentials = new LinkedList<>(); - private String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; + private String entityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; + private String assertionConsumerServiceLocation; private ProviderDetails.Builder providerDetails = new ProviderDetails.Builder(); + private List credentials = new LinkedList<>(); private Builder(String registrationId) { this.registrationId = registrationId; @@ -384,49 +594,54 @@ public Builder registrationId(String id) { } /** - * Sets the {@code entityId} for the remote asserting party, the Identity Provider. - * @param entityId the IDP entityId - * @return this object - * @deprecated use {@link #providerDetails(Consumer< ProviderDetails.Builder >)} + * Set the relying party's + * EntityID. + * Equivalent to the value found in the relying party's + * <EntityDescriptor EntityID="..."/> + * + * This value may contain a number of placeholders. + * They are {@code baseUrl}, {@code registrationId}, + * {@code baseScheme}, {@code baseHost}, and {@code basePort}. + * + * @return the {@link Builder} for further configuration + * @since 5.4 */ - @Deprecated - public Builder remoteIdpEntityId(String entityId) { - this.providerDetails(idp -> idp.entityId(entityId)); + public Builder entityId(String entityId) { + this.entityId = entityId; return this; } /** - * Assertion Consumer - * Service URL template. It can contain variables {@code baseUrl}, {@code registrationId}, + * Set the AssertionConsumerService + * Location. + * + *

+ * Equivalent to the value found in <AssertionConsumerService Location="..."/> + * in the relying party's <SPSSODescriptor> + * + *

+ * This value may contain a number of placeholders. + * They are {@code baseUrl}, {@code registrationId}, * {@code baseScheme}, {@code baseHost}, and {@code basePort}. - * @param assertionConsumerServiceUrlTemplate the Assertion Consumer Service URL template (i.e. - * "{baseUrl}/login/saml2/sso/{registrationId}". - * @return this object + * + * @param assertionConsumerServiceLocation + * @return the {@link Builder} for further configuration + * @since 5.4 */ - public Builder assertionConsumerServiceUrlTemplate(String assertionConsumerServiceUrlTemplate) { - this.assertionConsumerServiceUrlTemplate = assertionConsumerServiceUrlTemplate; + public Builder assertionConsumerServiceLocation(String assertionConsumerServiceLocation) { + this.assertionConsumerServiceLocation = assertionConsumerServiceLocation; return this; } /** - * Sets the {@code SSO URL} for the remote asserting party, the Identity Provider. - * @param url - a URL that accepts authentication requests via REDIRECT or POST bindings - * @return this object - * @deprecated use {@link #providerDetails(Consumer< ProviderDetails.Builder >)} + * Apply this {@link Consumer} to further configure the Asserting Party details + * + * @param assertingPartyDetails The {@link Consumer} to apply + * @return the {@link Builder} for further configuration + * @since 5.4 */ - @Deprecated - public Builder idpWebSsoUrl(String url) { - providerDetails(config -> config.webSsoUrl(url)); - return this; - } - - /** - * Configures the IDP SSO endpoint - * @param providerDetails a consumer that configures the IDP SSO endpoint - * @return this object - */ - public Builder providerDetails(Consumer providerDetails) { - providerDetails.accept(this.providerDetails); + public Builder assertingPartyDetails(Consumer assertingPartyDetails) { + assertingPartyDetails.accept(this.providerDetails.assertingPartyDetailsBuilder); return this; } @@ -449,16 +664,68 @@ public Builder credentials(Consumer> credentials return this; } + /** + * Assertion Consumer + * Service URL template. It can contain variables {@code baseUrl}, {@code registrationId}, + * {@code baseScheme}, {@code baseHost}, and {@code basePort}. + * @param assertionConsumerServiceUrlTemplate the Assertion Consumer Service URL template (i.e. + * "{baseUrl}/login/saml2/sso/{registrationId}". + * @return this object + * @deprecated Use {@link #assertionConsumerServiceLocation} instead. + */ + @Deprecated + public Builder assertionConsumerServiceUrlTemplate(String assertionConsumerServiceUrlTemplate) { + this.assertionConsumerServiceLocation = assertionConsumerServiceUrlTemplate; + return this; + } + + /** + * Sets the {@code entityId} for the remote asserting party, the Identity Provider. + * @param entityId the IDP entityId + * @return this object + * @deprecated use {@link #assertingPartyDetails(Consumer< AssertingPartyDetails.Builder >)} + */ + @Deprecated + public Builder remoteIdpEntityId(String entityId) { + assertingPartyDetails(idp -> idp.entityId(entityId)); + return this; + } + + /** + * Sets the {@code SSO URL} for the remote asserting party, the Identity Provider. + * @param url - a URL that accepts authentication requests via REDIRECT or POST bindings + * @return this object + * @deprecated use {@link #assertingPartyDetails(Consumer< AssertingPartyDetails.Builder >)} + */ + @Deprecated + public Builder idpWebSsoUrl(String url) { + assertingPartyDetails(config -> config.singleSignOnServiceLocation(url)); + return this; + } + /** * Sets the local relying party, or Service Provider, entity Id template. * can generate it's entity ID based on possible variables of {@code baseUrl}, {@code registrationId}, * {@code baseScheme}, {@code baseHost}, and {@code basePort}, for example * {@code {baseUrl}/saml2/service-provider-metadata/{registrationId}} * @return a string containing the entity ID or entity ID template + * @deprecated Use {@link #entityId} instead */ - + @Deprecated public Builder localEntityIdTemplate(String template) { - this.localEntityIdTemplate = template; + this.entityId = template; + return this; + } + + /** + * Configures the IDP SSO endpoint + * @param providerDetails a consumer that configures the IDP SSO endpoint + * @return this object + * @deprecated Use {@link #assertingPartyDetails} instead + */ + @Deprecated + public Builder providerDetails(Consumer providerDetails) { + providerDetails.accept(this.providerDetails); return this; } @@ -469,10 +736,10 @@ public Builder localEntityIdTemplate(String template) { public RelyingPartyRegistration build() { return new RelyingPartyRegistration( this.registrationId, - this.assertionConsumerServiceUrlTemplate, + this.entityId, + this.assertionConsumerServiceLocation, this.providerDetails.build(), - this.credentials, - this.localEntityIdTemplate + this.credentials ); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2ServletUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2ServletUtils.java index 2088f55cf2c..91515b6a3ac 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2ServletUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2ServletUtils.java @@ -16,15 +16,15 @@ package org.springframework.security.saml2.provider.service.servlet.filter; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; + import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import javax.servlet.http.HttpServletRequest; -import java.util.HashMap; -import java.util.Map; - import static org.springframework.security.web.util.UrlUtils.buildFullRequestUrl; import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl; @@ -35,20 +35,13 @@ final class Saml2ServletUtils { private static final char PATH_DELIMITER = '/'; - static String getServiceProviderEntityId(RelyingPartyRegistration rp, HttpServletRequest request) { - return resolveUrlTemplate( - rp.getLocalEntityIdTemplate(), - getApplicationUri(request), - rp.getProviderDetails().getEntityId(), - rp.getRegistrationId() - ); - } - - static String resolveUrlTemplate(String template, String baseUrl, String entityId, String registrationId) { + static String resolveUrlTemplate(String template, String baseUrl, RelyingPartyRegistration relyingParty) { if (!StringUtils.hasText(template)) { return baseUrl; } + String entityId = relyingParty.getAssertingPartyDetails().getEntityId(); + String registrationId = relyingParty.getRegistrationId(); Map uriVariables = new HashMap<>(); UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(baseUrl) .replaceQuery(null) diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java index a332664be25..028a07973a3 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationFilter.java @@ -16,6 +16,9 @@ package org.springframework.security.saml2.provider.service.servlet.filter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.springframework.http.HttpMethod; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -30,9 +33,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import static java.nio.charset.StandardCharsets.UTF_8; import static org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes.RELYING_PARTY_REGISTRATION_NOT_FOUND; import static org.springframework.util.StringUtils.hasText; @@ -97,11 +97,12 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ "Relying Party Registration not found with ID: " + registrationId); throw new Saml2AuthenticationException(saml2Error); } - String localSpEntityId = Saml2ServletUtils.getServiceProviderEntityId(rp, request); + String applicationUri = Saml2ServletUtils.getApplicationUri(request); + String localSpEntityId = Saml2ServletUtils.resolveUrlTemplate(rp.getEntityId(), applicationUri, rp); final Saml2AuthenticationToken authentication = new Saml2AuthenticationToken( responseXml, request.getRequestURL().toString(), - rp.getProviderDetails().getEntityId(), + rp.getAssertingPartyDetails().getEntityId(), localSpEntityId, rp.getCredentials() ); @@ -116,5 +117,4 @@ private String inflateIfRequired(HttpServletRequest request, byte[] b) { return new String(b, UTF_8); } } - } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java index 03c628d2d60..f0270de2139 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilter.java @@ -16,8 +16,13 @@ package org.springframework.security.saml2.provider.service.servlet.filter; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.springframework.http.MediaType; -import org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory; import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; @@ -25,52 +30,117 @@ import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.web.DefaultSaml2AuthenticationRequestContextResolver; +import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestContextResolver; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriUtils; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -import static java.lang.String.format; import static java.nio.charset.StandardCharsets.ISO_8859_1; -import static org.springframework.util.StringUtils.hasText; /** + * This {@code Filter} formulates a + * SAML 2.0 AuthnRequest (line 1968) + * and redirects to a configured asserting party. + * + *

+ * It supports the + * HTTP-Redirect (line 520) + * and + * HTTP-POST (line 753) + * bindings. + * + *

+ * By default, this {@code Filter} responds to authentication requests + * at the {@code URI} {@code /oauth2/authorization/{registrationId}}. + * The {@code URI} template variable {@code {registrationId}} represents the + * {@link RelyingPartyRegistration#getRegistrationId() registration identifier} of the relying party + * that is used for initiating the authentication request. + * * @since 5.2 + * @author Filip Hanik + * @author Josh Cummings */ public class Saml2WebSsoAuthenticationRequestFilter extends OncePerRequestFilter { private final RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + private Saml2AuthenticationRequestFactory authenticationRequestFactory; + private Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver = new DefaultSaml2AuthenticationRequestContextResolver(); + private RequestMatcher redirectMatcher = new AntPathRequestMatcher("/saml2/authenticate/{registrationId}"); - private Saml2AuthenticationRequestFactory authenticationRequestFactory = new OpenSamlAuthenticationRequestFactory(); + /** + * Construct a {@link Saml2WebSsoAuthenticationRequestFilter} with the provided parameters + * + * @param relyingPartyRegistrationRepository a repository for relying party configurations + * @deprecated use the constructor that takes a {@link Saml2AuthenticationRequestFactory} + */ + @Deprecated public Saml2WebSsoAuthenticationRequestFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) { + this(relyingPartyRegistrationRepository, + new org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory()); + } + + /** + * Construct a {@link Saml2WebSsoAuthenticationRequestFilter} with the provided parameters + * + * @param relyingPartyRegistrationRepository a repository for relying party configurations + * @since 5.4 + */ + public Saml2WebSsoAuthenticationRequestFilter(RelyingPartyRegistrationRepository relyingPartyRegistrationRepository, + Saml2AuthenticationRequestFactory authenticationRequestFactory) { Assert.notNull(relyingPartyRegistrationRepository, "relyingPartyRegistrationRepository cannot be null"); + Assert.notNull(authenticationRequestFactory, "authenticationRequestFactory cannot be null"); this.relyingPartyRegistrationRepository = relyingPartyRegistrationRepository; + this.authenticationRequestFactory = authenticationRequestFactory; } + /** + * Use the given {@link Saml2AuthenticationRequestFactory} for formulating the SAML 2.0 AuthnRequest + * + * @param authenticationRequestFactory the {@link Saml2AuthenticationRequestFactory} to use + * @deprecated use the constructor instead + */ + @Deprecated public void setAuthenticationRequestFactory(Saml2AuthenticationRequestFactory authenticationRequestFactory) { Assert.notNull(authenticationRequestFactory, "authenticationRequestFactory cannot be null"); this.authenticationRequestFactory = authenticationRequestFactory; } + /** + * Use the given {@link RequestMatcher} that activates this filter for a given request + * + * @param redirectMatcher the {@link RequestMatcher} to use + */ public void setRedirectMatcher(RequestMatcher redirectMatcher) { Assert.notNull(redirectMatcher, "redirectMatcher cannot be null"); this.redirectMatcher = redirectMatcher; } + /** + * Use the given {@link Saml2AuthenticationRequestContextResolver} that creates a {@link Saml2AuthenticationRequestContext} + * + * @param authenticationRequestContextResolver the {@link Saml2AuthenticationRequestContextResolver} to use + * @since 5.4 + */ + public void setAuthenticationRequestContextResolver(Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver) { + Assert.notNull(authenticationRequestContextResolver, "authenticationRequestContextResolver cannot be null"); + this.authenticationRequestContextResolver = authenticationRequestContextResolver; + } + + /** + * {@inheritDoc} + */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + MatchResult matcher = this.redirectMatcher.matcher(request); if (!matcher.isMatch()) { filterChain.doFilter(request, response); @@ -78,55 +148,39 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } String registrationId = matcher.getVariables().get("registrationId"); - RelyingPartyRegistration relyingParty = this.relyingPartyRegistrationRepository.findByRegistrationId(registrationId); + RelyingPartyRegistration relyingParty = + this.relyingPartyRegistrationRepository.findByRegistrationId(registrationId); if (relyingParty == null) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } - if (this.logger.isDebugEnabled()) { - this.logger.debug(format("Creating SAML2 SP Authentication Request for IDP[%s]", relyingParty.getRegistrationId())); - } - Saml2AuthenticationRequestContext authnRequestCtx = createRedirectAuthenticationRequestContext(relyingParty, request); - if (relyingParty.getProviderDetails().getBinding() == Saml2MessageBinding.REDIRECT) { - sendRedirect(response, authnRequestCtx); + Saml2AuthenticationRequestContext context = authenticationRequestContextResolver.resolve(request, relyingParty); + if (relyingParty.getAssertingPartyDetails().getSingleSignOnServiceBinding() == Saml2MessageBinding.REDIRECT) { + sendRedirect(response, context); + } else { + sendPost(response, context); } - else { - sendPost(response, authnRequestCtx); - } - } - - private void sendRedirect(HttpServletResponse response, Saml2AuthenticationRequestContext authnRequestCtx) - throws IOException { - String redirectUrl = createSamlRequestRedirectUrl(authnRequestCtx); - response.sendRedirect(redirectUrl); } - private void sendPost(HttpServletResponse response, Saml2AuthenticationRequestContext authnRequestCtx) + private void sendRedirect(HttpServletResponse response, Saml2AuthenticationRequestContext context) throws IOException { - Saml2PostAuthenticationRequest authNData = - this.authenticationRequestFactory.createPostAuthenticationRequest(authnRequestCtx); - String html = createSamlPostRequestFormData(authNData); - response.setContentType(MediaType.TEXT_HTML_VALUE); - response.getWriter().write(html); - } - - private String createSamlRequestRedirectUrl(Saml2AuthenticationRequestContext authnRequestCtx) { - - Saml2RedirectAuthenticationRequest authNData = - this.authenticationRequestFactory.createRedirectAuthenticationRequest(authnRequestCtx); - UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(authNData.getAuthenticationRequestUri()); - addParameter("SAMLRequest", authNData.getSamlRequest(), uriBuilder); - addParameter("RelayState", authNData.getRelayState(), uriBuilder); - addParameter("SigAlg", authNData.getSigAlg(), uriBuilder); - addParameter("Signature", authNData.getSignature(), uriBuilder); - return uriBuilder + Saml2RedirectAuthenticationRequest authenticationRequest = + this.authenticationRequestFactory.createRedirectAuthenticationRequest(context); + UriComponentsBuilder uriBuilder = UriComponentsBuilder + .fromUriString(authenticationRequest.getAuthenticationRequestUri()); + addParameter("SAMLRequest", authenticationRequest.getSamlRequest(), uriBuilder); + addParameter("RelayState", authenticationRequest.getRelayState(), uriBuilder); + addParameter("SigAlg", authenticationRequest.getSigAlg(), uriBuilder); + addParameter("Signature", authenticationRequest.getSignature(), uriBuilder); + String redirectUrl = uriBuilder .build(true) .toUriString(); + response.sendRedirect(redirectUrl); } private void addParameter(String name, String value, UriComponentsBuilder builder) { Assert.hasText(name, "name cannot be empty or null"); - if (hasText(value)) { + if (StringUtils.hasText(value)) { builder.queryParam( UriUtils.encode(name, ISO_8859_1), UriUtils.encode(value, ISO_8859_1) @@ -134,38 +188,19 @@ private void addParameter(String name, String value, UriComponentsBuilder builde } } - private Saml2AuthenticationRequestContext createRedirectAuthenticationRequestContext( - RelyingPartyRegistration relyingParty, - HttpServletRequest request) { - String localSpEntityId = Saml2ServletUtils.getServiceProviderEntityId(relyingParty, request); - return Saml2AuthenticationRequestContext - .builder() - .issuer(localSpEntityId) - .relyingPartyRegistration(relyingParty) - .assertionConsumerServiceUrl( - Saml2ServletUtils.resolveUrlTemplate( - relyingParty.getAssertionConsumerServiceUrlTemplate(), - Saml2ServletUtils.getApplicationUri(request), - relyingParty.getProviderDetails().getEntityId(), - relyingParty.getRegistrationId() - ) - ) - .relayState(request.getParameter("RelayState")) - .build() - ; - } - - private String htmlEscape(String value) { - if (hasText(value)) { - return HtmlUtils.htmlEscape(value); - } - return value; + private void sendPost(HttpServletResponse response, Saml2AuthenticationRequestContext context) + throws IOException { + Saml2PostAuthenticationRequest authenticationRequest = + this.authenticationRequestFactory.createPostAuthenticationRequest(context); + String html = createSamlPostRequestFormData(authenticationRequest); + response.setContentType(MediaType.TEXT_HTML_VALUE); + response.getWriter().write(html); } - private String createSamlPostRequestFormData(Saml2PostAuthenticationRequest request) { - String destination = request.getAuthenticationRequestUri(); - String relayState = htmlEscape(request.getRelayState()); - String samlRequest = htmlEscape(request.getSamlRequest()); + private String createSamlPostRequestFormData(Saml2PostAuthenticationRequest authenticationRequest) { + String authenticationRequestUri = authenticationRequest.getAuthenticationRequestUri(); + String relayState = authenticationRequest.getRelayState(); + String samlRequest = authenticationRequest.getSamlRequest(); StringBuilder postHtml = new StringBuilder() .append("\n") .append("\n") @@ -180,16 +215,15 @@ private String createSamlPostRequestFormData(Saml2PostAuthenticationRequest requ .append("

\n") .append(" \n") .append(" \n") - .append("
\n") + .append(" \n") .append("
\n") .append(" \n") - ; - if (hasText(relayState)) { + .append(HtmlUtils.htmlEscape(samlRequest)) + .append("\"/>\n"); + if (StringUtils.hasText(relayState)) { postHtml .append(" \n"); } postHtml @@ -202,8 +236,7 @@ private String createSamlPostRequestFormData(Saml2PostAuthenticationRequest requ .append(" \n") .append(" \n") .append(" \n") - .append("") - ; + .append(""); return postHtml.toString(); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java new file mode 100644 index 00000000000..7910b74bb92 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolver.java @@ -0,0 +1,123 @@ +/* + * 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.saml2.provider.service.web; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.springframework.security.web.util.UrlUtils.buildFullRequestUrl; +import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl; + +/** + * The default implementation for {@link Saml2AuthenticationRequestContextResolver} + * which uses the current request and given relying party to formulate a {@link Saml2AuthenticationRequestContext} + * + * @author Shazin Sadakath + * @since 5.4 + */ +public final class DefaultSaml2AuthenticationRequestContextResolver implements Saml2AuthenticationRequestContextResolver { + + private final Log logger = LogFactory.getLog(getClass()); + + private static final char PATH_DELIMITER = '/'; + + /** + * {@inheritDoc} + */ + @Override + public Saml2AuthenticationRequestContext resolve(HttpServletRequest request, + RelyingPartyRegistration relyingParty) { + Assert.notNull(request, "request cannot be null"); + Assert.notNull(relyingParty, "relyingParty cannot be null"); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Creating SAML 2.0 Authentication Request for Asserting Party [" + + relyingParty.getRegistrationId() + "]"); + } + return createRedirectAuthenticationRequestContext(request, relyingParty); + } + + private Saml2AuthenticationRequestContext createRedirectAuthenticationRequestContext( + HttpServletRequest request, RelyingPartyRegistration relyingParty) { + + String applicationUri = getApplicationUri(request); + Function resolver = templateResolver(applicationUri, relyingParty); + String localSpEntityId = resolver.apply(relyingParty.getEntityId()); + String assertionConsumerServiceUrl = resolver.apply(relyingParty.getAssertionConsumerServiceLocation()); + return Saml2AuthenticationRequestContext.builder() + .issuer(localSpEntityId) + .relyingPartyRegistration(relyingParty) + .assertionConsumerServiceUrl(assertionConsumerServiceUrl) + .relayState(request.getParameter("RelayState")) + .build(); + } + + private Function templateResolver(String applicationUri, RelyingPartyRegistration relyingParty) { + return template -> resolveUrlTemplate(template, applicationUri, relyingParty); + } + + private static String resolveUrlTemplate(String template, String baseUrl, RelyingPartyRegistration relyingParty) { + String entityId = relyingParty.getAssertingPartyDetails().getEntityId(); + String registrationId = relyingParty.getRegistrationId(); + Map uriVariables = new HashMap<>(); + UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(baseUrl) + .replaceQuery(null) + .fragment(null) + .build(); + String scheme = uriComponents.getScheme(); + uriVariables.put("baseScheme", scheme == null ? "" : scheme); + String host = uriComponents.getHost(); + uriVariables.put("baseHost", host == null ? "" : host); + // following logic is based on HierarchicalUriComponents#toUriString() + int port = uriComponents.getPort(); + uriVariables.put("basePort", port == -1 ? "" : ":" + port); + String path = uriComponents.getPath(); + if (StringUtils.hasLength(path)) { + if (path.charAt(0) != PATH_DELIMITER) { + path = PATH_DELIMITER + path; + } + } + uriVariables.put("basePath", path == null ? "" : path); + uriVariables.put("baseUrl", uriComponents.toUriString()); + uriVariables.put("entityId", StringUtils.hasText(entityId) ? entityId : ""); + uriVariables.put("registrationId", StringUtils.hasText(registrationId) ? registrationId : ""); + + return UriComponentsBuilder.fromUriString(template) + .buildAndExpand(uriVariables) + .toUriString(); + } + + private static String getApplicationUri(HttpServletRequest request) { + UriComponents uriComponents = fromHttpUrl(buildFullRequestUrl(request)) + .replacePath(request.getContextPath()) + .replaceQuery(null) + .fragment(null) + .build(); + return uriComponents.toUriString(); + } +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java new file mode 100644 index 00000000000..1c86ec239e4 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/Saml2AuthenticationRequestContextResolver.java @@ -0,0 +1,43 @@ +/* + * 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.saml2.provider.service.web; + +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +import javax.servlet.http.HttpServletRequest; + +/** + * This {@code Saml2AuthenticationRequestContextResolver} formulates a + * SAML 2.0 AuthnRequest (line 1968) + * + * @author Shazin Sadakath + * @since 5.4 + */ +public interface Saml2AuthenticationRequestContextResolver { + + /** + * This {@code resolve} method is defined to create a {@link Saml2AuthenticationRequestContext} + * + * + * @param request the current request + * @param relyingParty the relying party responsible for saml2 sso authentication + * @return the created {@link Saml2AuthenticationRequestContext} for request/relying party combination + */ + Saml2AuthenticationRequestContext resolve(HttpServletRequest request, + RelyingPartyRegistration relyingParty); +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2X509Credentials.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/TestSaml2X509Credentials.java similarity index 88% rename from saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2X509Credentials.java rename to saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/TestSaml2X509Credentials.java index faef9743b59..001e864f897 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2X509Credentials.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/credentials/TestSaml2X509Credentials.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -14,12 +14,7 @@ * limitations under the License. */ -package org.springframework.security.saml2.provider.service.authentication; - -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.credentials.Saml2X509Credential; - -import org.opensaml.security.crypto.KeySupport; +package org.springframework.security.saml2.credentials; import java.io.ByteArrayInputStream; import java.security.KeyException; @@ -27,8 +22,10 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; + +import org.opensaml.security.crypto.KeySupport; + +import org.springframework.security.saml2.Saml2Exception; import static java.nio.charset.StandardCharsets.UTF_8; import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION; @@ -36,37 +33,29 @@ import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING; import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION; -final class TestSaml2X509Credentials { - static List assertingPartyCredentials() { - return Arrays.asList( - new Saml2X509Credential( - idpPrivateKey(), - idpCertificate(), - SIGNING, - DECRYPTION - ), - new Saml2X509Credential( - spCertificate(), - ENCRYPTION, - VERIFICATION - ) - ); +public final class TestSaml2X509Credentials { + public static Saml2X509Credential assertingPartySigningCredential() { + return new Saml2X509Credential(idpPrivateKey(), idpCertificate(), SIGNING); + } + + public static Saml2X509Credential assertingPartyEncryptingCredential() { + return new Saml2X509Credential(spCertificate(), ENCRYPTION); + } + + public static Saml2X509Credential assertingPartyPrivateCredential() { + return new Saml2X509Credential(idpPrivateKey(), idpCertificate(), SIGNING, DECRYPTION); + } + + public static Saml2X509Credential relyingPartyVerifyingCredential() { + return new Saml2X509Credential(idpCertificate(), VERIFICATION); + } + + public static Saml2X509Credential relyingPartySigningCredential() { + return new Saml2X509Credential(spPrivateKey(), spCertificate(), SIGNING); } - static List relyingPartyCredentials() { - return Arrays.asList( - new Saml2X509Credential( - spPrivateKey(), - spCertificate(), - SIGNING, - DECRYPTION - ), - new Saml2X509Credential( - idpCertificate(), - ENCRYPTION, - VERIFICATION - ) - ); + public static Saml2X509Credential relyingPartyDecryptingCredential() { + return new Saml2X509Credential(spPrivateKey(), spCertificate(), DECRYPTION); } private static X509Certificate certificate(String cert) { diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java index 387302323e6..35e5a1756bd 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -19,351 +19,328 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; - -import org.springframework.security.core.Authentication; +import java.io.StringReader; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.joda.time.DateTime; import org.joda.time.Duration; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.Marshaller; import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.AttributeValue; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedID; import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.Response; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; -import static java.util.Collections.emptyList; -import static org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationObjects.assertion; -import static org.springframework.security.saml2.provider.service.authentication.TestSaml2AuthenticationObjects.response; -import static org.springframework.security.saml2.provider.service.authentication.Saml2CryptoTestSupport.encryptAssertion; -import static org.springframework.security.saml2.provider.service.authentication.Saml2CryptoTestSupport.encryptNameId; -import static org.springframework.security.saml2.provider.service.authentication.Saml2CryptoTestSupport.signXmlObject; -import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.assertingPartyCredentials; -import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.relyingPartyCredentials; -import static org.springframework.test.util.AssertionErrors.assertTrue; +import org.springframework.security.core.Authentication; +import org.springframework.security.saml2.credentials.Saml2X509Credential; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.assertingPartyEncryptingCredential; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.assertingPartyPrivateCredential; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.assertingPartySigningCredential; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyDecryptingCredential; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential; +import static org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects.assertion; +import static org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects.attributeStatements; +import static org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects.encrypted; +import static org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects.response; +import static org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects.signed; import static org.springframework.util.StringUtils.hasText; +/** + * Tests for {@link OpenSamlAuthenticationProvider} + * + * @author Filip Hanik + * @author Josh Cummings + */ public class OpenSamlAuthenticationProviderTests { - private static String username = "test@saml.user"; - private static String recipientUri = "https://localhost/login/saml2/sso/idp-alias"; - private static String recipientEntityId = "https://localhost/saml2/service-provider-metadata/idp-alias"; - private static String idpEntityId = "https://some.idp.test/saml2/idp"; + private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias"; + private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias"; + private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp"; + + private OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider(); + private OpenSamlImplementation saml = OpenSamlImplementation.getInstance(); - private OpenSamlAuthenticationProvider provider; - private OpenSamlImplementation saml; - private Saml2AuthenticationToken token; @Rule public ExpectedException exception = ExpectedException.none(); - @Before - public void setup() { - saml = OpenSamlImplementation.getInstance(); - provider = new OpenSamlAuthenticationProvider(); - token = new Saml2AuthenticationToken( - "responseXml", - recipientUri, - idpEntityId, - recipientEntityId, - relyingPartyCredentials() - ); - } - @Test public void supportsWhenSaml2AuthenticationTokenThenReturnTrue() { - assertTrue( - OpenSamlAuthenticationProvider.class + "should support " + token.getClass(), - provider.supports(token.getClass()) - ); + assertThat(this.provider.supports(Saml2AuthenticationToken.class)) + .withFailMessage(OpenSamlAuthenticationProvider.class + "should support " + Saml2AuthenticationToken.class) + .isTrue(); } @Test public void supportsWhenNotSaml2AuthenticationTokenThenReturnFalse() { - assertTrue( - OpenSamlAuthenticationProvider.class + "should not support " + Authentication.class, - !provider.supports(Authentication.class) - ); + assertThat(!this.provider.supports(Authentication.class)) + .withFailMessage(OpenSamlAuthenticationProvider.class + "should not support " + Authentication.class) + .isTrue(); } @Test public void authenticateWhenUnknownDataClassThenThrowAuthenticationException() { - Assertion assertion = defaultAssertion(); - token = responseXml(assertion, idpEntityId); - exception.expect(authenticationMatcher(Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS)); - provider.authenticate(token); + this.exception.expect(authenticationMatcher(Saml2ErrorCodes.UNKNOWN_RESPONSE_CLASS)); + + Assertion assertion = this.saml.buildSamlObject(Assertion.DEFAULT_ELEMENT_NAME); + this.provider.authenticate(token(this.saml.serialize(assertion))); } @Test public void authenticateWhenXmlErrorThenThrowAuthenticationException() { - token = new Saml2AuthenticationToken( - "invalid xml string", - recipientUri, - idpEntityId, - recipientEntityId, - relyingPartyCredentials() - ); - exception.expect(authenticationMatcher(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA)); - provider.authenticate(token); + this.exception.expect(authenticationMatcher(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA)); + + Saml2AuthenticationToken token = token("invalid xml"); + this.provider.authenticate(token); } @Test public void authenticateWhenInvalidDestinationThenThrowAuthenticationException() { - Response response = response(recipientUri + "invalid", idpEntityId); - token = responseXml(response, idpEntityId); - exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_DESTINATION)); - provider.authenticate(token); + this.exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_DESTINATION)); + + Response response = response(DESTINATION + "invalid", ASSERTING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion()); + signed(response, assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential()); + this.provider.authenticate(token); } @Test public void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException() { - Response response = response(recipientUri, idpEntityId); - token = responseXml(response, idpEntityId); - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, - "No assertions found in response." - ) + this.exception.expect( + authenticationMatcher(Saml2ErrorCodes.MALFORMED_RESPONSE_DATA, "No assertions found in response.") ); - provider.authenticate(token); + + Saml2AuthenticationToken token = token(response(), assertingPartySigningCredential()); + this.provider.authenticate(token); } @Test public void authenticateWhenInvalidSignatureOnAssertionThenThrowAuthenticationException() { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); - response.getAssertions().add(assertion); - token = responseXml(response, idpEntityId); - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.INVALID_SIGNATURE - ) - ); - provider.authenticate(token); + this.exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_SIGNATURE)); + + Response response = response(); + response.getAssertions().add(assertion()); + Saml2AuthenticationToken token = token(response); + this.provider.authenticate(token); } @Test public void authenticateWhenOpenSAMLValidationErrorThenThrowAuthenticationException() throws Exception { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); + this.exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_ASSERTION)); + + Response response = response(); + Assertion assertion = assertion(); assertion .getSubject() .getSubjectConfirmations() .get(0) .getSubjectConfirmationData() .setNotOnOrAfter(DateTime.now().minus(Duration.standardDays(3))); - signXmlObject( - assertion, - assertingPartyCredentials(), - recipientEntityId - ); + signed(assertion, assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); response.getAssertions().add(assertion); - token = responseXml(response, idpEntityId); - - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.INVALID_ASSERTION - ) - ); - provider.authenticate(token); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential()); + this.provider.authenticate(token); } @Test public void authenticateWhenMissingSubjectThenThrowAuthenticationException() { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); + this.exception.expect(authenticationMatcher(Saml2ErrorCodes.SUBJECT_NOT_FOUND)); + + Response response = response(); + Assertion assertion = assertion(); assertion.setSubject(null); - signXmlObject( - assertion, - assertingPartyCredentials(), - recipientEntityId - ); + signed(assertion, assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); response.getAssertions().add(assertion); - token = responseXml(response, idpEntityId); - - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.SUBJECT_NOT_FOUND - ) - ); - provider.authenticate(token); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential()); + this.provider.authenticate(token); } @Test public void authenticateWhenUsernameMissingThenThrowAuthenticationException() throws Exception { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); + this.exception.expect(authenticationMatcher(Saml2ErrorCodes.USERNAME_NOT_FOUND)); + + Response response = response(); + Assertion assertion = assertion(); assertion .getSubject() .getNameID() .setValue(null); - signXmlObject( - assertion, - assertingPartyCredentials(), - recipientEntityId - ); + signed(assertion, assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); response.getAssertions().add(assertion); - token = responseXml(response, idpEntityId); - - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.USERNAME_NOT_FOUND - ) - ); - provider.authenticate(token); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential()); + this.provider.authenticate(token); } @Test public void authenticateWhenAssertionContainsValidationAddressThenItSucceeds() throws Exception { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); + Response response = response(); + Assertion assertion = assertion(); assertion.getSubject().getSubjectConfirmations().forEach( sc -> sc.getSubjectConfirmationData().setAddress("10.10.10.10") ); - signXmlObject( - assertion, - assertingPartyCredentials(), - recipientEntityId - ); + signed(assertion, assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); response.getAssertions().add(assertion); - token = responseXml(response, idpEntityId); - provider.authenticate(token); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential()); + this.provider.authenticate(token); + } + + @Test + public void authenticateWhenAssertionContainsAttributesThenItSucceeds() { + Response response = response(); + Assertion assertion = assertion(); + List attributes = attributeStatements(); + assertion.getAttributeStatements().addAll(attributes); + signed(assertion, assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential()); + Authentication authentication = this.provider.authenticate(token); + Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); + + Map expected = new LinkedHashMap<>(); + expected.put("email", Arrays.asList("john.doe@example.com", "doe.john@example.com")); + expected.put("name", Collections.singletonList("John Doe")); + expected.put("age", Collections.singletonList(21)); + expected.put("website", Collections.singletonList("https://johndoe.com/")); + expected.put("registered", Collections.singletonList(true)); + Instant registeredDate = Instant.ofEpochMilli(DateTime.parse("1970-01-01T00:00:00Z").getMillis()); + expected.put("registeredDate", Collections.singletonList(registeredDate)); + + assertThat((String) principal.getFirstAttribute("name")).isEqualTo("John Doe"); + assertThat(principal.getAttributes()).isEqualTo(expected); + } + + @Test + public void authenticateWhenAttributeValueMarshallerConfiguredThenUses() throws Exception { + Response response = response(); + Assertion assertion = assertion(); + List attributes = attributeStatements(); + assertion.getAttributeStatements().addAll(attributes); + signed(assertion, assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); + response.getAssertions().add(assertion); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential()); + + Element attributeElement = element("value"); + Marshaller marshaller = mock(Marshaller.class); + when(marshaller.marshall(any(XMLObject.class))).thenReturn(attributeElement); + + try { + XMLObjectProviderRegistrySupport.getMarshallerFactory().registerMarshaller(AttributeValue.DEFAULT_ELEMENT_NAME, marshaller); + this.provider.authenticate(token); + verify(marshaller, atLeastOnce()).marshall(any(XMLObject.class)); + } finally { + XMLObjectProviderRegistrySupport.getMarshallerFactory().deregisterMarshaller(AttributeValue.DEFAULT_ELEMENT_NAME); + } } @Test public void authenticateWhenEncryptedAssertionWithoutSignatureThenItFails() throws Exception { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); - EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials()); + this.exception.expect(authenticationMatcher(Saml2ErrorCodes.INVALID_SIGNATURE)); + + Response response = response(); + EncryptedAssertion encryptedAssertion = encrypted(assertion(), assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - token = responseXml(response, idpEntityId); - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.INVALID_SIGNATURE - ) - ); - provider.authenticate(token); + Saml2AuthenticationToken token = token(response, relyingPartyDecryptingCredential()); + this.provider.authenticate(token); } @Test public void authenticateWhenEncryptedAssertionWithSignatureThenItSucceeds() throws Exception { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); - signXmlObject( - assertion, - assertingPartyCredentials(), - recipientEntityId - ); - EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials()); + Response response = response(); + Assertion assertion = signed(assertion(), assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); + EncryptedAssertion encryptedAssertion = encrypted(assertion, assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - token = responseXml(response, idpEntityId); - provider.authenticate(token); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential(), relyingPartyDecryptingCredential()); + this.provider.authenticate(token); } @Test public void authenticateWhenEncryptedAssertionWithResponseSignatureThenItSucceeds() throws Exception { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); - EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials()); + Response response = response(); + EncryptedAssertion encryptedAssertion = encrypted(assertion(), assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - signXmlObject( - response, - assertingPartyCredentials(), - recipientEntityId - ); - token = responseXml(response, idpEntityId); - provider.authenticate(token); + signed(response, assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential(), relyingPartyDecryptingCredential()); + this.provider.authenticate(token); } @Test public void authenticateWhenEncryptedNameIdWithSignatureThenItSucceeds() throws Exception { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); + Response response = response(); + Assertion assertion = assertion(); NameID nameId = assertion.getSubject().getNameID(); - EncryptedID encryptedID = encryptNameId(nameId, assertingPartyCredentials()); + EncryptedID encryptedID = encrypted(nameId, assertingPartyEncryptingCredential()); assertion.getSubject().setNameID(null); assertion.getSubject().setEncryptedID(encryptedID); - signXmlObject( - assertion, - assertingPartyCredentials(), - recipientEntityId - ); response.getAssertions().add(assertion); - token = responseXml(response, idpEntityId); - provider.authenticate(token); + signed(assertion, assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential(), relyingPartyDecryptingCredential()); + this.provider.authenticate(token); } @Test public void authenticateWhenDecryptionKeysAreMissingThenThrowAuthenticationException() throws Exception { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); - EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials()); - response.getEncryptedAssertions().add(encryptedAssertion); - token = responseXml(response, idpEntityId); - - token = new Saml2AuthenticationToken( - token.getSaml2Response(), - recipientUri, - idpEntityId, - recipientEntityId, - emptyList() + this.exception.expect( + authenticationMatcher(Saml2ErrorCodes.DECRYPTION_ERROR, "No valid decryption credentials found.") ); - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.DECRYPTION_ERROR, - "No valid decryption credentials found." - ) - ); - provider.authenticate(token); + Response response = response(); + EncryptedAssertion encryptedAssertion = encrypted(assertion(), assertingPartyEncryptingCredential()); + response.getEncryptedAssertions().add(encryptedAssertion); + Saml2AuthenticationToken token = token(this.saml.serialize(response)); + this.provider.authenticate(token); } @Test public void authenticateWhenDecryptionKeysAreWrongThenThrowAuthenticationException() throws Exception { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); - EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials()); - response.getEncryptedAssertions().add(encryptedAssertion); - token = responseXml(response, idpEntityId); - - token = new Saml2AuthenticationToken( - token.getSaml2Response(), - recipientUri, - idpEntityId, - recipientEntityId, - assertingPartyCredentials() + this.exception.expect( + authenticationMatcher(Saml2ErrorCodes.DECRYPTION_ERROR, "Failed to decrypt EncryptedData") ); - exception.expect( - authenticationMatcher( - Saml2ErrorCodes.DECRYPTION_ERROR, - "Failed to decrypt EncryptedData" - ) - ); - provider.authenticate(token); + Response response = response(); + EncryptedAssertion encryptedAssertion = encrypted(assertion(), assertingPartyEncryptingCredential()); + response.getEncryptedAssertions().add(encryptedAssertion); + Saml2AuthenticationToken token = token(this.saml.serialize(response), assertingPartyPrivateCredential()); + this.provider.authenticate(token); } @Test public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOException { - Response response = response(recipientUri, idpEntityId); - Assertion assertion = defaultAssertion(); - signXmlObject( - assertion, - assertingPartyCredentials(), - recipientEntityId - ); - EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, assertingPartyCredentials()); + Response response = response(); + Assertion assertion = signed(assertion(), assertingPartySigningCredential(), RELYING_PARTY_ENTITY_ID); + EncryptedAssertion encryptedAssertion = encrypted(assertion, assertingPartyEncryptingCredential()); response.getEncryptedAssertions().add(encryptedAssertion); - token = responseXml(response, idpEntityId); - - Saml2Authentication authentication = (Saml2Authentication) provider.authenticate(token); + Saml2AuthenticationToken token = token(response, relyingPartyVerifyingCredential(), relyingPartyDecryptingCredential()); + Saml2Authentication authentication = (Saml2Authentication) this.provider.authenticate(token); // the following code will throw an exception if authentication isn't serializable ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024); @@ -372,52 +349,23 @@ public void writeObjectWhenTypeIsSaml2AuthenticationThenNoException() throws IOE objectOutputStream.flush(); } - private Assertion defaultAssertion() { - return assertion( - username, - idpEntityId, - recipientEntityId, - recipientUri - ); - } - - private Saml2AuthenticationToken responseXml( - XMLObject object, - String issuerEntityId - ) { - String xml = saml.toXml(object, emptyList(), issuerEntityId); - return new Saml2AuthenticationToken( - xml, - recipientUri, - idpEntityId, - recipientEntityId, - relyingPartyCredentials() - ); - - } - - private BaseMatcher authenticationMatcher(String code) { + private Matcher authenticationMatcher(String code) { return authenticationMatcher(code, null); } - private BaseMatcher authenticationMatcher(String code, String description) { + private Matcher authenticationMatcher(String code, String description) { return new BaseMatcher() { - private Object value = null; - @Override public boolean matches(Object item) { if (!(item instanceof Saml2AuthenticationException)) { - value = item; return false; } Saml2AuthenticationException ex = (Saml2AuthenticationException) item; if (!code.equals(ex.getError().getErrorCode())) { - value = item; return false; } if (hasText(description)) { if (!description.equals(ex.getError().getDescription())) { - value = item; return false; } } @@ -432,4 +380,21 @@ public void describeTo(Description desc) { } }; } + + private Saml2AuthenticationToken token(Response response, Saml2X509Credential... credentials) { + String payload = this.saml.serialize(response); + return token(payload, credentials); + } + + private Saml2AuthenticationToken token(String payload, Saml2X509Credential... credentials) { + return new Saml2AuthenticationToken(payload, + DESTINATION, ASSERTING_PARTY_ENTITY_ID, RELYING_PARTY_ENTITY_ID, Arrays.asList(credentials)); + } + + private static Element element(String xml) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new InputSource(new StringReader(xml))); + return doc.getDocumentElement(); + } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java index a795538f501..c4c5db23fdf 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlAuthenticationRequestFactoryTests.java @@ -23,6 +23,7 @@ import org.junit.rules.ExpectedException; import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.core.AuthnRequest; + import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; @@ -30,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.containsString; import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlDecode; -import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.relyingPartyCredentials; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartySigningCredential; import static org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.withRelyingPartyRegistration; import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.POST; import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.REDIRECT; @@ -51,11 +52,11 @@ public class OpenSamlAuthenticationRequestFactoryTests { @Before public void setUp() { relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("id") - .assertionConsumerServiceUrlTemplate("template") + .assertionConsumerServiceLocation("template") .providerDetails(c -> c.webSsoUrl("https://destination/sso")) .providerDetails(c -> c.entityId("remote-entity-id")) .localEntityIdTemplate("local-entity-id") - .credentials(c -> c.addAll(relyingPartyCredentials())) + .credentials(c -> c.add(relyingPartySigningCredential())) .build(); contextBuilder = Saml2AuthenticationRequestContext.builder() .issuer("https://issuer") diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementationTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementationTests.java index b142c24b071..7e1015c2e8b 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementationTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlImplementationTests.java @@ -16,24 +16,22 @@ package org.springframework.security.saml2.provider.service.authentication; +import java.util.Arrays; +import java.util.Map; + import org.junit.Test; -import org.opensaml.security.credential.BasicCredential; -import org.opensaml.security.credential.Credential; -import org.opensaml.security.credential.CredentialSupport; -import org.opensaml.security.credential.UsageType; import org.opensaml.xmlsec.crypto.XMLSigningUtil; + import org.springframework.security.saml2.credentials.Saml2X509Credential; import org.springframework.web.util.UriUtils; -import java.util.List; -import java.util.Map; - import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.opensaml.xmlsec.signature.support.SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256; -import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.assertingPartyCredentials; -import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.relyingPartyCredentials; +import static org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects.getSigningCredential; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.assertingPartySigningCredential; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential; public class OpenSamlImplementationTests { @@ -45,12 +43,12 @@ public void getInstance() { @Test public void signQueryParametersWhenDataSuppliedReturnsValidSignature() throws Exception { OpenSamlImplementation impl = OpenSamlImplementation.getInstance(); - List signCredentials = relyingPartyCredentials(); - List verifyCredentials = assertingPartyCredentials(); + Saml2X509Credential signingCredential = assertingPartySigningCredential(); + Saml2X509Credential verifyingCredential = relyingPartyVerifyingCredential(); String samlRequest = "saml-request-example"; String encoded = Saml2Utils.samlEncode(samlRequest.getBytes(UTF_8)); String relayState = "test relay state"; - Map parameters = impl.signQueryParameters(signCredentials, encoded, relayState); + Map parameters = impl.signQueryParameters(Arrays.asList(signingCredential), encoded, relayState); String queryString = "SAMLRequest=" + UriUtils.encode(encoded, ISO_8859_1) + @@ -62,21 +60,11 @@ public void signQueryParametersWhenDataSuppliedReturnsValidSignature() throws Ex byte[] signature = Saml2Utils.samlDecode(parameters.get("Signature")); boolean result = XMLSigningUtil.verifyWithURI( - getOpenSamlCredential(verifyCredentials.get(1), "local-sp-entity-id", UsageType.SIGNING), + getSigningCredential(verifyingCredential, "local-sp-entity-id"), ALGO_ID_SIGNATURE_RSA_SHA256, signature, queryString.getBytes(UTF_8) ); assertThat(result).isTrue(); } - - private Credential getOpenSamlCredential(Saml2X509Credential credential, String localSpEntityId, UsageType usageType) { - BasicCredential cred = CredentialSupport.getSimpleCredential( - credential.getCertificate(), - credential.getPrivateKey() - ); - cred.setEntityId(localSpEntityId); - cred.setUsageType(usageType); - return cred; - } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java index 7b66577fbf9..563473a35d9 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AuthenticationRequestFactoryTests.java @@ -16,15 +16,16 @@ package org.springframework.security.saml2.provider.service.authentication; +import java.util.UUID; + import org.junit.Test; -import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; -import java.util.UUID; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlDecode; import static org.springframework.security.saml2.provider.service.authentication.Saml2Utils.samlInflate; -import static org.springframework.security.saml2.provider.service.authentication.TestSaml2X509Credentials.relyingPartyCredentials; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartySigningCredential; /** * Tests for {@link Saml2AuthenticationRequestFactory} default interface methods @@ -36,7 +37,7 @@ public class Saml2AuthenticationRequestFactoryTests { .providerDetails(c -> c.webSsoUrl("https://example.com/destination")) .providerDetails(c -> c.entityId("remote-entity-id")) .localEntityIdTemplate("local-entity-id") - .credentials(c -> c.addAll(relyingPartyCredentials())) + .credentials(c -> c.add(relyingPartySigningCredential())) .build(); @Test diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2CryptoTestSupport.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2CryptoTestSupport.java deleted file mode 100644 index 29cdf8aae22..00000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2CryptoTestSupport.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2002-2019 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.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.credentials.Saml2X509Credential; - -import org.apache.xml.security.algorithms.JCEMapper; -import org.apache.xml.security.encryption.XMLCipherParameters; -import org.opensaml.core.xml.io.MarshallingException; -import org.opensaml.saml.common.SignableSAMLObject; -import org.opensaml.saml.saml2.core.Assertion; -import org.opensaml.saml.saml2.core.EncryptedAssertion; -import org.opensaml.saml.saml2.core.EncryptedID; -import org.opensaml.saml.saml2.core.NameID; -import org.opensaml.saml.saml2.encryption.Encrypter; -import org.opensaml.security.SecurityException; -import org.opensaml.security.credential.BasicCredential; -import org.opensaml.security.credential.Credential; -import org.opensaml.security.credential.CredentialSupport; -import org.opensaml.security.credential.UsageType; -import org.opensaml.security.x509.BasicX509Credential; -import org.opensaml.xmlsec.SignatureSigningParameters; -import org.opensaml.xmlsec.encryption.support.DataEncryptionParameters; -import org.opensaml.xmlsec.encryption.support.EncryptionException; -import org.opensaml.xmlsec.encryption.support.KeyEncryptionParameters; -import org.opensaml.xmlsec.signature.support.SignatureConstants; -import org.opensaml.xmlsec.signature.support.SignatureException; -import org.opensaml.xmlsec.signature.support.SignatureSupport; - -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.X509Certificate; -import java.util.List; -import javax.crypto.SecretKey; - -import static java.util.Arrays.asList; -import static org.opensaml.security.crypto.KeySupport.generateKey; - -final class Saml2CryptoTestSupport { - static void signXmlObject(SignableSAMLObject object, List signingCredentials, String entityId) { - SignatureSigningParameters parameters = new SignatureSigningParameters(); - Credential credential = getSigningCredential(signingCredentials, entityId); - parameters.setSigningCredential(credential); - parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); - parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); - parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); - try { - SignatureSupport.signObject(object, parameters); - } catch (MarshallingException | SignatureException | SecurityException e) { - throw new Saml2Exception(e); - } - - } - - static EncryptedAssertion encryptAssertion(Assertion assertion, List encryptionCredentials) { - X509Certificate certificate = getEncryptionCertificate(encryptionCredentials); - Encrypter encrypter = getEncrypter(certificate); - try { - Encrypter.KeyPlacement keyPlacement = Encrypter.KeyPlacement.valueOf("PEER"); - encrypter.setKeyPlacement(keyPlacement); - return encrypter.encrypt(assertion); - } - catch (EncryptionException e) { - throw new Saml2Exception("Unable to encrypt assertion.", e); - } - } - - static EncryptedID encryptNameId(NameID nameID, List encryptionCredentials) { - X509Certificate certificate = getEncryptionCertificate(encryptionCredentials); - Encrypter encrypter = getEncrypter(certificate); - try { - Encrypter.KeyPlacement keyPlacement = Encrypter.KeyPlacement.valueOf("PEER"); - encrypter.setKeyPlacement(keyPlacement); - return encrypter.encrypt(nameID); - } - catch (EncryptionException e) { - throw new Saml2Exception("Unable to encrypt nameID.", e); - } - } - - private static Encrypter getEncrypter(X509Certificate certificate) { - Credential credential = CredentialSupport.getSimpleCredential(certificate, null); - final String dataAlgorithm = XMLCipherParameters.AES_256; - final String keyAlgorithm = XMLCipherParameters.RSA_1_5; - SecretKey secretKey = generateKeyFromURI(dataAlgorithm); - BasicCredential dataCredential = new BasicCredential(secretKey); - DataEncryptionParameters dataEncryptionParameters = new DataEncryptionParameters(); - dataEncryptionParameters.setEncryptionCredential(dataCredential); - dataEncryptionParameters.setAlgorithm(dataAlgorithm); - - KeyEncryptionParameters keyEncryptionParameters = new KeyEncryptionParameters(); - keyEncryptionParameters.setEncryptionCredential(credential); - keyEncryptionParameters.setAlgorithm(keyAlgorithm); - - Encrypter encrypter = new Encrypter(dataEncryptionParameters, asList(keyEncryptionParameters)); - - return encrypter; - } - - private static SecretKey generateKeyFromURI(String algoURI) { - try { - String jceAlgorithmName = JCEMapper.getJCEKeyAlgorithmFromURI(algoURI); - int keyLength = JCEMapper.getKeyLengthFromURI(algoURI); - return generateKey(jceAlgorithmName, keyLength, null); - } - catch (NoSuchAlgorithmException | NoSuchProviderException e) { - throw new Saml2Exception(e); - } - } - - private static X509Certificate getEncryptionCertificate(List encryptionCredentials) { - X509Certificate certificate = null; - for (Saml2X509Credential credential : encryptionCredentials) { - if (credential.isEncryptionCredential()) { - certificate = credential.getCertificate(); - break; - } - } - if (certificate == null) { - throw new Saml2Exception("No valid encryption certificate found"); - } - return certificate; - } - - private static Saml2X509Credential hasSigningCredential(List credentials) { - for (Saml2X509Credential c : credentials) { - if (c.isSigningCredential()) { - return c; - } - } - return null; - } - - private static Credential getSigningCredential(List signingCredential, - String localSpEntityId - ) { - Saml2X509Credential credential = hasSigningCredential(signingCredential); - if (credential == null) { - throw new Saml2Exception("no signing credential configured"); - } - BasicCredential cred = getBasicCredential(credential); - cred.setEntityId(localSpEntityId); - cred.setUsageType(UsageType.SIGNING); - return cred; - } - - private static BasicX509Credential getBasicCredential(Saml2X509Credential credential) { - return CredentialSupport.getSimpleCredential( - credential.getCertificate(), - credential.getPrivateKey() - ); - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/SimpleSaml2AuthenticatedPrincipalTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/SimpleSaml2AuthenticatedPrincipalTests.java index 5948ab7ca93..bd937e78f40 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/SimpleSaml2AuthenticatedPrincipalTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/SimpleSaml2AuthenticatedPrincipalTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -16,15 +16,58 @@ package org.springframework.security.saml2.provider.service.authentication; -import org.junit.Assert; +import org.joda.time.DateTime; import org.junit.Test; +import java.time.Instant; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + public class SimpleSaml2AuthenticatedPrincipalTests { @Test public void createSimpleSaml2AuthenticatedPrincipal() { - SimpleSaml2AuthenticatedPrincipal principal = new SimpleSaml2AuthenticatedPrincipal("user"); + Map> attributes = new LinkedHashMap<>(); + attributes.put("email", Arrays.asList("john.doe@example.com", "doe.john@example.com")); + SimpleSaml2AuthenticatedPrincipal principal = new SimpleSaml2AuthenticatedPrincipal("user", attributes); + assertThat(principal.getName()).isEqualTo("user"); + assertThat(principal.getAttributes()).isEqualTo(attributes); + } + + @Test + public void getFirstAttributeWhenStringValueThenReturnsValue() { + Map> attributes = new LinkedHashMap<>(); + attributes.put("email", Arrays.asList("john.doe@example.com", "doe.john@example.com")); + SimpleSaml2AuthenticatedPrincipal principal = new SimpleSaml2AuthenticatedPrincipal("user", attributes); + assertThat(principal.getFirstAttribute("email")).isEqualTo(attributes.get("email").get(0)); + } + + @Test + public void getAttributeWhenStringValuesThenReturnsValues() { + Map> attributes = new LinkedHashMap<>(); + attributes.put("email", Arrays.asList("john.doe@example.com", "doe.john@example.com")); + SimpleSaml2AuthenticatedPrincipal principal = new SimpleSaml2AuthenticatedPrincipal("user", attributes); + assertThat(principal.getAttribute("email")).isEqualTo(attributes.get("email")); + } + + @Test + public void getAttributeWhenDistinctValuesThenReturnsValues() { + final Boolean registered = true; + final Instant registeredDate = Instant.ofEpochMilli(DateTime.parse("1970-01-01T00:00:00Z").getMillis()); + + Map> attributes = new LinkedHashMap<>(); + attributes.put("registration", Arrays.asList(registered, registeredDate)); + + SimpleSaml2AuthenticatedPrincipal principal = new SimpleSaml2AuthenticatedPrincipal("user", attributes); + + List registrationInfo = principal.getAttribute("registration"); - Assert.assertEquals("user", principal.getName()); + assertThat(registrationInfo).isNotNull(); + assertThat((Boolean) registrationInfo.get(0)).isEqualTo(registered); + assertThat((Instant) registrationInfo.get(1)).isEqualTo(registeredDate); } } 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 new file mode 100644 index 00000000000..79b8823141c --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -0,0 +1,308 @@ +/* + * 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.saml2.provider.service.authentication; + +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.UUID; +import java.util.List; +import java.util.ArrayList; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.xml.security.encryption.XMLCipherParameters; +import org.joda.time.DateTime; +import org.joda.time.Duration; +import org.opensaml.core.xml.io.MarshallingException; + +import org.opensaml.core.xml.schema.XSAny; +import org.opensaml.core.xml.schema.XSBoolean; +import org.opensaml.core.xml.schema.XSBooleanValue; +import org.opensaml.core.xml.schema.XSDateTime; +import org.opensaml.core.xml.schema.XSInteger; +import org.opensaml.core.xml.schema.XSString; +import org.opensaml.core.xml.schema.XSURI; +import org.opensaml.core.xml.schema.impl.XSAnyBuilder; +import org.opensaml.core.xml.schema.impl.XSBooleanBuilder; +import org.opensaml.core.xml.schema.impl.XSDateTimeBuilder; +import org.opensaml.core.xml.schema.impl.XSIntegerBuilder; +import org.opensaml.core.xml.schema.impl.XSStringBuilder; +import org.opensaml.core.xml.schema.impl.XSURIBuilder; +import org.opensaml.saml.common.SAMLVersion; +import org.opensaml.saml.common.SignableSAMLObject; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.AttributeValue; +import org.opensaml.saml.saml2.core.Conditions; +import org.opensaml.saml.saml2.core.EncryptedAssertion; +import org.opensaml.saml.saml2.core.EncryptedID; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.NameID; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.Subject; +import org.opensaml.saml.saml2.core.SubjectConfirmation; +import org.opensaml.saml.saml2.core.SubjectConfirmationData; +import org.opensaml.saml.saml2.core.impl.AttributeBuilder; +import org.opensaml.saml.saml2.core.impl.AttributeStatementBuilder; +import org.opensaml.saml.saml2.encryption.Encrypter; +import org.opensaml.security.SecurityException; +import org.opensaml.security.credential.BasicCredential; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.security.credential.UsageType; +import org.opensaml.xmlsec.SignatureSigningParameters; +import org.opensaml.xmlsec.encryption.support.DataEncryptionParameters; +import org.opensaml.xmlsec.encryption.support.EncryptionException; +import org.opensaml.xmlsec.encryption.support.KeyEncryptionParameters; +import org.opensaml.xmlsec.signature.support.SignatureConstants; +import org.opensaml.xmlsec.signature.support.SignatureException; +import org.opensaml.xmlsec.signature.support.SignatureSupport; + +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.credentials.Saml2X509Credential; + +final class TestOpenSamlObjects { + private static OpenSamlImplementation saml = OpenSamlImplementation.getInstance(); + + private static String USERNAME = "test@saml.user"; + private static String DESTINATION = "https://localhost/login/saml2/sso/idp-alias"; + private static String RELYING_PARTY_ENTITY_ID = "https://localhost/saml2/service-provider-metadata/idp-alias"; + private static String ASSERTING_PARTY_ENTITY_ID = "https://some.idp.test/saml2/idp"; + private static SecretKey SECRET_KEY = + new SecretKeySpec(Base64.getDecoder().decode("shOnwNMoCv88HKMEa91+FlYoD5RNvzMTAL5LGxZKIFk="), "AES"); + + static Response response() { + return response(DESTINATION, ASSERTING_PARTY_ENTITY_ID); + } + + static Response response(String destination, String issuerEntityId) { + Response response = saml.buildSamlObject(Response.DEFAULT_ELEMENT_NAME); + response.setID("R"+UUID.randomUUID().toString()); + response.setIssueInstant(DateTime.now()); + response.setVersion(SAMLVersion.VERSION_20); + response.setID("_" + UUID.randomUUID().toString()); + response.setDestination(destination); + response.setIssuer(issuer(issuerEntityId)); + return response; + } + + static Assertion assertion() { + return assertion(USERNAME, ASSERTING_PARTY_ENTITY_ID, RELYING_PARTY_ENTITY_ID, DESTINATION); + } + + static Assertion assertion( + String username, + String issuerEntityId, + String recipientEntityId, + String recipientUri + ) { + Assertion assertion = saml.buildSamlObject(Assertion.DEFAULT_ELEMENT_NAME); + assertion.setID("A"+ UUID.randomUUID().toString()); + assertion.setIssueInstant(DateTime.now()); + assertion.setVersion(SAMLVersion.VERSION_20); + assertion.setIssueInstant(DateTime.now()); + assertion.setIssuer(issuer(issuerEntityId)); + assertion.setSubject(subject(username)); + assertion.setConditions(conditions()); + + SubjectConfirmation subjectConfirmation = subjectConfirmation(); + subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); + SubjectConfirmationData confirmationData = subjectConfirmationData(recipientEntityId); + confirmationData.setRecipient(recipientUri); + subjectConfirmation.setSubjectConfirmationData(confirmationData); + assertion.getSubject().getSubjectConfirmations().add(subjectConfirmation); + return assertion; + } + + + static Issuer issuer(String entityId) { + Issuer issuer = saml.buildSamlObject(Issuer.DEFAULT_ELEMENT_NAME); + issuer.setValue(entityId); + return issuer; + } + + static Subject subject(String principalName) { + Subject subject = saml.buildSamlObject(Subject.DEFAULT_ELEMENT_NAME); + + if (principalName != null) { + subject.setNameID(nameId(principalName)); + } + + return subject; + } + + static NameID nameId(String principalName) { + NameID nameId = saml.buildSamlObject(NameID.DEFAULT_ELEMENT_NAME); + nameId.setValue(principalName); + return nameId; + } + + static SubjectConfirmation subjectConfirmation() { + return saml.buildSamlObject(SubjectConfirmation.DEFAULT_ELEMENT_NAME); + } + + static SubjectConfirmationData subjectConfirmationData(String recipient) { + SubjectConfirmationData subject = saml.buildSamlObject(SubjectConfirmationData.DEFAULT_ELEMENT_NAME); + subject.setRecipient(recipient); + subject.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); + subject.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); + return subject; + } + + static Conditions conditions() { + Conditions conditions = saml.buildSamlObject(Conditions.DEFAULT_ELEMENT_NAME); + conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); + conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); + return conditions; + } + + static Credential getSigningCredential(Saml2X509Credential credential, String entityId) { + BasicCredential cred = getBasicCredential(credential); + cred.setEntityId(entityId); + cred.setUsageType(UsageType.SIGNING); + return cred; + } + + static BasicCredential getBasicCredential(Saml2X509Credential credential) { + return CredentialSupport.getSimpleCredential( + credential.getCertificate(), + credential.getPrivateKey() + ); + } + + static T signed(T signable, Saml2X509Credential credential, String entityId) { + SignatureSigningParameters parameters = new SignatureSigningParameters(); + Credential signingCredential = getSigningCredential(credential, entityId); + parameters.setSigningCredential(signingCredential); + parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); + parameters.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256); + parameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + try { + SignatureSupport.signObject(signable, parameters); + } catch (MarshallingException | SignatureException | SecurityException e) { + throw new Saml2Exception(e); + } + + return signable; + } + + static EncryptedAssertion encrypted(Assertion assertion, Saml2X509Credential credential) { + X509Certificate certificate = credential.getCertificate(); + Encrypter encrypter = getEncrypter(certificate); + try { + return encrypter.encrypt(assertion); + } + catch (EncryptionException e) { + throw new Saml2Exception("Unable to encrypt assertion.", e); + } + } + + static EncryptedID encrypted(NameID nameId, Saml2X509Credential credential) { + X509Certificate certificate = credential.getCertificate(); + Encrypter encrypter = getEncrypter(certificate); + try { + return encrypter.encrypt(nameId); + } + catch (EncryptionException e) { + throw new Saml2Exception("Unable to encrypt nameID.", e); + } + } + + private static Encrypter getEncrypter(X509Certificate certificate) { + String dataAlgorithm = XMLCipherParameters.AES_256; + String keyAlgorithm = XMLCipherParameters.RSA_1_5; + + BasicCredential dataCredential = new BasicCredential(SECRET_KEY); + DataEncryptionParameters dataEncryptionParameters = new DataEncryptionParameters(); + dataEncryptionParameters.setEncryptionCredential(dataCredential); + dataEncryptionParameters.setAlgorithm(dataAlgorithm); + + Credential credential = CredentialSupport.getSimpleCredential(certificate, null); + KeyEncryptionParameters keyEncryptionParameters = new KeyEncryptionParameters(); + keyEncryptionParameters.setEncryptionCredential(credential); + keyEncryptionParameters.setAlgorithm(keyAlgorithm); + + Encrypter encrypter = new Encrypter(dataEncryptionParameters, keyEncryptionParameters); + Encrypter.KeyPlacement keyPlacement = Encrypter.KeyPlacement.valueOf("PEER"); + encrypter.setKeyPlacement(keyPlacement); + + return encrypter; + } + + static List attributeStatements() { + List attributeStatements = new ArrayList<>(); + + AttributeStatementBuilder attributeStatementBuilder = new AttributeStatementBuilder(); + AttributeBuilder attributeBuilder = new AttributeBuilder(); + + AttributeStatement attrStmt1 = attributeStatementBuilder.buildObject(); + + Attribute emailAttr = attributeBuilder.buildObject(); + emailAttr.setName("email"); + XSAny email1 = new XSAnyBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME); + email1.setTextContent("john.doe@example.com"); + emailAttr.getAttributeValues().add(email1); + XSAny email2 = new XSAnyBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME); + email2.setTextContent("doe.john@example.com"); + emailAttr.getAttributeValues().add(email2); + attrStmt1.getAttributes().add(emailAttr); + + Attribute nameAttr = attributeBuilder.buildObject(); + nameAttr.setName("name"); + XSString name = new XSStringBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + name.setValue("John Doe"); + nameAttr.getAttributeValues().add(name); + attrStmt1.getAttributes().add(nameAttr); + + Attribute ageAttr = attributeBuilder.buildObject(); + ageAttr.setName("age"); + XSInteger age = new XSIntegerBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSInteger.TYPE_NAME); + age.setValue(21); + ageAttr.getAttributeValues().add(age); + attrStmt1.getAttributes().add(ageAttr); + + attributeStatements.add(attrStmt1); + + AttributeStatement attrStmt2 = attributeStatementBuilder.buildObject(); + + Attribute websiteAttr = attributeBuilder.buildObject(); + websiteAttr.setName("website"); + XSURI uri = new XSURIBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSURI.TYPE_NAME); + uri.setValue("https://johndoe.com/"); + websiteAttr.getAttributeValues().add(uri); + attrStmt2.getAttributes().add(websiteAttr); + + Attribute registeredAttr = attributeBuilder.buildObject(); + registeredAttr.setName("registered"); + XSBoolean registered = new XSBooleanBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSBoolean.TYPE_NAME); + registered.setValue(new XSBooleanValue(true, false)); + registeredAttr.getAttributeValues().add(registered); + attrStmt2.getAttributes().add(registeredAttr); + + Attribute registeredDateAttr = attributeBuilder.buildObject(); + registeredDateAttr.setName("registeredDate"); + XSDateTime registeredDate = new XSDateTimeBuilder().buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSDateTime.TYPE_NAME); + registeredDate.setValue(DateTime.parse("1970-01-01T00:00:00Z")); + registeredDateAttr.getAttributeValues().add(registeredDate); + attrStmt2.getAttributes().add(registeredDateAttr); + + attributeStatements.add(attrStmt2); + + return attributeStatements; + } +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationObjects.java deleted file mode 100644 index acc9d9f2abd..00000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationObjects.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2002-2019 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.joda.time.DateTime; -import org.joda.time.Duration; -import org.opensaml.saml.common.SAMLVersion; -import org.opensaml.saml.saml2.core.Assertion; -import org.opensaml.saml.saml2.core.Conditions; -import org.opensaml.saml.saml2.core.Issuer; -import org.opensaml.saml.saml2.core.NameID; -import org.opensaml.saml.saml2.core.Response; -import org.opensaml.saml.saml2.core.Subject; -import org.opensaml.saml.saml2.core.SubjectConfirmation; -import org.opensaml.saml.saml2.core.SubjectConfirmationData; - -import java.util.UUID; - -final class TestSaml2AuthenticationObjects { - private static OpenSamlImplementation saml = OpenSamlImplementation.getInstance(); - - static Response response(String destination, String issuerEntityId) { - Response response = saml.buildSAMLObject(Response.class); - response.setID("R"+UUID.randomUUID().toString()); - response.setIssueInstant(DateTime.now()); - response.setVersion(SAMLVersion.VERSION_20); - response.setID("_" + UUID.randomUUID().toString()); - response.setDestination(destination); - response.setIssuer(issuer(issuerEntityId)); - return response; - } - static Assertion assertion( - String username, - String issuerEntityId, - String recipientEntityId, - String recipientUri - ) { - Assertion assertion = saml.buildSAMLObject(Assertion.class); - assertion.setID("A"+ UUID.randomUUID().toString()); - assertion.setIssueInstant(DateTime.now()); - assertion.setVersion(SAMLVersion.VERSION_20); - assertion.setIssueInstant(DateTime.now()); - assertion.setIssuer(issuer(issuerEntityId)); - assertion.setSubject(subject(username)); - assertion.setConditions(conditions()); - - SubjectConfirmation subjectConfirmation = subjectConfirmation(); - subjectConfirmation.setMethod(SubjectConfirmation.METHOD_BEARER); - SubjectConfirmationData confirmationData = subjectConfirmationData(recipientEntityId); - confirmationData.setRecipient(recipientUri); - subjectConfirmation.setSubjectConfirmationData(confirmationData); - assertion.getSubject().getSubjectConfirmations().add(subjectConfirmation); - return assertion; - } - - - static Issuer issuer(String entityId) { - Issuer issuer = saml.buildSAMLObject(Issuer.class); - issuer.setValue(entityId); - return issuer; - } - - static Subject subject(String principalName) { - Subject subject = saml.buildSAMLObject(Subject.class); - - if (principalName != null) { - subject.setNameID(nameId(principalName)); - } - - return subject; - } - - static NameID nameId(String principalName) { - NameID nameId = saml.buildSAMLObject(NameID.class); - nameId.setValue(principalName); - return nameId; - } - - static SubjectConfirmation subjectConfirmation() { - return saml.buildSAMLObject(SubjectConfirmation.class); - } - - static SubjectConfirmationData subjectConfirmationData(String recipient) { - SubjectConfirmationData subject = saml.buildSAMLObject(SubjectConfirmationData.class); - subject.setRecipient(recipient); - subject.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); - subject.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); - return subject; - } - - static Conditions conditions() { - Conditions conditions = saml.buildSAMLObject(Conditions.class); - conditions.setNotBefore(DateTime.now().minus(Duration.millis(5 * 60 * 1000))); - conditions.setNotOnOrAfter(DateTime.now().plus(Duration.millis(5 * 60 * 1000))); - return conditions; - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationRequestContexts.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationRequestContexts.java new file mode 100644 index 00000000000..57a721146e6 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestSaml2AuthenticationRequestContexts.java @@ -0,0 +1,32 @@ +/* + * 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.saml2.provider.service.authentication; + +import static org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations.relyingPartyRegistration; + +/** + * Test {@link Saml2AuthenticationRequestContext}s + */ +public class TestSaml2AuthenticationRequestContexts { + public static Saml2AuthenticationRequestContext.Builder authenticationRequestContext() { + return Saml2AuthenticationRequestContext.builder() + .relayState("relayState") + .issuer("issuer") + .relyingPartyRegistration(relyingPartyRegistration().build()) + .assertionConsumerServiceUrl("assertionConsumerServiceUrl"); + } +} 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 b060599fd81..cdf99d7d715 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 @@ -17,16 +17,21 @@ package org.springframework.security.saml2.provider.service.registration; import org.junit.Test; -import org.springframework.security.saml2.credentials.Saml2X509Credential; + import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.POST; +import static org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations.relyingPartyRegistration; public class RelyingPartyRegistrationTests { @Test public void withRelyingPartyRegistrationWorks() { - RelyingPartyRegistration registration = relyingPartyRegistration(); + RelyingPartyRegistration registration = relyingPartyRegistration() + .providerDetails(p -> p.binding(POST)) + .providerDetails(p -> p.signAuthNRequest(false)) + .build(); RelyingPartyRegistration copy = RelyingPartyRegistration.withRelyingPartyRegistration(registration).build(); compareRegistrations(registration, copy); } @@ -37,9 +42,13 @@ private void compareRegistrations(RelyingPartyRegistration registration, Relying .isEqualTo("simplesamlphp"); assertThat(copy.getProviderDetails().getEntityId()) .isEqualTo(registration.getProviderDetails().getEntityId()) + .isEqualTo(copy.getAssertingPartyDetails().getEntityId()) + .isEqualTo(registration.getAssertingPartyDetails().getEntityId()) .isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"); assertThat(copy.getAssertionConsumerServiceUrlTemplate()) .isEqualTo(registration.getAssertionConsumerServiceUrlTemplate()) + .isEqualTo(copy.getAssertionConsumerServiceLocation()) + .isEqualTo(registration.getAssertionConsumerServiceLocation()) .isEqualTo("{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI); assertThat(copy.getCredentials()) .containsAll(registration.getCredentials()) @@ -49,44 +58,23 @@ private void compareRegistrations(RelyingPartyRegistration registration, Relying ); assertThat(copy.getLocalEntityIdTemplate()) .isEqualTo(registration.getLocalEntityIdTemplate()) + .isEqualTo(copy.getEntityId()) + .isEqualTo(registration.getEntityId()) .isEqualTo("{baseUrl}/saml2/service-provider-metadata/{registrationId}"); assertThat(copy.getProviderDetails().getWebSsoUrl()) .isEqualTo(registration.getProviderDetails().getWebSsoUrl()) + .isEqualTo(copy.getAssertingPartyDetails().getSingleSignOnServiceLocation()) + .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()) .isEqualTo("https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php"); assertThat(copy.getProviderDetails().getBinding()) .isEqualTo(registration.getProviderDetails().getBinding()) - .isEqualTo(Saml2MessageBinding.POST); + .isEqualTo(copy.getAssertingPartyDetails().getSingleSignOnServiceBinding()) + .isEqualTo(registration.getAssertingPartyDetails().getSingleSignOnServiceBinding()) + .isEqualTo(POST); assertThat(copy.getProviderDetails().isSignAuthNRequest()) .isEqualTo(registration.getProviderDetails().isSignAuthNRequest()) + .isEqualTo(copy.getAssertingPartyDetails().getWantAuthnRequestsSigned()) + .isEqualTo(registration.getAssertingPartyDetails().getWantAuthnRequestsSigned()) .isFalse(); } - - - private RelyingPartyRegistration relyingPartyRegistration() { - //remote IDP entity ID - String idpEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"; - //remote WebSSO Endpoint - Where to Send AuthNRequests to - String webSsoEndpoint = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php"; - //local registration ID - String registrationId = "simplesamlphp"; - //local entity ID - autogenerated based on URL - String localEntityIdTemplate = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; - //local signing (and decryption key) - Saml2X509Credential signingCredential = TestSaml2X509Credentials.relyingPartyCredentials().get(0); - //IDP certificate for verification of incoming messages - Saml2X509Credential idpVerificationCertificate = TestSaml2X509Credentials.relyingPartyCredentials().get(1); - String acsUrlTemplate = "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; - return RelyingPartyRegistration.withRegistrationId(registrationId) - .providerDetails(c -> { - c.webSsoUrl(webSsoEndpoint); - c.binding(Saml2MessageBinding.POST); - c.signAuthNRequest(false); - c.entityId(idpEntityId); - }) - .credentials(c -> c.add(signingCredential)) - .credentials(c -> c.add(idpVerificationCertificate)) - .localEntityIdTemplate(localEntityIdTemplate) - .assertionConsumerServiceUrlTemplate(acsUrlTemplate) - .build(); - } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java new file mode 100644 index 00000000000..5aa604610c8 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestRelyingPartyRegistrations.java @@ -0,0 +1,52 @@ +/* + * 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.saml2.provider.service.registration; + +import org.springframework.security.saml2.credentials.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; + +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartySigningCredential; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential; + +/** + * Preconfigured test data for {@link RelyingPartyRegistration} objects + */ +public class TestRelyingPartyRegistrations { + + public static RelyingPartyRegistration.Builder relyingPartyRegistration() { + String registrationId = "simplesamlphp"; + + String rpEntityId = "{baseUrl}/saml2/service-provider-metadata/{registrationId}"; + Saml2X509Credential signingCredential = relyingPartySigningCredential(); + String assertionConsumerServiceLocation = "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; + + String apEntityId = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php"; + Saml2X509Credential verificationCertificate = relyingPartyVerifyingCredential(); + String singleSignOnServiceLocation = "https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php"; + + return RelyingPartyRegistration.withRegistrationId(registrationId) + .entityId(rpEntityId) + .assertionConsumerServiceLocation(assertionConsumerServiceLocation) + .credentials(c -> c.add(signingCredential)) + .providerDetails(c -> c + .entityId(apEntityId) + .webSsoUrl(singleSignOnServiceLocation)) + .credentials(c -> c.add(verificationCertificate)); + } + + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestSaml2X509Credentials.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestSaml2X509Credentials.java deleted file mode 100644 index b49c32eb90e..00000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/TestSaml2X509Credentials.java +++ /dev/null @@ -1,143 +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.saml2.provider.service.registration; - -import org.opensaml.security.crypto.KeySupport; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.credentials.Saml2X509Credential; - -import java.io.ByteArrayInputStream; -import java.security.KeyException; -import java.security.PrivateKey; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.List; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION; -import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.ENCRYPTION; -import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING; -import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION; - -final class TestSaml2X509Credentials { - static List relyingPartyCredentials() { - return Arrays.asList( - new Saml2X509Credential( - spPrivateKey(), - spCertificate(), - SIGNING, - DECRYPTION - ), - new Saml2X509Credential( - idpCertificate(), - ENCRYPTION, - VERIFICATION - ) - ); - } - - private static X509Certificate certificate(String cert) { - ByteArrayInputStream certBytes = new ByteArrayInputStream(cert.getBytes()); - try { - return (X509Certificate) CertificateFactory - .getInstance("X.509") - .generateCertificate(certBytes); - } - catch (CertificateException e) { - throw new Saml2Exception(e); - } - } - - private static PrivateKey privateKey(String key) { - try { - return KeySupport.decodePrivateKey(key.getBytes(UTF_8), new char[0]); - } - catch (KeyException e) { - throw new Saml2Exception(e); - } - } - - private static X509Certificate idpCertificate() { - return certificate("-----BEGIN CERTIFICATE-----\n" - + "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD\n" - + "VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD\n" - + "VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX\n" - + "c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw\n" - + "aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ\n" - + "BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa\n" - + "BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD\n" - + "DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr\n" - + "QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62\n" - + "E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz\n" - + "2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW\n" - + "RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ\n" - + "nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5\n" - + "cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph\n" - + "iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5\n" - + "ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD\n" - + "AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO\n" - + "nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v\n" - + "ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu\n" - + "xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z\n" - + "V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3\n" - + "lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" - + "-----END CERTIFICATE-----\n"); - } - - - private static X509Certificate spCertificate() { - - return certificate("-----BEGIN CERTIFICATE-----\n" + - "MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC\n" + - "VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcMCVZhbmNvdXZlcjEdMBsG\n" + - "A1UECgwUU3ByaW5nIFNlY3VyaXR5IFNBTUwxCzAJBgNVBAsMAnNwMSAwHgYDVQQD\n" + - "DBdzcC5zcHJpbmcuc2VjdXJpdHkuc2FtbDAeFw0xODA1MTQxNDMwNDRaFw0yODA1\n" + - "MTExNDMwNDRaMIGEMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGluZ3RvbjES\n" + - "MBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQKDBRTcHJpbmcgU2VjdXJpdHkgU0FN\n" + - "TDELMAkGA1UECwwCc3AxIDAeBgNVBAMMF3NwLnNwcmluZy5zZWN1cml0eS5zYW1s\n" + - "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRu7/EI0BlNzMEBFVAcbx+lLos\n" + - "vzIWU+01dGTY8gBdhMQNYKZ92lMceo2CuVJ66cUURPym3i7nGGzoSnAxAre+0YIM\n" + - "+U0razrWtAUE735bkcqELZkOTZLelaoOztmWqRbe5OuEmpewH7cx+kNgcVjdctOG\n" + - "y3Q6x+I4qakY/9qhBQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAAeViTvHOyQopWEi\n" + - "XOfI2Z9eukwrSknDwq/zscR0YxwwqDBMt/QdAODfSwAfnciiYLkmEjlozWRtOeN+\n" + - "qK7UFgP1bRl5qksrYX5S0z2iGJh0GvonLUt3e20Ssfl5tTEDDnAEUMLfBkyaxEHD\n" + - "RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B\n" + - "-----END CERTIFICATE-----"); - } - - private static PrivateKey spPrivateKey() { - return privateKey("-----BEGIN PRIVATE KEY-----\n" + - "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE\n" + - "VUBxvH6Uuiy/MhZT7TV0ZNjyAF2ExA1gpn3aUxx6jYK5UnrpxRRE/KbeLucYbOhK\n" + - "cDECt77Rggz5TStrOta0BQTvfluRyoQtmQ5Nkt6Vqg7O2ZapFt7k64Sal7AftzH6\n" + - "Q2BxWN1y04bLdDrH4jipqRj/2qEFAgMBAAECgYEAj4ExY1jjdN3iEDuOwXuRB+Nn\n" + - "x7pC4TgntE2huzdKvLJdGvIouTArce8A6JM5NlTBvm69mMepvAHgcsiMH1zGr5J5\n" + - "wJz23mGOyhM1veON41/DJTVG+cxq4soUZhdYy3bpOuXGMAaJ8QLMbQQoivllNihd\n" + - "vwH0rNSK8LTYWWPZYIECQQDxct+TFX1VsQ1eo41K0T4fu2rWUaxlvjUGhK6HxTmY\n" + - "8OMJptunGRJL1CUjIb45Uz7SP8TPz5FwhXWsLfS182kRAkEA3l+Qd9C9gdpUh1uX\n" + - "oPSNIxn5hFUrSTW1EwP9QH9vhwb5Vr8Jrd5ei678WYDLjUcx648RjkjhU9jSMzIx\n" + - "EGvYtQJBAMm/i9NR7IVyyNIgZUpz5q4LI21rl1r4gUQuD8vA36zM81i4ROeuCly0\n" + - "KkfdxR4PUfnKcQCX11YnHjk9uTFj75ECQEFY/gBnxDjzqyF35hAzrYIiMPQVfznt\n" + - "YX/sDTE2AdVBVGaMj1Cb51bPHnNC6Q5kXKQnj/YrLqRQND09Q7ParX0CQQC5NxZr\n" + - "9jKqhHj8yQD6PlXTsY4Occ7DH6/IoDenfdEVD5qlet0zmd50HatN2Jiqm5ubN7CM\n" + - "INrtuLp4YHbgk1mi\n" + - "-----END PRIVATE KEY-----"); - } - -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java index 4b5bb37bb4b..f9613e080d8 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/Saml2WebSsoAuthenticationRequestFilterTests.java @@ -16,31 +16,39 @@ package org.springframework.security.saml2.provider.service.servlet.filter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import javax.servlet.ServletException; + import org.junit.Before; import org.junit.Test; + import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestFactory; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.UriUtils; -import javax.servlet.ServletException; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.assertingPartyPrivateCredential; import static org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding.POST; -import static org.springframework.security.saml2.provider.service.servlet.filter.TestSaml2SigningCredentials.signingCredential; public class Saml2WebSsoAuthenticationRequestFilterTests { private static final String IDP_SSO_URL = "https://sso-url.example.com/IDP/SSO"; private Saml2WebSsoAuthenticationRequestFilter filter; private RelyingPartyRegistrationRepository repository = mock(RelyingPartyRegistrationRepository.class); + private Saml2AuthenticationRequestFactory factory = mock(Saml2AuthenticationRequestFactory.class); private MockHttpServletRequest request; private MockHttpServletResponse response; private MockFilterChain filterChain; @@ -60,7 +68,7 @@ public void setup() { .providerDetails(c -> c.entityId("idp-entity-id")) .providerDetails(c -> c.webSsoUrl(IDP_SSO_URL)) .assertionConsumerServiceUrlTemplate("template") - .credentials(c -> c.add(signingCredential())); + .credentials(c -> c.add(assertingPartyPrivateCredential())); } @Test @@ -147,4 +155,82 @@ public void doFilterWhenPostFormDataIsPresent() throws Exception { .contains("value=\""+relayStateEncoded+"\""); } + @Test + public void doFilterWhenSetAuthenticationRequestFactoryThenUses() throws Exception { + RelyingPartyRegistration relyingParty = this.rpBuilder + .providerDetails(c -> c.binding(POST)) + .build(); + Saml2PostAuthenticationRequest authenticationRequest = mock(Saml2PostAuthenticationRequest.class); + when(authenticationRequest.getAuthenticationRequestUri()).thenReturn("uri"); + when(authenticationRequest.getRelayState()).thenReturn("relay"); + when(authenticationRequest.getSamlRequest()).thenReturn("saml"); + when(this.repository.findByRegistrationId("registration-id")).thenReturn(relyingParty); + when(this.factory.createPostAuthenticationRequest(any())) + .thenReturn(authenticationRequest); + + Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter + (this.repository); + filter.setAuthenticationRequestFactory(this.factory); + filter.doFilterInternal(this.request, this.response, this.filterChain); + assertThat(this.response.getContentAsString()) + .contains("
") + .contains(" c.binding(POST)) + .build(); + Saml2PostAuthenticationRequest authenticationRequest = mock(Saml2PostAuthenticationRequest.class); + when(authenticationRequest.getAuthenticationRequestUri()).thenReturn("uri"); + when(authenticationRequest.getRelayState()).thenReturn("relay"); + when(authenticationRequest.getSamlRequest()).thenReturn("saml"); + when(this.repository.findByRegistrationId("registration-id")).thenReturn(relyingParty); + when(this.factory.createPostAuthenticationRequest(any())) + .thenReturn(authenticationRequest); + + Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter + (this.repository, this.factory); + filter.doFilterInternal(this.request, this.response, this.filterChain); + assertThat(this.response.getContentAsString()) + .contains("") + .contains(" filter.setRedirectMatcher(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setAuthenticationRequestFactoryWhenNullThenException() { + Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(this.repository); + assertThatCode(() -> filter.setAuthenticationRequestFactory(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void doFilterWhenRequestMatcherFailsThenSkipsFilter() throws Exception { + Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter + (this.repository); + filter.setRedirectMatcher(request -> false); + filter.doFilter(this.request, this.response, this.filterChain); + verifyNoInteractions(this.repository); + } + + @Test + public void doFilterWhenRelyingPartyRegistrationNotFoundThenUnauthorized() throws Exception { + Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter + (this.repository); + filter.doFilter(this.request, this.response, this.filterChain); + assertThat(this.response.getStatus()).isEqualTo(401); + } } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/TestSaml2SigningCredentials.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/TestSaml2SigningCredentials.java deleted file mode 100644 index 3aa718227e2..00000000000 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/servlet/filter/TestSaml2SigningCredentials.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2002-2019 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.servlet.filter; - -import java.io.ByteArrayInputStream; -import java.security.KeyException; -import java.security.PrivateKey; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - -import org.opensaml.security.crypto.KeySupport; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.credentials.Saml2X509Credential; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION; -import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING; - -final class TestSaml2SigningCredentials { - - static Saml2X509Credential signingCredential() { - return new Saml2X509Credential(idpPrivateKey(), idpCertificate(), SIGNING, DECRYPTION); - } - - private static X509Certificate certificate(String cert) { - ByteArrayInputStream certBytes = new ByteArrayInputStream(cert.getBytes()); - try { - return (X509Certificate) CertificateFactory - .getInstance("X.509") - .generateCertificate(certBytes); - } - catch (CertificateException e) { - throw new Saml2Exception(e); - } - } - - private static PrivateKey privateKey(String key) { - try { - return KeySupport.decodePrivateKey(key.getBytes(UTF_8), new char[0]); - } - catch (KeyException e) { - throw new Saml2Exception(e); - } - } - - private static X509Certificate idpCertificate() { - return certificate("-----BEGIN CERTIFICATE-----\n" - + "MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD\n" - + "VQQGEwJVUzELMAkGA1UECAwCQ08xFDASBgNVBAcMC0Nhc3RsZSBSb2NrMRwwGgYD\n" - + "VQQKDBNTYW1sIFRlc3RpbmcgU2VydmVyMQswCQYDVQQLDAJJVDEgMB4GA1UEAwwX\n" - + "c2ltcGxlc2FtbHBocC5jZmFwcHMuaW8xIDAeBgkqhkiG9w0BCQEWEWZoYW5pa0Bw\n" - + "aXZvdGFsLmlvMB4XDTE1MDIyMzIyNDUwM1oXDTI1MDIyMjIyNDUwM1owgZ8xCzAJ\n" - + "BgNVBAYTAlVTMQswCQYDVQQIDAJDTzEUMBIGA1UEBwwLQ2FzdGxlIFJvY2sxHDAa\n" - + "BgNVBAoME1NhbWwgVGVzdGluZyBTZXJ2ZXIxCzAJBgNVBAsMAklUMSAwHgYDVQQD\n" - + "DBdzaW1wbGVzYW1scGhwLmNmYXBwcy5pbzEgMB4GCSqGSIb3DQEJARYRZmhhbmlr\n" - + "QHBpdm90YWwuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4cn62\n" - + "E1xLqpN34PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz\n" - + "2ZivLwZXW+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWW\n" - + "RDodcoHEfDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQ\n" - + "nX8Ttl7hZ6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5\n" - + "cljz0X/TXy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gph\n" - + "iJH3jvZ7I+J5lS8VAgMBAAGjUDBOMB0GA1UdDgQWBBTTyP6Cc5HlBJ5+ucVCwGc5\n" - + "ogKNGzAfBgNVHSMEGDAWgBTTyP6Cc5HlBJ5+ucVCwGc5ogKNGzAMBgNVHRMEBTAD\n" - + "AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAvMS4EQeP/ipV4jOG5lO6/tYCb/iJeAduO\n" - + "nRhkJk0DbX329lDLZhTTL/x/w/9muCVcvLrzEp6PN+VWfw5E5FWtZN0yhGtP9R+v\n" - + "ZnrV+oc2zGD+no1/ySFOe3EiJCO5dehxKjYEmBRv5sU/LZFKZpozKN/BMEa6CqLu\n" - + "xbzb7ykxVr7EVFXwltPxzE9TmL9OACNNyF5eJHWMRMllarUvkcXlh4pux4ks9e6z\n" - + "V9DQBy2zds9f1I3qxg0eX6JnGrXi/ZiCT+lJgVe3ZFXiejiLAiKB04sXW3ti0LW3\n" - + "lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk\n" - + "-----END CERTIFICATE-----\n"); - } - - private static PrivateKey idpPrivateKey() { - return privateKey("-----BEGIN PRIVATE KEY-----\n" - + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4cn62E1xLqpN3\n" - + "4PmbrKBbkOXFjzWgJ9b+pXuaRft6A339uuIQeoeH5qeSKRVTl32L0gdz2ZivLwZX\n" - + "W+cqvftVW1tvEHvzJFyxeTW3fCUeCQsebLnA2qRa07RkxTo6Nf244mWWRDodcoHE\n" - + "fDUSbxfTZ6IExSojSIU2RnD6WllYWFdD1GFpBJOmQB8rAc8wJIBdHFdQnX8Ttl7h\n" - + "Z6rtgqEYMzYVMuJ2F2r1HSU1zSAvwpdYP6rRGFRJEfdA9mm3WKfNLSc5cljz0X/T\n" - + "Xy0vVlAV95l9qcfFzPmrkNIst9FZSwpvB49LyAVke04FQPPwLgVH4gphiJH3jvZ7\n" - + "I+J5lS8VAgMBAAECggEBAKyxBlIS7mcp3chvq0RF7B3PHFJMMzkwE+t3pLJcs4cZ\n" - + "nezh/KbREfP70QjXzk/llnZCvxeIs5vRu24vbdBm79qLHqBuHp8XfHHtuo2AfoAQ\n" - + "l4h047Xc/+TKMivnPQ0jX9qqndKDLqZDf5wnbslDmlskvF0a/MjsLU0TxtOfo+dB\n" - + "t55FW11cGqxZwhS5Gnr+cbw3OkHz23b9gEOt9qfwPVepeysbmm9FjU+k4yVa7rAN\n" - + "xcbzVb6Y7GCITe2tgvvEHmjB9BLmWrH3mZ3Af17YU/iN6TrpPd6Sj3QoS+2wGtAe\n" - + "HbUs3CKJu7bIHcj4poal6Kh8519S+erJTtqQ8M0ZiEECgYEA43hLYAPaUueFkdfh\n" - + "9K/7ClH6436CUH3VdizwUXi26fdhhV/I/ot6zLfU2mgEHU22LBECWQGtAFm8kv0P\n" - + "zPn+qjaR3e62l5PIlSYbnkIidzoDZ2ztu4jF5LgStlTJQPteFEGgZVl5o9DaSZOq\n" - + "Yd7G3XqXuQ1VGMW58G5FYJPtA1cCgYEAz5TPUtK+R2KXHMjUwlGY9AefQYRYmyX2\n" - + "Tn/OFgKvY8lpAkMrhPKONq7SMYc8E9v9G7A0dIOXvW7QOYSapNhKU+np3lUafR5F\n" - + "4ZN0bxZ9qjHbn3AMYeraKjeutHvlLtbHdIc1j3sxe/EzltRsYmiqLdEBW0p6hwWg\n" - + "tyGhYWVyaXMCgYAfDOKtHpmEy5nOCLwNXKBWDk7DExfSyPqEgSnk1SeS1HP5ctPK\n" - + "+1st6sIhdiVpopwFc+TwJWxqKdW18tlfT5jVv1E2DEnccw3kXilS9xAhWkfwrEvf\n" - + "V5I74GydewFl32o+NZ8hdo9GL1I8zO1rIq/et8dSOWGuWf9BtKu/vTGTTQKBgFxU\n" - + "VjsCnbvmsEwPUAL2hE/WrBFaKocnxXx5AFNt8lEyHtDwy4Sg1nygGcIJ4sD6koQk\n" - + "RdClT3LkvR04TAiSY80bN/i6ZcPNGUwSaDGZEWAIOSWbkwZijZNFnSGOEgxZX/IG\n" - + "yd39766vREEMTwEeiMNEOZQ/dmxkJm4OOVe25cLdAoGACOtPnq1Fxay80UYBf4rQ\n" - + "+bJ9yX1ulB8WIree1hD7OHSB2lRHxrVYWrglrTvkh63Lgx+EcsTV788OsvAVfPPz\n" - + "BZrn8SdDlQqalMxUBYEFwnsYD3cQ8yOUnijFVC4xNcdDv8OIqVgSk4KKxU5AshaA\n" - + "xk6Mox+u8Cc2eAK12H13i+8=\n" - + "-----END PRIVATE KEY-----\n"); - } -} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolverTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolverTests.java new file mode 100644 index 00000000000..182b7009653 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultSaml2AuthenticationRequestContextResolverTests.java @@ -0,0 +1,112 @@ +/* + * 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.saml2.provider.service.web; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationRequestContext; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.springframework.security.saml2.credentials.TestSaml2X509Credentials.relyingPartyVerifyingCredential; + +/** + * Tests for {@link DefaultSaml2AuthenticationRequestContextResolver} + * + * @author Shazin Sadakath + * @author Josh Cummings + */ +public class DefaultSaml2AuthenticationRequestContextResolverTests { + + private static final String ASSERTING_PARTY_SSO_URL = "https://idp.example.com/sso"; + private static final String RELYING_PARTY_SSO_URL = "https://sp.example.com/sso"; + private static final String ASSERTING_PARTY_ENTITY_ID = "asserting-party-entity-id"; + private static final String RELYING_PARTY_ENTITY_ID = "relying-party-entity-id"; + private static final String REGISTRATION_ID = "registration-id"; + + private MockHttpServletRequest request; + private RelyingPartyRegistration.Builder relyingPartyBuilder; + private Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver + = new DefaultSaml2AuthenticationRequestContextResolver(); + + @Before + public void setup() { + this.request = new MockHttpServletRequest(); + this.relyingPartyBuilder = RelyingPartyRegistration + .withRegistrationId(REGISTRATION_ID) + .localEntityIdTemplate(RELYING_PARTY_ENTITY_ID) + .providerDetails(c -> c.entityId(ASSERTING_PARTY_ENTITY_ID)) + .providerDetails(c -> c.webSsoUrl(ASSERTING_PARTY_SSO_URL)) + .assertionConsumerServiceUrlTemplate(RELYING_PARTY_SSO_URL) + .credentials(c -> c.add(relyingPartyVerifyingCredential())); + } + + @Test + public void resolveWhenRequestAndRelyingPartyNotNullThenCreateSaml2AuthenticationRequestContext() { + this.request.addParameter("RelayState", "relay-state"); + RelyingPartyRegistration relyingParty = this.relyingPartyBuilder.build(); + Saml2AuthenticationRequestContext context = + this.authenticationRequestContextResolver.resolve(this.request, relyingParty); + + assertThat(context).isNotNull(); + assertThat(context.getAssertionConsumerServiceUrl()).isEqualTo(RELYING_PARTY_SSO_URL); + assertThat(context.getRelayState()).isEqualTo("relay-state"); + assertThat(context.getDestination()).isEqualTo(ASSERTING_PARTY_SSO_URL); + assertThat(context.getIssuer()).isEqualTo(RELYING_PARTY_ENTITY_ID); + assertThat(context.getRelyingPartyRegistration()).isSameAs(relyingParty); + } + + @Test + public void resolveWhenAssertionConsumerServiceUrlTemplateContainsRegistrationIdThenResolves() { + RelyingPartyRegistration relyingParty = this.relyingPartyBuilder + .assertionConsumerServiceUrlTemplate("/saml2/authenticate/{registrationId}") + .build(); + Saml2AuthenticationRequestContext context = + this.authenticationRequestContextResolver.resolve(this.request, relyingParty); + + assertThat(context.getAssertionConsumerServiceUrl()).isEqualTo("/saml2/authenticate/registration-id"); + } + + @Test + public void resolveWhenAssertionConsumerServiceUrlTemplateContainsBaseUrlThenResolves() { + RelyingPartyRegistration relyingParty = this.relyingPartyBuilder + .assertionConsumerServiceUrlTemplate("{baseUrl}/saml2/authenticate/{registrationId}") + .build(); + Saml2AuthenticationRequestContext context = + this.authenticationRequestContextResolver.resolve(this.request, relyingParty); + + assertThat(context.getAssertionConsumerServiceUrl()) + .isEqualTo("http://localhost/saml2/authenticate/registration-id"); + } + + @Test + public void resolveWhenRequestNullThenException() { + assertThatCode(() -> + this.authenticationRequestContextResolver.resolve(this.request, null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void resolveWhenRelyingPartyNullThenException() { + assertThatCode(() -> + this.authenticationRequestContextResolver.resolve(null, this.relyingPartyBuilder.build())) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/samples/boot/hellorsocket/spring-security-samples-boot-hellorsocket.gradle b/samples/boot/hellorsocket/spring-security-samples-boot-hellorsocket.gradle index b2fdafe9fdf..714fb3b6ffb 100644 --- a/samples/boot/hellorsocket/spring-security-samples-boot-hellorsocket.gradle +++ b/samples/boot/hellorsocket/spring-security-samples-boot-hellorsocket.gradle @@ -1,7 +1,5 @@ apply plugin: 'io.spring.convention.spring-sample-boot' -ext['rsocket.version'] = '1.0.0-RC6' - dependencies { compile project(':spring-security-core') compile project(':spring-security-config') @@ -11,3 +9,5 @@ dependencies { testCompile project(':spring-security-test') testCompile 'org.springframework.boot:spring-boot-starter-test' } + +ext['rsocket.version'] = '1.0.1' diff --git a/samples/boot/kotlin-webflux/spring-security-samples-boot-kotlin-webflux.gradle.kts b/samples/boot/kotlin-webflux/spring-security-samples-boot-kotlin-webflux.gradle.kts new file mode 100644 index 00000000000..20eb7b4060a --- /dev/null +++ b/samples/boot/kotlin-webflux/spring-security-samples-boot-kotlin-webflux.gradle.kts @@ -0,0 +1,41 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("io.spring.convention.spring-sample-boot") + kotlin("jvm") + kotlin("plugin.spring") version "1.3.71" +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":spring-security-core")) + implementation(project(":spring-security-config")) + implementation(project(":spring-security-web")) + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity5") + implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + + testImplementation(project(":spring-security-test")) + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } + testImplementation("io.projectreactor:reactor-test") +} + +tasks.withType { + useJUnitPlatform() +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "1.8" + } +} diff --git a/samples/boot/kotlin-webflux/src/main/kotlin/org/springframework/security/samples/KotlinWebfluxApplication.kt b/samples/boot/kotlin-webflux/src/main/kotlin/org/springframework/security/samples/KotlinWebfluxApplication.kt new file mode 100644 index 00000000000..572be2a6e3c --- /dev/null +++ b/samples/boot/kotlin-webflux/src/main/kotlin/org/springframework/security/samples/KotlinWebfluxApplication.kt @@ -0,0 +1,27 @@ +/* + * 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.samples + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class KotlinWebfluxApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/samples/boot/kotlin-webflux/src/main/kotlin/org/springframework/security/samples/config/SecurityConfig.kt b/samples/boot/kotlin-webflux/src/main/kotlin/org/springframework/security/samples/config/SecurityConfig.kt new file mode 100644 index 00000000000..e3193c24c67 --- /dev/null +++ b/samples/boot/kotlin-webflux/src/main/kotlin/org/springframework/security/samples/config/SecurityConfig.kt @@ -0,0 +1,56 @@ +/* + * 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.samples.config + +import org.springframework.context.annotation.Bean +import org.springframework.security.config.Customizer +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.config.web.server.invoke +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService +import org.springframework.security.core.userdetails.ReactiveUserDetailsService +import org.springframework.security.core.userdetails.User +import org.springframework.security.web.server.SecurityWebFilterChain + +@EnableWebFluxSecurity +class SecurityConfig { + + @Bean + fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize("/log-in", permitAll) + authorize("/", permitAll) + authorize("/css/**", permitAll) + authorize("/user/**", hasAuthority("ROLE_USER")) + } + formLogin { + loginPage = "/log-in" + } + } + } + + @Bean + fun userDetailsService(): ReactiveUserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build() + return MapReactiveUserDetailsService(userDetails) + } +} diff --git a/samples/boot/kotlin-webflux/src/main/kotlin/org/springframework/security/samples/web/MainController.kt b/samples/boot/kotlin-webflux/src/main/kotlin/org/springframework/security/samples/web/MainController.kt new file mode 100644 index 00000000000..991c0195c06 --- /dev/null +++ b/samples/boot/kotlin-webflux/src/main/kotlin/org/springframework/security/samples/web/MainController.kt @@ -0,0 +1,39 @@ +/* + * 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.samples.web + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping + +@Controller +class MainController { + + @GetMapping("/") + fun index(): String { + return "index" + } + + @GetMapping("/user/index") + fun userIndex(): String { + return "user/index" + } + + @GetMapping("/log-in") + fun login(): String { + return "login" + } +} diff --git a/samples/boot/kotlin-webflux/src/main/resources/application.yml b/samples/boot/kotlin-webflux/src/main/resources/application.yml new file mode 100644 index 00000000000..8c01e005bcd --- /dev/null +++ b/samples/boot/kotlin-webflux/src/main/resources/application.yml @@ -0,0 +1,6 @@ +server: + port: 8080 + +spring: + thymeleaf: + cache: false diff --git a/samples/boot/kotlin-webflux/src/main/resources/css/main.css b/samples/boot/kotlin-webflux/src/main/resources/css/main.css new file mode 100644 index 00000000000..de0941ecd58 --- /dev/null +++ b/samples/boot/kotlin-webflux/src/main/resources/css/main.css @@ -0,0 +1,8 @@ +body { + font-family: sans; + font-size: 1em; +} + +div.logout { + float: right; +} diff --git a/samples/boot/kotlin-webflux/src/main/resources/templates/index.html b/samples/boot/kotlin-webflux/src/main/resources/templates/index.html new file mode 100644 index 00000000000..f637854f047 --- /dev/null +++ b/samples/boot/kotlin-webflux/src/main/resources/templates/index.html @@ -0,0 +1,24 @@ + + + + Hello Spring Security + + + + +
+ Logged in user: | + Roles: +
+ + + +
+
+

Hello Spring Security

+

This is an unsecured page, but you can access the secured pages after authenticating.

+ + + diff --git a/samples/boot/kotlin-webflux/src/main/resources/templates/login.html b/samples/boot/kotlin-webflux/src/main/resources/templates/login.html new file mode 100644 index 00000000000..2ee92169376 --- /dev/null +++ b/samples/boot/kotlin-webflux/src/main/resources/templates/login.html @@ -0,0 +1,20 @@ + + + + Login page + + + + +

Login page

+

Example user: user / password

+
+ : +
+ : +
+ +
+

Back to home page

+ + diff --git a/samples/boot/kotlin-webflux/src/main/resources/templates/user/index.html b/samples/boot/kotlin-webflux/src/main/resources/templates/user/index.html new file mode 100644 index 00000000000..393f6d37051 --- /dev/null +++ b/samples/boot/kotlin-webflux/src/main/resources/templates/user/index.html @@ -0,0 +1,13 @@ + + + + Hello Spring Security + + + + +
+

This is a secured page!

+

Back to home page

+ + diff --git a/samples/boot/kotlin-webflux/src/test/kotlin/org/springframework/security/samples/KotlinWebfluxApplicationTests.kt b/samples/boot/kotlin-webflux/src/test/kotlin/org/springframework/security/samples/KotlinWebfluxApplicationTests.kt new file mode 100644 index 00000000000..fc126e58b7f --- /dev/null +++ b/samples/boot/kotlin-webflux/src/test/kotlin/org/springframework/security/samples/KotlinWebfluxApplicationTests.kt @@ -0,0 +1,67 @@ +/* + * 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.samples + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.ApplicationContext +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.security.test.context.support.WithMockUser + +@SpringBootTest +class KotlinWebfluxApplicationTests { + + lateinit var rest: WebTestClient + + @Autowired + fun setup(context: ApplicationContext) { + rest = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .build() + } + + @Test + fun `index page is not protected`() { + rest + .get() + .uri("/") + .exchange() + .expectStatus().isOk + } + + @Test + fun `protected page when unauthenticated then redirects to login `() { + rest + .get() + .uri("/user/index") + .exchange() + .expectStatus().is3xxRedirection + .expectHeader().valueEquals("Location", "/log-in") + } + + @Test + @WithMockUser + fun `protected page can be accessed when authenticated`() { + rest + .get() + .uri("/user/index") + .exchange() + .expectStatus().isOk + } +} diff --git a/samples/boot/kotlin/spring-security-samples-boot-kotlin.gradle.kts b/samples/boot/kotlin/spring-security-samples-boot-kotlin.gradle.kts index 4d3409c7d63..bd841b8b710 100644 --- a/samples/boot/kotlin/spring-security-samples-boot-kotlin.gradle.kts +++ b/samples/boot/kotlin/spring-security-samples-boot-kotlin.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("io.spring.convention.spring-sample-boot") kotlin("jvm") - kotlin("plugin.spring") version "1.3.61" + kotlin("plugin.spring") version "1.3.71" } repositories { diff --git a/samples/boot/oauth2login-webflux/src/integration-test/java/sample/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login-webflux/src/integration-test/java/sample/OAuth2LoginApplicationTests.java index 32bb75fad8b..77be0fa63bc 100644 --- a/samples/boot/oauth2login-webflux/src/integration-test/java/sample/OAuth2LoginApplicationTests.java +++ b/samples/boot/oauth2login-webflux/src/integration-test/java/sample/OAuth2LoginApplicationTests.java @@ -21,11 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; @@ -46,14 +42,6 @@ public class OAuth2LoginApplicationTests { @Autowired ReactiveClientRegistrationRepository clientRegistrationRepository; - @TestConfiguration - static class AuthorizedClient { - @Bean - ServerOAuth2AuthorizedClientRepository authorizedClientRepository() { - return new WebSessionServerOAuth2AuthorizedClientRepository(); - } - } - @Test public void requestWhenMockOidcLoginThenIndex() { this.clientRegistrationRepository.findByRegistrationId("github") diff --git a/samples/boot/oauth2login-webflux/src/test/java/sample/OAuth2LoginControllerTests.java b/samples/boot/oauth2login-webflux/src/test/java/sample/OAuth2LoginControllerTests.java index 86a0726f64d..d1fb3c5b3e6 100644 --- a/samples/boot/oauth2login-webflux/src/test/java/sample/OAuth2LoginControllerTests.java +++ b/samples/boot/oauth2login-webflux/src/test/java/sample/OAuth2LoginControllerTests.java @@ -28,7 +28,6 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.web.reactive.result.method.annotation.OAuth2AuthorizedClientArgumentResolver; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository; import org.springframework.security.web.reactive.result.method.annotation.AuthenticationPrincipalArgumentResolver; import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter; import org.springframework.test.context.junit4.SpringRunner; @@ -55,13 +54,13 @@ public class OAuth2LoginControllerTests { @Mock ReactiveClientRegistrationRepository clientRegistrationRepository; + @Mock + ServerOAuth2AuthorizedClientRepository authorizedClientRepository; + WebTestClient rest; @Before public void setup() { - ServerOAuth2AuthorizedClientRepository authorizedClientRepository = - new WebSessionServerOAuth2AuthorizedClientRepository(); - this.rest = WebTestClient .bindToController(this.controller) .apply(springSecurity()) @@ -69,7 +68,7 @@ public void setup() { .argumentResolvers(c -> { c.addCustomResolver(new AuthenticationPrincipalArgumentResolver(new ReactiveAdapterRegistry())); c.addCustomResolver(new OAuth2AuthorizedClientArgumentResolver - (this.clientRegistrationRepository, authorizedClientRepository)); + (this.clientRegistrationRepository, this.authorizedClientRepository)); }) .viewResolvers(c -> c.viewResolver(this.viewResolver)) .build(); diff --git a/samples/boot/oauth2login/src/integration-test/java/sample/OAuth2LoginApplicationTests.java b/samples/boot/oauth2login/src/integration-test/java/sample/OAuth2LoginApplicationTests.java index 71e0f3d7b2e..077a51abf34 100644 --- a/samples/boot/oauth2login/src/integration-test/java/sample/OAuth2LoginApplicationTests.java +++ b/samples/boot/oauth2login/src/integration-test/java/sample/OAuth2LoginApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -41,7 +41,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -53,9 +53,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; @@ -86,7 +84,7 @@ * @since 5.0 */ @RunWith(SpringRunner.class) -@SpringBootTest(classes={ OAuth2LoginApplication.class, OAuth2LoginApplicationTests.SecurityTestConfig.class }) +@SpringBootTest @AutoConfigureMockMvc public class OAuth2LoginApplicationTests { private static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization"; @@ -329,6 +327,7 @@ private WebResponse followLinkDisableRedirects(HtmlAnchor anchorElement) throws } @EnableWebSecurity + @TestConfiguration public static class SecurityTestConfig extends WebSecurityConfigurerAdapter { // @formatter:off @@ -381,10 +380,5 @@ private OAuth2UserService mockUserService() { when(userService.loadUser(any())).thenReturn(user); return userService; } - - @Bean - OAuth2AuthorizedClientRepository authorizedClientRepository() { - return new HttpSessionOAuth2AuthorizedClientRepository(); - } } } diff --git a/samples/boot/oauth2login/src/test/java/sample/web/OAuth2LoginControllerTests.java b/samples/boot/oauth2login/src/test/java/sample/web/OAuth2LoginControllerTests.java index 033a55b7c40..3779bb7ff8a 100644 --- a/samples/boot/oauth2login/src/test/java/sample/web/OAuth2LoginControllerTests.java +++ b/samples/boot/oauth2login/src/test/java/sample/web/OAuth2LoginControllerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -23,13 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Bean; import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; @@ -50,17 +44,6 @@ public class OAuth2LoginControllerTests { @Autowired MockMvc mvc; - @MockBean - ClientRegistrationRepository clientRegistrationRepository; - - @TestConfiguration - static class AuthorizedClient { - @Bean - public OAuth2AuthorizedClientRepository authorizedClientRepository() { - return new HttpSessionOAuth2AuthorizedClientRepository(); - } - } - @Test public void rootWhenAuthenticatedReturnsUserAndClient() throws Exception { this.mvc.perform(get("/").with(oauth2Login())) diff --git a/samples/boot/oauth2resourceserver-multitenancy/README.adoc b/samples/boot/oauth2resourceserver-multitenancy/README.adoc index 85065e31b8a..97674479bde 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/README.adoc +++ b/samples/boot/oauth2resourceserver-multitenancy/README.adoc @@ -27,14 +27,14 @@ The Resource Server subsequently verifies with the Authorization Server and auth phrase ```bash -Hello, subject for tenantOne! +Hello, subject for tenant one! ``` where "subject" is the value of the `sub` field in the JWT sent in the `Authorization` header, or the phrase ```bash -Hello, subject for tenantTwo! +Hello, subject for tenant two! ``` where "subject" is the value of the `sub` field in the Introspection response from the Authorization Server. @@ -60,13 +60,13 @@ export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ And then make this request: ```bash -curl -H "Authorization: Bearer $TOKEN" localhost:8080/tenantOne +curl -H "tenant: one" -H "Authorization: Bearer $TOKEN" localhost:8080 ``` Which will respond with the phrase: ```bash -Hello, subject for tenantOne! +Hello, subject for tenant one! ``` where `subject` is the value of the `sub` field in the JWT sent in the `Authorization` header. @@ -76,13 +76,13 @@ Or this: ```bash export TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQiLCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgUOBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwINafU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_QlR44vmRqS5ncrF-1R0EGcPX49U6A -curl -H "Authorization: Bearer $TOKEN" localhost:8080/tenantOne/message +curl -H "tenant: one" -H "Authorization: Bearer $TOKEN" localhost:8080/message ``` Will respond with: ```bash -secret message for tenantOne +secret message for tenant one ``` === Authorizing with tenantTwo (Opaque token) @@ -96,13 +96,13 @@ export TOKEN=00ed5855-1869-47a0-b0c9-0f3ce520aee7 And then make this request: ```bash -curl -H "Authorization: Bearer $TOKEN" localhost:8080/tenantTwo +curl -H "tenant: two" -H "Authorization: Bearer $TOKEN" localhost:8080 ``` Which will respond with the phrase: ```bash -Hello, subject for tenantTwo! +Hello, subject for tenant two! ``` where `subject` is the value of the `sub` field in the Introspection response from the Authorization Server. @@ -112,13 +112,13 @@ Or this: ```bash export TOKEN=b43d1500-c405-4dc9-b9c9-6cfd966c34c9 -curl -H "Authorization: Bearer $TOKEN" localhost:8080/tenantTwo/message +curl -H "tenant: two" -H "Authorization: Bearer $TOKEN" localhost:8080/message ``` Will respond with: ```bash -secret message for tenantTwo +secret message for tenant two ``` == 2. Testing against other Authorization Servers diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java b/samples/boot/oauth2resourceserver-multitenancy/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java index 2bca10f1ea9..c50157dd92f 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java +++ b/samples/boot/oauth2resourceserver-multitenancy/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -22,11 +22,8 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.RequestPostProcessor; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -42,7 +39,6 @@ @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc -@ActiveProfiles("test") public class OAuth2ResourceServerApplicationITests { String tenantOneNoScopesToken = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiZXhwIjo0NjgzODA1MTI4fQ.ULEPdHG-MK5GlrTQMhgqcyug2brTIZaJIrahUeq9zaiwUSdW83fJ7W1IDd2Z3n4a25JY2uhEcoV95lMfccHR6y_2DLrNvfta22SumY9PEDF2pido54LXG6edIGgarnUbJdR4rpRe_5oRGVa8gDx8FnuZsNv6StSZHAzw5OsuevSTJ1UbJm4UfX3wiahFOQ2OI6G-r5TB2rQNdiPHuNyzG5yznUqRIZ7-GCoMqHMaC-1epKxiX8gYXRROuUYTtcMNa86wh7OVDmvwVmFioRcR58UWBRoO1XQexTtOQq_t8KYsrPZhb9gkyW8x2bAQF-d0J0EJY8JslaH6n4RBaZISww"; @@ -57,18 +53,11 @@ public class OAuth2ResourceServerApplicationITests { public void tenantOnePerformWhenValidBearerTokenThenAllows() throws Exception { - this.mvc.perform(get("/tenantOne").with(bearerToken(this.tenantOneNoScopesToken))) + this.mvc.perform(get("/") + .header("tenant", "one") + .header("Authorization", "Bearer " + this.tenantOneNoScopesToken)) .andExpect(status().isOk()) - .andExpect(content().string(containsString("Hello, subject for tenantOne!"))); - } - - @Test - public void tenantOnePerformWhenValidBearerTokenWithServletPathThenAllows() - throws Exception { - - this.mvc.perform(get("/tenantOne").servletPath("/tenantOne").with(bearerToken(this.tenantOneNoScopesToken))) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("Hello, subject for tenantOne!"))); + .andExpect(content().string(containsString("Hello, subject for tenant one!"))); } // -- tests with scopes @@ -77,16 +66,20 @@ public void tenantOnePerformWhenValidBearerTokenWithServletPathThenAllows() public void tenantOnePerformWhenValidBearerTokenThenScopedRequestsAlsoWork() throws Exception { - this.mvc.perform(get("/tenantOne/message").with(bearerToken(this.tenantOneMessageReadToken))) + this.mvc.perform(get("/message") + .header("tenant", "one") + .header("Authorization", "Bearer " + this.tenantOneMessageReadToken)) .andExpect(status().isOk()) - .andExpect(content().string(containsString("secret message for tenantOne"))); + .andExpect(content().string(containsString("secret message for tenant one"))); } @Test public void tenantOnePerformWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess() throws Exception { - this.mvc.perform(get("/tenantOne/message").with(bearerToken(this.tenantOneNoScopesToken))) + this.mvc.perform(get("/message") + .header("tenant", "one") + .header("Authorization", "Bearer " + this.tenantOneNoScopesToken)) .andExpect(status().isForbidden()) .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("Bearer error=\"insufficient_scope\""))); @@ -96,9 +89,11 @@ public void tenantOnePerformWhenInsufficientlyScopedBearerTokenThenDeniesScopedM public void tenantTwoPerformWhenValidBearerTokenThenAllows() throws Exception { - this.mvc.perform(get("/tenantTwo").with(bearerToken(this.tenantTwoNoScopesToken))) + this.mvc.perform(get("/") + .header("tenant", "two") + .header("Authorization", "Bearer " + this.tenantTwoNoScopesToken)) .andExpect(status().isOk()) - .andExpect(content().string(containsString("Hello, subject for tenantTwo!"))); + .andExpect(content().string(containsString("Hello, subject for tenant two!"))); } // -- tests with scopes @@ -107,16 +102,20 @@ public void tenantTwoPerformWhenValidBearerTokenThenAllows() public void tenantTwoPerformWhenValidBearerTokenThenScopedRequestsAlsoWork() throws Exception { - this.mvc.perform(get("/tenantTwo/message").with(bearerToken(this.tenantTwoMessageReadToken))) + this.mvc.perform(get("/message") + .header("tenant", "two") + .header("Authorization", "Bearer " + this.tenantTwoMessageReadToken)) .andExpect(status().isOk()) - .andExpect(content().string(containsString("secret message for tenantTwo"))); + .andExpect(content().string(containsString("secret message for tenant two"))); } @Test public void tenantTwoPerformWhenInsufficientlyScopedBearerTokenThenDeniesScopedMethodAccess() throws Exception { - this.mvc.perform(get("/tenantTwo/message").with(bearerToken(this.tenantTwoNoScopesToken))) + this.mvc.perform(get("/message") + .header("tenant", "two") + .header("Authorization", "Bearer " + this.tenantTwoNoScopesToken)) .andExpect(status().isForbidden()) .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("Bearer error=\"insufficient_scope\""))); @@ -126,24 +125,8 @@ public void tenantTwoPerformWhenInsufficientlyScopedBearerTokenThenDeniesScopedM public void invalidTenantPerformWhenValidBearerTokenThenThrowsException() throws Exception { - this.mvc.perform(get("/tenantThree").with(bearerToken(this.tenantOneNoScopesToken))); - } - - private static class BearerTokenRequestPostProcessor implements RequestPostProcessor { - private String token; - - BearerTokenRequestPostProcessor(String token) { - this.token = token; - } - - @Override - public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - request.addHeader("Authorization", "Bearer " + this.token); - return request; - } - } - - private static BearerTokenRequestPostProcessor bearerToken(String token) { - return new BearerTokenRequestPostProcessor(token); + this.mvc.perform(get("/") + .header("tenant", "three") + .header("Authorization", "Bearer " + this.tenantOneNoScopesToken)); } } diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerController.java b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerController.java index e14a29b0328..18165789a73 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerController.java +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -18,7 +18,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; /** @@ -27,14 +27,14 @@ @RestController public class OAuth2ResourceServerController { - @GetMapping("/{tenantId}") - public String index(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal token, @PathVariable("tenantId") String tenantId) { + @GetMapping("/") + public String index(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal token, @RequestHeader("tenant") String tenant) { String subject = token.getAttribute("sub"); - return String.format("Hello, %s for %s!", subject, tenantId); + return String.format("Hello, %s for tenant %s!", subject, tenant); } - @GetMapping("/{tenantId}/message") - public String message(@PathVariable("tenantId") String tenantId) { - return String.format("secret message for %s", tenantId); + @GetMapping("/message") + public String message(@RequestHeader("tenant") String tenant) { + return String.format("secret message for tenant %s", tenant); } } diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java index ff44461855f..a52933b648a 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/OAuth2ResourceServerSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -15,25 +15,13 @@ */ package sample; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; import javax.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManagerResolver; 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.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; -import org.springframework.security.oauth2.server.resource.authentication.JwtBearerTokenAuthenticationConverter; -import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; -import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector; -import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; /** * @author Josh Cummings @@ -41,59 +29,20 @@ @EnableWebSecurity public class OAuth2ResourceServerSecurityConfiguration extends WebSecurityConfigurerAdapter { - @Value("${tenantOne.jwk-set-uri}") - String jwkSetUri; - - @Value("${tenantTwo.introspection-uri}") - String introspectionUri; - - @Value("${tenantTwo.introspection-client-id}") - String introspectionClientId; - - @Value("${tenantTwo.introspection-client-secret}") - String introspectionClientSecret; + @Autowired + AuthenticationManagerResolver authenticationManagerResolver; @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests(authorizeRequests -> - authorizeRequests - .antMatchers("/**/message/**").hasAuthority("SCOPE_message:read") - .anyRequest().authenticated() + .authorizeRequests(authz -> authz + .antMatchers("/message/**").hasAuthority("SCOPE_message:read") + .anyRequest().authenticated() ) - .oauth2ResourceServer(oauth2ResourceServer -> - oauth2ResourceServer - .authenticationManagerResolver(multitenantAuthenticationManager()) + .oauth2ResourceServer(oauth2 -> oauth2 + .authenticationManagerResolver(this.authenticationManagerResolver) ); // @formatter:on } - - @Bean - AuthenticationManagerResolver multitenantAuthenticationManager() { - Map authenticationManagers = new HashMap<>(); - authenticationManagers.put("tenantOne", jwt()); - authenticationManagers.put("tenantTwo", opaque()); - return request -> { - String[] pathParts = request.getRequestURI().split("/"); - String tenantId = pathParts.length > 0 ? pathParts[1] : null; - return Optional.ofNullable(tenantId) - .map(authenticationManagers::get) - .orElseThrow(() -> new IllegalArgumentException("unknown tenant")); - }; - } - - AuthenticationManager jwt() { - JwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build(); - JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(jwtDecoder); - authenticationProvider.setJwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter()); - return authenticationProvider::authenticate; - } - - AuthenticationManager opaque() { - OpaqueTokenIntrospector introspectionClient = - new NimbusOpaqueTokenIntrospector(this.introspectionUri, - this.introspectionClientId, this.introspectionClientSecret); - return new OpaqueTokenAuthenticationProvider(introspectionClient)::authenticate; - } } diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/TenantAuthenticationManagerResolver.java b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/TenantAuthenticationManagerResolver.java new file mode 100644 index 00000000000..939bfc0b8b5 --- /dev/null +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/java/sample/TenantAuthenticationManagerResolver.java @@ -0,0 +1,58 @@ +/* + * 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 sample; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationManagerResolver; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.JwtBearerTokenAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.stereotype.Component; + +@Component +public class TenantAuthenticationManagerResolver + implements AuthenticationManagerResolver { + + private AuthenticationManager jwt; + private AuthenticationManager opaqueToken; + + public TenantAuthenticationManagerResolver( + JwtDecoder jwtDecoder, OpaqueTokenIntrospector opaqueTokenIntrospector) { + + JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtDecoder); + jwtAuthenticationProvider.setJwtAuthenticationConverter(new JwtBearerTokenAuthenticationConverter()); + this.jwt = new ProviderManager(jwtAuthenticationProvider); + this.opaqueToken = new ProviderManager(new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector)); + } + + @Override + public AuthenticationManager resolve(HttpServletRequest request) { + String tenant = request.getHeader("tenant"); + if ("one".equals(tenant)) { + return this.jwt; + } + if ("two".equals(tenant)) { + return this.opaqueToken; + } + throw new IllegalArgumentException("unknown tenant"); + } +} diff --git a/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml b/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml index de938e33b46..6447ad0d940 100644 --- a/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml +++ b/samples/boot/oauth2resourceserver-multitenancy/src/main/resources/application.yml @@ -1,4 +1,10 @@ -tenantOne.jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json -tenantTwo.introspection-uri: ${mockwebserver.url}/introspect -tenantTwo.introspection-client-id: client -tenantTwo.introspection-client-secret: secret +spring: + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json + opaquetoken: + introspection-uri: ${mockwebserver.url}/introspect + client-id: client + client-secret: secret diff --git a/samples/boot/oauth2webclient-webflux/src/test/java/sample/OAuth2WebClientControllerTests.java b/samples/boot/oauth2webclient-webflux/src/test/java/sample/OAuth2WebClientControllerTests.java index fc50c142a0f..52445dd2128 100644 --- a/samples/boot/oauth2webclient-webflux/src/test/java/sample/OAuth2WebClientControllerTests.java +++ b/samples/boot/oauth2webclient-webflux/src/test/java/sample/OAuth2WebClientControllerTests.java @@ -22,18 +22,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import sample.config.SecurityConfig; -import sample.web.OAuth2WebClientController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -42,7 +37,7 @@ import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Login; @WebFluxTest -@Import({ SecurityConfig.class, OAuth2WebClientController.class }) +@Import(SecurityConfig.class) @AutoConfigureWebTestClient @RunWith(SpringRunner.class) public class OAuth2WebClientControllerTests { @@ -51,9 +46,6 @@ public class OAuth2WebClientControllerTests { @Autowired private WebTestClient client; - @MockBean - ReactiveClientRegistrationRepository clientRegistrationRepository; - @AfterClass public static void shutdown() throws Exception { web.shutdown(); @@ -96,16 +88,11 @@ public void publicImplicitWhenAuthenticatedThenUsesDefaultRegistration() throws .expectStatus().isOk(); } - @Configuration + @TestConfiguration static class WebClientConfig { @Bean WebClient web() { return WebClient.create(web.url("/").toString()); } - - @Bean - ServerOAuth2AuthorizedClientRepository authorizedClientRepository() { - return new WebSessionServerOAuth2AuthorizedClientRepository(); - } } } diff --git a/samples/boot/oauth2webclient-webflux/src/test/java/sample/RegisteredOAuth2AuthorizedClientControllerTests.java b/samples/boot/oauth2webclient-webflux/src/test/java/sample/RegisteredOAuth2AuthorizedClientControllerTests.java index be4cc4a57cc..32fc9095b7c 100644 --- a/samples/boot/oauth2webclient-webflux/src/test/java/sample/RegisteredOAuth2AuthorizedClientControllerTests.java +++ b/samples/boot/oauth2webclient-webflux/src/test/java/sample/RegisteredOAuth2AuthorizedClientControllerTests.java @@ -22,18 +22,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import sample.config.SecurityConfig; -import sample.web.RegisteredOAuth2AuthorizedClientController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.server.WebSessionServerOAuth2AuthorizedClientRepository; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -42,7 +37,7 @@ import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Login; @WebFluxTest -@Import({ SecurityConfig.class, RegisteredOAuth2AuthorizedClientController.class }) +@Import(SecurityConfig.class) @AutoConfigureWebTestClient @RunWith(SpringRunner.class) public class RegisteredOAuth2AuthorizedClientControllerTests { @@ -51,9 +46,6 @@ public class RegisteredOAuth2AuthorizedClientControllerTests { @Autowired private WebTestClient client; - @MockBean - ReactiveClientRegistrationRepository clientRegistrationRepository; - @AfterClass public static void shutdown() throws Exception { web.shutdown(); @@ -96,16 +88,11 @@ public void publicAnnotationImplicitWhenAuthenticatedThenUsesDefaultRegistration .expectStatus().isOk(); } - @Configuration + @TestConfiguration static class WebClientConfig { @Bean WebClient web() { return WebClient.create(web.url("/").toString()); } - - @Bean - ServerOAuth2AuthorizedClientRepository authorizedClientRepository() { - return new WebSessionServerOAuth2AuthorizedClientRepository(); - } } } diff --git a/samples/boot/oauth2webclient/src/test/java/sample/OAuth2WebClientControllerTests.java b/samples/boot/oauth2webclient/src/test/java/sample/OAuth2WebClientControllerTests.java index a22ef70e502..8829c004715 100644 --- a/samples/boot/oauth2webclient/src/test/java/sample/OAuth2WebClientControllerTests.java +++ b/samples/boot/oauth2webclient/src/test/java/sample/OAuth2WebClientControllerTests.java @@ -21,19 +21,12 @@ import org.junit.AfterClass; import org.junit.Test; import org.junit.runner.RunWith; -import sample.config.SecurityConfig; -import sample.web.OAuth2WebClientController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.reactive.function.client.WebClient; @@ -44,7 +37,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest -@Import({ SecurityConfig.class, OAuth2WebClientController.class }) @AutoConfigureMockMvc @RunWith(SpringRunner.class) public class OAuth2WebClientControllerTests { @@ -53,9 +45,6 @@ public class OAuth2WebClientControllerTests { @Autowired private MockMvc mockMvc; - @MockBean - ClientRegistrationRepository clientRegistrationRepository; - @AfterClass public static void shutdown() throws Exception { web.shutdown(); @@ -94,16 +83,11 @@ public void publicImplicitWhenAuthenticatedThenUsesDefaultRegistration() throws .andExpect(status().isOk()); } - @Configuration + @TestConfiguration static class WebClientConfig { @Bean WebClient web() { return WebClient.create(web.url("/").toString()); } - - @Bean - OAuth2AuthorizedClientRepository authorizedClientRepository() { - return new HttpSessionOAuth2AuthorizedClientRepository(); - } } } diff --git a/samples/boot/oauth2webclient/src/test/java/sample/RegisteredOAuth2AuthorizedClientControllerTests.java b/samples/boot/oauth2webclient/src/test/java/sample/RegisteredOAuth2AuthorizedClientControllerTests.java index 18a2ca6cd9b..91e3a2ec913 100644 --- a/samples/boot/oauth2webclient/src/test/java/sample/RegisteredOAuth2AuthorizedClientControllerTests.java +++ b/samples/boot/oauth2webclient/src/test/java/sample/RegisteredOAuth2AuthorizedClientControllerTests.java @@ -21,19 +21,12 @@ import org.junit.AfterClass; import org.junit.Test; import org.junit.runner.RunWith; -import sample.config.SecurityConfig; -import sample.web.RegisteredOAuth2AuthorizedClientController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.reactive.function.client.WebClient; @@ -44,7 +37,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest -@Import({ SecurityConfig.class, RegisteredOAuth2AuthorizedClientController.class }) @AutoConfigureMockMvc @RunWith(SpringRunner.class) public class RegisteredOAuth2AuthorizedClientControllerTests { @@ -53,9 +45,6 @@ public class RegisteredOAuth2AuthorizedClientControllerTests { @Autowired private MockMvc mockMvc; - @MockBean - ClientRegistrationRepository clientRegistrationRepository; - @AfterClass public static void shutdown() throws Exception { web.shutdown(); @@ -94,16 +83,11 @@ public void publicAnnotationImplicitWhenAuthenticatedThenUsesDefaultRegistration .andExpect(status().isOk()); } - @Configuration + @TestConfiguration static class WebClientConfig { @Bean WebClient web() { return WebClient.create(web.url("/").toString()); } - - @Bean - OAuth2AuthorizedClientRepository authorizedClientRepository() { - return new HttpSessionOAuth2AuthorizedClientRepository(); - } } } diff --git a/samples/boot/saml2login/README.adoc b/samples/boot/saml2login/README.adoc index 94feb1c8261..d51b1bba4be 100644 --- a/samples/boot/saml2login/README.adoc +++ b/samples/boot/saml2login/README.adoc @@ -1,26 +1,20 @@ -= OAuth 2.0 Login Sample += SAML 2.0 Login Sample -This guide provides instructions on setting up the sample application with SAML 2.0 Login using -Spring Security's `saml2Login()` feature. +This guide provides instructions on setting up this SAML 2.0 Login sample application. -The sample application uses Spring Boot 2.2.0.M5 and the `spring-security-saml2-service-provider` +The sample application uses Spring Boot and the `spring-security-saml2-service-provider` module which is new in Spring Security 5.2. == Goals -`saml2Login()` provides a very simple, basic, implementation of a Service Provider -that can receive a SAML 2 Response XML object via the HTTP-POST and HTTP-REDIRECT bindings -against a known SAML reference implementation by SimpleSAMLPhp. +`saml2Login()` provides a very simple implementation of a Service Provider that can receive a SAML 2.0 Response via the HTTP-POST and HTTP-REDIRECT bindings against the SimpleSAMLphp SAML 2.0 reference implementation. +The following features are implemented in the MVP: -The following features are implemented in the MVP - -1. Receive and validate a SAML 2.0 Response object containing an assertion -and create a valid authentication in Spring Security -2. Send a SAML 2 AuthNRequest object to an Identity Provider -3. Provide a framework for components used in SAML 2.0 authentication that can -be swapped by configuration -4. Sample working against the SimpleSAMLPhP reference implementation +1. Receive and validate a SAML 2.0 Response containing an assertion, and create a corresponding authentication in Spring Security +2. Send a SAML 2.0 AuthNRequest to an Identity Provider +3. Provide a framework for components used in SAML 2.0 authentication that can be swapped by configuration +4. Work against the SimpleSAMLphp reference implementation == Run the Sample @@ -33,7 +27,7 @@ be swapped by configuration http://localhost:8080/ -You will be redirect to the SimpleSAMLPhp IDP +You will be redirect to the SimpleSAMLphp IDP === Type in your credentials diff --git a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java index 1cb224713ec..c2b8abde21b 100644 --- a/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java +++ b/samples/boot/saml2login/src/integration-test/java/org/springframework/security/saml2/provider/service/authentication/Saml2LoginIntegrationTests.java @@ -77,7 +77,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.startsWith; import static org.springframework.security.saml2.provider.service.authentication.OpenSamlActionTestingSupport.buildConditions; @@ -242,8 +241,8 @@ public void authenticateWhenSignatureKeysDontMatchThenItFails() throws Exception sendResponse(response, "/login?error") .andExpect( saml2AuthenticationExceptionMatcher( - "invalid_signature", - equalTo("Assertion doesn't have a valid signature.") + "invalid_assertion", + containsString("Invalid assertion [assertion] for SAML response") ) ); } @@ -258,7 +257,7 @@ public void authenticateWhenNotOnOrAfterDontMatchThenItFails() throws Exception .andExpect( saml2AuthenticationExceptionMatcher( "invalid_assertion", - containsString("Assertion 'assertion' with NotOnOrAfter condition of") + containsString("Invalid assertion [assertion] for SAML response") ) ); } @@ -273,7 +272,7 @@ public void authenticateWhenNotOnOrBeforeDontMatchThenItFails() throws Exception .andExpect( saml2AuthenticationExceptionMatcher( "invalid_assertion", - containsString("Assertion 'assertion' with NotBefore condition of") + containsString("Invalid assertion [assertion] for SAML response") ) ); } @@ -290,8 +289,7 @@ public void authenticateWhenIssuerIsInvalidThenItFails() throws Exception { saml2AuthenticationExceptionMatcher( "invalid_issuer", containsString( - "Response issuer 'invalid issuer' doesn't match "+ - "'https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php'" + "Invalid issuer [invalid issuer] for SAML response" ) ) ); diff --git a/samples/boot/saml2login/src/main/java/sample/IndexController.java b/samples/boot/saml2login/src/main/java/sample/IndexController.java index 3c336c4dfac..8da3c251eb8 100644 --- a/samples/boot/saml2login/src/main/java/sample/IndexController.java +++ b/samples/boot/saml2login/src/main/java/sample/IndexController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * 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. @@ -16,20 +16,21 @@ package sample; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import sample.Saml2LoginApplication; - @Controller public class IndexController { - private static final Log logger = LogFactory.getLog(Saml2LoginApplication.class); - @GetMapping("/") - public String index() { + public String index(Model model, + @AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) { + String emailAddress = principal.getFirstAttribute("emailAddress"); + model.addAttribute("emailAddress", emailAddress); + model.addAttribute("userAttributes", principal.getAttributes()); return "index"; } } diff --git a/samples/boot/saml2login/src/main/resources/application.yml b/samples/boot/saml2login/src/main/resources/application.yml index c8cbdd45ce2..afee02e6f78 100644 --- a/samples/boot/saml2login/src/main/resources/application.yml +++ b/samples/boot/saml2login/src/main/resources/application.yml @@ -4,13 +4,11 @@ spring: relyingparty: registration: simplesamlphp: - signing: - credentials: - - private-key-location: "classpath:credentials/rp-private.key" - certificate-location: "classpath:credentials/rp-certificate.crt" + signing.credentials: + - private-key-location: "classpath:credentials/rp-private.key" + certificate-location: "classpath:credentials/rp-certificate.crt" identityprovider: - verification: - credentials: - - certificate-location: "classpath:credentials/idp-certificate.crt" entity-id: https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php + verification.credentials: + - certificate-location: "classpath:credentials/idp-certificate.crt" sso-url: https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php diff --git a/samples/boot/saml2login/src/main/resources/templates/index.html b/samples/boot/saml2login/src/main/resources/templates/index.html index 5251b3a8e9e..0fb8ad872a0 100644 --- a/samples/boot/saml2login/src/main/resources/templates/index.html +++ b/samples/boot/saml2login/src/main/resources/templates/index.html @@ -1,5 +1,5 @@ + - \ No newline at end of file + diff --git a/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/config/MessageSecurityWebApplicationInitializer.java b/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/config/MessageSecurityWebApplicationInitializer.java index 4ed43fc3cbf..3a4ec0ad52c 100644 --- a/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/config/MessageSecurityWebApplicationInitializer.java +++ b/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/config/MessageSecurityWebApplicationInitializer.java @@ -20,6 +20,9 @@ /** * No customizations of {@link AbstractSecurityWebApplicationInitializer} are necessary. * + * @deprecated 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. * @author Rob Winch */ public class MessageSecurityWebApplicationInitializer extends diff --git a/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/config/SecurityConfig.java b/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/config/SecurityConfig.java index dc9134832c8..452a80bdd03 100644 --- a/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/config/SecurityConfig.java +++ b/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/config/SecurityConfig.java @@ -20,6 +20,11 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.samples.security.CustomUserDetailsService; +/** + * @deprecated 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. + */ @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // @formatter:off diff --git a/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/mvc/UserController.java b/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/mvc/UserController.java index ebed04dd9cf..d700a3e832a 100644 --- a/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/mvc/UserController.java +++ b/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/mvc/UserController.java @@ -21,6 +21,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +/** + * @deprecated 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. + */ @Controller @RequestMapping("/user/") public class UserController { diff --git a/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/security/CustomUserDetailsService.java b/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/security/CustomUserDetailsService.java index fd421b880e6..faaa81afbe9 100644 --- a/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/security/CustomUserDetailsService.java +++ b/samples/javaconfig/openid/src/main/java/org/springframework/security/samples/security/CustomUserDetailsService.java @@ -22,6 +22,11 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.openid.OpenIDAuthenticationToken; +/** + * @deprecated 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. + */ public class CustomUserDetailsService implements AuthenticationUserDetailsService { public UserDetails loadUserDetails(OpenIDAuthenticationToken token) @@ -29,4 +34,4 @@ public UserDetails loadUserDetails(OpenIDAuthenticationToken token) return new User(token.getName(), "", AuthorityUtils.createAuthorityList("ROLE_USER")); } -} \ No newline at end of file +} diff --git a/samples/javaconfig/openid/src/main/resources/views/login.html b/samples/javaconfig/openid/src/main/resources/views/login.html index 0d46e3b163b..4c6f86c51a2 100644 --- a/samples/javaconfig/openid/src/main/resources/views/login.html +++ b/samples/javaconfig/openid/src/main/resources/views/login.html @@ -7,6 +7,11 @@
+

+ 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. +

Sign-in or Create New Account @@ -43,4 +48,4 @@
- \ No newline at end of file + diff --git a/samples/javaconfig/saml2login/src/main/java/org/springframework/security/samples/config/SecurityConfig.java b/samples/javaconfig/saml2login/src/main/java/org/springframework/security/samples/config/SecurityConfig.java index 6acdca5bd64..ddc28ca4af7 100644 --- a/samples/javaconfig/saml2login/src/main/java/org/springframework/security/samples/config/SecurityConfig.java +++ b/samples/javaconfig/saml2login/src/main/java/org/springframework/security/samples/config/SecurityConfig.java @@ -15,6 +15,12 @@ */ package org.springframework.security.samples.config; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -25,12 +31,6 @@ import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter; -import java.io.ByteArrayInputStream; -import java.nio.charset.StandardCharsets; -import java.security.PrivateKey; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; - import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.DECRYPTION; import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.SIGNING; import static org.springframework.security.saml2.credentials.Saml2X509Credential.Saml2X509CredentialType.VERIFICATION; @@ -54,12 +54,13 @@ RelyingPartyRegistration getSaml2AuthenticationConfiguration() throws Exception Saml2X509Credential idpVerificationCertificate = getVerificationCertificate(); String acsUrlTemplate = "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; return RelyingPartyRegistration.withRegistrationId(registrationId) - .providerDetails(config -> config.entityId(idpEntityId)) - .providerDetails(config -> config.webSsoUrl(webSsoEndpoint)) + .entityId(localEntityIdTemplate) + .assertionConsumerServiceLocation(acsUrlTemplate) .credentials(c -> c.add(signingCredential)) - .credentials(c -> c.add(idpVerificationCertificate)) - .localEntityIdTemplate(localEntityIdTemplate) - .assertionConsumerServiceUrlTemplate(acsUrlTemplate) + .assertingPartyDetails(config -> config + .entityId(idpEntityId) + .singleSignOnServiceLocation(webSsoEndpoint)) + .credentials(c -> c.add(idpVerificationCertificate)) .build(); } diff --git a/samples/xml/openid/src/main/java/org/springframework/security/samples/openid/CustomUserDetails.java b/samples/xml/openid/src/main/java/org/springframework/security/samples/openid/CustomUserDetails.java index f0b1bc4ee13..7a5e400ec2c 100644 --- a/samples/xml/openid/src/main/java/org/springframework/security/samples/openid/CustomUserDetails.java +++ b/samples/xml/openid/src/main/java/org/springframework/security/samples/openid/CustomUserDetails.java @@ -23,6 +23,9 @@ /** * Customized {@code UserDetails} implementation. * + * @deprecated 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. * @author Luke Taylor * @since 3.1 */ diff --git a/samples/xml/openid/src/main/java/org/springframework/security/samples/openid/CustomUserDetailsService.java b/samples/xml/openid/src/main/java/org/springframework/security/samples/openid/CustomUserDetailsService.java index ad23c3fc300..27a3bb409d4 100644 --- a/samples/xml/openid/src/main/java/org/springframework/security/samples/openid/CustomUserDetailsService.java +++ b/samples/xml/openid/src/main/java/org/springframework/security/samples/openid/CustomUserDetailsService.java @@ -32,6 +32,9 @@ * Custom UserDetailsService which accepts any OpenID user, "registering" new users in a * map so they can be welcomed back to the site on subsequent logins. * + * @deprecated 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. * @author Luke Taylor * @since 3.1 */ diff --git a/samples/xml/openid/src/main/resources/logback.xml b/samples/xml/openid/src/main/resources/logback.xml index 3ebbcc0ddd6..1f54c087538 100644 --- a/samples/xml/openid/src/main/resources/logback.xml +++ b/samples/xml/openid/src/main/resources/logback.xml @@ -1,3 +1,7 @@ + + diff --git a/samples/xml/openid/src/main/webapp/WEB-INF/applicationContext-security.xml b/samples/xml/openid/src/main/webapp/WEB-INF/applicationContext-security.xml index d76dbdc9c8f..c79478c9223 100644 --- a/samples/xml/openid/src/main/webapp/WEB-INF/applicationContext-security.xml +++ b/samples/xml/openid/src/main/webapp/WEB-INF/applicationContext-security.xml @@ -16,7 +16,7 @@ - + diff --git a/samples/xml/openid/src/main/webapp/index.jsp b/samples/xml/openid/src/main/webapp/index.jsp index 1ea6bb94ce3..868f99600e8 100644 --- a/samples/xml/openid/src/main/webapp/index.jsp +++ b/samples/xml/openid/src/main/webapp/index.jsp @@ -1,3 +1,4 @@ +<%@ page import="org.springframework.security.web.csrf.CsrfToken" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> @@ -6,6 +7,12 @@

OpenID Sample Home Page

+

+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. +

+

Welcome back, ! @@ -21,6 +28,11 @@ by the application and will be recognized if you return.

Your principal object is....: <%= request.getUserPrincipal() %>

-

Logout +<% CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); %> + + + +

Logout

diff --git a/samples/xml/openid/src/main/webapp/js/jquery-1.2.6.min.js b/samples/xml/openid/src/main/webapp/js/jquery-1.2.6.min.js deleted file mode 100644 index 82b98e1d766..00000000000 --- a/samples/xml/openid/src/main/webapp/js/jquery-1.2.6.min.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * jQuery 1.2.6 - New Wave Javascript - * - * Copyright (c) 2008 John Resig (jquery.com) - * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. - * - * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $ - * $Rev: 5685 $ - */ -(function(){var _jQuery=window.jQuery,_$=window.$;var jQuery=window.jQuery=window.$=function(selector,context){return new jQuery.fn.init(selector,context);};var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,isSimple=/^.[^:#\[\.]*$/,undefined;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem){if(elem.id!=match[3])return jQuery().find(selector);return jQuery(elem);}selector=[];}}else -return jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(jQuery.makeArray(selector));},jquery:"1.2.6",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;return jQuery.inArray(elem&&elem.jquery?elem[0]:elem,this);},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value===undefined)return this[0]&&jQuery[type||"attr"](this[0],name);else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div");container.appendChild(clone);return jQuery.clean([container.innerHTML])[0];}else -return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else -selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return this.pushStack(jQuery.unique(jQuery.merge(this.get(),typeof selector=='string'?jQuery(selector):jQuery.makeArray(selector))));},is:function(selector){return!!selector&&jQuery.multiFilter(selector,this).length>0;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=jQuery.makeArray(value);jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else -this.value=value;});},html:function(value){return value==undefined?(this[0]?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},data:function(key,value){var parts=key.split(".");parts[1]=parts[1]?"."+parts[1]:"";if(value===undefined){var data=this.triggerHandler("getData"+parts[1]+"!",[parts[0]]);if(data===undefined&&this.length)data=jQuery.data(this[0],key);return data===undefined&&parts[1]?this.data(parts[0]):data;}else -return this.trigger("setData"+parts[1]+"!",[parts[0],value]).each(function(){jQuery.data(this,key,value);});},removeData:function(key){return this.each(function(){jQuery.removeData(this,key);});},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script"))scripts=scripts.add(elem);else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.fn.init.prototype=jQuery.fn;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else -jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}function now(){return+new Date;}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==i){target=this;--i;}for(;i-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else -jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret,style=elem.style;function color(elem){if(!jQuery.browser.safari)return false;var ret=defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=style.outline;style.outline="0 solid black";style.outline=save;}if(name.match(/float/i))name=styleFloat;if(!force&&style&&style[name])ret=style[name];else if(defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var computedStyle=defaultView.getComputedStyle(elem,null);if(computedStyle&&!color(elem))ret=computedStyle.getPropertyValue(name);else{var swap=[],stack=[],a=elem,i=0;for(;a&&color(a);a=a.parentNode)stack.unshift(a);for(;i]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("",""]||!tags.indexOf("",""]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
"]||!tags.indexOf("",""]||(!tags.indexOf("",""]||!tags.indexOf("",""]||jQuery.browser.msie&&[1,"div
","
"]||[0,"",""];div.innerHTML=wrap[1]+elem+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){var tbody=!tags.indexOf(""&&tags.indexOf("=0;--j)if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)tbody[j].parentNode.removeChild(tbody[j]);if(/^\s/.test(elem))div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);}elem=jQuery.makeArray(div.childNodes);}if(elem.length===0&&(!jQuery.nodeName(elem,"form")&&!jQuery.nodeName(elem,"select")))return;if(elem[0]==undefined||jQuery.nodeName(elem,"form")||elem.options)ret.push(elem);else -ret=jQuery.merge(ret,elem);});return ret;},attr:function(elem,name,value){if(!elem||elem.nodeType==3||elem.nodeType==8)return undefined;var notxml=!jQuery.isXMLDoc(elem),set=value!==undefined,msie=jQuery.browser.msie;name=notxml&&jQuery.props[name]||name;if(elem.tagName){var special=/href|src|style/.test(name);if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(name in elem&¬xml&&!special){if(set){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem[name]=value;}if(jQuery.nodeName(elem,"form")&&elem.getAttributeNode(name))return elem.getAttributeNode(name).nodeValue;return elem[name];}if(msie&¬xml&&name=="style")return jQuery.attr(elem.style,"cssText",value);if(set)elem.setAttribute(name,""+value);var attr=msie&¬xml&&special?elem.getAttribute(name,2):elem.getAttribute(name);return attr===null?undefined:attr;}if(msie&&name=="opacity"){if(set){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(value)+''=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter&&elem.filter.indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100)+'':"";}name=name.replace(/-([a-z])/ig,function(all,letter){return letter.toUpperCase();});if(set)elem[name]=value;return elem[name];},trim:function(text){return(text||"").replace(/^\s+|\s+$/g,"");},makeArray:function(array){var ret=[];if(array!=null){var i=array.length;if(i==null||array.split||array.setInterval||array.call)ret[0]=array;else -while(i)ret[--i]=array[i];}return ret;},inArray:function(elem,array){for(var i=0,length=array.length;i*",this).remove();while(this.firstChild)this.removeChild(this.firstChild);}},function(name,fn){jQuery.fn[name]=function(){return this.each(fn,arguments);};});jQuery.each(["Height","Width"],function(i,name){var type=name.toLowerCase();jQuery.fn[type]=function(size){return this[0]==window?jQuery.browser.opera&&document.body["client"+name]||jQuery.browser.safari&&window["inner"+name]||document.compatMode=="CSS1Compat"&&document.documentElement["client"+name]||document.body["client"+name]:this[0]==document?Math.max(Math.max(document.body["scroll"+name],document.documentElement["scroll"+name]),Math.max(document.body["offset"+name],document.documentElement["offset"+name])):size==undefined?(this.length?jQuery.css(this[0],type):null):this.css(type,size.constructor==String?size:size+"px");};});function num(elem,prop){return elem[0]&&parseInt(jQuery.curCSS(elem[0],prop,true),10)||0;}var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},"#":function(a,i,m){return a.getAttribute("id")==m[2];},":":{lt:function(a,i,m){return im[3]-0;},nth:function(a,i,m){return m[3]-0==i;},eq:function(a,i,m){return m[3]-0==i;},first:function(a,i){return i==0;},last:function(a,i,m,r){return i==r.length-1;},even:function(a,i){return i%2==0;},odd:function(a,i){return i%2;},"first-child":function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},"last-child":function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},"only-child":function(a){return!jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},parent:function(a){return a.firstChild;},empty:function(a){return!a.firstChild;},contains:function(a,i,m){return(a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},visible:function(a){return"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},hidden:function(a){return"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},enabled:function(a){return!a.disabled;},disabled:function(a){return a.disabled;},checked:function(a){return a.checked;},selected:function(a){return a.selected||jQuery.attr(a,"selected");},text:function(a){return"text"==a.type;},radio:function(a){return"radio"==a.type;},checkbox:function(a){return"checkbox"==a.type;},file:function(a){return"file"==a.type;},password:function(a){return"password"==a.type;},submit:function(a){return"submit"==a.type;},image:function(a){return"image"==a.type;},reset:function(a){return"reset"==a.type;},button:function(a){return"button"==a.type||jQuery.nodeName(a,"button");},input:function(a){return/input|select|textarea|button/i.test(a.nodeName);},has:function(a,i,m){return jQuery.find(m[3],a).length;},header:function(a){return/h\d/i.test(a.nodeName);},animated:function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&context.nodeType!=1&&context.nodeType!=9)return[];context=context||document;var ret=[context],done=[],last,nodeName;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false,re=quickChild,m=re.exec(t);if(m){nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var merge={};nodeName=m[2].toUpperCase();m=m[1];for(var j=0,rl=ret.length;j=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=isSimple.test(m[3])?jQuery.filter(m[3],r,true).r:jQuery(r).not(m[3]);else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"0n+"+m[3]||m[3]),first=(test[1]+(test[2]||1))-0,last=test[3]-0;for(var i=0,rl=r.length;i=0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var fn=jQuery.expr[m[1]];if(typeof fn=="object")fn=fn[m[2]];if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+";}");r=jQuery.grep(r,function(elem,i){return fn(elem,i,m,r);},not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[],cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&n!=elem)r.push(n);}return r;}});jQuery.event={add:function(elem,types,handler,data){if(elem.nodeType==3||elem.nodeType==8)return;if(jQuery.browser.msie&&elem.setInterval)elem=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=this.proxy(fn,function(){return fn.apply(this,arguments);});handler.data=data;}var events=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",function(){if(typeof jQuery!="undefined"&&!jQuery.event.triggered)return jQuery.event.handle.apply(arguments.callee.elem,arguments);});handle.elem=elem;jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];handler.type=parts[1];var handlers=events[type];if(!handlers){handlers=events[type]={};if(!jQuery.event.special[type]||jQuery.event.special[type].setup.call(elem)===false){if(elem.addEventListener)elem.addEventListener(type,handle,false);else if(elem.attachEvent)elem.attachEvent("on"+type,handle);}}handlers[handler.guid]=handler;jQuery.event.global[type]=true;});elem=null;},guid:1,global:{},remove:function(elem,types,handler){if(elem.nodeType==3||elem.nodeType==8)return;var events=jQuery.data(elem,"events"),ret,index;if(events){if(types==undefined||(typeof types=="string"&&types.charAt(0)=="."))for(var type in events)this.remove(elem,type+(types||""));else{if(types.type){handler=types.handler;types=types.type;}jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];if(events[type]){if(handler)delete events[type][handler.guid];else -for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(!jQuery.event.special[type]||jQuery.event.special[type].teardown.call(elem)===false){if(elem.removeEventListener)elem.removeEventListener(type,jQuery.data(elem,"handle"),false);else if(elem.detachEvent)elem.detachEvent("on"+type,jQuery.data(elem,"handle"));}ret=null;delete events[type];}}});}for(ret in events)break;if(!ret){var handle=jQuery.data(elem,"handle");if(handle)handle.elem=null;jQuery.removeData(elem,"events");jQuery.removeData(elem,"handle");}}},trigger:function(type,data,elem,donative,extra){data=jQuery.makeArray(data);if(type.indexOf("!")>=0){type=type.slice(0,-1);var exclusive=true;}if(!elem){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{if(elem.nodeType==3||elem.nodeType==8)return undefined;var val,ret,fn=jQuery.isFunction(elem[type]||null),event=!data[0]||!data[0].preventDefault;if(event){data.unshift({type:type,target:elem,preventDefault:function(){},stopPropagation:function(){},timeStamp:now()});data[0][expando]=true;}data[0].type=type;if(exclusive)data[0].exclusive=true;var handle=jQuery.data(elem,"handle");if(handle)val=handle.apply(elem,data);if((!fn||(jQuery.nodeName(elem,'a')&&type=="click"))&&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)val=false;if(event)data.shift();if(extra&&jQuery.isFunction(extra)){ret=extra.apply(elem,val==null?data:data.concat(val));if(ret!==undefined)val=ret;}if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(elem,'a')&&type=="click")){this.triggered=true;try{elem[type]();}catch(e){}}this.triggered=false;}return val;},handle:function(event){var val,ret,namespace,all,handlers;event=arguments[0]=jQuery.event.fix(event||window.event);namespace=event.type.split(".");event.type=namespace[0];namespace=namespace[1];all=!namespace&&!event.exclusive;handlers=(jQuery.data(this,"events")||{})[event.type];for(var j in handlers){var handler=handlers[j];if(all||handler.type==namespace){event.handler=handler;event.data=handler.data;ret=handler.apply(this,arguments);if(val!==false)val=ret;if(ret===false){event.preventDefault();event.stopPropagation();}}}return val;},fix:function(event){if(event[expando]==true)return event;var originalEvent=event;event={originalEvent:originalEvent};var props="altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target timeStamp toElement type view wheelDelta which".split(" ");for(var i=props.length;i;i--)event[props[i]]=originalEvent[props[i]];event[expando]=true;event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};event.timeStamp=event.timeStamp||now();if(!event.target)event.target=event.srcElement||document;if(event.target.nodeType==3)event.target=event.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var doc=document.documentElement,body=document.body;event.pageX=event.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc.clientLeft||0);event.pageY=event.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc.clientTop||0);}if(!event.which&&((event.charCode||event.charCode===0)?event.charCode:event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;},proxy:function(fn,proxy){proxy.guid=fn.guid=fn.guid||proxy.guid||this.guid++;return proxy;},special:{ready:{setup:function(){bindReady();return;},teardown:function(){return;}},mouseenter:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseover",jQuery.event.special.mouseenter.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseover",jQuery.event.special.mouseenter.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseenter";return jQuery.event.handle.apply(this,arguments);}},mouseleave:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseout",jQuery.event.special.mouseleave.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseout",jQuery.event.special.mouseleave.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseleave";return jQuery.event.handle.apply(this,arguments);}}}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){var one=jQuery.event.proxy(fn||data,function(event){jQuery(this).unbind(event,one);return(fn||data).apply(this,arguments);});return this.each(function(){jQuery.event.add(this,type,one,fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){return this[0]&&jQuery.event.trigger(type,data,this[0],false,fn);},toggle:function(fn){var args=arguments,i=1;while(i=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,dataType:"html",data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("
").append(res.responseText.replace(//g,"")).find(selector):res.responseText);self.each(callback,[res.responseText,status,res]);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(val,i){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=now();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{url:location.href,global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null,username:null,password:null,accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(s){s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));var jsonp,jsre=/=\?(&|$)/g,status,data,type=s.type.toUpperCase();if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);if(s.dataType=="jsonp"){if(type=="GET"){if(!s.url.match(jsre))s.url+=(s.url.match(/\?/)?"&":"?")+(s.jsonp||"callback")+"=?";}else if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&(s.data&&s.data.match(jsre)||s.url.match(jsre))){jsonp="jsonp"+jsc++;if(s.data)s.data=(s.data+"").replace(jsre,"="+jsonp+"$1");s.url=s.url.replace(jsre,"="+jsonp+"$1");s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();complete();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}if(head)head.removeChild(script);};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&type=="GET"){var ts=now();var ret=s.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+ts+"$2");s.url=ret+((ret==s.url)?(s.url.match(/\?/)?"&":"?")+"_="+ts:"");}if(s.data&&type=="GET"){s.url+=(s.url.match(/\?/)?"&":"?")+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");var remote=/^(?:\w+:)?\/\/([^\/?#]+)/;if(s.dataType=="script"&&type=="GET"&&remote.test(s.url)&&remote.exec(s.url)[1]!=location.host){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(s.scriptCharset)script.charset=s.scriptCharset;if(!jsonp){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return undefined;}var requestDone=false;var xhr=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();if(s.username)xhr.open(type,s.url,s.async,s.username,s.password);else -xhr.open(type,s.url,s.async);try{if(s.data)xhr.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xhr.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");xhr.setRequestHeader("Accept",s.dataType&&s.accepts[s.dataType]?s.accepts[s.dataType]+", */*":s.accepts._default);}catch(e){}if(s.beforeSend&&s.beforeSend(xhr,s)===false){s.global&&jQuery.active--;xhr.abort();return false;}if(s.global)jQuery.event.trigger("ajaxSend",[xhr,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xhr&&(xhr.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xhr)&&"error"||s.ifModified&&jQuery.httpNotModified(xhr,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xhr,s.dataType,s.dataFilter);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xhr.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else -jQuery.handleError(s,xhr,status);complete();if(s.async)xhr=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xhr){xhr.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xhr.send(s.data);}catch(e){jQuery.handleError(s,xhr,null,e);}if(!s.async)onreadystatechange();function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xhr,s]);}function complete(){if(s.complete)s.complete(xhr,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xhr,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}return xhr;},handleError:function(s,xhr,status,e){if(s.error)s.error(xhr,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xhr,s,e]);},active:0,httpSuccess:function(xhr){try{return!xhr.status&&location.protocol=="file:"||(xhr.status>=200&&xhr.status<300)||xhr.status==304||xhr.status==1223||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpNotModified:function(xhr,url){try{var xhrRes=xhr.getResponseHeader("Last-Modified");return xhr.status==304||xhrRes==jQuery.lastModified[url]||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpData:function(xhr,type,filter){var ct=xhr.getResponseHeader("content-type"),xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0,data=xml?xhr.responseXML:xhr.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(filter)data=filter(data,type);if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else -for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else -s.push(encodeURIComponent(j)+"="+encodeURIComponent(jQuery.isFunction(a[j])?a[j]():a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock||"";if(jQuery.css(this,"display")=="none"){var elem=jQuery("<"+this.tagName+" />").appendTo("body");this.style.display=elem.css("display");if(this.style.display=="none")this.style.display="block";elem.remove();}}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle.apply(this,arguments):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var optall=jQuery.speed(speed,easing,callback);return this[optall.queue===false?"each":"queue"](function(){if(this.nodeType!=1)return false;var opt=jQuery.extend({},optall),p,hidden=jQuery(this).is(":hidden"),self=this;for(p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return opt.complete.call(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),start=e.cur(true)||0;if(parts){var end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=(end||1)+unit;start=((end||1)/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-="?-1:1)*end)+start;e.custom(start,end,unit);}else -e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(jQuery.isFunction(type)||(type&&type.constructor==Array)){fn=type;type="fx";}if(!type||(typeof type=="string"&&!fn))return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.call(this);}});},stop:function(clearQueue,gotoEnd){var timers=jQuery.timers;if(clearQueue)this.queue([]);this.each(function(){for(var i=timers.length-1;i>=0;i--)if(timers[i].elem==this){if(gotoEnd)timers[i](true);timers.splice(i,1);}});if(!gotoEnd)this.dequeue();return this;}});var queue=function(elem,type,array){if(elem){type=type||"fx";var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",jQuery.makeArray(array));}return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].call(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:jQuery.fx.speeds[opt.duration])||jQuery.fx.speeds.def;opt.old=opt.complete;opt.complete=function(){if(opt.queue!==false)jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.call(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],timerId:null,fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.call(this.elem,this.now,this);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.css(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.curCSS(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=now();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(gotoEnd){return self.step(gotoEnd);}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timerId==null){jQuery.timerId=setInterval(function(){var timers=jQuery.timers;for(var i=0;ithis.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done)this.options.complete.call(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.extend(jQuery.fx,{speeds:{slow:600,fast:200,def:400},step:{scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}}});jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var parent=elem.parentNode,offsetChild=elem,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&parseInt(version)<522&&!/adobeair/i.test(userAgent),css=jQuery.curCSS,fixed=css(elem,"position")=="fixed";if(elem.getBoundingClientRect){var box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));add(-doc.documentElement.clientLeft,-doc.documentElement.clientTop);}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&!/^t(able|d|h)$/i.test(offsetParent.tagName)||safari&&!safari2)border(offsetParent);if(!fixed&&css(offsetParent,"position")=="fixed")fixed=true;offsetChild=/^body$/i.test(offsetParent.tagName)?offsetChild:offsetParent;offsetParent=offsetParent.offsetParent;}while(parent&&parent.tagName&&!/^body|html$/i.test(parent.tagName)){if(!/^inline|table.*$/i.test(css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if((safari2&&(fixed||css(offsetChild,"position")=="absolute"))||(mozilla&&css(offsetChild,"position")!="absolute"))add(-doc.body.offsetLeft,-doc.body.offsetTop);if(fixed)add(Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));}results={top:top,left:left};}function border(elem){add(jQuery.curCSS(elem,"borderLeftWidth",true),jQuery.curCSS(elem,"borderTopWidth",true));}function add(l,t){left+=parseInt(l,10)||0;top+=parseInt(t,10)||0;}return results;};jQuery.fn.extend({position:function(){var left=0,top=0,results;if(this[0]){var offsetParent=this.offsetParent(),offset=this.offset(),parentOffset=/^body|html$/i.test(offsetParent[0].tagName)?{top:0,left:0}:offsetParent.offset();offset.top-=num(this,'marginTop');offset.left-=num(this,'marginLeft');parentOffset.top+=num(offsetParent,'borderTopWidth');parentOffset.left+=num(offsetParent,'borderLeftWidth');results={top:offset.top-parentOffset.top,left:offset.left-parentOffset.left};}return results;},offsetParent:function(){var offsetParent=this[0].offsetParent;while(offsetParent&&(!/^body|html$/i.test(offsetParent.tagName)&&jQuery.css(offsetParent,'position')=='static'))offsetParent=offsetParent.offsetParent;return jQuery(offsetParent);}});jQuery.each(['Left','Top'],function(i,name){var method='scroll'+name;jQuery.fn[method]=function(val){if(!this[0])return;return val!=undefined?this.each(function(){this==window||this==document?window.scrollTo(!i?val:jQuery(window).scrollLeft(),i?val:jQuery(window).scrollTop()):this[method]=val;}):this[0]==window||this[0]==document?self[i?'pageYOffset':'pageXOffset']||jQuery.boxModel&&document.documentElement[method]||document.body[method]:this[0][method];};});jQuery.each(["Height","Width"],function(i,name){var tl=i?"Left":"Top",br=i?"Right":"Bottom";jQuery.fn["inner"+name]=function(){return this[name.toLowerCase()]()+num(this,"padding"+tl)+num(this,"padding"+br);};jQuery.fn["outer"+name]=function(margin){return this["inner"+name]()+num(this,"border"+tl+"Width")+num(this,"border"+br+"Width")+(margin?num(this,"margin"+tl)+num(this,"margin"+br):0);};});})(); \ No newline at end of file diff --git a/samples/xml/openid/src/main/webapp/js/jquery-3.5.1.min.js b/samples/xml/openid/src/main/webapp/js/jquery-3.5.1.min.js new file mode 100644 index 00000000000..b0614034ad3 --- /dev/null +++ b/samples/xml/openid/src/main/webapp/js/jquery-3.5.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 - +